🔥

[Hono🔥] Next.js + HonoでジャックしたNext.jsのモノレポ構成で楽な運用と開発体験を両立させる!

に公開

こんにちは、ゲンマイです!
最近は生成AIの力をフル活用しつつ、爆速でFull-Stack TypeScriptなプロダクトを作る方法を模索しています💪

概要

最近のNext.jsはServer ActionsやRoute Handlersなどサーバー側の機能が次々と強化されています。これらの便利な機能を使いこなしたいところですが、全てをNext.js一本で実装しようとすると、どうしても設計が複雑になりがちです...。またフロントエンド/バックエンドを同時に扱えるのは便利である一方、開発をしているとコンテキストの境界がどこにあるのか認識するコストが高いという意見を耳にすることがあります。

この記事では、Next.jsのRoute HandlersをHonoで乗っ取って作ったAPIサーバーをバックエンド、シンプルなNext.jsアプリケーションをフロントエンドとして構築し、Bunのworkspace機能でこれらを管理する方法を紹介します。この方法ならフロントとバックを明確に分離しながらもどちらも簡単にVercelにデプロイできるようになります!

具体例として、/helloからJSONを取得して表示するだけのシンプルなアプリケーションをVercelにデプロイするまでの流れを解説していきます!

hono-nextjs-monorepo-sample
├── backend   # Next.jsだけど中身はほぼHono
└── frontend  # Next.js

①rootの設定

まずはrootにpackage.jsonを作成し、Bunのworkspace設定を行います

package.json
{
  "name": "hono-nextjs-monorepo-sample",
  "private": true,
  "workspaces": [
    "frontend",
    "backend"
  ]
}

workspacesにfrontend, backendを指定することでBunがpackageとして認識してくれます。
packagesディレクトリを切ってそこにパッケージを追加して管理するのが一般的らしいですがディレクトリ構造は浅い方が好みなのでこの記事ではroot直下にbackendとfrontendを作成していきます。

Bunのworkspace機能の詳細はこちらを確認して下さい
https://bun.sh/docs/install/workspaces

.gitignoreを作ってnode_modulesを指定しておきましょう

# .gitignore
node_modules/

② backendの作成

バックエンド用のNext.jsプロジェクトをcreate next-appで作成します。
--apiフラグを指定することで、ページ表示に関するファイルが生成されなくなるため便利です!
それ以外のオプション指定は、Yes/No形式で回答できるものなので指定しなくても大丈夫です。

bun create next-app@latest backend --api --ts --no-eslint --src-dir --no-turbopack --app --use-bun --import-alias "@/*"

各オプションとその内容は以下の通りです、好みに応じて変更して下さい!

オプション 内容
--ts TypeScriptを利用
--no-eslint ESLintを利用しない
--src-dir src/ ディレクトリを採用
--no-turbopack Turbopackを無効化
--app App Routerを利用
--use-bun パッケージマネージャーにBunを使用
--import-alias "@/*" @/src/ へのエイリアスとして設定

backendに移動してhonoを追加します

cd backend

bun add hono

次にsrc/app/[[...route]]/route.ts を作成します。
(src/app配下に出来る初期ファイルは全て削除して問題ないです。)

[[...route]]としてディレクトリを指定することで、Optional Catch-all Segments という機能で、すべてのルーティングをこの単一ファイルで受けられるようになります。

Catch-all Segmentsを利用したルーティングの設定方法に関してはこちらの記事を参考にしました🙏
https://zenn.dev/chot/articles/e109287414eb8c

Honoのドキュメントでも同様の方法が紹介されています
https://hono.dev/docs/getting-started/vercel

こうすることでリクエストを受けた後に、その後の処理を全てHonoに移譲できます。
/helloに{ "message": "Hello from Hono🔥" }という簡単なJSONを返す処理を実装します。

route.ts
import { Hono } from "hono";
import { cors } from "hono/cors";
import { handle } from "hono/vercel";

export const runtime = "edge";

const app = new Hono()
  .use(
    "*",
    cors({
      origin: "*",  // 本番環境では正しくoriginを設定する
      allowMethods: ["POST", "GET", "OPTIONS"],
    })
  )
  .get("/hello", (c) => {
    return c.json({
      message: "Hello from Hono🔥",
    });
  });

export type AppType = typeof app;

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

ローカル環境では8080番で起動するように変更しておきます

package.json
   "scripts": {
-    "dev": "next dev",
+    "dev": "next dev --port 8080",
     "build": "next build",
     "start": "next start"
   },

bun devを実行後に localhost:8080/hello からメッセージが返ってくるとokです

create honoを利用する方法

今回はcreate next-appでbackendを作成しましたが、create honoを実行後にNext.jsのテンプレートを選択することでもほぼ同じものが作れます。ただし描画に関連するライブラリ(react, react-dom)も含まれてしまうのでcreate next-appで--apiフラグを使って作る方が楽かもしれません。

backendは以上です!

③ frontendの作成

rootに戻ってfrontendを作成します!今回はTailwind CSSを利用します

bun create next-app@latest frontend --ts --no-eslint --tailwind --src-dir --no-turbopack --app --use-bun --import-alias "@/*"

Hono RPC機能を使うためにhonoとbackendの依存関係を追加します!

cd frontend

bun add hono backend

Hono RPCについてはHono作者のyusukebeさんの記事を確認して下さい
https://zenn.dev/yusukebe/articles/a00721f8b3b92e

libディレクトリにhono/clientを使ったAPIクライアントを簡単に実装します

まず.envにAPIのURL(NEXT_PUBLIC_API_URL)を設定します

.env
NEXT_PUBLIC_API_URL=http://localhost:8080
frontend/src/lib/client.ts
import { hc } from "hono/client";
import type { AppType } from "backend/src/app/[[...route]]/route";

export const apiClient = hc<AppType>(process.env.NEXT_PUBLIC_API_URL!);

既存のfrontend/src/app/page.tsxを/helloから返ってきたJSONを表示するだけのシンプルな処理に置き換えます

frontend/src/app/page.tsx
import { apiClient } from "@/lib/client";

export default async function Home() {
  const res = await apiClient.hello.$get();
  const data = await res.json();

  return (
    <div className="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900">
      <h1 className="text-6xl font-bold">
        {data.message}
      </h1>
    </div>
  );
}

bun dev でサーバーを起動した後にlocalhost:3000で以下のように表示されればokです!

以上でfrontend, backendが完成したのでGitHubにあげましょう
ここでは省略します!

④ vercelへデプロイ

次に完成したものをvercelへデプロイしていきます。

https://vercel.com/new

Import Git Repositoryから先ほどgithubに挙げたもの選択してImportします

まずはbackendから設定します
root directoryとしてbackendを選択してdeployを押します!

続いて、frontendのデプロイも同じように行います。

NEXT_PUBLIC_API_URLの環境変数の設定しましょう

monorepoをvercelでデプロイした時に与えられるデフォルトのURLは(多分)https://<repo name>-<subdirectory>.vercel.app なので、今回作成したbackendであれば https://hono-nextjs-monorepo-sample-backend.vercel.app になります。正確なURLは先ほどデプロイしたプロジェクトから確認しましょう。

デプロイされたURLでローカル環境と同じくHello from Hono🔥が確認できれば成功です!

⑤ 終わり

frontendとbackendを明確に分けつつ、どちらもvercelへ簡単にデプロイできるようになりました。
またHonoからNext.jsへの依存も最小限なのでHono単体で使いたくなった場合も簡単にNextから引き剥がすことができます。

個人的には規模が大きくなった場合でも快適に開発できる上にデプロイ運用もやりやすい良い設計だと感じでいます!もしより良い方法や疑問点があればコメントで教えて下さい!

参考

Honoのディレクトリ構成については以下の記事が参考になります
https://zenn.dev/yodaka/articles/ad49f29a54ceba

Discussion