Hono + Cloudflare + ClerkでAPIバックエンド作りました。【前編】
以前に、React+TypeScriptでドリル管理アプリをつくりました。
ただし、このアプリのデータ永続化は、localStorageで実施、のちに、FirebaseかSupabaseでバックエンド対応しようとおもっていたのですが、最近、Hono + Cloudflare の構成が流行っているとか、Full-Stack TypeScriptというキーワードもでてきたので、バックエンドの学習も兼ねて、Full-Stack TypeScript的な構成ででバックエンドも構築してみようと思いました。
(筆者はtoC向けサービスのフロントエンドエンジニアです。React,TypeScriptを使用しています)
技術構成
今回バックエンドAPI構築で使用した、フレームワーク、サービスは以下の通り
- Hono
- Cloudflare workers
- Cloudflare D1 (sqlite)
- Drizzle ORM
- Clerk (Auth)
デプロイ先
バックエンド:Cloudflare
フロントエンド:Vercel
アプリ(React+vite)
ソースコード
前述した、バックエンドとフロントエンドは、別々のサーバーにデプロイしましたので、
CORS対応しました。
Step1:データベース構築(sqlite)
テーブルスキーマ
(localStorageの置き換えなので、とてもシンプルです。)
import { sql } from "drizzle-orm";
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
// drillsテーブル
export const drills = sqliteTable("drills", {
id: integer("id").primaryKey({ autoIncrement: true }),
userId: text("user_id").notNull(),
columnId: text("column_id").notNull(),
content: text("content").notNull(),
url: text("url").notNull(),
status: integer("status").notNull().default(sql`0`), // sqliteはboolean使えないので、0 or 1で管理
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
});
// historyテーブル
export const history = sqliteTable('history', {
id: integer('id').primaryKey({ autoIncrement: true }),
userId: text('user_id').notNull(),
memo: text('memo').notNull(),
drills: text('drills').notNull(), // JSON文字列として保存
createdAt: text('created_at').notNull()
.$defaultFn(() => {
const now = new Date();
return now.toISOString().slice(0, 16).replace('T', ' '); // 'YYYY-MM-DD HH:MM' 形式
}),
});
DB 作成手順
1)環境設定、DB作成
- D1 DB作成コマンド
- wrangler.toml設定
- Bindingsの登録
- drizzle install
- drizzle config 設定
2)上記、テーブル情報をもとに、schema.tsを作成
3)migration file 作成
"generate": "drizzle-kit generate",
localDB環境構築
1)migration local
"migrate:local": "wrangler d1 migrations apply drill-app-db --local"
2)ダミーデータ作成&ローカルDBに登録
- db/dummy-data.sql
- "seed:local": "npx wrangler d1 execute drill-app-db --local --file=./db/dummy-data.sql",
- ローカルでのデータ参照
- 実態のファイルパス:.wrangler/state/v3/d1/miniflare-D1DatabaseObject- DB Browser for sqlite
データ参照できました。
RemoteDB構築
1)migration remote
"migrate:remote": "wrangler d1 migrations apply drill-app-db --remote",
2)ダミーデータをremote dbに登録
"seed:remote": "npx wrangler d1 execute drill-app-db --remote --file=./db/dummy-data.sql"
Cloudflareコンソールから、データ参照できました。
API構築(CORSと認証)
・ライブラリインストール
$ npm i hono
$ npm i @clerk/backend @clerk/clerk-sdk-node @hono/clerk-auth
・環境変数の設定(ClerkのKey情報の登録)は後半の記事で解説予定です。
・CORS設定とClerk認証のmiddleware設定を行います。
・drill apiエンドポイントでは、クライアントから送られたトークン情報から、ユーザーを判定し、該当ユーザーの情報のみをresponseとして返すようにしています。
Backendのコード
import { Hono } from "hono";
import { drizzle } from "drizzle-orm/d1";
import { drills, history } from "./../db/schema";
import { eq, sql, and } from "drizzle-orm";
import { cors } from 'hono/cors';
import { clerkMiddleware, getAuth } from "@hono/clerk-auth";
import { CORS_ORIGIN } from "./const";
import { HistoryEntry } from './types'
type Bindings = {
DB: D1Database;
CLERK_PUBLISHABLE_KEY: string;
CLERK_SECRET_KEY: string;
CORS_ORIGIN: string;
};
const app = new Hono<{ Bindings: Bindings }>().basePath("/api");
// origin ドメインを明示的に示すもの。はずすとCORSエラーになる。
app.use('*', async (c, next) => {
const corsMiddlewareHandler = cors({
origin: [...CORS_ORIGIN],
})
return corsMiddlewareHandler(c, next)
})
app.options('*', (c) => {
return new Response(null, { status: 204 });
})
app.use('/api/*', cors({
origin: [...CORS_ORIGIN], // Reactアプリのオリジン
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization', 'Access-Control-Allow-Origin'],
exposeHeaders: [],
maxAge: 600,// preflight requestのキャッシュ時間
credentials: true,
}));
app.use('*', clerkMiddleware())
// drills api
app.get("/drills", async (c) => {
const auth = getAuth(c)
if (!auth?.userId) {
return c.json({
message: 'You are not logged in.',
})
}
try {
const db = drizzle(c.env.DB);
const results = await db.select().from(drills).where(eq(drills.userId, auth.userId));;
return c.json(results);
} catch (e) {
return c.json({ err: e }, 500);
}
});
// 以下省略
Frontendのコード
API通信用のコード (初期段階のコード。最終的にはカスタムHookにまとめるなど書き換えました。)
Clerkのtoken情報を、headerに埋め込んで、APIにアクセスします。
// API related code
useEffect(() => {
const fetchData = async () => {
const response = await fetch(`${API_BASE_URL}/drills`, {
method:"GET",
mode: 'cors',
headers: {
'Authorization': `Bearer ${await getToken()}`,
'content-type': 'application/json'
},
credentials: "same-origin"
});
const data = await response.json();
console.log("data",data)
}
fetchData();
}, []);
【後編】 の記事内容 (作成中、 12月公開予定)
- Step3 Clerkの設定とFrontend
- Clerk設定
- frontend code
- まとめと振り返り。
参考資料 (動画、記事)
バックエンドについては、RDBMSのリレーション、正規化など初歩的な部分の理解はありましたが、generate、migrateについては、よくわかっていませんでした。
いきなり公式ドキュメントを読んでも、わかりにくいところがあったので、動画などで概要を理解し、次に詳細ドキュメントを確認するステップをとりました。
日本語
【Cloudflare Workers】Hono+Cloudflare D1+Drizzle ORM 30分でREST APIを作成
AIの合成音声で作成された解説動画、わかりやすく構成されている。概要欄にソースコードへのリンクあり。
【Cloudflare Workers】HonoとCloudflare D1を使って20分でREST APIを作成する
上記と同じシリーズ。似たような動画を何度もみて理解を深めました。
記事:Cloudflare Workers から D1 を操作する
英語
How to configure Drizzle ORM (sqlite3) with NextJS 14
Serverless API with Cloudflare Workers (Hono, D1 & Drizzle ORM)
概要欄にソースコードへのリンクあり。
Easiest Database Setup in Next.js With Drizzle ORM & Turso?
概要欄にソースコードへのリンクあり。
Cloudflare Workers with Bun, Hono and D1, everything you need to know
Сloudflare D1 - everything you need to know (API, local development, testing, migrations)
Discussion