🔥

Hono + Cloudflare Workers で REST API を作ってみよう

2022/08/29に公開1

Hono とは?

Hono は TypeScript/JavaScript のシンプルな Web フレームワークです。Hono という名前は日本語の「炎」に由来します。

Hono の特徴としては以下の点が挙げられています。

  • ウルトラファスト
  • 依存関係ゼロ
  • ミドルウェア
  • TypeScript
  • マルチプラットフォーム

ウルトラファスト

Benchmarks の示すとおり、Hono はその他のライブラリに比べて早い結果がでています。Hono が早い理由として、内部で使うルーターに秘密があります。Hono では以下の 2 種類のルーターどちらを使用するか選択できます。

  • TrieRouter
  • RegExpRouter

「TrieRouter」URL のパスマッチングは URL のパスマッチングにTrie 木という構造を使っています。「RegExpRouter」は登録されたルーティングをあらかじめひとつの大きな正規表現にして、リクエストが来たら、マッチさせるという仕組みです。どちらのルーターも高速に動作します。

「TrieRouter」「RegExpRouter」の詳細な解説については Hono の開発者の記事に譲ります。

依存関係ゼロ

Hono は Service Worker と Web Standard API に準拠して提供されています。例えばルーティングのハンドラにおいてレスポンスを返却する際には Web API の Response を返却できます。

app.get("/", () => {
  return new Response("Hey!", {
    status: 200,
    headers: {
      "Content-Type": "text/plain",
    },
  });
});

ミドルウェア

多くのビルドインミドルウェアサードパーティミドルウェアを備えており、またカスタムミドルウェアも簡単に定義できます。

TypeScript

TypeScript をファーストクラスでサポートしています。ゼロコンフィグで始めることができ、開発体験も非常に良いです。

マルチプラットフォーム

Hono は現在以下のプラットフォームをサポートしています。

import の書き方やエントリーポイントはプラットフォームごとに若干異なるものの、大半のコードは全く同じコードで多くのプラットフォームで動作するのは大きな特徴です。

はじめての Hono

それでは早速 Hono を使用してアプリケーションを作成してみましょう。前述のとおり Hono はさまざまなプラットフォームで実行させることができますが、今回は Cloudflare Workers を選択します。Cloudflare Workers はエッジサーバーで JavaScript を実行してくれるサーバーレスのサービスです。

Cloudflare Workers アカウントの作成

Cloudflare Workers を動かすためには Cloudflare Workers のアカウントを作成する必要があります。アカウントを作成するには下記サイトから「Sign up」をクリックします。

https://workers.cloudflare.com/

スクリーンショット 2022-08-27 20.53.23

プランの選択は無料プランである「Free」プランで問題ありません。

プロジェクトの作成

Cloudflare Workers のプロジェクトを作成するためにはコマンドラインツールである Wrangler をインストールします。

npm install -g wrangler

インストールが完了したことを確認しましょう。Wrangler は最新のバージョンである 2.x を使うことをおすすめします。

wrangler --version
 ⛅️ wrangler 2.0.27 
--------------------

Cloudflare のリソースにアクセスするためには認証が必要です。以下のコマンドを実行して認証を完了させましょう。

wrangler login

コマンドを実行するとブラウザのタブが開きアクセスを許可するか聞かれますので「Allow」を選択します。認証が完了するとターミナルには「Successfully logged in.」と表示されます。

Cloudflare Workers のプロジェクトを作成するために以下のコマンドを実行します。

wrangler init -y hono-todo-app
cd hono-todo-app

hono をインストールします。

npm install hono

Hello World

はじめに簡単な Hello World を表示するコードを作成しましょう。src/index.ts に以下を記述します。

src/index.ts
import { Hono } from "hono";

const app = new Hono(); // ①
app.get("/", (c) => c.text("Hello 🔥")); // ②

export default app; // ③

①:new Hono()Hono インスタンスを作成します。Hono インスタンスに対してルーティングやミドルウェアを追加することでサーバーの処理を記述します。

②:ルーティング関数は指定したルートと HTTP メソッドへのリクエストを受け取ったときに呼び出されるコールバック関数(ハンドラ)を指定します。ここでは app.get("/", (c) => c.text("Hello 🔥"));/ パスへの GET リクエストに対するハンドラを記述します。このような書き方は Express のルーティング関数ともよく似ていています。

コールバック関数の引数に Context オブジェクトを受け取ります。Context オブジェクトはリクエストとレスポンスをハンドリングするために使用されます。例えば c.req.query() メソッドからクエリパラメータを取得したり、c.body() メソッドでレスポンスボディを返却できます。

ここでは c.text() メソッドを呼び出しています。これは Content-Type:text/plain としてテキストをレスポンスとして返却します。c.text()c.body()c.json() などのレスポンスを返却するメソッドはすべて Response オブジェクトを返却する便利メソッドです。

③:Cloudflare Workers においては、export default appHono インスタンスを公開することにより Worker がリクエストを受け取ることができます。ここがアプリケーションのエントリーポイントとなります。

Cloudflare Workers には Service Workers モードと Module Workers モードがありますが、export default app と記述した場合には Module Workers モードとして扱われます(Service Workers モードを使うには app.fire() と記述します)。Module Workers モードでは Durable Objects を使用できる・KV などのバインディングをグローバルスコープではなくローカルスコープで扱える・ES Modules のシンタックス(import/export)を使用できるといったメリットがあるので、基本的には Module Workers モードを使用することをおすすめします。

https://developers.cloudflare.com/workers/learning/migrating-to-module-workers/

コードの記述が完了したら、ローカル環境で開発サーバーを起動して確認しましょう。以下コマンドを実行して http://localhost:8787 にアクセスします。

npm start

ブラウザに次のように表示されているはずです🔥

スクリーンショット 2022-08-27 12.14.51

TODO アプリの作成

それでは Hono を使用して簡単な CRUD 操作を備えた API サーバーを作成してみましょう。完成形は以下のレポジトリを参照してください。

https://github.com/azukiazusa1/hono-todo-app

まずは src/todos/api.ts ファイルを作成します。

src/todos/api.ts
import { Hono } from "hono";

let todoList = [
  { id: "1", title: "Learning Hono", completed: false },
  { id: "2", title: "Watch the movie", completed: true },
  { id: "3", title: "Buy milk", completed: false },
];

const todos = new Hono();
todos.get("/", (c) => c.json(todoList));

export { todos };

さきほどの Hello World と同様に Hono のインスタンスを作成してルーティングを設定しています。まずはじめに Todo の一覧を返却するエンドポイントを作成します。ひとまずダミーデータとして todoList を定義して c.json() メソッドで JSON 形式として返却しています。

src/index.ts ファイルにおいて作成した todos ルーティングを使用するように変更します。

src/index.ts
import { Hono } from "hono";
import { todos } from "./todos/api";

const app = new Hono();
app.route("/api/todos", todos);

export default app;

app.route() メソッドでさきほど作成した todos インスタンスを指定しています。app.route() を使用すると、ルーティングの定義をグループ化できます。Todo 一覧取得のパスは /api/todos/ としてマッチングします。

作成した API を実際にテストしてみましょう。この記事では HTTP クライアントに Thunder Client と呼ばれる VSCode の拡張機能を使用しますが、お好みのツールを使用していただいて構いません。

http://localhost:8787/api/todos に対して GET リクエストを送信します。Thunder Client の拡張機能をインストールしたら左のメニューから Thunder Client のアイコンをクリックします。その後、サイドメニューから「New Request」ボタンをクリックすると API をコールする画面が表示されます。上部の入力欄に http://localhost:8787/api/todos と入力した後「Send」ボタンをクリックしましょう。ここまででうまくいけば、TodoList がレスポンスとして返却されるはずです。

スクリーンショット 2022-08-27 15.12.54

Todo の作成

続いて Todo を作成するエンドポイントを実装しましょう。src/todos/api.ts の続きに書いていきます。

src/todos/api.ts
todos.post("/", async (c) => {
  const param = await c.req.json<{ title: string }>();
  const newTodo = {
    id: String(todoList.length + 1),
    completed: false,
    title: param.title,
  };
  todoList = [...todoList, newTodo];

  return c.json(newTodo, 201);
});

Todo の作成には POST リクエストを使用するので app.post() メソッドを使用します。リクエストボディを取得するためには c.req.json() メソッドを使用します。その後 todoList の末尾に追加し、新たに作成した Todo を c.json() で返却します。c.json() の第 2 引数には HTTP ステータスコードを渡すことができるので、201 Created を指定しています。

作成した API をテストしましょう。Thunder Client では「Body」タブからリクエストボディを指定できます。「Json」形式を選択して title プロパティを持った JSON を記述し POST リクエストを送信してみましょう。

スクリーンショット 2022-08-27 20.42.08

正しい実装できていれば、新たに作成された Todo がレスポンスとして返却されるはずです。Todo 一覧の API をコールして、たった今作成した Todo が追加されていることを確認してみましょう。

スクリーンショット 2022-08-27 20.46.00

Todo の更新

続いて ID を指定して Todo を更新するエンドポイントを実装します。

src/todos/api.ts
todos.put("/:id", async (c) => {
  const id = c.req.param("id");
  const todo = todoList.find((todo) => todo.id === id);
  if (!todo) {
    return c.json({ message: "not found" }, 404);
  }
  const param = (await c.req.parseBody()) as {
    title?: string;
    completed?: boolean;
  };
  todoList = todoList.map((todo) => {
    if (todo.id === id) {
      return {
        ...todo,
        ...param,
      };
    } else {
      return todo;
    }
  });
  return new Response(null, { status: 204 });
});

ID はパスパラメータで指定しています。パスパラメータは c.req.param() メソッドで取得できます。素晴らしいことに c.req.param() メソッドはルート定義から補完が効くようになります。

スクリーンショット 2022-08-27 21.27.57

パスパラメータの ID を元に todoList から対象の Todo を取得し、もし存在しない ID であった場合には 404 Not Found を返却して処理を終了します。Todo が存在する場合には c.req.json() メソッドでリクエストボディを取得して todoList.map() を利用して対象の Todo を更新します。

Todo の更新が完了したら 204 No Content を返却します。ルーティングのハンドラは c.text()c.json() を返却する代わりに Web API の Response オブジェクトを直接返却できます。

実装が完了したらエンドポイントをテストしましょう。作成時と同様に「Body」タブからボディリクエストを指定します。

スクリーンショット 2022-08-27 22.37.45

Todo の削除

最後に ID を指定して Todo を削除するエンドポイントです。

src/todo/api.ts
todos.delete("/:id", async (c) => {
  const id = c.req.param("id");
  const todo = todoList.find((todo) => todo.id === id);
  if (!todo) {
    return c.json({ message: "not found" }, 404);
  }
  todoList = todoList.filter((todo) => todo.id !== id);

  return new Response(null, { status: 204 });
});

更新のエンドポイントと大きく内容は変わりません。パスパラメータから ID を取得し、存在しない ID であれば 404 Not Found を返却します。Todo が存在する場合には todoList.filter() で指定した ID の要素を取り除きます。処理が完了したら更新時と同様に 204 No Content を返却します。

スクリーンショット 2022-08-27 22.40.13

DELETE リクエストの送信後、一覧取得 API から要素が取り除かれていることを確認しておきましょう。

スクリーンショット 2022-08-27 22.41.32

ミドルウェア

Hono はルーティングの他にミドルウェアを備えています。ミドルウェアはハンドラの前後で動作し、リクエストオブジェクトやレスポンスオブジェクトを変更できます。Hono にはロギング、認証、CORS など多くのビルドインミドルウェアが用意されています。

Basic 認証

ここまで Todo API の実装をしてきましたが、世界中の誰でも Todo の閲覧・作成・削除が行えることを望むユーザーはほとんどいないでしょう。通常このような操作は認証したユーザーのみが実行できるはずです。ここでは認証機能として Basic 認証を導入してみましょう。

通常 Cloudflare Workers に Basic 認証を実装するのは案外面倒なのですが、Hono のビルドインミドルウェアを使用すれば簡単に実装できます。src/index.ts に以下のようにミドルウェアを記述します。

src/index.ts
const app = new Hono();

app.use(
  "/api/*",
  basicAuth({
    username: "charizard",
    password: "super-secret",
  })
);

app.use() メソッドでミドルウェアを登録できます。app.use() の第 1 引数はミドルウェアを適用させるパスです。ここでは /api 配下のすべてのパスに対して Basic 認証を有効にします。

Basic 認証が有効となっているか確認しましょう。Todo 一覧取得 API にリクエストを送信すると 401 Unauthorized が返却されます。

スクリーンショット 2022-08-28 11.36.56

Thunder Client では「Auth」のタブから「Basic」を選択することで認証情報を入力できます。ミドルウェアで指定したユーザー名とパスワードを入力してリクエストを送信すると、正しくレスポンスが返却されます。

スクリーンショット 2022-08-28 11.39.23

バリデーションミドルウェア

さらにミドルウェアを追加してみましょう。サードパーティのミドルウェアであるバリデーションミドルウェアを導入します。

https://github.com/honojs/validator

このミドルウェアは validator.js をラップしたものです。サードパーティとビルドインのミドルウェアの違いは外部のライブラリに依存しているかどうかです。ビルドインのミドルウェアは外部ライブラリに一切依存しません。

まずは Validator Middleware をインストールします。

npm install @honojs/validator

Todo を作成する際にはタイトルを必須項目とするように修正しましょう。以下のように Todo 作成 API に対してバリデーションミドルウェアを挟み込みます。

src/todos/api.ts
import { validation } from "@honojs/validator";

todos.post(
  "/",
  validation((v, message) => ({
    body: {
      title: [v.trim, [v.required, message("Title is required")]],
    },
  })),
  async (c) => {
    const param = await c.req.json<{ title: string }>();
    const newTodo = {
      id: String(todoList.length + 1),
      completed: false,
      title: param.title,
    };
    todoList = [...todoList, newTodo];

    return c.json(newTodo, 201);
  }
);

body.title に対して v.trimv.required ルールを指定しています。まずは v.trim でバリデーションを実施する前にサニタイズを実行します。v.required ルールは JSON のプロパティに title が存在しないか、title の値が空文字のときエラーを返却します。message を使用するとエラーメッセージをカスタマイズできます。

実際リクエストを送信してバリデーションが実施されるか確認しましょう。title が空文字の時や空白のみの場合には 400 Bad Request が返されるはずです。

スクリーンショット 2022-08-28 11.59.45

Workers KV に永続化する

ここまでは Todo のデータを変数に保存していたため、プロセスを終了するたびにデータが失われてしまいます。Workers KV にデータを保存して永続化されるように実装しましょう。Workers KV は Cloudflare のエッジサーバーからアクセスできる、グローバルなキーバリューストアです。

Workers KV の作成

Workers KV を wrangler コマンドを利用して作成します。

wrangler kv:namespace create "HONO_TODO"

🌀 Creating namespace with title "worker-HONO_TODO"
✨ Success!
Add the following to your configuration file in your kv_namespaces array:
{ binding = "HONO_TODO", id = "1a136855b23f4b14aab395ab6247282a" }
wrangler kv:namespace create "HONO_TODO" --preview

🌀 Creating namespace with title "worker-HONO_TODO_preview"
✨ Success!
Add the following to your configuration file in your kv_namespaces array:
{ binding = "HONO_TODO", preview_id = "9d766fea526043be8e40f6550436bc96" }

ここでは「HONO_TODO」という名前で KV を作成しました。--preview を付与したものは開発環境用の KV です。それぞれコマンドの実行結果に idpreview_id が記載されているので、これを wrangler.toml に追記します。

wrangler.toml
kv_namespaces = [
  { binding = "HONO_TODO", preview_id = "9d766fea526043be8e40f6550436bc96", id = "1a136855b23f4b14aab395ab6247282a" }
]

アプリケーションコードから Workers KB にアクセスする

Workers KV の作成が完了したのでアプリケーションコードで KV からデータを取得したり保存したりできるように修正しましょう。wrangler.toml にバインディング(binding)した nemespace はハンドラ関数の c.env から利用できるようになります。bindings.d.ts ファイルを作成して c.env の型定義を作成しましょう。

bindings.d.ts
export interface Bindings {
  HONO_TODO: KVNamespace;
}

new Hono() に対して Bindings の型引数を与えることで c.env の型に型を付けることができます。

src/todos/api.ts
import { Bindings } from "../bindings";

const todos = new Hono<Bindings>();

スクリーンショット 2022-08-28 15.09.47

データの操作は Model 層で抽象化するように実装しましょう。src/todos/model.ts ファイルを作成します。

src/todos/model.ts
export interface Todo {
  id: string;
  title: string;
  completed: boolean;
}

export interface CreateTodo {
  title: string;
}

export interface UpdateTodo {
  title?: string;
  completed?: boolean;
}

export const PREFIX = "v1:todo:";

はじめに TODO の型定義と、定数として PREFIX を定義しています。PREFIX は KV のキーのプレフィックです。KV からリストで値を取得する際にプレフィックを使用してフィルタリングできます。プレフィックはキー名をコロンで区切って構成します(v1:todo:<key>)。

はじめに Todo の一覧を取得する関数です。

src/todos/model.ts
export const getTodos = async (KV: KVNamespace): Promise<Todo[]> => {
  const list = await KV.list({ prefix: PREFIX });
  const todos: Todo[] = [];

  for (const key of list.keys) {
    const value = await KV.get<Todo>(key.name, "json");
    if (value) {
      todos.push(value);
    }
  }

  return todos;
};

引数として KV を受け取っています。これはルーティングのハンドラ関数から e.env.HONO_TODO を渡してもらうことで Workers KV の API にアクセスするため使用します。引数として KV を受け取るのはすべての関数で共通です。

KV.list() メソッドで特定のプレフィックを持つキーをすべて取得しています。list.keys からキーの一覧を取得できるので for...of でループしてすべての値を取得しています。キーから値を取得するためには KV.get() メソッドを使用します。KV.get() の第 2 引数ではどのような形式に値を取得するかを指定できます。デフォルトは "text" として取得しますが、"json" 形式を選択するとオブジェクトに変換してから値を返却してくれます。

続いて id を指定して特定の Todo を取得する関数です。ただ単にラップしているだけですので、あまり特筆すべき点もないでしょう。

src/todos/model.ts
export const getTodo = (KV: KVNamespace, id: string): Promise<Todo | null> => {
  return KV.get<Todo>(`${PREFIX}${id}`, "json");
};

Todo を作成する関数は引数にボディパラメータを受け取ります。

src/todos/model.ts
export const createTodo = async (KV: KVNamespace, param: CreateTodo): Promise<Todo> => {
  const id = crypto.randomUUID();
  const newTodo: Todo = {
    id,
    title: param.title,
    completed: false,
  };
  await KV.put(`${PREFIX}${id}`, JSON.stringify(newTodo));

  return newTodo;
};

id の生成には Web Crypto API である crypto.randomUUID() を使用しています。このように Cloudflare Workers は Web Crypto もサポートしています。

KV にデータを保存するには KV.put() メソッドを使用します。キーにはプレフィックを含めるのと、値を string 型として書き込むために JSON.stringify を使用していることに注意してください。

続いて Todo を更新する関数です。

src/todos/model.ts
export const updateTodo = async (
  KV: KVNamespace,
  id: string,
  param: UpdateTodo
): Promise<void> => {
  const todo = await getTodo(KV, id);
  if (!todo) {
    return;
  }

  const updateTodo = {
    ...todo,
    ...param,
  };

  await KV.put(`${PREFIX}${id}`, JSON.stringify(updateTodo));
};

getTodo 関数で Todo を取得してから引数のパラメータで値を更新し、KV.put で KV に保存します。

最後に Todo の削除をする関数です。この関数も単に KV の操作をラップしています。

src/todos/model.ts
export const deleteTodo = (KV: KVNamespace, id: string) => {
  return KV.delete(`${PREFIX}${id}`);
};

モデルの作成が完了したら src/todos/api.ts ファイルでモデルを使用するように修正しましょう。

src/todos/api.ts
import { Hono } from "hono";
import { validation } from "@honojs/validator";
import {
  createTodo,
  CreateTodo,
  deleteTodo,
  getTodo,
  getTodos,
  updateTodo,
  UpdateTodo,
} from "./model";
import { Bindings } from "../bindings";

const todos = new Hono<Bindings>();

todos.get("/", async (c) => {
  const todos = await getTodos(c.env.HONO_TODO);
  return c.json(todos);
});

todos.post(
  "/",
  validation((v, message) => ({
    body: {
      title: [v.trim, [v.required, message("Title is required")]],
    },
  })),
  async (c) => {
    const param = await c.req.json<CreateTodo>();
    const newTodo = await createTodo(c.env.HONO_TODO, param);

    return c.json(newTodo, 201);
  }
);

todos.put("/:id", async (c) => {
  const id = c.req.param("id");
  const todo = await getTodo(c.env.HONO_TODO, id);
  if (!todo) {
    return c.json({ message: "not found" }, 404);
  }
  const param = await c.req.json<UpdateTodo>();
  await updateTodo(c.env.HONO_TODO, id, param);
  return new Response(null, { status: 204 });
});

todos.delete("/:id", async (c) => {
  const id = c.req.param("id");
  const todo = await getTodo(c.env.HONO_TODO, id);
  if (!todo) {
    return c.json({ message: "not found" }, 404);
  }

  await deleteTodo(c.env.HONO_TODO, id);

  return new Response(null, { status: 204 });
});

export { todos };

ここまでの修正が完了したらそれぞれの API をテストしてみてください。

テストコードの実装

最後に作成したコードに対してテストコードを実装しましょう。Hono では app.request() または app.fetch() メソッドを使用することで API に対するリクエストを送信し、テストを記述できます。

test('GET /hello is ok', async () => {
  const res = await app.request('http://localhost/hello')
  expect(res.status).toBe(200)
})

テスト環境の構築

まずはテストに必要なパッケージをインストールします。

npm install -D jest jest-environment-miniflare @types/jest esbuild-jest

Miniflare は Cloudflare Workers テストするためのシミュレータです。Miniflare は Jest の実行環境として指定できます(Jest 27 以降が必要です)。

jest.config.js に Jest の設定を記述します。

jest.config.js
module.exports = {
  testEnvironment: "miniflare",
  testMatch: ["**/*.test.ts"],
  transform: {
    "^.+\\.tsx?$": "esbuild-jest",
  },
  moduleNameMapper: {
    "jsonpath-plus":
      "<rootDir>/node_modules/jsonpath-plus/dist/index-node-cjs.cjs",
  },
};

tsconfig.jsontypes"@types/jest" を追加します。

tsconfig.json
{
  "compilerOptions": {
    "types": [
      "@cloudflare/workers-types", 
      "@types/jest"
    ]
  }
}

テストファイルの中ではバインディングされた envgetMiniflareBindings() という関数から取得できます。このグローバル関数が存在することを TypeScript に伝えるため src/bindings.d.ts ファイルで宣言します。

src/bindings.d.ts
declare global {
  function getMiniflareBindings(): Bindings
}

package.json にテスト用のコマンドを追加します。

package.json
{
  "scripts": {
    "test": "jest --verbose --watch"
  },
}

テストを記述する

テスト環境の準備が完了したので、実際にテストを記述していきましょう。src/todos/api.spec.ts ファイルを作成します。

src/todos/api.spec.ts
import { Hono } from "hono";
import { todos, app } from "./api";
import { PREFIX, Todo } from "./model";

const env = getMiniflareBindings();
const seed = async () => {
  const todoList: Todo[] = [
    { id: "1", title: "Learning Hono", completed: false },
    { id: "2", title: "Watch the movie", completed: true },
    { id: "3", title: "Buy milk", completed: false },
  ];
  for (const todo of todoList) {
    await env.HONO_TODO.put(`${PREFIX}${todo.id}`, JSON.stringify(todo));
  }
};

describe("Todos API", () => {
  beforeEach(() => {
    seed();
  });
});

getMiniflareBindings 関数から env を取得しています。

seed 関数では Todo の初期データを投入しています。seed 関数は beforeEach で各テストごとに呼び出しています。Miniflare 環境では各テストで KV、キャッシュなどに分離ストレージを使用します。これは基本的に、test または describe ブロックで行った変更は、その後自動的に取り消されることを意味します。

はじめに Todo 一覧を取得するテストです。

src/todos/api.spec.ts
test("Todo 一覧を取得する", async () => {
  const res = await app.fetch(new Request("http://localhost"), env);
  expect(res.status).toBe(200);

  const body = await res.json();
  expect(body).toEqual([
    { id: "1", title: "Learning Hono", completed: false },
    { id: "2", title: "Watch the movie", completed: true },
    { id: "3", title: "Buy milk", completed: false },
  ]);
});

app.fetch() メソッドでリクエストを送信し、レスポンスが期待した値かどうか検証しています。ハンドラ関数がバインディングされた KV を使用するために app.fetch の第 2 引数に getMiniflareBindings 関数から取得した env を含める必要がある点に注意してください。

テストを実行して Pass するか確認してみましょう。

npm runt test

 PASS  src/todos/api.test.ts
  Todos API
    ✓ Todo 一覧を取得する (43 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.488 s

この調子で残りのテストも実装します。

src/todos/api.spec.ts
test("Todo を作成する", async () => {
  const newTodo: CreateTodo = { title: "new-todo" };
  const res1 = await app.fetch(
    new Request("http://localhost", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(newTodo),
    }),
    env
  );
  expect(res1.status).toBe(201);
  const body = await res1.json();
  expect(body).toEqual({
    id: expect.any(String),
    title: "new-todo",
    completed: false,
  });

  const res2 = await app.fetch(new Request("http://localhost"), env);
  const list = await res2.json();
  expect(list).toEqual([
    { id: "1", title: "Learning Hono", completed: false },
    { id: "2", title: "Watch the movie", completed: true },
    { id: "3", title: "Buy milk", completed: false },
    { id: expect.any(String), title: "new-todo", completed: false },
  ]);
});

test("Todo を作成する:title は必須", async () => {
  const newTodo: CreateTodo = { title: " " };
  const res = await app.fetch(
    new Request("http://localhost", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(newTodo),
    }),
    env
  );
  expect(res.status).toBe(400);
});

test("Todo を更新する", async () => {
  const updateTodo: UpdateTodo = { completed: true };
  const res1 = await app.fetch(
    new Request("http://localhost/3", {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(updateTodo),
    }),
    env
  );
  expect(res1.status).toBe(204);

  const res2 = await app.fetch(new Request("http://localhost"), env);
  const list = await res2.json();
  expect(list).toEqual([
    { id: "1", title: "Learning Hono", completed: false },
    { id: "2", title: "Watch the movie", completed: true },
    { id: "3", title: "Buy milk", completed: true },
  ]);
});

test("Todo を削除する", async () => {
  const res1 = await app.fetch(
    new Request("http://localhost/2", {
      method: "DELETE",
    }),
    env
  );
  expect(res1.status).toBe(204);

  const res2 = await app.fetch(new Request("http://localhost"), env);
  const list = await res2.json();
  expect(list).toEqual([
    { id: "1", title: "Learning Hono", completed: false },
    { id: "3", title: "Buy milk", completed: false },
  ]);
});

Todo の作成・更新・削除のテストを実装しました。それぞれレスポンスが正しいことを確認してから一覧取得 API をコールして実際にデータが変更されているかどうかを確認しています。

アプリケーションのデプロイ

それでは、作成したアプリケーションを実際に Cloudflare Workers へデプロイしてみましょう。以下のコマンドで簡単にデプロイできます。

npm run deploy

Retrieving cached values for userId from node_modules/.cache/wrangler
Your worker has access to the following bindings:
- KV Namespaces:
  - HONO_TODO: 1a136855b23f4b14aab395ab6247282a
Total Upload: 275.07 KiB / gzip: 54.50 KiB
Uploaded hono-todo (1.16 sec)
Published hono-todo (0.22 sec)
  https://hono-todo.azukiazusa.workers.dev

デプロイが完了すると URL が表示されるので確認してみましょう。

おわりに

Hono の API は Express にインスパイアされていて、かつシンプルなので学習コストは少なく進めることができました。Web スタンダードの API のみに依存している点も触りやすいポイントでした。RemixFresh もそうですが、Fetch APIRequestResponse オブジェクトを使用するフレームワークが増えてきたような気がします。Fetch API を一度覚えてしまえばブラウザ・Cloudflare Workers・Deno・Bun のようにさまざまなプラットフォームで使用できるのは嬉しい限りですね。

本記事のコードは以下の exampels を参考にさせていただきました。

https://github.com/honojs/examples

参考

GitHubで編集を提案

Discussion

Daisuke YamamotoDaisuke Yamamoto

Awesome!
「Module Workers モード」のところ、とても参考になりました。
ありがとうございます。