はじめに
以前、Honoを使って、シンプルなTodo APIを作りました。
最初は、Todoの一覧取得・追加・更新ができればいいかな〜くらいの気持ちで実装していました。
実際に動くAPIは作れたのですが、あとから見直してみると、HTTPメソッドの使い方やステータスコードの返し方など、API設計として改善できそうな点がいくつかありました。
そこで今回は、最初に作ったTodo APIを少し見直して、RESTfulなAPI設計に近づけるためにリファクタしてみました。
この記事では、Honoで作ったTodo APIを題材に、以下のような点を整理します。
- 作成時は
201 Createdを返す - Todoの完了状態だけを更新する処理は
PATCHを使う - エラー時もJSONで返す
-
DELETE /todos/:idを追加して、Todoリソースの操作を揃える
厳密なRESTのすべてを扱うわけではありませんが、実装を見直しながら、RESTfulなAPI設計の基本を少し理解することを目的にしました。
最初に作っていたTodo API
最初に作っていたTodo APIでは、以下の3つのエンドポイントと役割を用意していました。
GET /todos Todo一覧を取得する
POST /todos Todoを作成する
PUT /todos/:id Todoの完了状態を更新する
この時点でも、Todoの一覧取得・追加・完了状態の更新はできていました。
ただ、あとから見直してみると、いくつか改善できそうな点がありました。
-
POST /todosで作成成功時もデフォルトの200 OKを返していた - 完了状態だけを変更しているのに
PUTを使っていた - エラー時にJSON形式で返していなかった
- Todoを削除するためのエンドポイントがなかった
動くAPIにはなっていましたが、RESTfulなAPI設計として考えると、もう少し意味が伝わりやすい形にできそうだと感じました。
RESTっぽくするために見直したこと
最初の実装でもTodo APIとしては動いていましたが、RESTfulなAPI設計に近づけるために、いくつか見直しました。
今回意識したのは、主に以下の4つです。
- 作成時は
201 Createdを返す - 完了状態だけを更新する処理は
PUTではなくPATCHを使う - エラー時もJSON形式で返す
-
DELETE /todos/:idを追加する
作成時は201 Createdを返す
Honoでは、ステータスコードを指定しない場合、デフォルトで200 OKが返ります。
ただ、POST /todosは新しいTodoを作成する処理です。
新しいリソースを作成した場合は、200 OKよりも201 Createdを返す方が意味として自然です。
MDN - 201 Created
そこで、c.json()の第2引数に201を指定しました。
return c.json({ todo }, 201);
これで、Todoの作成に成功したときに201 Createdが返るようになりました。
PUTではなくPATCHで完了状態を更新する
最初は、Todoの完了状態を更新する処理をPUT /todos/:idとして実装していました。
app.put("/todos/:id", async (c) => {
const { id } = c.req.param();
const { completed } = await c.req.json();
// 更新処理
});
ただ、今回更新しているのはTodo全体ではなく、completedだけです。
PUTはリソース全体を置き換えるときに使われることが多いため、今回のように一部の項目だけを変更する場合は、PATCHの方が意味として近いと感じました。
MDN - PUT request method
そこで、エンドポイントをPATCH /todos/:idに変更しました。
app.patch("/todos/:id", async (c) => {
const { id } = c.req.param();
const { completed } = await c.req.json();
// 更新処理
});
React側の呼び出しも、PUTからPATCHに変更しました。
const response = await fetch(`http://localhost:3000/todos/${id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
completed: !todos.find((todo) => todo.id === id)?.completed,
}),
});
これで、「Todoの一部を更新している」という意図がHTTPメソッドからも伝わりやすくなりました。
エラー時もJSONで返す
最初は、対象のTodoが見つからなかった場合にc.notFound()を返していました。
return c.notFound();
これでも404 Not Foundは返せますが、APIとしてはエラー時もJSON形式で返した方が、フロントエンド側で扱いやすいです。
そこで、エラー時のレスポンスをJSON形式に変更しました。
return c.json({ error: "Todo not found" }, 404);
これにより、ステータスコードだけでなく、エラーの内容もレスポンスとして返せるようになりました。
{
"error": "Todo not found"
}
今回はシンプルにerrorだけを返していますが、今後はcodeやmessageを分けるなど、エラー形式を統一するとさらに扱いやすくなりそうです。
DELETE /todos/:idを追加する
最初の実装では、Todoを削除するAPIはありませんでした。
ただ、Todoというリソースを扱うAPIとして考えると、取得・作成・更新だけでなく、削除もできると操作が揃います。
そこで、DELETE /todos/:idを追加しました。
app.delete("/todos/:id", async (c) => {
const { id } = c.req.param();
try {
await prisma.todo.delete({
where: { id: Number(id) },
});
return c.body(null, 204);
} catch {
return c.json({ error: "Todo not found" }, 404);
}
});
削除に成功した場合は、返すデータがないため204 No Contentを返すようにしました。
return c.body(null, 204);
対象のTodoが見つからなかった場合は、他のエラーと同じようにJSONで404 Not Foundを返します。
return c.json({ error: "Todo not found" }, 404);
これで、Todoリソースに対する基本的な操作が揃いました。
GET /todos 一覧取得
POST /todos 作成
PATCH /todos/:id 部分更新
DELETE /todos/:id 削除
設計のまとめ
今回のリファクタでは、Todo APIをRESTfulな設計に少し近づけるために、URL・HTTPメソッド・ステータスコード・エラーレスポンスを見直しました。
最終的なエンドポイントは以下のようになりました。
GET /todos Todo一覧を取得する
POST /todos Todoを作成する
PATCH /todos/:id Todoの完了状態を更新する
DELETE /todos/:id Todoを削除する
URLでは、createTodoやdeleteTodoのような動詞ではなく、Todoというリソースを表す/todosを使いました。
操作の内容はURLではなく、HTTPメソッドで表すようにしました。
GET 取得
POST 作成
PATCH 一部更新
DELETE 削除
また、レスポンスのステータスコードも意味を意識して返すようにしました。
200 OK 取得・更新に成功したとき
201 Created 作成に成功したとき
204 No Content 削除に成功したとき
404 Not Found 対象のTodoが見つからないとき
エラー時には、ステータスコードだけでなくJSON形式でエラー内容を返すようにしました。
return c.json({ error: "Todo not found" }, 404);
今回の設計で意識したことをまとめると、以下の4つです。
- URLはリソースを表す名詞にする
- 操作はHTTPメソッドで表す
- ステータスコードを意味のある値で返す
- エラー時もJSON形式で返す
まだバリデーションやエラー形式の統一など改善できる点はありますが、最初の実装よりもAPIの意図が伝わりやすくなったと感じました。
実装して初めて分かったこと
今回、最初は「動くTodo APIを作る」ことを優先して実装しましたが、あとからRESTfulな設計を意識して見直してみると、HTTPメソッドやステータスコードにはそれぞれ意味があることに気づきました。
例えば、Todoを作成したときに200 OKではなく201 Createdを返すだけでも、「新しいリソースが作成された」という意味がレスポンスから伝わりやすくなります。
また、Todoの完了状態だけを変更する場合、PUTよりもPATCHの方が意図に近いことも、実際に実装を見直す中で理解できました。
設計が先にあると、実装が楽
今回のリファクタを通して、API設計を先に整理しておくと実装時に迷いにくいと感じました。
例えば、Todoを削除する処理を追加するときも、
DELETE /todos/:id
と決めておくと、URLやHTTPメソッドで迷わずに実装できます。
ステータスコードも先に決めておくと、レスポンスの返し方を考えやすくなります。
201 Created 作成に成功したとき
204 No Content 削除に成功したとき
404 Not Found 対象のTodoが見つからないとき
最初は「とりあえず動けばOK」と思っていましたが、設計を少し意識するだけで、コードの意味がわかりやすくなると感じました。
APIを作るときは、いきなり実装するだけでなく、
- どのURLにするか
- どのHTTPメソッドを使うか
- 成功時にどのステータスコードを返すか
- エラー時にどんな形式で返すか
を先に整理しておくと、実装時に迷うことが減りそうです。
学んだこと
今回Honoで作ったTodo APIを見直すことで、RESTfulなAPI設計の基本を少し理解できました。
特に、URLにはリソースを表す名詞を使い、操作はHTTPメソッドで表すという考え方が印象に残りました。
GET /todos 一覧取得
POST /todos 作成
PATCH /todos/:id 一部更新
DELETE /todos/:id 削除
また、ステータスコードを意識して返すことの大切さも学びました。
c.json()をそのまま使うとデフォルトで200 OKが返りますが、作成時には201 Created、削除時には204 No Contentのように、処理の意味に合ったステータスコードを返す方がAPIとしてわかりやすくなります。
エラー時のレスポンスも、ただ404を返すだけでなく、JSON形式で返すことでフロントエンド側から扱いやすくなると感じました。
return c.json({ error: "Todo not found" }, 404)
今回の実装を通して、API設計は単に「動くURLを作る」ことではなく、URL・HTTPメソッド・ステータスコード・レスポンス形式を使って、処理の意味を伝えることでもあると学びました。
まだ厳密なREST設計を完全に理解できたわけではありませんが、Todo APIのような小さな題材でも、設計を見直すことで学べることが多いと感じました。
APIの処理の流れも少し見えた

今回Todo APIを作ってみて、APIの中でどのような処理が行われているのかも少しイメージできるようになりました。
例えば、POST /todosでTodoを作成する場合、処理の流れは以下のようになります。
1. クライアントからAPIにリクエストを送る
2. Hono APIがリクエストを受け取る
3. リクエストボディからtitleを取り出す
4. DBにTodoを保存する
5. 作成したTodoをレスポンスとして返す
実際のコードでは、まずc.req.json()でリクエストボディを受け取ります。
const { title } = await c.req.json();
その後、受け取ったtitleをもとにTodoを作成します。
const todo = await prisma.todo.create({
data: {
title,
completed: false,
},
});
最後に、作成したTodoをJSONで返します。
return c.json({ todo }, 201);
この流れを実装したことで、APIは単にURLを用意するだけではなく、
- リクエストを受け取る
- 必要なデータを取り出す
- 処理を実行する
- レスポンスを返す
という流れで動いていることがわかりました。