😊

Honoとprismaとcloudflare D1を使ってTodoアプリを作る

に公開

honoとprisma,cloudflare D1を使ってデプロイされる記事があまり見当たらなかったので調査した。
今回はサンプルとしてTodoアプリを作っていく。

honoのインストール

https://hono.dev/

npm create hono@latest

この時cloudflare workersを選択。

prismaのインストール

prismaはNode.jsでデータベース操作ができるパッケージ。ORMの一種。
https://www.prisma.io/

ライブラリをインストールする。

npm i prisma
npm i @prisma/client @prisma/adapter-d1

D1がsqliteベースなのでオプションを付けてprismaを初期化する。

npx prisma init --datasource-provider sqlite

するとprismaフォルダとschema.prismaが追加される。

次にschema.prismaに以下を記載する。

generator client {
  provider = "prisma-client-js"
  previewFeatures = ["driverAdapters"] // これを追加する
  output   = "../src/generated/prisma" 
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

// 使いたいモデルを記載
model TodoModel {
  id    Int     @id @default(autoincrement())
  title String
}

cloudflare D1の設定

cloudflare D1はSQLiteベースのサーバーレスSQLデータベース。
https://www.cloudflare.com/ja-jp/developer-platform/products/d1/

今回はDB名をtodo-testとする。
D1のデータベースを用意

npx wrangler d1 create todo-test

hono上で使用するためにwrangler.jsoncのコメントアウトを外しdatabase_name・idを記載する。

  "compatibility_flags": [
    "nodejs_compat"
  ],
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "todo-test",
      "database_id": "j"
    }
  ],

テーブルを作成する。

npx wrangler d1 migrations create todo-test create_todos_table

prismaのスキーマをwranglerで作ったsqlに反映させる。

npx prisma migrate diff --from-empty --to-schema-datamodel ./prisma/schema.prisma --script --output migrations/0001_create_todos_table.sql

これでsqlが以下のように書き換えられている。

-- CreateTable
CREATE TABLE "TodoModel" (
    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    "title" TEXT NOT NULL
);

リモートへマイグレーションをする。

npx wrangler d1 migrations apply todo-test --remote

Prisma Client生成のためのコマンドを実行する。

npx prisma generate

参考資料

https://developers.cloudflare.com/d1/tutorials/d1-and-prisma-orm/
https://tech.fusic.co.jp/posts/hono-prisma-cloudflare-d1/
https://blog.henteko07.com/entry/2024/09/07/004629
https://zenn.dev/takuyanagai0213/articles/4cd72bc30c594d

Todoアプリを作っていく

今回はファイル分割してapiを記載したいので以下のように書いていく。

|_index.ts # アプリのルーティング、初期設定他
|_create.ts # DB流し込み用のサイト
|_todo.ts # todoアプリ

それぞれ以下のように書いていく。
index.ts

import { Hono } from "hono";
import { todoApp } from "./todo";
import { createDB } from "./create";
import { basicAuth } from "hono/basic-auth";

const app = new Hono();

app.use(
  "/api/*",
  basicAuth({
    username: process.env.BASIC_USERNAME ? process.env.BASIC_USERNAME : "a",
    password: process.env.BASIC_PASSWORD ? process.env.BASIC_PASSWORD : "a",
  })
);

app.get("/", (c) => c.text("Hono!"));
app.route("api/todo", todoApp);
app.route("api/create", createDB);

export default app;

app.useを使って認証を書ける際はrouteの前にかけないと認証がかからない。

create.ts

import { Hono } from "hono";
import { PrismaClient } from "./generated/prisma";
import { PrismaD1 } from "@prisma/adapter-d1";

// Import D1Database type from the appropriate package
import type { D1Database } from "@cloudflare/workers-types";

type Bindings = {
  DB: D1Database;
};

export const createDB = new Hono<{ Bindings: Bindings }>();

const todos = [
  { title: "起床後すぐにカーテンを開けて朝日を浴びる" },
  { title: "白湯または水を一杯飲む" },
  { title: "今日の目標をノートに1〜3個書く" },
  { title: "軽いストレッチまたはヨガ(5〜10分)" },
  { title: "朝食をしっかりとる(できればタンパク質多め)" },
  { title: "メール/メッセージの確認と返信" },
  { title: "優先順位の高い作業を1つ終わらせる" },
  { title: "25分集中 × 3セット(ポモドーロ法)" },
  { title: "資料やファイルの整理整頓(デスクトップなど)" },
  { title: "学習(新しい知識を1つ学ぶ:記事、動画、講座など)" },
  { title: "5分だけ目を閉じて深呼吸(マインドフルネス)" },
  { title: "今日あった良いことを3つ書き出す" },
  { title: "日記または手帳に感情や出来事を記録" },
  { title: "スマホの使用時間をチェック&制限設定" },
  { title: "自分を褒める/ねぎらう言葉を1つつぶやく" },
  { title: "部屋を5分だけでも片づける(タイマー活用)" },
  { title: "洗濯・掃除・ゴミ出しのどれかを実行" },
  { title: "1箇所だけでも不要な物を処分" },
  { title: "寝る90分前に画面から離れる(読書など)" },
  { title: "明日のToDoを3つ書き出しておく" },
];

createDB.get("/", async (c) => {
  const adapter = new PrismaD1(c.env.DB);
  const prisma = new PrismaClient({ adapter });

  for (const todo of todos) {
    await prisma.todoModel.create({
      data: {
        title: todo.title,
      },
    });
  }

  const returnTodo = await prisma.todoModel.findMany();
  return c.json({ todos: returnTodo });
});

アクセスするとDBに値が保存されるプログラム。

todo.ts

import { Hono } from "hono";
import { PrismaClient } from "./generated/prisma";
import { PrismaD1 } from "@prisma/adapter-d1";
import type { D1Database } from "@cloudflare/workers-types";

type Bindings = {
  DB: D1Database;
};

export const todoApp = new Hono<{ Bindings: Bindings }>();

todoApp.get("/", async (c) => {
  const adapter = new PrismaD1(c.env.DB);
  const prisma = new PrismaClient({ adapter });
  const returnTodo = await prisma.todoModel.findMany();
  return c.json({ todos: returnTodo });
});

todoApp.get("/:id", async (c) => {
  const adapter = new PrismaD1(c.env.DB);
  const prisma = new PrismaClient({ adapter });
  try {
    const id = Number(c.req.param("id"));
    if (isNaN(id)) {
      return c.json({ error: "Invalid ID format" }, 400);
    }

    const todos = await prisma.todoModel.findMany({
      where: { id: id },
      orderBy: { title: "desc" },
    });

    if (todos.length === 0) {
      return c.json({ error: "Todo not found" }, 404);
    }

    return c.json(todos);
  } catch (error) {
    console.error(error);
    return c.json({ error: "Failed to retrieve todos" }, 500);
  }
  //   return c.text("Get Todo: " + id);
});

todoApp.post("/create", async (c) => {
  const adapter = new PrismaD1(c.env.DB);
  const prisma = new PrismaClient({ adapter });
  try {
    const body = await c.req.json();
    const { title } = body;
    console.log("Received title:", title);

    if (!title || typeof title !== "string") {
      return c.json({ error: "Title is required and must be a string" }, 400);
    }

    const todo = await prisma.todoModel.create({
      data: { title: title },
    });

    return c.json(todo, 201);
  } catch (error) {
    console.error(error);
    return c.json({ error: "Failed to create todo" }, 500);
  }
});

todoApp.put("/update/:id", async (c) => {
  const adapter = new PrismaD1(c.env.DB);
  const prisma = new PrismaClient({ adapter });
  try {
    const id = Number(c.req.param("id"));
    if (isNaN(id)) {
      return c.json({ error: "Invalid ID format" }, 400);
    }

    const body = await c.req.json();
    const { title } = body;

    if (!title || typeof title !== "string") {
      return c.json({ error: "Title is required and must be a string" }, 400);
    }

    const todo = await prisma.todoModel.update({
      where: { id: id },
      data: { title: title },
    });

    return c.json(todo);
  } catch (error) {
    console.error(error);
    return c.json({ error: "Failed to update todo" }, 500);
  }
});

todoApp.delete("/delete/:id", async (c) => {
  const adapter = new PrismaD1(c.env.DB);
  const prisma = new PrismaClient({ adapter });
  try {
    const id = Number(c.req.param("id"));
    if (isNaN(id)) {
      return c.json({ error: "Invalid ID format" }, 400);
    }

    await prisma.todoModel.delete({
      where: { id: id },
    });

    return c.json({ message: "Todo deleted successfully" });
  } catch (error) {
    console.error(error);
    return c.json({ error: "Failed to delete todo" }, 500);
  }
});

ハマったポイントとしては、

  • import { PrismaClient } from "./generated/prisma";のようにnpx prisma generateで出力されたprismaClientでないとインポートエラーを吐いたこと。
  • アダプターを定義しないとうまく動かなかったこと。
      const adapter = new PrismaD1(c.env.DB);
      const prisma = new PrismaClient({ adapter });
    
  • todo.tsでのルーティングは/todoではなく、/にしないと/api/todo/todoになってしまうこと

参考資料

https://bufferings.hatenablog.com/entry/2025/01/14/003313
https://blog.sat.ne.jp/2025/02/05/hono-jsを試してみた/
https://zenn.dev/azukiazusa/articles/hono-cloudflare-workers-rest-api#todo-の作成
https://dev.classmethod.jp/articles/cdk-hono-crud-api-lambda-api-gateway-rds-aurora/

本番環境へ反映させる

npm run deployすることで反映可能になる。

わからなかったこと

ローカルで操作したDBの中身をcloudflare D1上へ反映させる方法


こんな感じにローカル上でのDBにてデータを流し込んだ後同じようにリモート上へデータを反映させる方法がわからなかった。

prisma studioを使う方法

今回実装したやり方では、dev.dbではなく.wranglerフォルダ内にあるdbへデータを流し込む。そのためprisma studioでデータを見ようとするとエラーを吐いてしまうがどう修正すればいいのかわからなかった。

Discussion