概要
フロントエンドをちょっと試すのはとても簡単になりました。コマンド1つでプロジェクトを作成し、VercelやAmplifyを使えばわずかな時間でデプロイまでできます。
ただ、バックエンドを作るのはちょっとめんどくさい。
なので備忘録がてら、HonoとCloudflareを使って、完全無料で、データを永続的に保持できて、いざデプロイしたくなった時にそれほど大変ではない環境を作ってみようと思います。
利用技術紹介
Hono
Honoは、軽量・高速なWebフレームワークです。Expressのような書き心地でAPIを実装できますが、最大の特徴はCloudflare Workersのようなエッジ環境で動くことを前提に設計されている点です。Node.js特有のAPIに依存していないため、後述するWorkers上でも余計な変換なしにそのまま高速に動作します。
Cloudflare
Cloudflareは、今回のバックエンド構成を「完全無料」で実現するための土台です。使うサービスは以下の3つです。
- Cloudflare Workers:APIサーバーそのものを動かすサーバーレス実行環境。1日10万リクエストまで無料で使えます。個人開発やちょっとしたプロトタイプであれば無料で行けることが多い
- Cloudflare D1:SQLiteベースのサーバーレスDB。5GBのストレージと500万行/日の読み取りまで無料。「とりあえずDBが欲しい」というときの第一候補になります。
- wrangler:Cloudflareが提供する公式CLIツール。ローカルでの開発・動作確認から本番へのデプロイまで、一貫してこのツール一本で完結します。
今回はAPI + DBのシンプルな構成なので、Workers + D1 の組み合わせを中心に進めていきます。
また、何かの間違いで有料になってしまうことを防ぐため、ローカル開発環境の中で完結するように環境づくりを行います。ローカル環境に閉じこもれば、何百万リクエスト実行しようが、全て無料です。
プロジェクトの作成
以下のコマンドの my-app は好きなアプリ名でOK。今回はbackend-using-hono-cloudflareにします。
npm create hono@latest backend-using-hono-cloudflare
# → template: cloudflare-workers
# → install: yes
# → pnpmを選択
-
実行ログ
$ npm create hono@latest backend-using-hono-cloudflare Need to install the following packages: create-hono@0.19.4 Ok to proceed? (y) y > npx > 'create-hono' backend-using-hono-cloudflare create-hono version 0.19.4 ✔ Using target directory … backend-using-hono-cloudflare ✔ Which template do you want to use? cloudflare-workers ✔ Do you want to install project dependencies? Yes ✔ Which package manager do you want to use? pnpm ✔ Cloning the template ✔ Installing project dependencies 🎉 Copied project files Get started with: cd backend-using-hono-cloudflare
プロジェクト作成が完了したら、ディレクトリを移動して、サーバーを立ち上げてみます。
cd backend-using-hono-cloudflare
npm run dev
http://localhost:8787/ にアクセスして「Hello Hono!」と表示されたら成功です!
現在のディレクトリ構成は以下の通り。
backend-using-hono-cloudflare/
│
├── src/
│ └── index.ts ── Hono アプリ本体(GET / → "Hello Hono!")/ Worker のエントリ
│
├── wrangler.jsonc ── Cloudflare Workers 設定(main=src/index.ts, compat 2026-08-29)
├── package.json ── 依存: hono ^4.13.5 / devDeps: wrangler ^4.110.0
│ scripts: dev, deploy, cf-typegen
├── pnpm-lock.yaml ── pnpm ロックファイル
├── tsconfig.json ── TS 設定(ESNext, strict, jsx: hono/jsx)
├── README.md
├── .gitignore
│
├── node_modules/ ── 依存パッケージ(管理対象外)
└── .wrangler/ ── wrangler のローカル作業ディレクトリ(自動生成)
├── state/
└── tmp/
git管理したかったら以下を実行しておきます。
git init
git add .
git commit -m "chore: 初期構築"
今後特に記載はしませんので、お好みの粒度でコミットしてください。
Cloudflare を使ってローカルDBを構築
やることは大きく4つ。
- D1 データベースを作る(設定ファイルに登録)
- 型を生成して Hono に接続情報を教える
- マイグレーションファイル(テーブルを作る SQL)を書く
- マイグレーションをローカルに適用して、テーブルができたか確認する
Cloudflare D1データベースを作成
以下のコマンドを実行する。createの後にDB名を指定するので、今回はbackend-using-hono-cloudflare-dbします。
npx wrangler d1 create backend-using-hono-cloudflare-db
DB作成に成功したよというメッセージの後に、以下のような質問をされるかもしれません。
- Would you like Wrangler to add it on your behalf?
YesでOK。Yesを選択するとWrangler が設定ファイル(wrangler.jsonc)を編集して d1_databases の項目を追記してくれます。
"d1_databases": [
{
"binding": "backend_using_hono_cloudflare_db",
"database_name": "backend-using-hono-cloudflare-db",
"database_id": "44527bf2-3f0f-4905-8f60-ce605d4946b0"
}
]
- What binding name would you like to use? … backend_using_hono_cloudflare_db
作成したDBをWorkerコードから呼ぶときの名前を設定できます。ここで hoge と入力したら、コード上で hoge を指定すればbackend_using_hono_cloudflare_dbを呼び出したことになる。Cloudflare上のDBとは無関係のローカルな呼び名というイメージ。
設定ファイル(wrangler.jsonc)のbindingから自由に変更することができます。
- For local dev, do you want to connect to the remote resource instead of a local resource? … no
wrangler dev でローカル開発するとき、本番(Cloudflareのクラウド上)の D1 に直接つなぐか、 PC 内のローカル D1 につなぐかという質問。no を選択するとローカルD1に繋いでくれます。この設定ならオフラインでも動くし、課金されることはないです。
(やらなくてもいい)binding名を変更
"binding": "backend_using_hono_cloudflare_db",
このようにbindingを設定しましたが、コードやドキュメントに毎回backend_using_hono_cloudflare_dbと書くのは面倒なので、DBでこれを呼び出せるようにしたい。
"binding": "backend_using_hono_cloudflare_db", → "binding": "DB",
これでOK。
マイグレーションファイルを作成し、テーブルを定義する
D1ではマイグレーションファイルでDBの構造を保持するようになっています。
まずはuserテーブルを作成するマイグレーションのファイルを作ります。
npx wrangler d1 migrations create backend-using-hono-cloudflare-db create_user_table
# migrationフォルダがないから作っていいかと聞かれることがある。YesでOK
create の後に書くのはDB名であり、bindingした値ではないことに注意。
コマンド実行後、migrationsフォルダとmigrations/0001_create_user_table.sqlファイルが作成されていたらOK。
migrations/0001_create_user_table.sqlファイルに以下のように追記して、userテーブルを作成します。
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
以下のコマンドでこれを実行し、ローカルD1DBにuserテーブルを作成します。
npx wrangler d1 migrations apply backend-using-hono-cloudflare-db --local
--local をつけることで、PC 内のローカル DB に対して実行しています。確認を求められたら yでOK(マイグレーション実行中はDBにアクセスしたりできなくなるかもだけど大丈夫? という意)。
適用すると .wrangler/state/v3/d1/ 以下に SQLite ファイルが作られ、「0001_create_user_table.sql を適用した」旨が出れば成功!
┌────────────────────────────┬────────┐
│ name │ status │
├────────────────────────────┼────────┤
│ 0001_create_user_table.sql │ ✅ │
└────────────────────────────┴────────┘
データを挿入して確かめてみます。
npx wrangler d1 execute backend-using-hono-cloudflare-db --local --command "INSERT INTO user (name) VALUES ('Alice')"
npx wrangler d1 execute backend-using-hono-cloudflare-db --local --command "SELECT * FROM user"
# SELECTの結果
# created_atはINSERTを実行した日時が入っていればOK
┌────┬───────┬─────────────────────┐
│ id │ name │ created_at │
├────┼───────┼─────────────────────┤
│ 1 │ Alice │ 2026-08-30 05:47:56 │
└────┴───────┴─────────────────────┘
これが確認できればOK!
honoでバックエンドを実装する
wrangler.jsoncについての型を生成
以下のコマンドを実行して、型定義ファイルを作成します。
npx wrangler types --env-interface CloudflareBindings
プロジェクトルートにworker-configuration.d.tsが生成されたら成功。
src/index.tsの3行目を以下のように書き換えます。
const app = new Hono<{ Bindings: CloudflareBindings }>();
これでOK。
なぜこのようなことをするのかについてですが、今後型安全にAPIを実装するのが目的です。
上記の修正前だと、c.envに該当する型が当てられておらず、補完が効きませんでした。
修正後、補完が効くようになります。
これは手動で型を実装しても実現することはできるのですが、これだとwrangler.jsoncを修正する度に、型も修正しなければならず、手間がかかってしまいます。
コマンド実行→型自動生成に頼ることで、この手間は解決することができます。
また、D1固有の.prepare(), batch()などのメソッドについての型定義もこれに含まれているため、手動で型を定義せず、コマンドで型を生成しましょう。
apiを実装してみる
一覧取得
以下のものをsrc/index.tsに追記して、ユーザー一覧を取得するAPIを実装しましょう。
// 一覧取得
app.get("/users", async (c) => {
const { results } = await c.env.DB.prepare(
"SELECT id, name, created_at FROM user ORDER BY id",
).all();
return c.json(results);
});
- GET /users にリクエストが来たとき実行される
-
cは Context(文脈オブジェクト)。1リクエストにつき1個作られ、「リクエスト情報の読み取り」「レスポンスの組み立て」「環境(バインディング)へのアクセス」を全部ここから行う。名前は慣習でc。 -
c.env.DB- Cloudflare のバインディングが入っているオブジェクト。
wrangler.jsoncのd1_databases[].bindingに書いた名前がプロパティになる。今回は"DB"にしたのでc.env.DB。 -
c.env.DBの型はD1Database。これは先ほど自動生成したworker-configuration.d.tsのinterface CloudflareBindings { DB: D1Database }によって TypeScript に伝わっている。 -
c.envはリクエストの中でしか触れない。だから DB アクセスは必ずハンドラの中で書く。
- Cloudflare のバインディングが入っているオブジェクト。
-
.prepare("SQL...")は プリペアドステートメント(D1PreparedStatement)というものを作る。これを作った段階ではまだ実行はしていない。「この SQL を実行する準備をした」状態。 -
.all()は準備した SELECT を実際に実行し、全行を取得するメソッド - **
c.json(results)**は「xをJSON.stringifyして、ヘッダContent-Type: application/jsonを付けたResponseを作る」ヘルパー関数。第2引数にステータスコードを渡すこともできる。
await c.env.DB.prepare … all() は以下のようなオブジェクトを返します。今回は分割代入でresultsのみ取得しています。
{
results: [
{ id: 1, name: "Alice", created_at: "2026-08-30 12:00:00" },
{ id: 2, name: "Bob", created_at: "2026-08-30 12:05:00" }
],
success: true,
meta: { duration: 0.3, rows_read: 2, rows_written: 0, /* ... */ }
}
-
results:行の配列。各行は「列名 → 値」のオブジェクト。0件なら[]。 -
success:成否フラグ。 -
meta:実行時間や読み書き行数などの統計。課金カウント(rows_read)もここで分かる。
1件追加
以下のものをsrc/index.tsに追記して、ユーザーを追加するAPIを実装しましょう。
// 1件追加
app.post("/users", async (c) => {
const body = await c.req.json<{ name: string }>();
if (!body?.name) return c.json({ error: "name is required" }, 400);
const { results } = await c.env.DB.prepare(
"INSERT INTO user (name) VALUES (?) RETURNING id, name, created_at",
)
.bind(body.name)
.all();
return c.json(results[0], 201);
});
- POST /users のリクエストが来たときに実行される。
-
const body = await c.req.json<{ name: string }>()-
c.reqはリクエスト側の情報を読むためのオブジェクト(c.envが環境、c.reqがリクエスト、と役割が分かれている)。 -
c.req.json()は、リクエストボディ(クライアントが送ってきた JSON 文字列)をパースして JavaScript のオブジェクトに変換するメソッド。 - 例えばクライアントが
{"name":"Bob"}を送ってきたら、bodyは{ name: "Bob" }というオブジェクトになる。
-
-
if (!body?.name) return c.json({ error: 'name is required' }, 400)- 入力チェック(バリデーション)。
nameが無いリクエストを弾く。
- 入力チェック(バリデーション)。
-
.prepare('INSERT INTO user (name) VALUES (?) RETURNING id, name, created_at')-
?はプレースホルダ。ここに文字列を直接埋め込まず?にしておくのが重要で、次の.bind()で安全に値を差し込む。これで SQL インジェクションを防げる。 -
RETURNING id, name, created_at… INSERT した行の中身をそのまま返させる SQLite / D1 の機能。これが無いと「追加はできたが、採番された id や日時が分からない」ので、別途 SELECT が必要になる。
-
-
.bind(body.name)-
.prepare()の SQL に開けた?の穴に、順番どおり値を差し込む。?が1個なので引数も1個。複数なら.bind(a, b, c)のように並べる。 - ここで差し込まれた値は「データ」として扱われ、SQL 文の一部としては解釈されない。
-
動作確認
2つとも実装できたら、npx wrangler devを実行してバックエンドサーバーを立ち上げ、別ターミナルで以下のコマンドを実行し、結果を確かめてみます。
# 一覧(Alice が返るはず)
curl http://localhost:8787/users
# 追加
curl -X POST http://localhost:8787/users -H 'Content-Type: application/json' -d '{"name":"Bob"}'
# もう一度一覧(Alice と Bob)
curl http://localhost:8787/users
これでDBからデータを取得したり、追加したりすることができるようになりました!
一度作ったデータは.wranglerフォルダを削除したりしない限り残ります。サーバーを止めても、PCを再起動しても問題なし!
作ったものをサービスとしてリリースしたくなったら、ローカルDBの構造やデータを本番DBに上げることもできます。

