📷

Cloudflare Workers + D1 向け Hono/Drizzle 薄ラッパー @nanokajs/core を作った

に公開

はじめに:なぜ "nanoka" という名前なのか

G.W.が明けた。現実が戻ってきた。

ライブラリの名前を決めるとき、たいてい「かっこいいエンジニアっぽい単語」か「アルファベットの略称」に落ち着く。だが今回は違うアプローチをとった。

名前の元ネタは、崩壊:スターレイルのピノコニー編に登場するキャラクター、サンデーの思想だ。

「誰も悲しまない楽園。週休7日制が理想の世の中」

©HoYoverse

ピノコニー編をプレイした人には、この言葉の受け取り方がいろいろあると思う。それはぜひ自分でたしかめてほしい。

ただ、Webアプリのルーティングを書くたびに「スキーマ定義 → 型生成 → バリデーション → クエリ → またマイグレーション」という儀式を繰り返すエンジニアには、週休7日という響きは刺さるものがあるのではないだろうか。

最初はそのまま sunday という名前にしようと思ったのだが……さすがにあからさますぎる。そこで「週休7日」から「七日(なのか)」をとって nanoka と命名した。

ちなみに、このライブラリが内包する Hono も「炎」という日本語をそのままローマ字にした名前だ。nanoka(七日)も同じく日本語のローマ字読み。意図してそう合わせた。先人へのリスペクトである。

なお、崩壊:スターレイルには「三月なのか」という超絶美少女キャラが存在するが、ネーミングがたまたま似ただけであり一切関係ない。


nanoka とは何か

@nanokajs/core は、Hono + Drizzle + Zod を毎回手で繋ぐ儀式を削るための薄いラッパーだ。

npm install @nanokajs/core

コアの思想は「モデル定義を DB スキーマと型の中核に置く」こと。ただしそれが API 入出力と完全に一致する、というナイーブな前提は捨てている。passwordHash は DB に存在するがレスポンスには絶対に出してはいけない、というような「DB と API が意図的に乖離する場面」は必ずある。だから 80% 自動、20% 明示 を設計方針とした。

import { nanoka, d1Adapter, t } from '@nanokajs/core'

const app = nanoka(d1Adapter(env.DB))

const User = app.model('users', {
  id:           t.uuid().primary(),
  name:         t.string(),
  email:        t.string().email(),
  passwordHash: t.string(),
})

// omit でレスポンスから除外するのは「明示的に書く」
app.get('/users', async (c) => {
  const users = await User.findMany({ limit: 20, offset: 0, orderBy: 'id' })
  return c.json(users)
})

app.post('/users', User.validator('json', { omit: f => [f.passwordHash] }), async (c) => {
  const data = c.req.valid('json')
  const user = await User.create(data)
  return c.json(user, 201)
})

export default app

Hono も Drizzle も捨てていない。生の Drizzle クエリが書きたければ app.db でエスケープハッチが常に開いている。


現状できること

  • モデル定義 → DB スキーマ・TypeScript 型・Zod バリデーターが自動派生
  • CRUD メソッド: findMany(limit 必須)、findOnecreateupdatedelete
  • バリデーター: User.validator('json', { pick/omit }) で Hono validator として直接使える
  • OpenAPI 自動生成: app.openapi(metadata) + app.generateOpenAPISpec()、Swagger UI 同梱
  • アダプター: Cloudflare D1 ファーストクラス、Turso/libSQL も @nanokajs/core/turso で対応
  • CLI: create-nanoka-app でプロジェクトの足場を一発生成
  • Relations: t.hasMany() / t.belongsTo() を絶賛実装中

次のロードマップ:週休7日への道はまだ途中

1. @nanokajs/auth ――認証もオールインワンで

個人開発・スモールチームがいちばん「毎回書きたくない」と感じるのが、認証まわりだ。hono/jwt とパスワードハッシュと Drizzle をまた手で繋ぐのか、という徒労感。

そこで @nanokajs/core 本体を汚さず、必要なプロジェクトだけ追加できる独立パッケージとして @nanokajs/auth を計画している(Issue #74 参照)。

import { createAuth } from '@nanokajs/auth'

const auth = createAuth({
  model: User,
  secret: c.env.JWT_SECRET,
  fields: { identifier: 'email', password: 'passwordHash' },
})

// ログインハンドラー、JWT ミドルウェア、リフレッシュトークンが即使える
app.post('/auth/login',   auth.loginHandler())
app.post('/auth/refresh', auth.refreshHandler())
app.use('/api/*',         auth.middleware())

デフォルト実装は Web Crypto API(PBKDF2)ベースで zero-dependency。Cloudflare Workers でも Node.js でも動く。argon2 のようなカスタム実装に差し替えるための hasher インターフェースも用意する予定だ。

パスワード + JWT というよくあるフローを、モデル定義と一行のセットアップで使えるようにしたい。

2. ElysiaJS 対応

崩壊:スターレイルに三月なのかというキャラクターがいる。では崩壊3rdには? そう、エリシアがいる。

それだけの理由で、筆者は ElysiaJS への対応を個人的にやりたいと思っている。ユーザーからのニーズは特にない。完全に筆者の趣味である。


まとめ

nanoka はまだ若いライブラリだ。G.W.は終わり、週休7日の楽園はまだ遠い。

でも、Hono + Drizzle + Zod を毎回手で繋ぐ という現状から、モデルを書けば型もバリデーションも CRUD も OpenAPI も出てくる という世界へ向けて、着実に歩みを進めている。

興味を持ってもらえたら、ぜひ試してみてほしい。Issues も PR も大歓迎だ。そしてできれば、週休7日の楽園を一緒に建設しよう。

npm create nanoka-app@latest <dir-name>

サンデーはいいぞ

Discussion