1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

@hono/zod-openapi、さいっこーーー!!!!

Posted at

はじめに

やばい!忘れないうちに書くぞ!@hono/zod-openapiを山ほどカスタマイズしたら開発体験が本当に超気持ちよくなったので共有します。「超気持ちいい」といえばアテネ五輪の北島康介ですね、最近不倫してました。なんか文春でかなりダルい感じの記事にされてましたね、あれ読み応えあったな。いや北島康介の不倫の話は今いいか。

とにかく、HonoとZodでOpenAPI付きのバックエンドを書く体験を最高にした話をします。

注意
この記事はどちらかというと読み物として面白くしているので、記事を読むのが面倒くさい人は以下のリポジトリを先に読んでください。
https://github.com/watabegg/hono-template

えーHono RPCあるからOpenAPIとか要らないと思う場合も無視で結構です。

@hono/zod-openapiについて

公式ドキュメントによるとこうらしいです。

Zod OpenAPI Hono is an extended Hono class that supports OpenAPI. With it, you can validate values and types using Zod and generate OpenAPI Swagger documentation. On this website, only basic usage is shown.

あんまり英語に明るくないんですがHonoのClassをOpenAPI用に拡張してくれて、しかもZodでバリデートも出来ちゃう上、OpenAPIのドキュメントを生成してくれるらしい。明るくなくても読みやすい英語でした。

書き方は、公式ドキュメントをちょっと変えて、明示的にファイルを分けるとこんな感じで出来ます。

schema.ts
import { z } from '@hono/zod-openapi'

export const ParamsSchema = z.object({
  id: z
    .string()
    .min(3)
    .openapi({
      param: {
        name: 'id',
        in: 'path',
      },
      example: '1212121',
    }),
})

export const UserSchema = z
  .object({
    id: z.string().openapi({
      example: '123',
    }),
    name: z.string().openapi({
      example: 'John Doe',
    }),
    age: z.number().openapi({
      example: 42,
    }),
  })
  .openapi('User')
route.ts
import { createRoute } from '@hono/zod-openapi'
import { ParamsSchema, UserSchema } from './schema'

export const route = createRoute({
  method: 'get',
  path: '/users/{id}',
  request: {
    params: ParamsSchema,
  },
  responses: {
    200: {
      content: {
        'application/json': {
          schema: UserSchema,
        },
      },
      description: 'Retrieve the user',
    },
  },
})
index.ts
import { OpenAPIHono } from '@hono/zod-openapi'
import { route } from './route'

const app = new OpenAPIHono()

app.openapi(route, (c) => {
  const { id } = c.req.valid('param')
  return c.json({
    id,
    age: 20,
    name: 'Ultra-man',
  })
})

// The OpenAPI documentation will be available at /doc
app.doc('/doc', {
  openapi: '3.0.0',
  info: {
    version: '1.0.0',
    title: 'My API',
  },
})

はい、こう書くだけでhttp://localhost:8787/docにアクセスするとOpenAPIを見ることができてしまうわけです。
OpenAPIを自動生成みたいなフレームワークは、もうみなさんご存じの通り世の中に跋扈しているわけですが、Honoのようなシンプルで軽量なフレームワークでも手軽にこれができるというのがうれしいところです。

でも...

上記のコードを見て、実際に@hono/zod-openapiを触ってみて、じゃあこれを使ってバックエンドを書こう!となると、ちょっと話が変わってきます。このままだと見通しが悪くなってしまうんですよね。

Honoの良さはとてもシンプルなところです。最低限

app.get('/api/hello', (c) => {
  return c.json({
    ok: true,
    message: 'Hello Hono!',
  })
})

とか書けばもうAPIとしては十分ですから本当はこんな書きぶりで行きたいです。
むしろrouteはシンプルに行きたいところです。ロジックなど書きたくありませんから

import { Hono, type Context } from "hono"

const root = (async (c: Context) => {
  return c.json({
    ok: true,
    message: 'Hello Hono!',
  })
})

const app = new Hono()

app.get('/', root)

とか!ロジックの分離をしたいです。生身のHonoならこれで十分型補間が効きます。

やっと本題

では同じHonoでも@hono/zod-openapiで同じことをしてみます(APIの中身違うけど)。

index.ts
import { OpenAPIHono, type RouteHandler } from '@hono/zod-openapi'
import { route } from './route'

const root:RouteHandler<typeof route> = (async (c) => {
  const { id } = c.req.valid('param')
  return c.json({
    id,
    age: 20,
    name: 'Ultra-man',
  })
})

const app = new OpenAPIHono()

app.openapi(route, root)

こんな感じ。
注意が必要なのはハンドラを定義する際に、その関数の返り値がルート定義からジェネレートしたRouteHandlerにしなければならないところです。これがないとContextの型エラーが面倒くさいし、型補間が効かなくて不便です。

ただ運用上では環境変数とかまあなんかをContextとして持たせることが多いと思います。RouteHandlerは環境変数定義も引数に持つので、実用上は

import type { RouteConfig, RouteHandler } from '@hono/zod-openapi'
import type Env from '@/config/env' // 適宜

export const handler = <R extends RouteConfig>( h: RouteHandler<R, Env> ) => h

このように関数としてラップして

index.ts
import { OpenAPIHono } from '@hono/zod-openapi'
import { handler } from '../config'
import { route } from './route'

const root = handler<typeof route>(async (c) => {
  const { id } = c.req.valid('param')
  return c.json({
    id,
    age: 20,
    name: 'Ultra-man',
  })
})

const app = new OpenAPIHono()

app.openapi(route, root)

とすると、中~大規模のプロジェクトでも耐えうる設計にできます。
この状態で!実際のバックエンドを想定した設計にしたテンプレートがさっきのリポジトリだったわけです。

@hono/zod-openapiのテンプレート

さきほども出しましたが、これがバックエンドのテンプレートになります。
https://github.com/watabegg/hono-template

READMEにもありますがディレクトリ構造が

src
├── config // 設定ファイル
├── db // dbとりまとめ
│   ├── index.ts
│   └── schema.ts
├── features
│   └── articles
│       ├── routes.ts // openapi.tsとhandlers.tsをとりまとめた薄いルーター
│       ├── handlers.ts // service.tsから@hono/zod-openapiのためのハンドラ設計
│       ├── openapi.ts // schema.tsから@hono/zod-openapiのためのルート設定
│       ├── service.ts // アプリケーションサービス、ユースケースとDTO
│       ├── repository.ts // ドメインリポジトリ、DB操作
│       ├── schema.ts // Req, Resのスキーマ定義、OpenAPIの軽い例
│       └── types.ts // drizzleスキーマから型生成したりとか
├── index.ts
├── lib
│   ├── auth // Auth.jsの設定とヘルパー関数とりまとめ
│   ├── http // レスポンスユーティリティとりまとめ
│   ├── openapi 
│   │   ├── index.ts // OpenAPIの設定とりまとめ
│   │   └── tags.ts
│   └── types.ts
└── middleware
    ├── auth.ts // 認証Middleware
    └── db.ts // HonoのContextにdb注入するようmiddleware

となっています。packeage by featureと偽物クリーンもどきアーキテクチャを採用してみましたが、これが手軽なのにめちゃくちゃいい。
開発速度も速くできるし、見通しが立てやすいし、あとGitHub Copilotの補完が一番よく働いてくれる気がします。Copilotが補完してくれる瞬間がマジで一番気持ち良い。

まあこのリポジトリはかなり雑に、これを書きながら修正してたので、serviceとかめちゃくちゃ薄いし、あとこれなら全然featureで分けずにlayerでもいいと思います。てかバックエンドってlayerのイメージあるから僕がおかしいか、まあいいか

終わりに

以上です。Next.jsで作ったフルスタックのプロジェクトが遅すぎて手を出したHonoですが、速いし開発しやすいし本当にうれしいです。その内RPCにも手を出そうと思います。マジで楽しいから

以上、@hono/zod-openapiでのバックエンド開発、マジで最高です。という話でした。

じゃあみんなでせーの! 『『なんも言えねぇ~!!!』』

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?