Honoに入門する🔥

Honoの存在は以前から知っていましたが、実際に試したことはありませんでした。
そこで今回は、Honoを使って簡単なタスク管理アプリケーションを作成してみます。
タスクの作成・取得・編集・削除を実現するアプリケーションを作ります。

## 前提
あくまで入門なので、できるだけ設定が少なく済む構成にしました。
- **API**
  - Hono
  - SQLite
  - Drizzle ORM
- **フロントエンド**
  - React
  - Vite
  - SWR

## Githubリポジトリ
https://github.com/htoyoda18/task-app

## API側の作成
プロジェクト用のディレクトリを作成します。
```zsh
mkdir task-app
cd task-app
```

API側のプロジェクトをセットアップしていきます。
```
pnpm create hono@latest
```
インストール時の対話式プロンプトでは以下のように設定しました。
Target directory: `api`
Which template do you want to use?: `nodejs`
Do you want to install project dependencies?: `yes`
Which package manager do you want to use?: `pnpm`

プロジェクトの土台が出来ました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953258/a894a82e-6710-ac55-1c6b-7c5bbf7d6b08.png)

DBの設定周りの処理を書いていきます。
必要なファイルの作成と、ライブラリのインストールを行います。
```
cd ./api/src/
touch db.ts
touch tasks.db
pnpm i better-sqlite3 drizzle-orm
pnpm i -D @types/better-sqlite3
```
テーブル作成用のSQL、db インスタンスの定義、テーブルスキーマの定義を書きます。
```ts
// ./db.ts
import { drizzle } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3";
import { sqliteTable, int, text } from "drizzle-orm/sqlite-core";

const sqlite = new Database("./tasks.db");
export const db = drizzle(sqlite);

db.run(
  `
  CREATE TABLE IF NOT EXISTS tasks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL
  );
  `
);

export const tasks = sqliteTable(
  "tasks",
  {
    id: int().primaryKey({ autoIncrement: true }),
    title: text("title").notNull(),
  },
  () => []
);
```
アプリケーションのミドルウェアの設定を書いていきます。
```
touch middleware.ts
```
Honoインスタンスの定義、corsの設定、エラーの補足を書いていきます。
```ts
// ./middleware.ts
import { Hono } from "hono";
import { serve } from "@hono/node-server";

export const app = new Hono();

app.use("*", async (c, next) => {
  c.res.headers.append("Access-Control-Allow-Origin", "*");
  c.res.headers.append(
    "Access-Control-Allow-Methods",
    "GET, POST, PUT, DELETE, OPTIONS"
  );
  c.res.headers.append(
    "Access-Control-Allow-Headers",
    "Content-Type, Authorization"
  );

  if (c.req.method === "OPTIONS") {
    return c.text("", 204);
  }

  await next();
});

app.onError((err, c) => {
  console.error(err);
  return c.json({ error: err.message || "Internal Server Error" }, 500);
});

serve(app);
```
次にAPIのエンドポイントを定義していきます。
エンドポイントは以下のようにします。
- `/tasks` [GET] タスクの取得
- `/tasks` [POST] タスクの登録
- `/tasks/:id `[PUT] タスクの編集
- `/tasks/:id` [DELETE] タスクの削除

エンドポイント定義のファイルを作成します。
すでに、index.tsファイルは生成されてるので上書きします。
```
echo -n > index.ts
```
先ほど作成した、Honoインスタンスとdbインスタンスを利用してエンドポイントを実装していきます。
タスクの取得
``` ts
// index.ts
import { eq } from "drizzle-orm";
import { db, tasks } from "./db.js";
import { app } from "./middleware.js";

app.get("/tasks", async (c) => {
  const allTasks = db.select().from(tasks).all();
  return c.json(allTasks);
});
```
タスクの登録
```ts
// index.ts
app.post("/tasks", async (c) => {
  const { title } = await c.req.json<{ title?: string }>();
  if (!title || title.trim() === "") {
    return c.json({ error: "Invalid input" }, 400);
  }

  const result = db.insert(tasks).values({ title }).run();
  const newTask = db
    .select()
    .from(tasks)
    .where(eq(tasks.id, result.lastInsertRowid as number))
    .get();
  return c.json(newTask, 201);
});
```
タスクの編集
```ts
// index.ts
app.put("/tasks/:id", async (c) => {
  const { id } = c.req.param();

  const taskId = Number(id);
  if (Number.isNaN(taskId)) {
    return c.json({ error: "Invalid ID" }, 400);
  }

  const { title } = await c.req.json<{ title?: string }>();
  if (!title || title.trim() === "") {
    return c.json({ error: "Invalid input" }, 400);
  }

  const result = db
    .update(tasks)
    .set({ title })
    .where(eq(tasks.id, taskId))
    .run();
  if (result.changes === 0) {
    return c.json({ error: "Task not found" }, 404);
  }

  const updatedTask = db.select().from(tasks).where(eq(tasks.id, taskId)).get();
  return c.json(updatedTask);
});
```
タスクの削除
```ts
// index.ts
app.delete("/tasks/:id", async (c) => {
  const { id } = c.req.param();
  const taskId = Number(id);
  if (Number.isNaN(taskId)) {
    return c.json({ error: "Invalid ID" }, 400);
  }

  const result = db.delete(tasks).where(eq(tasks.id, taskId)).run();
  if (result.changes === 0) {
    return c.json({ error: "Task not found" }, 404);
  }

  return c.json(204);
});
```
以上で、API側は完了です。

## フロント側の作成
Vite + Reactでプロジェクトを作成します。
```
cd ../../
pnpm create vite@latest
```
viteの対話式プロンプトでは以下のように設定しました。
Project name: `front`
Select a framework: `React`
Select a variant: `TypeScript`

不要なファイルを削除します。
```
cd front
rm -rf ./src/ ./public/
```
モジュールを取得し、起点となる`main.tsx`ファイルを作成します。
```
pnpm i
mkdir src/
cd src/
touch main.tsx
```
`createRoot`関数を利用して、ルート要素にReactコンポーネントを描画します。
```ts
// main.tsx
import { createRoot } from "react-dom/client";
import App from "./App.tsx";

createRoot(document.getElementById("root")!).render(<App />);
```
次に、必要なモジュールを追加し、App.tsxファイルを作成します。
```
pnpm i axios swr
touch App.tsx
```
App.tsx では、以下を定義します。
- エンドポイントの定数 (API_URL)
- タスク型 (Task)
- useSWR 用の fetcher
- 新規作成・編集用の状態 (newTask, editingTask)
- useSWR で取得するタスクデータ (tasks)
- 作成・編集・削除の各イベントハンドラ (addTask, editTask, deleteTask)
- エラーとロード中表示の UI
- タスク一覧と新規タスク追加の UI
```ts
import { useState } from "react";
import axios from "axios";
import useSWR, { mutate } from "swr";

const API_URL = "http://localhost:3000";

type Task = {
  id: number;
  title: string;
};

const fetcher = (url: string) => axios.get(url).then((res) => res.data);

const App = () => {
  const [newTask, setNewTask] = useState("");
  const [editingTask, setEditingTask] = useState<{
    id: number;
    title: string;
  } | null>(null);
  const { data: tasks, error } = useSWR<Task[]>(`${API_URL}/tasks`, fetcher);

  const addTask = async (title: string) => {
    if (!title.trim()) return;
    await axios.post(`${API_URL}/tasks`, { title });
    mutate(`${API_URL}/tasks`);
    setNewTask("");
  };

  const editTask = async (id: number, title: string) => {
    if (!title.trim()) return;
    await axios.put(`${API_URL}/tasks/${id}`, { title });
    mutate(`${API_URL}/tasks`);
    setEditingTask(null);
  };

  const deleteTask = async (id: number) => {
    await axios.delete(`${API_URL}/tasks/${id}`);
    mutate(`${API_URL}/tasks`);
  };

  if (error)
    return (
      <div style={{ textAlign: "center", padding: "20px", color: "#d32f2f" }}>
        Error loading tasks
      </div>
    );
  if (!tasks)
    return (
      <div style={{ textAlign: "center", padding: "20px" }}>Loading...</div>
    );

  return (
    <div
      style={{
        maxWidth: "500px",
        margin: "40px auto",
        padding: "20px",
        boxShadow: "0 4px 10px rgba(0,0,0,0.1)",
        borderRadius: "10px",
        backgroundColor: "#f9fafb",
        fontFamily: "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif",
      }}
    >
      <h1
        style={{
          fontSize: "28px",
          fontWeight: "600",
          marginBottom: "30px",
          textAlign: "center",
          color: "#333",
        }}
      >
        Task Manager
      </h1>
      <ul
        style={{
          listStyle: "none",
          padding: 0,
          margin: "0 0 20px 0",
        }}
      >
        {tasks.map((task) => (
          <li
            key={task.id}
            style={{
              display: "flex",
              alignItems: "center",
              borderBottom: "1px solid #e0e0e0",
              padding: "12px 0",
              fontSize: "16px",
            }}
          >
            {editingTask?.id === task.id ? (
              <>
                <input
                  type="text"
                  value={editingTask.title}
                  onChange={(e) =>
                    setEditingTask({ ...editingTask, title: e.target.value })
                  }
                  style={{
                    flex: 1,
                    padding: "8px",
                    fontSize: "16px",
                    border: "1px solid #ccc",
                    borderRadius: "4px",
                    marginRight: "10px",
                  }}
                />
                <button
                  onClick={() => editTask(task.id, editingTask.title)}
                  style={{
                    padding: "8px 14px",
                    backgroundColor: "#4caf50",
                    color: "#fff",
                    border: "none",
                    borderRadius: "4px",
                    cursor: "pointer",
                    marginRight: "8px",
                    transition: "background-color 0.2s",
                  }}
                  onMouseEnter={(e) =>
                    (e.currentTarget.style.backgroundColor = "#43a047")
                  }
                  onMouseLeave={(e) =>
                    (e.currentTarget.style.backgroundColor = "#4caf50")
                  }
                >
                  Save
                </button>
                <button
                  onClick={() => setEditingTask(null)}
                  style={{
                    padding: "8px 14px",
                    backgroundColor: "#757575",
                    color: "#fff",
                    border: "none",
                    borderRadius: "4px",
                    cursor: "pointer",
                    transition: "background-color 0.2s",
                  }}
                  onMouseEnter={(e) =>
                    (e.currentTarget.style.backgroundColor = "#616161")
                  }
                  onMouseLeave={(e) =>
                    (e.currentTarget.style.backgroundColor = "#757575")
                  }
                >
                  Cancel
                </button>
              </>
            ) : (
              <>
                <span style={{ flex: 1, marginRight: "10px", color: "#333" }}>
                  {task.title}
                </span>
                <button
                  onClick={() =>
                    setEditingTask({ id: task.id, title: task.title })
                  }
                  style={{
                    padding: "8px 14px",
                    backgroundColor: "#2196f3",
                    color: "#fff",
                    border: "none",
                    borderRadius: "4px",
                    cursor: "pointer",
                    marginRight: "8px",
                    transition: "background-color 0.2s",
                  }}
                  onMouseEnter={(e) =>
                    (e.currentTarget.style.backgroundColor = "#1e88e5")
                  }
                  onMouseLeave={(e) =>
                    (e.currentTarget.style.backgroundColor = "#2196f3")
                  }
                >
                  Edit
                </button>
                <button
                  onClick={() => deleteTask(task.id)}
                  style={{
                    padding: "8px 14px",
                    backgroundColor: "#f44336",
                    color: "#fff",
                    border: "none",
                    borderRadius: "4px",
                    cursor: "pointer",
                    transition: "background-color 0.2s",
                  }}
                  onMouseEnter={(e) =>
                    (e.currentTarget.style.backgroundColor = "#e53935")
                  }
                  onMouseLeave={(e) =>
                    (e.currentTarget.style.backgroundColor = "#f44336")
                  }
                >
                  Delete
                </button>
              </>
            )}
          </li>
        ))}
      </ul>
      <div
        style={{
          display: "flex",
          gap: "10px",
        }}
      >
        <input
          type="text"
          value={newTask}
          onChange={(e) => setNewTask(e.target.value)}
          placeholder="Add a new task..."
          style={{
            flex: 1,
            padding: "10px",
            fontSize: "16px",
            border: "1px solid #ccc",
            borderRadius: "4px",
            boxSizing: "border-box",
          }}
        />
        <button
          onClick={() => addTask(newTask)}
          style={{
            padding: "10px 16px",
            backgroundColor: "#4caf50",
            color: "#fff",
            border: "none",
            borderRadius: "4px",
            cursor: "pointer",
            transition: "background-color 0.2s",
          }}
          onMouseEnter={(e) =>
            (e.currentTarget.style.backgroundColor = "#43a047")
          }
          onMouseLeave={(e) =>
            (e.currentTarget.style.backgroundColor = "#4caf50")
          }
        >
          Add
        </button>
      </div>
    </div>
  );
};

export default App;

```

ここまででアプリケーションのフロントエンドが完成しました。
あとはアプリケーションを以下のコマンドで実行できます。
```
cd ../../ && cd api/ && pnpm run dev && cd ../
cd front/ && pnpm run dev
```
これでタスク管理アプリケーションの完成です！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953258/f7fd7b91-fd4c-2719-882d-e6afa9b1c141.png)

## 最後に
HonoでAPIを作成してみました。
Honoはルーティングやミドルウェアの設定がわかりやすく、必要最低限の構成でAPIを作りやすいと感じました。
また、Node.jsだけでなくCloudflare Workersなどさまざまなランタイム環境でも動作するため、ユースケースに合わせて柔軟に使えそうです。
今回は小規模なアプリケーションでしたが、他のプロジェクトでも積極的に活用してみようと思います。