Chapter 03

【バックエンド】Honoを使ってAPIを作成する

Dai
Dai
2024.04.24に更新

この章では今回のTodoアプリで使用するAPIを作成していきます。

Honoの基礎

まずはHonoの基礎について解説します。現状localhost:8787にアクセスするとHello Hono!と表示されていますが、これはindex.tsに下記の記述があるからです。

src/index.ts
app.get('/', (c) => {
  return c.text('Hello Hono!')
})

上記の記述ではルートディレクトリにgetメソッドでリクエストを送るとHello Hono!というテキストがreturnされているので、画面上に表示されるというわけです。シンプルでとてもわかりやすいですよね。

今回のプロジェクトでは下記の4つのAPIを作成していきます。

  • Todo一覧取得API
  • Todo登録API
  • Todo更新API
  • Todo削除API

なお、今回はDBを使用せずに仮置きで変数を作成して、そこからデータを取得したり登録するようにしたいと思います。

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

const app = new Hono();

+ type Todo = {
+   id: number;
+   title: string;
+   delete_flg: boolean;
+ };

+ let allTodos: Todo[] = [
+   { id: 1, title: "Reactを勉強する", delete_flg: false },
+   { id: 2, title: "Vue.jsを勉強する", delete_flg: false },
+   { id: 3, title: "Next.jsを勉強する", delete_flg: false },
+ ];

app.get("/", (c) => {
  return c.text("Hello Hono!");
});

export default app;

上記のようなTodo型の配列を用意して、これをDBに見立ててAPIを作成していきます。まずはTodo一覧取得APIから作成していきます。

Todo一覧取得API

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

const app = new Hono();

type Todo = {
  id: number;
  title: string;
  delete_flg: boolean;
};

let todos: Todo[] = [
  { id: 1, title: "Reactを勉強する", delete_flg: false },
  { id: 2, title: "Vue.jsを勉強する", delete_flg: false },
  { id: 3, title: "Next.jsを勉強する", delete_flg: false },
];

app.get("/", (c) => {
  return c.text("Hello Hono!");
});

+ /*
+  * Todo一覧取得API
+  */
+ app.get("/api/todos", (c) => c.json(todos.filter((todo) => !todo.delete_flg)));

export default app;

このTodo一覧取得APIではエンドポイント"/todos"にGETメソッドでリクエストを送るとtodosの中のdelete_flgがfalseのtodoだけがJSON形式でreturnされるという内容になっています。続いてTodo登録APIを作成していきましょう。

Todo登録API

src/index.ts
...

/*
 * Todo一覧取得API
 */
app.get("/", (c) => c.json(todos.filter((todo) => !todo.delete_flg)));

+ /*
+ * Todo登録API
+ */
+ app.post("/api/todos", async (c) => {
+   const { title } = await c.req.json<{ title: string }>();
+   if (!title) {
+    return c.json({ message: "タイトルは必須です" }, 400);
+   }
+   const newId = todos[todos.length - 1].id + 1;
+   const newTodo: Todo = { id: newId, title, delete_flg: false };
+   todos = [...todos, newTodo];
+   return c.json(newTodo);
+ });

export default app;

Todo登録APIはPOSTメソッドなので非同期処理としてasync awaitをつけています。リクエストから送られてきたデータと現状のtodosのデータから新たなtodoを作成して、スプレッド構文を利用してtodoにデータを追加しています。

続いてTodo更新APIを作成していきましょう。

Todo更新API

src/index.ts

...

/*
 * Todo登録API
 */
app.post("/api/todos", async (c) => {
  const { title } = await c.req.json<{ title: string }>();
  if (!title) {
    return c.json({ message: "タイトルは必須です" }, 400);
  }
  const newId = todos[todos.length - 1].id + 1;
  const newTodo: Todo = { id: newId, title, delete_flg: false };
  todos = [...todos, newTodo];
  return c.json(newTodo);
});

+ /*
+ * Todo更新API
+ */
+ app.put("/api/todos/:id", async (c) => {
+   const id = c.req.param("id");
+   const index = todos.findIndex((todo) => todo.id === Number(id));
+
+   if (index === -1) {
+     return c.json({ message: "Todoは存在しません" }, 404);
+   }
+
+   const { title } = await c.req.json<{ title: string }>();
+   if (!title) {
+     return c.json({ message: "タイトルは必須です" }, 400);
+   }
+   todos[index] = { ...todos[index], title };
+   return c.json(todos[index]);
+ });

export default app;

Todo更新APIではパスパラメータからid,リクエストパラメータからtitleを取得してデータの更新を行なっています。最後にTodo削除APIを作成しましょう。

Todo削除API

Todo削除APIは物理削除ではなく、delete_flgをtrueにする論理削除で実装していきます。

src/index.ts

...

/*
 * Todo更新API
 */
app.put("/api/todos/:id", async (c) => {
  const id = c.req.param("id");
  const index = todos.findIndex((todo) => todo.id === Number(id));

  if (index === -1) {
    return c.json({ message: "Todoは存在しません" }, 404);
  }

  const { title } = await c.req.json<{ title: string }>();
  if (!title) {
    return c.json({ message: "タイトルは必須です" }, 400);
  }
  todos[index] = { ...todos[index], title };
  return c.json(todos[index]);
});

+ /*
+  * Todo削除API
+  */
+ app.put("/api/todos/:id/delete", async (c) => {
+   const id = c.req.param("id");
+   const index = todos.findIndex((todo) => todo.id === Number(id));
+
+   if (index === -1) {
+     return c.json({ message: "Todoは存在しません" }, 404);
+   }
+
+   todos[index] = { ...todos[index], delete_flg: true };
+   return c.json(todos[index]);
+ });

export default app;

リファクタリングしておく

index.tsに全てのAPIを記述してしまうと可読性が下がってしましますので、最後にリファクタリングしておきましょう。srcディレクトリの中にtodosディレクトリを作成して、そこにTodoのAPIを移しましょう。

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

const app = new Hono();

+ app.route("/api/todos", todos);

export default app;

app.route("/api/todos", todos);という記述を追加することにより/api/todosにHTTPリクエストを送ると、todos関数が呼び出され、その関数がリクエストを処理してレスポンスを返します。この設定により、APIの特定のパスに対するロジックを組織的に管理できるようになります。

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

const app = new Hono();

type Todo = {
  id: number;
  title: string;
  delete_flg: boolean;
};

let todos: Todo[] = [
  { id: 1, title: "Reactを勉強する", delete_flg: false },
  { id: 2, title: "Vue.jsを勉強する", delete_flg: false },
  { id: 3, title: "Next.jsを勉強する", delete_flg: false },
];

/*
 * Todo一覧取得API
 */
app.get("/", (c) => c.json(todos.filter((todo) => !todo.delete_flg)));

/*
 * Todo登録API
 */
app.post("/", async (c) => {
  const { title } = await c.req.json<{ title: string }>();
  if (!title) {
    return c.json({ message: "タイトルは必須です" }, 400);
  }
  const newId = todos[todos.length - 1].id + 1;
  const newTodo: Todo = { id: newId, title, delete_flg: false };
  todos = [...todos, newTodo];
  return c.json(newTodo);
});

/*
 * Todo更新API
 */
app.put("/:id", async (c) => {
  const id = c.req.param("id");
  const index = todos.findIndex((todo) => todo.id === Number(id));

  if (index === -1) {
    return c.json({ message: "Todoは存在しません" }, 404);
  }

  const { title } = await c.req.json<{ title: string }>();
  if (!title) {
    return c.json({ message: "タイトルは必須です" }, 400);
  }
  todos[index] = { ...todos[index], title };
  return c.json(todos[index]);
});

/*
 * Todo削除API
 */
app.put("/:id/delete", async (c) => {
  const id = c.req.param("id");
  const index = todos.findIndex((todo) => todo.id === Number(id));

  if (index === -1) {
    return c.json({ message: "Todoは存在しません" }, 404);
  }

  todos[index] = { ...todos[index], delete_flg: true };
  return c.json(todos[index]);
});

export default app;

src/index.tsにapp.route("/api/todos", todos);という記述を追加したことでエンドポイントURLを変更していますので、併せて修正しておきましょう。

お疲れ様でした!これでTodoアプリのAPIは全て作成することができました。次の章ではNext.jsを使ってアプリのフロント部分を作成していきます。