本文へ移動

Hono スタック ​

Hono は簡単なことを簡単にし、難しいことも簡単にします。 JSON を返す用途だけでなく、REST API サーバーとクライアントを含むフルスタックアプリケーションの構築にも適しています。

RPC ​

Hono の RPC 機能を使うと、少しのコード変更で API 定義を共有できます。 hc が生成するクライアントは定義を読み取り、型安全にエンドポイントへアクセスします。

次のライブラリを組み合わせて実現します。

これらのコンポーネントの組み合わせを Hono スタックと呼ぶことができます。 このスタックで API サーバーとクライアントを作ってみましょう。

API の実装 ​

まず、GET リクエストを受け取り、JSON を返すエンドポイントを実装します。

ts
import { 
Hono
} from 'hono'
const
app
= new
Hono
()
app
.
get
('/hello', (
c
) => {
return
c
.
json
({
message
: `Hello!`,
}) })

Zod によるバリデーション ​

Zod でクエリパラメーターを検証し、その値を取得します。

ts
import { zValidator } from '@hono/zod-validator'
import * as z from 'zod'

app.get(
  '/hello',
  zValidator(
    'query',
    z.object({
      name: z.string(),
    })
  ),
  (c) => {
    const { name } = c.req.valid('query')
    return c.json({
      message: `Hello! ${name}`,
    })
  }
)

型の共有 ​

エンドポイントの型をエクスポートして、API 定義を公開します。

注意

RPC がルートを正しく推論できるように、対象となるすべてのメソッドをチェーンで呼び出し、宣言した変数からエンドポイントまたはアプリケーションの型を推論する必要があります。詳しくは RPC のベストプラクティスをご覧ください。

ts
const route = app.get(
  '/hello',
  zValidator(
    'query',
    z.object({
      name: z.string(),
    })
  ),
  (c) => {
    const { name } = c.req.valid('query')
    return c.json({
      message: `Hello! ${name}`,
    })
  }
)

export type AppType = typeof route

クライアント ​

次に、クライアント側を実装します。 AppType をジェネリック型引数として hc に渡し、クライアントオブジェクトを作ります。 すると、エンドポイントのパスやリクエスト型が自動補完されるようになります。

ts
import type { AppType } from './server'
import { hc } from 'hono/client'

const client = hc<AppType>('/api')
const res = await client.hello.$get({
  query: {
    name: 'Hono',
  },
})

返される Response は fetch API と互換性があり、json() で取得するデータにも型が付いています。

ts
const data = await res.json()
console.log(`${data.message}`)

API 定義を共有することで、サーバー側の変更をクライアントでも把握できます。

React との組み合わせ ​

React を使って Cloudflare Workers 向けのアプリケーションを作れます。

API サーバーの実装です。

ts
// src/index.ts
import { Hono } from 'hono'
import * as z from 'zod'
import { zValidator } from '@hono/zod-validator'

const schema = z.object({
  id: z.string(),
  title: z.string(),
})

type Todo = z.infer<typeof schema>

const todos: Todo[] = []

const api = new Hono()
  .post('/todo', zValidator('form', schema), (c) => {
    const todo = c.req.valid('form')
    todos.push(todo)
    return c.json({
      message: 'created!',
    })
  })
  .get('/todo', (c) => {
    return c.json({
      todos,
    })
  })

export type AppType = typeof api

const app = new Hono()
app.route('/api', api)

export default app

React と React Query を使ったクライアントです。

tsx
// src/App.tsx
import {
  useQuery,
  useMutation,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'
import type { AppType } from '../functions/api/[[route]]'
import { hc, InferResponseType, InferRequestType } from 'hono/client'

const queryClient = new QueryClient()
const client = hc<AppType>('/api')

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Todos />
    </QueryClientProvider>
  )
}

const Todos = () => {
  const query = useQuery({
    queryKey: ['todos'],
    queryFn: async () => {
      const res = await client.todo.$get()
      return await res.json()
    },
  })

  const $post = client.todo.$post

  const mutation = useMutation<
    InferResponseType<typeof $post>,
    Error,
    InferRequestType<typeof $post>['form']
  >({
    mutationFn: async (todo) => {
      const res = await $post({
        form: todo,
      })
      return await res.json()
    },
    onSuccess: async () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
    onError: (error) => {
      console.log(error)
    },
  })

  return (
    <div>
      <button
        onClick={() => {
          mutation.mutate({
            id: Date.now().toString(),
            title: 'Write code',
          })
        }}
      >
        Add Todo
      </button>

      <ul>
        {query.data?.todos.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </div>
  )
}

MIT ライセンスで公開されています。