Workerプロジェクト作成

Page content

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.tomlbinding = "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.tswrangler 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)でデプロイサイクルを分けられるため、大規模なプロジェクトでも管理しやすくなります。