7
2

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 × 🥟Bun × 🔼Next.jsでWebアプリ開発ハンズオン

7
Last updated at Posted at 2026-05-18

フロントエンドをReact(Next.js)にするとしてバックエンドをどうするか。選択肢の一つとして「Hono」はいかがでしょうか。今回は、ここ数年で人気急上昇のHonoというフレームワークを試してみました。また、JavaScriptのランタイムにはBunを使い、とにかく速くてモダンそうな技術を体験してみます。

簡単にそれぞれの特徴をまとめました。

🔥 Honoとは:

  • 軽量で超高速なWebフレームワーク
  • マルチランタイム対応(Cloudflare Workers、Deno、Bun、Node.jsなどどこでも動く)
  • 作者が日本人(yusukebeさん)👏

🥟 Bunとは:

  • Node.jsのようなJavaScriptのランタイムのこと
  • 爆速ということで有名
  • マスコットが可愛い(餃子かと思ったけど、肉まん(pork bun)だった...)

🔼 Next.jsとは:

  • Reactベースのフルスタックフレームワーク
  • Reactベースのフレームワークの中でシェア1位
  • CSR・SSR・SSG・ISRなど複数のレンダリングを柔軟に使い分け可能

前提条件

Hono,Bun,Next.jsを触るのが初めてでも全く問題ありません!
私自身いずれも今回初めて使ってみましたが、情報も多いし公式ドキュメントも充実してるので色々と勉強になりました。

今回はECサイトを想定した簡単なデモアプリを作りながら、各技術を体験したいと思います。

今回やること:

  • 環境構築
  • Part1: GET編
    • HonoでGET APIを実装
    • Next.js Server Componentsで一覧表示
  • Part2: POST編
    • HonoでPOST APIを実装
    • Next.js Server Actionsで登録フォーム
  • Part3: HonoRPC編
    • Part1・2のコードをRPC+Zod Validatorで書き換え

(認証機能は今回作りません)

環境構築

Bunインストール

まずはBunをインストール

# Linux & macOS
curl -fsSL https://bun.sh/install | bash

プロジェクト作成

ディレクトリ構成:

demo-app/
├── apps/
│   ├── frontend/  # Next.js
│   └── backend/  # Hono
├── .gitignore
├── package.json
├── ...

はじめに、プロジェクト用ディレクトリを作成し、.gitignoreを用意してGitを初期化しておきましょう。

mkdir demo-app && cd demo-app
echo "node_modules" > .gitignore
git init

後でHonoとNext.jsをインストールするappsディレクトリも作っておきます。

mkdir apps && cd apps

Workspaces設定

HonoとNext.jsのインストール前に、BunのWorkspacesを設定します。
これにより依存関係が一元管理され、共通パッケージはルートのnode_modulesに集約されて重複を削減できます。

インストール後は、各パッケージのnode_modulesはルートのnode_modulesを参照する構成になり、シンボリックリンクで解決されます。

ルートにpackage.jsonを作成。

{
  "name": "demo-app",
  "private": true,
  "workspaces": ["apps/*"]
}

参考: https://bun.sh/guides/install/workspaces

Honoインストール

Honoをインストール

bun create hono@latest backend --template bun --install --pm bun
  • --template bun: ランタイムテンプレートとして Bun を選択
  • --install: プロジェクト生成後に依存パッケージを自動インストール
  • --pm bun: パッケージマネージャーとして Bun を使用(npm/yarn/pnpmではなく)

Next.jsインストール

Next.jsをインストールします。

# Run in apps/
bun create next-app@latest frontend

各設定は次のようにしました。

✔ Would you like to use the recommended Next.js defaults? › No, customize
✔ Would you like to use TypeScript?                       › Yes
✔ Which linter would you like to use?                    › ESLint
✔ Would you like to use React Compiler?                  › No
✔ Would you like to use Tailwind CSS?                    › Yes
✔ Would you like your code inside a `src/` directory?    › Yes
✔ Would you like to use App Router?                      › Yes
✔ Would you like to customize the import alias?          › No
✔ Would you like to include AGENTS.md?                   › Yes

ちなみに、HonoもNext.jsもnpmでのインストールも試してみましたが、bunが圧倒的に速かったです!!

デモ実装(Part1: GET編)

🔥 Honoの実装

まずはHonoでのAPI開発を体験するために、以下2つの簡単なAPIを作成してみます。

  • 商品一覧取得: GET /api/products
  • 商品詳細取得: GET /api/products/{id}

apps/backend/src/index.tsを以下のように書き換えます。

import { Hono } from 'hono'

type Product = {
  id: number
  name: string
  price: number
  stock: number
}

const products: Product[] = [
  { id: 1, name: 'Tシャツ', price: 2980, stock: 100 },
  { id: 2, name: 'デニムパンツ', price: 7980, stock: 50 },
  { id: 3, name: 'スニーカー', price: 5480, stock: 30 },
]

const app = new Hono()

app.get('/api/products', (c) => {
  return c.json(products)
})

app.get('/api/products/:id', (c) => {
  const id = Number(c.req.param('id'))
  const product = products.find((p) => p.id === id)
  if (!product) return c.json({ message: 'Not found' }, 404)
  return c.json(product)
})

export default {
  port: 3001, // Next.jsのデフォルトポート3000と被るため変更
  fetch: app.fetch,
}

開発サーバーを起動します。

cd apps/backend
bun dev  # → http://localhost:3001 で起動

ブラウザやcurlで動作確認してみましょう。

curl http://localhost:3001/api/products      # 商品一覧
curl http://localhost:3001/api/products/1    # 商品詳細
curl http://localhost:3001/api/products/999  # → 404

🔼 Next.jsの実装

次に、フロントエンドから先ほどのAPIを呼び出して商品一覧を表示してみましょう。

apps/frontend/app/page.tsxを以下のように書き換えます。
(見た目はメインではないのでTailwindCSSで適当に装飾)

type Product = {
  id: number
  name: string
  price: number
  stock: number
}

export default async function Page() {
  const res = await fetch('http://localhost:3001/api/products')
  const products: Product[] = await res.json()

  return (
    <main className="max-w-2xl p-8">
      <h1 className="text-xl font-bold mb-4">商品一覧</h1>
      <ul className="divide-y">
        {products.map((product) => (
          <li key={product.id} className="flex gap-4 py-2">
            <span>{product.name}</span>
            <span>¥{product.price.toLocaleString()}</span>
            <span>在庫: {product.stock}</span>
          </li>
        ))}
      </ul>
    </main>
  )
}

Next.jsの開発サーバーを起動します。

cd apps/frontend 
bun dev  # → http://localhost:3000 で起動

http://localhost:3000 を開くと、Honoから取得した商品一覧が表示されます。

Tips: 便利なBunコマンド

今回のような2つのパッケージがあるモノレポ構成であれば、--filterオプションを使って両方のdevスクリプトを一発で実行することもできます。

bun --filter '*' dev

それぞれ実行したい場合でも、プロジェクトルートから以下のように実行できます。

bun run --filter frontend dev
bun run --filter backend dev

デモ実装(Part2: POST編)

🔥 Honoの実装

apps/backend/src/index.ts にPOSTエンドポイントを追加します

// ...
app.post('/api/products', async (c) => {
  const body = await c.req.json<Omit<Product, 'id'>>()
  const newProduct = { id: products.length + 1, ...body }
  products.push(newProduct)
  return c.json(newProduct, 201)
})

curlで動作確認しておきます。

curl -X POST http://localhost:3001/api/products \
  -H "Content-Type: application/json" \
  -d '{"name":"キャップ","price":3980,"stock":20}'

curl http://localhost:3001/api/products

🔼 Next.jsの実装

次に商品登録フォームを作成します。apps/frontend/app/new/page.tsx を新規作成します。

import { redirect } from 'next/navigation'

async function createProduct(formData: FormData) {
  'use server'

  await fetch('http://localhost:3001/api/products', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      name: formData.get('name'),
      price: Number(formData.get('price')),
      stock: Number(formData.get('stock')),
    }),
  })

  redirect('/')
}

export default function NewPage() {
  return (
    <main className="p-8">
      <h1 className="text-xl font-bold mb-4">商品登録</h1>
      <form action={createProduct} className="space-y-4">
        <div>
          <label>商品名</label>
          <input name="name" type="text" className="border ml-2 px-2 py-1" />
        </div>
        <div>
          <label>価格</label>
          <input name="price" type="number" className="border ml-2 px-2 py-1" />
        </div>
        <div>
          <label>在庫</label>
          <input name="stock" type="number" className="border ml-2 px-2 py-1" />
        </div>
        <button type="submit" className="px-4 py-2 bg-black text-white">
          登録
        </button>
      </form>
    </main>
  )
}

page.tsxがあるディレクトリがそのままURLになります。

app/
├── page.tsx        → /
└── new/
    └── page.tsx    → /new

http://localhost:3000/new を開いてフォームを送信で登録できます。登録後に一覧ページへリダイレクトされるので一覧に追加されているか確認ぢます。

ポイント: Server Actions
'use server'をつけた関数はサーバー上で実行されます。フォームのactionに直接関数を渡せます。

デモ実装(Part3: HonoRPC編)

Part1・2では Product 型をフロントとバックエンドで別々に定義していました。
Part3ではHonoのRPCとZodバリデーターを組み合わせて、バックエンドの型をフロントエンドで自動的に利用できるようにします。

🔥 Honoの実装

まず必要なパッケージを追加します。

cd apps/backend
bun add zod @hono/zod-validator

apps/backend/src/index.tsを次のように修正します。

  • zValidator を使ってリクエストボディをバリデーション
  • ルートを .get().get().post() とメソッドチェーンでつなげて routes に代入
  • typeof routesAppType としてエクスポート
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

const schema = z.object({
  name: z.string(),
  price: z.number(),
  stock: z.number(),
})

type Product = z.infer<typeof schema> & { id: number }

const products: Product[] = // ...略

const app = new Hono()

const routes = app
  .get('/api/products', (c) => {
    return c.json(products)
  })
  .get('/api/products/:id', (c) => {
    const id = Number(c.req.param('id'))
    const product = products.find((p) => p.id === id)
    if (!product) return c.json({ message: 'Not found' }, 404)
    return c.json(product)
  })
  .post('/api/products', zValidator('json', schema), async (c) => {
    const body = c.req.valid('json')
    const newProduct = { id: products.length + 1, ...body }
    products.push(newProduct)
    return c.json(newProduct, 201)
  })

export type AppType = typeof routes

export default {
  port: 3001,
  fetch: app.fetch,
}

c.req.json() の代わりに c.req.valid('json') を使うことで、Zodのスキーマで検証済みの型安全なボディを取得できます。また、schema の型情報がRPCクライアントにそのまま伝わるため、フロントエンド側でも存在しないフィールドを渡すとVSCodeが警告で教えてくれます。

🔼 Next.jsの実装

フロントエンドでは hono/clienthc を使ってAPIクライアントを作成します。
AppType をインポートすることで、レスポンスの型が自動で効きます。

まずapps/frontendhonoを依存関係に追加します。

cd apps/frontend
bun add hono

商品一覧 apps/frontend/src/app/page.tsx

import { hc } from 'hono/client'
import type { AppType } from '../../../backend/src'

const client = hc<AppType>('http://localhost:3001')

export default async function Page() {
  const res = await client.api.products.$get()
  const products = await res.json()

  return (
    <main className="max-w-2xl p-8">
      <h1 className="text-xl font-bold mb-4">商品一覧</h1>
      <ul className="divide-y">
        {products.map((product) => (
          <li key={product.id} className="flex gap-4 py-2">
            <span>{product.name}</span>
            <span>¥{product.price.toLocaleString()}</span>
            <span>在庫: {product.stock}</span>
          </li>
        ))}
      </ul>
    </main>
  )
}

fetch('http://localhost:3001/api/products') と書いていた箇所が client.api.products.$get() になりました。URLの文字列がなくなり、エンドポイントの変更がコンパイルエラーとして検出できます。

商品登録 apps/frontend/src/app/new/page.tsx

import { hc } from 'hono/client'
import { redirect } from 'next/navigation'
import type { AppType } from '../../../../backend/src'

async function createProduct(formData: FormData) {
  'use server'

  const client = hc<AppType>('http://localhost:3001')
  await client.api.products.$post({
    json: {
      name: formData.get('name') as string,
      price: Number(formData.get('price')),
      stock: Number(formData.get('stock')),
    },
  })

  redirect('/')
}

export default function NewPage() {
  // ...略
}

fetch に渡していたJSONボディが $post({ json: { ... } }) になりました。schema の型がRPCを通じてフロントに伝わるため、存在しないフィールドを渡すとコンパイルエラーとして検出できます。

まとめ

本記事では、Hono + Bun + Next.jsのモノレポ構成で簡単なECサイトのデモアプリを作成しました。
今回はインメモリのダミーデータを使いましたが、PrismaなどのORMを導入してDBと繋いだり、認証機能を追加することで本格的なアプリへ拡張していくことができそうです。
今後もう少し深掘りしていこうと思います。

Pasted Graphic 3.png

参考

Hono

Honoインストール方法
https://hono.dev/docs/getting-started/basic

開発者ご本人によるRPCの紹介記事(Zenn)
https://zenn.dev/yusukebe/articles/a00721f8b3b92e

Zod、Zod Validator Middlewareについて
https://hono.dev/docs/guides/validation#with-zod

Bun

Bunインストール方法
https://bun.com/docs/installation

Bun Workspacesについて
https://bun.com/docs/pm/workspaces

bunスクリプトの--filterについて
https://bun.com/docs/pm/filter#running-scripts-with-filter

Next.js

Next.jsインストール方法
https://nextjs.org/docs/app/getting-started/installation

use serverについて
https://nextjs.org/docs/app/api-reference/directives/use-server#using-use-server-inline

Server Functionsについて
https://react.dev/reference/rsc/server-functions

7
2
1

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
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?