TL:DR
- Hono Web APIサーバーとReactフロントエンドを、Viteを使わずBun単体でまとめる最小構成ハンズオンを紹介
- Bunの
Bun.serve()はroutesにHTML importを渡すだけでフロントエンドを自動バンドル・HMRできる -
hc<AppType>()によるHonoのRPCクライアントで、サーバーのルート定義からフロントエンドの型を自動導出 -
bun build --compileでBunランタイムを同梱した単一実行ファイルまで作れる
対象読者
- Hono・Reactを触ったことがある、またはこれから触り始める
- TypeScriptの基本文法がわかる
- Bunの基本操作(
bun installなど)ができる
本記事の執筆には生成AIを利用しています
Hono + Reactの構成というと、フロントエンドのビルド・開発サーバーとしてViteを組み合わせるのが定番です。
ただ、最小構成のサンプルやプロトタイプ用途では、Vite一式(vite, @vitejs/plugin-react, vite.config.ts)を足すだけでも設定項目が増えてしまいます。
BunにはBun.serve()のHTML importという機能があり、HTMLファイルをそのままサーバーのroutesに渡すだけで、JSX/TSXのトランスパイル・バンドル・開発時のホットリロードまで面倒を見てくれます。
この記事では、この機能を使ってHono Web APIサーバーとReactフロントエンドを1プロセス・Bunオンリーで動かす最小構成を組み立てます。
-
依存が少ない:
viteや@vitejs/plugin-reactが不要 - 1プロセスで完結: 開発時にWeb APIサーバーとフロントエンドを別々に起動する必要がない
-
型安全なRPC: Honoの
hc<AppType>()でサーバーのルート定義からクライアントの型を自動生成 -
クロスプラットフォームな配布:
bun build --compileはWindows・macOS・Linux向けにそれぞれ単一実行ファイルを作れるため、Web APIサーバーとGUI(ブラウザ表示のフロントエンド)を1つのバイナリにまとめてOSを問わず配布できる
Step 1: プロジェクトを作る
mkdir hono-react-mvp && cd hono-react-mvp
bun init -y
bun add hono react react-dom
bun add -d @types/bun @types/react @types/react-dom
Step 2: Honoの最小Web APIサーバーを書く
src/server.tsにHonoアプリを定義します。ルート定義をチェーンしてappに代入することで、typeof appからルート情報付きの型(AppType)を導出できるようにしておきます。
src/server.ts
import { Hono } from "hono";
import index from "./index.html";
const app = new Hono().get("/api/v1/hello", (c) => {
return c.json({ message: "Hello Hono API!" });
});
export type AppType = typeof app;
export default {
fetch: app.fetch,
routes: {
"/": index,
},
};
./index.htmlをそのままimportしている点がポイントです。BunはこれをHTMLBundleとして扱い、内部のJS/CSSを自動でバンドルします。routesのキーはURLパス、値にはHTMLBundleオブジェクトを渡します。
Step 3: Reactフロントエンドを書く
src/index.htmlはエントリーポイントです。<script type="module">からTSXファイルを読み込みます。
src/index.html
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hono React MVP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
src/main.tsxでReactをマウントします。
src/main.tsx
import { createRoot } from "react-dom/client";
import App from "./App";
const rootElement = document.getElementById("root");
if (!rootElement) {
throw new Error('Root element "root" not found');
}
createRoot(rootElement).render(<App />);
src/App.tsxでは、Honoのhc()でRPCクライアントを作り、AppTypeを型引数に渡します。こうするとサーバー側のルート定義がそのままクライアントのメソッドチェーンとして補完されます。
src/App.tsx
import { hc } from "hono/client";
import { useState } from "react";
import type { AppType } from "./server";
const client = hc<AppType>("/");
export default function App() {
const [msg, setMsg] = useState("");
const clickHello = () => {
client.api.v1.hello
.$get()
.then((res) => res.json())
.then((data) => setMsg(data.message));
};
return (
<div>
<button type="button" onClick={clickHello}>
Hello, Hono!
</button>
<p>{msg}</p>
</div>
);
}
Step 4: tsconfig.jsonを用意する
*.htmlをモジュールとしてimportできるのは、@types/bun(実体はbun-types)がdeclare module "*.html"を提供しているためです。この型を有効にするには、tsconfig.jsonで明示的に指定しておく必要があります。
tsconfig.json
{
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"types": ["bun"],
"strict": true,
"skipLibCheck": true
}
}
Step 5: 開発コマンドを定義する
package.jsonのscriptsに、開発・ビルド・起動・単一実行ファイル化の4コマンドをまとめます。
package.json
{
"scripts": {
"dev": "bun --hot src/server.ts",
"build": "bun build src/server.ts --outdir dist --target bun --minify && bun build ./src/index.html --outdir dist --target browser --minify",
"start": "bun dist/server.js",
"compile": "bun build src/server.ts --compile --outfile dist/server --minify"
}
}
-
bun run dev… Web APIサーバーとフロントエンドをまとめて起動(ホットリロード付き) -
bun run build… サーバーとフロントエンドをそれぞれ本番用にバンドル -
bun run start… ビルド済みのdist/をHono経由で配信 -
bun run compile… Bunランタイム同梱のスタンドアロン実行ファイルを生成
Step 6: 開発サーバーを起動する
$ bun run dev
Started development server: http://localhost:3000
http://localhost:3000を開くとReact製の画面が表示され、/api/v1/helloはHonoのAPIとしてそのまま応答します。フロントエンドとAPIを1つのポートで両立できているのがポイントです。
Step 7: 本番ビルドと単一実行ファイル化
$ bun run build
Bundled 27 modules in 18ms
server.js 20.20 KB (entry point)
Bundled 16 modules in 42ms
index-9yqnqm2d.js 0.40 MB (entry point)
index.html 511 bytes (entry point)
$ bun run start
Started development server: http://localhost:3000
さらにbun run compileを使うと、Bunランタイムを内蔵した単一実行ファイルを生成できます。配布先にbunコマンドがなくても動きます。
$ bun run compile
[11ms] minify -54.83 KB (estimate)
[1ms] bundle 27 modules
[644ms] compile dist/server.exe
$ ./dist/server.exe
Started development server: http://localhost:3000
--targetをそれぞれのOS向け(bun-windows-x64、bun-darwin-arm64、bun-linux-x64など)に変えれば、クロスコンパイルも可能です。Web APIサーバーとブラウザ経由で使うGUIをまとめて1つの実行ファイルにできるため、Windows・macOS・Linuxを問わず「ダウンロードして実行するだけ」で動くGUIアプリケーションとして配布できます。
技術スタック選定基準
-
Bun: ランタイム・パッケージマネージャー・バンドラーを1つに集約できる。
Bun.serve()のHTML import機能でフロントエンドの開発サーバー・ホットリロード・本番バンドルまで内蔵しており、Viteのような別ツールを足さずに済む -
Hono: 軽量かつ
hc()によるRPCクライアントを標準で持つ。サーバーのルート定義からtypeof appでクライアントの型を導出できるため、APIのスキーマ定義やコード生成ツールを別途用意しなくても型安全な通信ができる - React: 採用実績・エコシステムが大きく、学習コストや情報の見つけやすさで有利。UIライブラリとしての機能自体は最小構成の要件を満たせば十分なので、状態管理ライブラリなどは今回あえて導入していない
いずれも「最小構成で完結させる」という目的に対し、追加ツールを増やさずに要件を満たせるかを基準に選んでいる。フロントエンドが複雑化し、ルーティングやコード分割の要件が増えてきた場合は、この基準自体を見直してVite導入や状態管理ライブラリの追加を検討する。
ハマりポイント
-
routesに文字列パスを渡すとエラーになる:routes: { "/": "index.html" }のように文字列を渡すと、TypeError: 'routes' expects a Record<string, Response | HTMLBundle | ...>という実行時エラーになります。必ずimport index from "./index.html"でHTML importしたHTMLBundleオブジェクトを渡してください -
buildでサーバーとフロントエンドのtargetを揃えるとエラーになる:src/server.ts(Bunランタイム向け)とindex.html(ブラウザ向け)を同じbun buildコマンド・同じ--targetでまとめてビルドしようとすると、片方の実行環境に合わないバンドルになります。--target bunと--target browserでコマンドを分けるのが安全です -
--compileには--outfileを使う:--compileは単一の実行ファイルを1つ生成するオプションなので、ディレクトリを指定する--outdirではなく--outfileを使います -
tsconfig.jsonがないと*.htmlのimportで型エラーになる:Cannot find module './index.html' or its corresponding type declarations.というエラーが出る場合、tsconfig.jsonのcompilerOptions.typesに"bun"を明示していないことが原因です
まとめ
ViteなしでもBunのBun.serve() + HTML importを使えば、Hono Web APIサーバーとReactフロントエンドを1プロセス・1ポートにまとめた最小構成が組めます。
依存パッケージが少なく済むぶん、プロトタイプや学習用途の最初の一歩としては扱いやすい構成です。さらにbun build --compileでOSごとに単一実行ファイル化すれば、Web APIサーバーとブラウザ経由のGUIをまとめて配布できるデスクトップ向けツールの選択肢にもなります。
応用としてバックエンド側にはBun標準搭載のbun:sqliteでローカルDBを持たせたり、Ollamaなどのローカル起動LLMをHTTP経由で呼び出したりする余地も残っています。外部サービスに依存しない、ネットワーク不要で完結するデスクトップアプリケーションまで発展させられそうです。本格的にフロントエンドが複雑化してきた場合は、Viteへの移行を検討する余地も残しておくとよさそうです。
本記事の公開リポジトリ