Workerプロジェクト作成
D1データベースを使うAPIエンドポイントの実装に必要なファイル構成と内容を説明します。
ファイル構成(変更・追加箇所)
my-worker/
├── src/
│ ├── index.ts ← ルーティング追加
│ └── api/
│ └── items.ts ← D1を使うAPIハンドラ(新規追加)
├── wrangler.toml ← D1バインディング追加
├── package.json
├── tsconfig.json
└── worker-configuration.d.ts ← D1の型定義追加
各ファイルの実装内容
wrangler.toml — D1バインディングを追加
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[[d1_databases]]
binding = "DB" # コード内で env.DB としてアクセスする名前
database_name = "my-database"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # wrangler d1 create で取得
worker-configuration.d.ts — D1の型定義を追加
interface Env {
DB: D1Database; // D1バインディングの型
}
src/index.ts — ルーティング
import { handleItemsApi } from "./api/items";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// /api/items へのリクエストをハンドラに委譲
if (url.pathname.startsWith("/api/items")) {
return handleItemsApi(request, env);
}
return new Response("Not Found", { status: 404 });
},
};
src/api/items.ts — D1を使うAPIハンドラ(新規追加)
export async function handleItemsApi(
request: Request,
env: Env
): Promise<Response> {
const url = new URL(request.url);
const id = url.pathname.split("/").pop();
switch (request.method) {
case "GET": {
if (id && id !== "items") {
// 1件取得
const item = await env.DB.prepare(
"SELECT * FROM items WHERE id = ?"
)
.bind(id)
.first();
if (!item) return json({ error: "Not found" }, 404);
return json(item);
}
// 一覧取得
const { results } = await env.DB.prepare(
"SELECT * FROM items ORDER BY id DESC"
).all();
return json(results);
}
case "POST": {
const body = await request.json<{ name: string }>();
const result = await env.DB.prepare(
"INSERT INTO items (name) VALUES (?) RETURNING *"
)
.bind(body.name)
.first();
return json(result, 201);
}
case "PUT": {
const body = await request.json<{ name: string }>();
await env.DB.prepare("UPDATE items SET name = ? WHERE id = ?")
.bind(body.name, id)
.run();
return json({ success: true });
}
case "DELETE": {
await env.DB.prepare("DELETE FROM items WHERE id = ?")
.bind(id)
.run();
return json({ success: true });
}
default:
return json({ error: "Method Not Allowed" }, 405);
}
}
function json(data: unknown, status = 200): Response {
return new Response(JSON.stringify(data), {
status,
headers: { "Content-Type": "application/json" },
});
}
セットアップの流れ
# 1. D1データベースを作成(database_id を取得)
npx wrangler d1 create my-database
# 2. テーブルをマイグレーション(ローカル)
npx wrangler d1 execute my-database --local --command \
"CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)"
# 3. ローカル開発サーバー起動
npx wrangler dev
# 4. 本番へデプロイ
npx wrangler deploy
ポイントまとめ
| 項目 | 内容 |
|---|---|
env.DB へのアクセス |
wrangler.toml の binding = "DB" と型定義が一致している必要がある |
| SQLインジェクション対策 | .bind() でプレースホルダーを使う(文字列結合は禁止) |
| レスポンス形式 | D1Result.results が配列、.first() が1件のオブジェクト |
| ローカルテスト | --local フラグでローカルのSQLiteに対して実行される |
エラーの原因は明確です。
"error code: 1042" — これはCloudflareのネットワークエラーで、fetch('/api/users') が失敗してJSONではないエラー文字列が返ってきています。
index.astro でこのように書いていると思いますが:
const res = await fetch(`${Astro.url.origin}/api/users`)
const users = await res.json() // ← ここでSyntaxError
本番環境では 自分自身へのfetchは使えません。代わりに直接DBを呼び出す必要があります。
修正方法
src/pages/index.astro を以下に変更してください:
---
import { env } from "cloudflare:workers"
type User = {
id: number
name: string
email: string
}
const { results: users } = await (env as any).MY_WORKER.fetch(
new Request('https://my-worker/api/users')
).then((res: Response) => res.json()) as { results: User[] }
---
ただし MY_WORKER 経由で取得する場合、my-worker が返すのは配列直接なので:
---
import { env } from "cloudflare:workers"
type User = {
id: number
name: string
email: string
}
const res = await (env as any).MY_WORKER.fetch(
new Request('https://my-worker/api/users')
)
const users = await res.json() as User[]
---
<html>
<body>
<h1>ユーザー一覧</h1>
<ul>
{users.map((u) => (
<li>{u.id}: {u.name} ({u.email})</li>
))}
</ul>
</body>
</html>
修正後に再デプロイしてください:
npm run deploy
なるほど、別々のWorkerプロジェクトをまたいでD1を参照する構成ですね。
この場合、D1はmy-worker側にバインドされているため、Service Bindingを使ってmy-siteからmy-workerのAPIを呼び出す方法が正解です。
Step 1 — my-worker にエンドポイントを作る
my-worker/src/index.ts:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url)
if (url.pathname === '/api/users') {
const { results } = await env.DB
.prepare('SELECT * FROM users LIMIT 5')
.all()
return Response.json(results)
}
return new Response('Not Found', { status: 404 })
}
}
my-worker/wrangler.toml:
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-09-23"
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "xxxx"
cd my-worker
npx wrangler deploy # 先にデプロイ
Step 2 — my-site の wrangler.toml に Service Binding を追加
my-site/wrangler.toml:
[[services]]
binding = "MY_WORKER"
service = "my-worker" # ← Step1でデプロイしたWorker名
Step 3 — my-site から my-worker を呼び出す
my-site/src/pages/api/users.ts:
import { env } from "cloudflare:workers"
export async function GET() {
const res = await (env as any).MY_WORKER.fetch(
new Request('https://my-worker/api/users')
)
const data = await res.json()
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' }
})
}
Step 4 — 画面に表示
my-site/src/pages/index.astro:
---
const res = await fetch(`${Astro.url.origin}/api/users`)
const users = await res.json()
---
<html>
<body>
<h1>ユーザー一覧</h1>
<ul>
{users.map((u: any) => (
<li>{u.id}: {u.name} ({u.email})</li>
))}
</ul>
</body>
</html>
構成まとめ
my-site(Astro)
└── Service Binding(MY_WORKER)
└── my-worker(Worker)
└── D1(my-db)← usersテーブル
my-workerを先にデプロイしてからmy-siteをデプロイするのがポイントです。
プロジェクト作成の全体フローをまず図で示してから、各フェーズを詳しく説明します。各ステップを順番に詳しく説明します。
ステップ 1 — Wrangler のインストール
Wrangler は Cloudflare Workers の公式 CLI ツールです。Node.js 18以上が必要です。
# グローバルにインストール(推奨)
npm install -g wrangler
# または npx で都度実行(インストール不要)
npx wrangler <コマンド>
# バージョン確認
wrangler --version
ステップ 2 — Cloudflare アカウントにログイン
wrangler login
実行するとブラウザが自動で開き、Cloudflare のダッシュボードで OAuth 認証が行われます。認証が完了するとターミナルに「Successfully logged in」と表示されます。
# ログイン状態の確認(アカウント情報が表示される)
wrangler whoami
# CI/CD 環境では API トークンを環境変数で渡す
CLOUDFLARE_API_TOKEN=xxxx wrangler deploy
ステップ 3 — プロジェクトの作成
npm create cloudflare@latest が現在の推奨コマンドです。対話形式でテンプレートを選べます。
npm create cloudflare@latest my-worker
実行すると以下のような質問が順番に出てきます。
╭ Create an application with Cloudflare Step 1 of 3
│
◆ What would you like to start with?
│ ● Hello World example ← 最もシンプル。まずはここから
│ ○ Framework Starter ← Hono / Next.js / Remix など
│ ○ Demo application ← Workers AI や D1 のサンプル付き
│ ○ Template from a Github repo ← GitHubのテンプレートを直接指定
◆ Which template would you like to use?
│ ● Hello World Worker ← TypeScript の基本Worker
│ ○ Hello World Durable Object
│ ○ Hello World Worker (Python) ← Python も選べる
◆ Do you want to use TypeScript?
│ ● Yes / ○ No
◆ Do you want to deploy your application?
│ ○ Yes / ● No ← まずNoにして手元で確認する
完了するとこのようなディレクトリ構成が生成されます。
my-worker/
├── src/
│ └── index.ts ← Worker のメインコード
├── wrangler.toml ← Wrangler の設定ファイル
├── package.json
├── tsconfig.json
└── worker-configuration.d.ts ← 環境変数の型定義(自動生成)
生成直後の src/index.ts はこのような内容になっています。
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
return new Response('Hello World!');
},
};
ステップ 4 — wrangler.toml の設定
生成された wrangler.toml を用途に合わせて編集します。
name = "my-worker" # Worker の名前(Cloudflare上の識別子)
main = "src/index.ts" # エントリポイント
compatibility_date = "2024-09-23" # 動作互換性の基準日
# ルーティング(独自ドメインに紐づける場合)
# route = { pattern = "example.com/api/*", zone_name = "example.com" }
# 環境変数(平文でOKな値)
[vars]
ENVIRONMENT = "development"
# ===== バインディングは必要なものだけ追記 =====
# KV
[[kv_namespaces]]
binding = "MY_KV"
id = "xxxxxxxxxxxxxxxx"
# D1
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "xxxxxxxxxxxxxxxx"
# R2
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "my-assets"
# ===== 環境別の上書き設定 =====
[env.production]
name = "my-worker-production"
vars = { ENVIRONMENT = "production" }
worker-configuration.d.ts は wrangler types コマンドで自動更新でき、env オブジェクトの型が常に wrangler.toml と同期されます。
wrangler types # → worker-configuration.d.ts を自動生成・更新
ステップ 5 — ローカル開発
# ローカルサーバーを起動(http://localhost:8787 でアクセス可能)
wrangler dev
# --port でポートを変更
wrangler dev --port 3000
# 完全オフラインモード(Miniflare を使用)
wrangler dev --local
# 本番のリソース(KV・D1など)に接続しながら開発
wrangler dev --remote
wrangler dev の起動中に使えるキーボードショートカットがあります。
[b] - ブラウザで開く
[d] - デバッガーを接続(Chrome DevTools)
[l] - ローカル / リモート モードを切り替え
[c] - コンソールをクリア
[x] - 停止
ステップ 6 — リソースの作成(バインディングを使う場合)
KV・D1・R2 などを使う場合は、デプロイ前にリソースを先に作成してIDを取得する必要があります。
# KV Namespace を作成
wrangler kv namespace create "MY_KV"
# → 出力されたIDを wrangler.toml の id に貼り付ける
# D1 データベースを作成
wrangler d1 create my-db
# → 出力されたIDを wrangler.toml の database_id に貼り付ける
# D1 マイグレーションの実行(ローカル)
wrangler d1 migrations apply my-db
# D1 マイグレーションの実行(本番)
wrangler d1 migrations apply my-db --remote
# R2 バケットを作成
wrangler r2 bucket create my-assets
# シークレットを登録(APIキーなど平文で書きたくない値)
wrangler secret put STRIPE_SECRET_KEY
ステップ 7 — デプロイ
# 本番にデプロイ(wrangler.toml のデフォルト設定を使用)
wrangler deploy
# 特定の環境にデプロイ
wrangler deploy --env production
wrangler deploy --env staging
# デプロイ結果の確認(ログをリアルタイムで見る)
wrangler tail
# 特定の環境のログを見る
wrangler tail --env production
よく使う管理コマンド一覧
# デプロイ済みWorkerの一覧
wrangler workers list
# 特定Workerの情報を確認
wrangler workers view my-worker
# Worker を削除
wrangler delete my-worker
# KV の操作
wrangler kv key put --binding=MY_KV "hello" "world"
wrangler kv key get --binding=MY_KV "hello"
wrangler kv key list --binding=MY_KV
# D1 の対話的クエリ実行
wrangler d1 execute my-db --command "SELECT * FROM users LIMIT 5"
wrangler d1 execute my-db --remote --command "SELECT count(*) FROM users"
# R2 の操作
wrangler r2 object put my-assets/image.png --file ./image.png
wrangler r2 object get my-assets/image.png --file ./downloaded.png
Hono などフレームワークを使う場合
フレームワークを選ぶと生成内容が変わります。Hono は Workers との相性が特に良く人気があります。
npm create cloudflare@latest my-api -- --framework=hono
生成される src/index.ts がルーティング付きになります。
import { Hono } from 'hono'
const app = new Hono<{ Bindings: Env }>()
app.get('/', (c) => c.text('Hello Hono!'))
app.get('/users/:id', async (c) => {
const id = c.req.param('id')
const user = await c.env.DB.prepare('SELECT * FROM users WHERE id = ?')
.bind(id).first()
return c.json(user)
})
export default app
その後の wrangler dev / wrangler deploy の流れはまったく同じです。
HonoとAstroはアプローチが根本的に異なります。まず対比の全体像を見てから、それぞれの詳細とコードを説明します。それぞれのプロジェクト作成から開発・デプロイの流れを詳しく説明します。
Hono — API・バックエンド中心の構成
プロジェクト作成
npm create cloudflare@latest my-api -- --framework=hono
cd my-api
生成されるディレクトリ構造はとてもシンプルです。
my-api/
├── src/
│ └── index.ts ← ルーティングをすべてここに書く
├── wrangler.toml
├── package.json
└── tsconfig.json
コードの書き方
Hono は Express に近い感覚で書けます。c.env 経由でバインディングに直接アクセスできます。
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { bearerAuth } from 'hono/bearer-auth'
// Env 型を Hono のジェネリクスに渡すと、c.env が型安全になる
const app = new Hono<{ Bindings: Env }>()
// ミドルウェアをグローバルに適用
app.use('/api/*', cors())
app.use('/api/admin/*', bearerAuth({ token: 'secret' }))
// GET /api/users/:id
app.get('/api/users/:id', async (c) => {
const id = c.req.param('id')
const user = await c.env.DB.prepare(
'SELECT * FROM users WHERE id = ?'
).bind(id).first()
if (!user) return c.json({ error: 'Not found' }, 404)
return c.json(user)
})
// POST /api/users
app.post('/api/users', async (c) => {
const body = await c.req.json<{ name: string; email: string }>()
await c.env.DB.prepare(
'INSERT INTO users (name, email) VALUES (?, ?)'
).bind(body.name, body.email).run()
return c.json({ ok: true }, 201)
})
// ルーターを分割して整理することも可能
import { userRoutes } from './routes/users'
app.route('/api/users', userRoutes)
export default app
wrangler.toml(Hono の場合)
ビルドステップがほぼ不要なので設定はシンプルです。
name = "my-api"
main = "src/index.ts"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]
[[d1_databases]]
binding = "DB"
database_name = "my-api-db"
database_id = "xxxx"
[[kv_namespaces]]
binding = "CACHE"
id = "xxxx"
[env.production]
name = "my-api-production"
開発・デプロイ
# ローカル開発(型定義も自動更新)
npm run dev # → wrangler dev を実行
# 型定義を最新化(バインディング追加後に実行)
wrangler types
# デプロイ
npm run deploy # → wrangler deploy を実行
Astro — フルスタックサイトの構成
プロジェクト作成
npm create cloudflare@latest my-site -- --framework=astro
cd my-site
Astro はディレクトリ構造が Hono より複雑です。
my-site/
├── src/
│ ├── pages/ ← ファイルがそのままURLになる
│ │ ├── index.astro → /
│ │ ├── about.astro → /about
│ │ ├── blog/
│ │ │ ├── index.astro → /blog
│ │ │ └── [slug].astro→ /blog/:slug(動的ルート)
│ │ └── api/
│ │ └── users.ts → /api/users(APIエンドポイント)
│ ├── components/ ← 再利用可能なコンポーネント
│ │ ├── Header.astro
│ │ └── Card.astro
│ ├── layouts/ ← ページの共通レイアウト
│ │ └── Base.astro
│ └── content/ ← Markdown コンテンツ(ブログ記事など)
│ └── blog/
│ └── hello.md
├── public/ ← 静的アセット(画像など)
├── astro.config.mjs ← Astro の設定
└── wrangler.toml
コードの書き方
Astro のページは .astro ファイルで書きます。フロントマターでサーバーサイドの処理を行い、HTMLをテンプレートとして返します。
---
// src/pages/blog/[slug].astro
// --- で囲まれたフロントマターはサーバー側で実行される
// Workers のバインディングには Astro.locals 経由でアクセス
const { slug } = Astro.params
const runtime = Astro.locals.runtime // Cloudflare の runtime 情報
// D1 からデータを取得
const post = await runtime.env.DB
.prepare('SELECT * FROM posts WHERE slug = ?')
.bind(slug)
.first()
if (!post) return Astro.redirect('/404')
---
<!-- ここからHTMLテンプレート。React に近い感覚 -->
<html lang="ja">
<head>
<title>{post.title}</title>
</head>
<body>
<h1>{post.title}</h1>
<p>{post.published_at}</p>
<div set:html={post.content} />
</body>
</html>
APIエンドポイントは pages/api/ 以下に .ts ファイルとして置きます。
// src/pages/api/users.ts
import type { APIContext } from 'astro'
// GET /api/users
export async function GET({ locals }: APIContext) {
const { results } = await locals.runtime.env.DB
.prepare('SELECT id, name, email FROM users')
.all()
return Response.json(results)
}
// POST /api/users
export async function POST({ request, locals }: APIContext) {
const body = await request.json() as { name: string; email: string }
await locals.runtime.env.DB
.prepare('INSERT INTO users (name, email) VALUES (?, ?)')
.bind(body.name, body.email)
.run()
return Response.json({ ok: true }, { status: 201 })
}
React や Vue のコンポーネントを混在させることもできます(Islands Architecture)。
---
// src/pages/index.astro
import ReactCounter from '../components/Counter.tsx'
---
<html>
<body>
<h1>静的なHTMLはゼロJS</h1>
<!-- client:load でこのコンポーネントだけハイドレーション -->
<ReactCounter client:load initialCount={0} />
<!-- client:idle で遅延ハイドレーション -->
<HeavyComponent client:idle />
</body>
</html>
astro.config.mjs(Workers 向け設定)
import { defineConfig } from 'astro/config'
import cloudflare from '@astrojs/cloudflare'
import react from '@astrojs/react' // React を使う場合
export default defineConfig({
output: 'server', // SSR モード(Workers での実行に必須)
adapter: cloudflare({
platformProxy: {
enabled: true, // wrangler dev でバインディングを使えるようにする
},
}),
integrations: [
react(), // React コンポーネントを使う場合
],
})
wrangler.toml(Astro の場合)
Astro はビルド成果物を Workers にデプロイするため、main が生成ファイルを指します。
name = "my-site"
main = "./dist/_worker.js" # ← astro build で生成される
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]
assets = { directory = "./dist" } # 静的ファイルも一緒にデプロイ
[[d1_databases]]
binding = "DB"
database_name = "my-site-db"
database_id = "xxxx"
開発・デプロイ
# ローカル開発
npm run dev # astro dev (Astro の dev サーバー)
# Workers のバインディングを使いたい場合はこちら
npm run preview # astro build → wrangler dev の順で実行
# デプロイ(ビルドが先に走る)
npm run deploy # astro build && wrangler deploy
Hono と Astro を組み合わせる構成
最も実用的なパターンは、Astro がフロントを担当し、Hono がバックエンド API を担当する分離構成です。Service Binding で繋ぐことでインターネットを介さずに通信できます。
# Astro 側の wrangler.toml
[[services]]
binding = "API"
service = "my-hono-api" # ← 別途デプロイした Hono Worker の名前
// Astro の APIエンドポイントから Hono Worker を呼び出す
// src/pages/api/proxy.ts
export async function GET({ locals }: APIContext) {
// ゼロレイテンシで Hono Worker に転送
const res = await locals.runtime.env.API.fetch(
new Request('https://internal/api/users')
)
return res
}
この構成のメリットは、Hono 側を独立してスケール・テスト・デプロイできることです。フロント(Astro)とAPI(Hono)でデプロイサイクルを分けられるため、大規模なプロジェクトでも管理しやすくなります。