このチュートリアルでは、D1 と Hono ↗ を使い、ブログのコメントを保存・取得する JSON API を構築します。D1 データベースを作成し、スキーマを定義し、データベースの読み書きを行う GET と POST エンドポイントを接続します。
- Cloudflare アカウント ↗ に登録します。
Node.js↗ をインストールします。
Node.js のバージョンマネージャー
権限の問題を避け、Node.js のバージョンを切り替えられるよう、Volta ↗ や nvm ↗ などの Node バージョンマネージャーを使います。このガイドの後半で説明する Wrangler には、Node バージョン 16.17.0 以降が必要です。
-
次のコマンドを実行し、
d1-comments-apiという名前の新しいプロジェクトを作成します。npm create cloudflare@latest -- d1-comments-apiyarn create cloudflare d1-comments-apipnpm create cloudflare@latest d1-comments-apiセットアップでは、次のオプションを選びます。
- What would you like to start with? では、
Hello World exampleを選びます。 - Which template would you like to use? では、
Worker onlyを選びます。 - Which language do you want to use? では、
TypeScriptを選びます。 - Do you want to use git for version control? では、
Yesを選びます。 - Do you want to deploy your application? では、
Noを選びます(デプロイ前にいくつか変更します)。
- What would you like to start with? では、
-
プロジェクトディレクトリに移動します。
cd d1-comments-api
Workers 上で API を構築するための軽量 Web フレームワーク Hono ↗ をインストールします。
npm i honoyarn add honopnpm add honobun add hono-
Wrangler で新しい D1 データベースを作成します。
npx wrangler@latest d1 create d1-comments-api -
Would you like Wrangler to add it on your behalf?と聞かれたらYesを選びます。Wrangler 設定ファイルにDBバインディングが自動で追加されます。Wrangler 設定ファイルに
d1_databasesバインディングとプロジェクト設定全体が入っていることを確認します。{ "$schema": "./node_modules/wrangler/config-schema.json", "name": "d1-comments-api", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-09-20", "d1_databases": [ { "binding": "DB", "database_name": "d1-comments-api", "database_id": "<YOUR_DATABASE_ID>" } ] }name = "d1-comments-api" main = "src/index.ts" # Set this to today's date compatibility_date = "2026-09-20" [[d1_databases]] binding = "DB" # available in your Worker on env.DB database_name = "d1-comments-api" database_id = "<YOUR_DATABASE_ID>"<YOUR_DATABASE_ID>を、wrangler d1 createコマンドが出力した ID に置き換えます。
バインディング を使うと、Worker はコード内の変数名で D1 データベース、KV 名前空間、R2 バケットなどのリソースにアクセスできます。D1 データベースには Worker 内の env.DB からアクセスします。
-
次の内容で
schemas/schema.sqlファイルを作成します。DROP TABLE IF EXISTS comments; CREATE TABLE IF NOT EXISTS comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, author TEXT NOT NULL, body TEXT NOT NULL, post_slug TEXT NOT NULL ); CREATE INDEX idx_comments_post_slug ON comments (post_slug); -- Optionally, uncomment the below query to insert seed data -- INSERT INTO comments (author, body, post_slug) VALUES ('Kristian', 'Great post!', 'hello-world'); -
まずローカルデータベースにスキーマを適用します。
npx wrangler d1 execute d1-comments-api --local --file schemas/schema.sql -
テーブルがローカルに作成されたことを確認します。
npx wrangler d1 execute d1-comments-api --local --command "SELECT name FROM sqlite_schema WHERE type = 'table'"┌──────────┐ │ name │ ├──────────┤ │ comments │ └──────────┘ -
スキーマに問題がなければ、リモート(本番)データベースに適用します。
npx wrangler d1 execute d1-comments-api --remote --file schemas/schema.sql
src/index.ts の内容を次のコードに置き換えます。型付きの Bindings インターフェイスを持つ Hono アプリケーションを用意し、env.DB が D1Database として正しく型付けされるようにします。
import { Hono } from "hono";
const app = new Hono();
app.get("/api/posts/:slug/comments", async (c) => {
// Do something and return an HTTP response
// Optionally, do something with c.req.param("slug")
});
app.post("/api/posts/:slug/comments", async (c) => {
// Do something and return an HTTP response
// Optionally, do something with c.req.param("slug")
});
export default app;import { Hono } from "hono";
type Bindings = {
DB: D1Database;
};
const app = new Hono<{ Bindings: Bindings }>();
app.get("/api/posts/:slug/comments", async (c) => {
// Do something and return an HTTP response
// Optionally, do something with c.req.param("slug")
});
app.post("/api/posts/:slug/comments", async (c) => {
// Do something and return an HTTP response
// Optionally, do something with c.req.param("slug")
});
export default app;指定した投稿のコメントを取得する GET エンドポイントのロジックを追加します。D1 の Workers Binding API を使い、パラメーター付きクエリを準備して実行します。
app.get("/api/posts/:slug/comments", async (c) => {
const { slug } = c.req.param();
const { results } = await c.env.DB.prepare(
"SELECT * FROM comments WHERE post_slug = ?",
)
.bind(slug)
.run();
return c.json(results);
});app.get("/api/posts/:slug/comments", async (c) => {
const { slug } = c.req.param();
const { results } = await c.env.DB.prepare(
"SELECT * FROM comments WHERE post_slug = ?",
)
.bind(slug)
.run();
return c.json(results);
});このコードは、prepare でパラメーター付きステートメントを作成し、bind で slug の値を安全に渡し(SQL インジェクションを防ぎ)、run でクエリを実行します。
新しいコメントを作成する POST エンドポイントを追加します。行を挿入する前にリクエストボディを検証します。
app.post("/api/posts/:slug/comments", async (c) => {
const { slug } = c.req.param();
const { author, body } = await c.req.json();
if (!author) return c.text("Missing author value for new comment", 400);
if (!body) return c.text("Missing body value for new comment", 400);
const { success } = await c.env.DB.prepare(
"INSERT INTO comments (author, body, post_slug) VALUES (?, ?, ?)",
)
.bind(author, body, slug)
.run();
if (success) {
c.status(201);
return c.text("Created");
} else {
c.status(500);
return c.text("Something went wrong");
}
});app.post("/api/posts/:slug/comments", async (c) => {
const { slug } = c.req.param();
const { author, body } = await c.req.json<{
author: string;
body: string;
}>();
if (!author) return c.text("Missing author value for new comment", 400);
if (!body) return c.text("Missing body value for new comment", 400);
const { success } = await c.env.DB.prepare(
"INSERT INTO comments (author, body, post_slug) VALUES (?, ?, ?)",
)
.bind(author, body, slug)
.run();
if (success) {
c.status(201);
return c.text("Created");
} else {
c.status(500);
return c.text("Something went wrong");
}
});別オリジンのフロントエンドからこの API を呼び出す場合は、CORS ミドルウェアを追加します。Hono から cors モジュールをインポートし、ルートより前に追加します。
import { Hono } from "hono";
import { cors } from "hono/cors";
const app = new Hono();
app.use("/api/*", cors());import { Hono } from "hono";
import { cors } from "hono/cors";
type Bindings = {
DB: D1Database;
};
const app = new Hono<{ Bindings: Bindings }>();
app.use("/api/*", cors());/api/* へのリクエストでは、Hono が API のレスポンスに CORS ヘッダーを自動で生成して追加します。
-
Cloudflare アカウントにログインします(まだの場合)。
npx wrangler whoamiログインしていない場合、Wrangler がログインを求めます。
-
Worker をデプロイします。
npx wrangler deploy -
コメントを挿入してから取得し、API をテストします。
# Replace <YOUR_SUBDOMAIN> with your workers.dev subdomain curl -X POST https://d1-comments-api.<YOUR_SUBDOMAIN>.workers.dev/api/posts/hello-world/comments \ -H "Content-Type: application/json" \ -d '{"author": "Kristian", "body": "Great post!"}'Createdcurl https://d1-comments-api.<YOUR_SUBDOMAIN>.workers.dev/api/posts/hello-world/comments[ { "id": 1, "author": "Kristian", "body": "Great post!", "post_slug": "hello-world" } ]
すべてのルートと CORS 対応を含む、完成版の src/index.ts です。
import { Hono } from "hono";
import { cors } from "hono/cors";
const app = new Hono();
app.use("/api/*", cors());
app.get("/api/posts/:slug/comments", async (c) => {
const { slug } = c.req.param();
const { results } = await c.env.DB.prepare(
"SELECT * FROM comments WHERE post_slug = ?",
)
.bind(slug)
.run();
return c.json(results);
});
app.post("/api/posts/:slug/comments", async (c) => {
const { slug } = c.req.param();
const { author, body } = await c.req.json();
if (!author) return c.text("Missing author value for new comment", 400);
if (!body) return c.text("Missing body value for new comment", 400);
const { success } = await c.env.DB.prepare(
"INSERT INTO comments (author, body, post_slug) VALUES (?, ?, ?)",
)
.bind(author, body, slug)
.run();
if (success) {
c.status(201);
return c.text("Created");
} else {
c.status(500);
return c.text("Something went wrong");
}
});
export default app;import { Hono } from "hono";
import { cors } from "hono/cors";
type Bindings = {
DB: D1Database;
};
const app = new Hono<{ Bindings: Bindings }>();
app.use("/api/*", cors());
app.get("/api/posts/:slug/comments", async (c) => {
const { slug } = c.req.param();
const { results } = await c.env.DB.prepare(
"SELECT * FROM comments WHERE post_slug = ?",
)
.bind(slug)
.run();
return c.json(results);
});
app.post("/api/posts/:slug/comments", async (c) => {
const { slug } = c.req.param();
const { author, body } = await c.req.json<{
author: string;
body: string;
}>();
if (!author) return c.text("Missing author value for new comment", 400);
if (!body) return c.text("Missing body value for new comment", 400);
const { success } = await c.env.DB.prepare(
"INSERT INTO comments (author, body, post_slug) VALUES (?, ?, ?)",
)
.bind(author, body, slug)
.run();
if (success) {
c.status(201);
return c.text("Created");
} else {
c.status(500);
return c.text("Something went wrong");
}
});
export default app;- 利用できるメソッドの一覧は D1 Workers Binding API を参照してください。
- デプロイせずにデータベースをテストするには、D1 のローカル開発 を確認してください。
- D1 上に構築されたコミュニティプロジェクト もご覧ください。