🐈

Hono + Cloudflare + ClerkでAPIバックエンド作りました。【前編】

2024/11/16に公開

以前に、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)

https://dnd-drill-api-client.vercel.app/

ソースコード

https://github.com/IoT-Arduino/dnd-drill-api
https://github.com/IoT-Arduino/dnd-drill-api-client

前述した、バックエンドとフロントエンドは、別々のサーバーにデプロイしましたので、
CORS対応しました。

Step1:データベース構築(sqlite)

テーブルスキーマ

(localStorageの置き換えなので、とてもシンプルです。)

/db/schema.ts
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

データ参照できました。

sqliteLocal

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コンソールから、データ参照できました。

D1-table

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のコード
/src/index.ts
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にアクセスします。

/src/index.tsx
// 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