第1弾では、Honoを使ってメモリ上にデータを保持する簡易CRUD APIを作成しました。
第2弾となる今回は、Cloudflareのサーバーレス分散SQLデータベース「Cloudflare D1」を導入してデータを永久保存(永続化)し、さらにReact (Vite)で作ったフロントエンド画面からデータの取得・追加・更新・削除を行えるように繋ぎ込みます!
1. はじめに
- 今回のゴール: 前回構築したバックエンドAPI(Hono)に、Reactで作成したフロントエンドとCloudflare D1を繋ぎ、ブラウザ画面からユーザーの取得・追加を行えるようにします。

- 全体の手順:
- React(Vite)プロジェクトのセットアップ
- D1データベースの構築
- Reactからの fetch API呼び出し&画面実装
2. 準備:D1データベース構築
cloudflare D1の詳細な解説は以前の記事を参照してください。ここでは必要なコマンドのみを実行していきます。
① D1データベースの作成
バックエンドが入っているディレクトリで実行します。
前回の記事の続きなら my-todo-api のディレクトリで実行します。
npx wrangler d1 create todo-db② スキーマの適用(ローカル)
schema.sql を準備し、ローカルDBにテーブルを作成します。
テスト用として以下のようなschema.sqlをmy-todo-apiのディレクトリに配置します。
-- すでにテーブルが存在する場合は削除(再初期化用)
DROP TABLE IF EXISTS todos;
-- ユーザーテーブルの作成
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER DEFAULT 0
);
-- 動作確認用の初期データ(サンプルレコード)
INSERT INTO todos (title, completed) VALUES ('部屋の掃除をする', 0);
INSERT INTO todos (title, completed) VALUES ('Reactの勉強をする', 1);ローカルDBにテーブルを作成します。
npx wrangler d1 execute todo-db --local --file=./schema.sql3. バックエンド(Hono)側のD1連携コード実装
前回のインメモリ(配列)管理から、D1データベースを操作する処理へ src/index.ts を書き換えます。
すでに追加済みだと思いますが、フロントエンド(http://localhost:5173 等)からAPIを叩けるよう、前回の記事で作成したHono側の src/index.ts に cors() ミドルウェアが入っていることを確認します。
また、テスト用の初期データ(インメモリ配列)の部分のコードは削除して、D1への接続用コードを追加して、冒頭部分は以下のように変更します。
import { Hono } from 'hono'
import { cors } from 'hono/cors'
type Bindings = {
todo_db: D1Database
}
const app = new Hono<{ Bindings: Bindings }>()
// Reactからの接続を許可
app.use('/api/*', cors())
// READ: 一覧取得
app.get('/api/todos', async (c) => {
const { results } = await c.env.todo_db.prepare('SELECT * FROM todos ORDER BY id DESC').all()
return c.json(results)
})
// CREATE: 新規追加
app.post('/api/todos', async (c) => {
const body = await c.req.json()
if (!body.title || !body.title.trim()) {
return c.json({ error: 'タイトルは必須です' }, 400)
}
const result = await c.env.todo_db
.prepare('INSERT INTO todos (title, completed) VALUES (?, 0) RETURNING *')
.bind(body.title)
.first()
return c.json(result, 201)
})
// UPDATE: 完了状態の更新
app.put('/api/todos/:id', async (c) => {
const id = c.req.param('id')
const body = await c.req.json()
const completedValue = body.completed ? 1 : 0
const result = await c.env.todo_db
.prepare('UPDATE todos SET completed = ? WHERE id = ? RETURNING *')
.bind(completedValue, id)
.first()
if (!result) {
return c.json({ error: '該当タスクがありません' }, 404)
}
return c.json(result)
})
// DELETE: 削除
app.delete('/api/todos/:id', async (c) => {
const id = c.req.param('id')
await c.env.todo_db.prepare('DELETE FROM todos WHERE id = ?').bind(id).run()
return c.json({ message: '削除が完了しました' })
})
export default app4. React (Vite) フロントエンドの構築
これも以前の記事で詳細を解説していますので参考にしてください。
ここではコマンドのみ紹介します。
① ViteでReactプロジェクトを作成
バックエンドのプロジェクト(my-todo-api)の「ひとつ上の親ディレクトリ」で実行するのがおすすめです。
📂 おすすめのディレクトリ構造
親フォルダ(project)で実行すると、バックエンドとフロントエンドが並んで配置されるため、管理がとてもスムーズになります。
Documents/
└── my-todo/ ★ここでコマンドを実行する!
├── my-todo-api/ (バックエンド:Hono + D1)
└── my-todo-frontend/ (フロントエンド:React)以下のコマンドでフロントエンドのプロジェクトを作成します。
npm create vite@latest my-todo-frontend -- --template react-ts
cd my-todo-frontend5. fetch を使った API 呼び出しと画面実装
React側の App.tsx に、データ取得(GET)とデータ追加(POST)の処理を記述します。
src/App.jsx のコード例
import { useEffect, useState } from 'react'
interface Todo {
id: number
title: string
completed: number | boolean // SQLiteから0/1で返る場合があるため
}
function App() {
const [todos, setTodos] = useState<Todo[]>([])
const [title, setTitle] = useState('')
// 1. 一覧取得 (GET)
const fetchTodos = async () => {
try {
const res = await fetch('http://localhost:8787/api/todos')
const data = await res.json()
setTodos(data)
} catch (error) {
console.error('取得エラー:', error)
}
}
useEffect(() => {
fetchTodos()
}, [])
// 2. 新規追加 (POST)
const handleAddTodo = async (e: React.FormEvent) => {
e.preventDefault()
if (!title.trim()) return
try {
await fetch('http://localhost:8787/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title }),
})
setTitle('')
fetchTodos()
} catch (error) {
console.error('追加エラー:', error)
}
}
// 3. 完了状態の切り替え (PUT)
const handleToggle = async (id: number, currentStatus: number | boolean) => {
try {
await fetch(`http://localhost:8787/api/todos/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed: !currentStatus }),
})
fetchTodos()
} catch (error) {
console.error('更新エラー:', error)
}
}
// 4. 削除 (DELETE)
const handleDelete = async (id: number) => {
try {
await fetch(`http://localhost:8787/api/todos/${id}`, {
method: 'DELETE',
})
fetchTodos()
} catch (error) {
console.error('削除エラー:', error)
}
}
return (
<div style={{ maxWidth: '500px', margin: '40px auto', padding: '0 20px', fontFamily: 'sans-serif', color: '#333' }}>
{/* ★タイトル部分:文字サイズを小さく (fontSize: '1.5rem')、折り返しを防ぐ (whiteSpace: 'nowrap') ★ */}
<h1 style={{ fontSize: '1.5rem', textAlign: 'center', marginBottom: '30px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
ToDoアプリ (React × D1)
</h1>
{/* 登録フォーム */}
<form onSubmit={handleAddTodo} style={{ display: 'flex', gap: '8px', marginBottom: '25px' }}>
<input
type="text"
placeholder="新しいタスクを入力..."
value={title}
onChange={(e) => setTitle(e.target.value)}
style={{ flex: 1, padding: '10px', border: '1px solid #ddd', borderRadius: '4px' }}
/>
<button type="submit" style={{ padding: '10px 20px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
追加
</button>
</form>
{/* タスク一覧 */}
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
{todos.map((todo) => {
const isCompleted = Boolean(todo.completed)
return (
<li
key={todo.id}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '15px 10px',
borderBottom: '1px solid #eee',
backgroundColor: isCompleted ? '#f9f9f9' : 'white'
}}
>
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', textDecoration: isCompleted ? 'line-through' : 'none', color: isCompleted ? '#888' : '#333', flex: 1, marginRight: '10px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<input
type="checkbox"
checked={isCompleted}
onChange={() => handleToggle(todo.id, isCompleted)}
style={{ marginRight: '12px', width: '18px', height: '18px', cursor: 'pointer' }}
/>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{todo.title}</span>
</label>
<button
onClick={() => handleDelete(todo.id)}
style={{ color: '#dc3545', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', padding: '5px 10px', fontSize: '0.9rem' }}
>
削除
</button>
</li>
)
})}
</ul>
</div>
)
}
export default App💡 コードの重要ポイント解説
- Boolean(todo.completed) による型変換 SQLite(D1)には真偽値(boolean)型が存在せず、0(false)または 1(true)の数値として保存されます。React側のチェックボックスで正しく判定させるため、Boolean() で真偽値に変換しています。
- 操作後の fetchTodos() 再実行 タスクの「追加」「更新」「削除」を行った直後に fetchTodos() を再度呼び出すことで、DBの最新状態を即座に画面へ反映させています。
6. ローカル環境での動作確認
両方のサーバーを別々のターミナルで起動します。
- バックエンド(Hono)を起動:
my-todo-api ディレクトリで実行します。
npx wrangler dev- フロントエンド(React)を起動:
my-todo-frontend ディレクトリで実行します。
npm run dev- ブラウザで確認: http://localhost:5173 にアクセスし、フォームからタスクを入力して「追加」を押すと、即座に一覧へ追加されることを確認します。
- 以下のようなフロントエンドがブラウザで確認できます。

6. まとめ
今回は、Reactから fetch APIを使ってCloudflare D1データベースと連携し、永続的なCRUD操作を行えるToDoアプリを構築しました。
- D1データベース: c.env.todo_db.prepare() で直感的なSQL操作が可能
- React連携: CORS許可と Boolean 型変換を押さえることでスムーズに疎通
次回(第3弾)予告: 次回は、今回作成したWebアプリを世界中に無料公開する 「Cloudflare Pages / Workers への一括デプロイ」 を解説します!お楽しみに!





コメント