Next.jsのAPIにHonoのRPCを統一的に使う

に公開

はじめに

  • Next.js の API に Hono の RPCを使いたい
  • 最初はtRPCを使おうと思ったが、Honoを使ってみたかったのでHonoを採用
  • コードはgithub にアップロードしています

https://github.com/FatRicePaddyyyy/nextjs-hono-rpc

ファイル構成

src/
├── app
│   ├── api
│   │   ├── [[...route]]
│   │   │   ├── route.ts // プロシージャーをAPIとして公開する
│   │   │   ├── hello.ts // プロシージャーの一つ
│   │   │   ├── weather.ts // プロシージャーの一つ
├── client.ts // プロジージャーを呼ぶためのクライアントの集まり
├── page.tsx

コード

インストール

npm install hono
npm install @hono/zod-validato
npm install zod

src/app/api/[[...route]]/hello.ts

import { Hono } from "hono";
import { z } from "zod";
import { zValidator } from "@hono/zod-validator";

const inputSchema = z.object({
  name: z.string(),
});

export const app = new Hono().post(
  "/",
  zValidator("json", inputSchema),
  async (c) => {
    try {
      const { name } = c.req.valid("json");
      console.log(name);
      return c.json({ message: `Hello, ${name}!` });
    } catch (error) {
      console.error(error);
      return c.json({ message: "Bad Request" }, 400);
    }
  },
);

  • name を引数にし、Hello, ${name}! を返すプロシージャーを定義しています。
  • 入力をzodでバリデーションしてます。zodエラーが起こると、ステータスコードが400で返ります。

src/app/api/[[...route]]/weather.ts

import { Hono } from "hono";

export const app = new Hono().get("/", async (c) => {
  return c.json({ message: `今日の天気は晴れだよ。` });
});

  • 今日の天気を返すプロシージャーです。データを取得するだけなので,get メソッドで定義しています。

src/app/api/[[...route]]/route.ts

import { Hono } from "hono";
import { handle } from "hono/vercel";
import { app as hello } from "./hello";
import { app as weather } from "./weather";
// basePath は API ルートのベースパスを指定します
// 以降、新たに生やす API ルートはこのパスを基準に追加されます
const app = new Hono().basePath("/api");

export const helloRoute = app.route("/hello", hello);

export const weatherRoute = app.route("/weather", weather);

export type AppType = typeof app;

export const GET = handle(app);
export const POST = handle(app);

  • ここで、プロシージャーをエンドポイントとして登録します。

src/app/client.ts

import type { InferResponseType } from "hono/client";
import { hc } from "hono/client";
import { helloRoute, weatherRoute } from "./api/[[...route]]/route";

const baseUrl = "http://localhost:3000";

const helloClient = hc<typeof helloRoute>(baseUrl);
export const hello = helloClient.api.hello.$post;
export type HelloResType = InferResponseType<typeof hello, 200>;
export type HelloReqErrType = InferResponseType<typeof hello, 400>;

const weatherClient = hc<typeof weatherRoute>(baseUrl);
export const weather = weatherClient.api.weather.$get;
export type WeatherResType = InferResponseType<typeof weather, 200>;

  • 登録したプロシージャーを型付きで呼び出すためのクライアントを定義しています。

src/app/page.tsx

import { hello, weather } from "./client";

export default async function Home() {
  const res = await hello({ json: { name: "John" } });
  const data = await res.json();
  console.log(data.message);
  const res2 = await weather();
  const data2 = await res2.json();
  console.log(data2.message);
  return (
    <div>
      はろー
    </div>
  );
}

  • 定義したプロシージャーを実際に呼び出してみましょう。

最後に

  • とりあえず、以下はできるようになりました。
    • 一つ一つのプロシージャーの処理を別のファイルに記述
    • プロシージャーをまとめてルーティング
    • プロシージャーを呼び出すためのクライアントの統一
  • ただ、これがベストプラクティスかどうかは怪しいところもあると思うので、なにかあればコメントしていただきたいです。

Discussion