😇

お手軽デプロイ!Hono + Amplify Gen2で軽量フルスタック環境を構築

に公開

はじめに

今回は、軽量Webフレームワーク HonoAWS Amplify Gen2 にデプロイする方法を紹介します。

Amplify Gen2はNext.jsが有名ですが、Honoのような軽量フレームワークとAmplifyの強力なBaaS(AppSync, Lambda)を手軽に組み合わせる方法を実証します。

技術スタックと構成

  • フロントエンド (SSR): Hono + JSX
  • フロントエンド (UI): Alpine.js (軽量なインタラクティブUIのため)
  • バックエンド (BaaS): Amplify Gen2 (AppSync + Lambda)
  • データフロー: Alpine.js (UI) → Hono (APIルート) → Amplify ClientAppSyncLambda

作成したアプリケーション

HonoのSSRでページを描画し、Alpine.jsでメッセージの作成・一覧取得・削除といった操作を行う、シンプルなメッセージボードを作成しました。

今回のサンプルリポジトリはこちらです。

https://github.com/takemo101/amplify-hono

1. Amplify Gen2によるバックエンド構築 (AppSync + Lambda)

まず、Amplify Gen2でバックエンドを定義します。

データスキーマの定義 (resource.ts)

amplify/data/resource.ts にTypeScriptでスキーマを定義します。
getMessages(取得)とcreateMessage(作成)の両方を、defineFunctionで定義したmessageというLambda関数で処理するように指定します。

amplify/data/resource.ts
import { a, defineData, type ClientSchema } from "@aws-amplify/backend";
import { message } from "../functions/message"; // defineFunctionで定義したLambdaリソース

const schema = a.schema({
  // 操作の種類を表す列挙型
  Operation: a.enum(["create", "remove"]),

  // 参照メッセージの型
  Message: a.customType({
    id: a.integer().required(),
    message: a.string().required(),
  }),

  // 操作されたメッセージの型
  OperatedMessage: a.customType({
    id: a.integer().required(),
    message: a.string().required(),
    operation: a.ref("Operation").required(),
  }),

  // メッセージの配列を表す型
  Messages: a.customType({
    messages: a.ref("Message").required().array(),
  }),
  // メッセージの取得を行うクエリ
  getMessages: a
    .query()
    .returns(a.ref("Messages"))
    .authorization((allow) => [allow.publicApiKey()])
    .handler(a.handler.function(message)),

  // メッセージの作成を行うミューテーション
  createMessage: a
    .mutation()
    .arguments({
      message: a.string().required(),
    })
    .returns(a.ref("OperatedMessage"))
    .authorization((allow) => [allow.publicApiKey()])
    .handler(a.handler.function(message)),

  // 中略...
});
// ... (exportなど) ...

Lambda関数の実装 (handler.ts)

amplify/functions/message/handler.ts に、AppSyncから呼び出されるLambda関数を実装します。event.info.fieldNamegetMessagesなど)に応じて、実行する処理を分岐させます。

amplify/functions/message/handler.ts
import type { AppSyncResolverHandler } from "aws-lambda";
// ... (個別の getMessages, createMessage 関数のインポート) ...

// フィールド名とハンドラー関数のマッピング
const handlers: Record<
  string,
  AppSyncResolverHandler<any, any>
> = {
  getMessages,
  createMessage,
  removeMessage,
};

// 中略...

// メインハンドラー
export const handler: AppSyncResolverHandler<any, any> = async (
  event,
  context,
  callback
) => {
  const { fieldName } = event.info;

  if (!isValidFieldName(fieldName)) {
    throw new Error(`Invalid field: ${fieldName}`);
  }

  // 対応するハンドラーを実行
  const handler = handlers[fieldName];
  return await handler(event, context, callback);
};

ポイント: 複数の操作を単一のLambda関数に集約することで、Lambdaのコールドスタートの影響を最小限に抑える狙いがあります。

お手軽な開発体験 (ampx sandbox)

Amplify Gen2の「お手軽さ」の核心は ampx sandbox コマンドです。

pnpm exec ampx sandbox

これを実行しておけば、バックエンドのコード(.tsファイル)を変更・保存するだけで、自動的にクラウド上のAppSyncやLambdaにデプロイされます
これにより、ローカルでHonoを動かしつつ、お手軽にクラウド上のバックエンドと連携したリアルタイムな開発が可能です。

2. Honoによるフロントエンド・APIサーバー構築

次に、Hono側を実装します。

APIルートの定義 (api.ts)

Alpine.jsから呼び出されるAPIルートを src/api.ts に定義します。
ここでHono(サーバーサイド)がAmplify Clientを呼び出します。

src/api.ts
import { Hono } from "hono";
// ... (amplify Clientのインポート) ...

const messages = new Hono();

// GET /api/messages (一覧取得)
messages.get("/", async (c) => {
  const messages = await getMessages(); // Amplifyバックエンドを呼び出し
  return c.json({ items: messages });
});

// POST /api/messages (新規作成)
messages.post("/", zValidator("json", messageSchema), async (c) => {
  const validated = c.req.valid("json");
  const message = await createMessage(validated.message); // Amplifyバックエンドを呼び出し
  return c.json({ ...message });
});

export default messages;

SSRとAmplify Clientの実装

src/index.tsx でHonoのSSRとAPIルーティングを設定します。

src/index.tsx
import { Hono } from "hono";
import { html } from "hono/html";
import { Layout } from "./components/Layout";
import { MessageBoard } from "./components/MessageBoard";
import api from "./api";

const app = new Hono();

// /api ルートは api.ts に委譲
app.route("/api", api);

// ルートパスはSSRでHTMLを返す
app.get("/", (c) => {
  return c.html(
    <Layout>
      <main>
        <MessageBoard />
      </main>
    </Layout>
  );
});

export default app;

Honoのサーバーサイドで実行されるAmplify Client(src/amplify.ts)は、{ ssr: true } オプションを付けて設定します。

src/amplify.ts (抜粋)
import { Amplify } from "aws-amplify";
import outputs from "../../amplify_outputs.json"; 

// サーバーサイドでAmplify設定を構成
Amplify.configure(outputs, { ssr: true });
// ... (generateClient) ...

3. Amplify Hostingへのデプロイ設定

HonoをAmplify Hostingにデプロイするための設定です。

Viteビルド設定 (vite.config.ts)

Hono公式のViteプラグインを使うだけで、お手軽に設定が完了します。

vite.config.ts
import { defineConfig } from "vite";
import build from "@hono/vite-build/node";
import devServer from "@hono/vite-dev-server";

export default defineConfig({
  plugins: [
    devServer({ entry: "./src/index.tsx" }),
    build({
      entry: "./src/index.tsx",
      output: "index.js",
      outputDir: "./dist",
    }),
  ],
  // 中略...
});

デプロイマニフェスト (deploy-manifest.json)

Amplify Hostingに「Honoアプリをどう実行するか」を伝えるため、deploy-manifest.json をプロジェクトルートに配置します。これが最も重要な設定ファイルです

deploy-manifest.json
{
  "version": 1,
  "framework": { "name": "hono", "version": "4.10.4" },
  "routes": [
    {
      "path": "/*.*",
      "target": {
        "kind": "Static"
      },
      "fallback": {
        "kind": "Compute",
        "src": "default"
      }
    },
    {
      "path": "/*",
      "target": {
        "kind": "Compute",
        "src": "default"
      }
    }
  ],
  "computeResources": [
    {
      "name": "default",
      "runtime": "nodejs22.x",
      "entrypoint": "index.js"
    }
  ]
}

ポイント:

  1. routes: /*.*(CSS/JSなど)は Static(静的)に、それ以外(/*)は Compute(サーバー実行)に振り分けます。
  2. computeResources: Compute の実体(default)を定義します。Viteでビルドされた index.jsnodejs22.x で実行するよう指定します。

まとめ

今回の試行により、HonoとAmplify Gen2を組み合わせた「お手軽」な開発が可能であることが分かりました。

  1. deploy-manifest.json を適切に設定すれば、HonoはAmplify Gen2で問題なく動作します。
  2. Honoのサーバーサイドから、Amplifyの型安全なクライアントを経由してAppSync/Lambdaと連携できます。
  3. ampx sandbox を活用することで、ローカル(Hono)とクラウド(Amplify)をシームレスに連携させた「お手軽な」開発体験が得られます。

【過激派の叫び】そのAmplify、本当にNext.jsが必要か?

ここからは完全に個人的な主張ですが、声を大にして言いたい。

とりあえずNext.js」という思考停止、やめませんか?

ええ、わかってますよ。当のAWS Amplify自身がNext.jsやReactの立派なテンプレートをこれでもかと用意して、「さあ、これをお使いなさい」と手厚く“推奨”していることは...

しかし、その公式の優しさ(という名の誘導)に、我々は思考停止で甘えていて良いのでしょうか?

せっかくAmplify Gen2という「お手軽インフラロケット」を手に入れたのに、なぜ我々は最初から「Next.jsという超重量級の居住モジュール」をドッキングさせようとするのでしょうか。

近所のコンビニ(=シンプルなWebアプリ)に行くのに、F1マシン(=Next.js)をガレージから引っ張り出すようなものです。確かに高性能ですが、明らかに過剰装備。App Router? RSC? キャッシュ戦略? そんなものを気にする前に、まず動くものを作るべきミッションもあるはずです。

その「とりあえず」の選択が、数ヶ月後に「なんでこんな複雑なんだ...」という未来のメンテナンス地獄を生み出すのです。

我々「軽量フレームワーク原理主義者」からすれば、Honoのようなシンプル極まりないツールこそ、Amplifyの手軽さを真に引き出す「革命的」な一手です。複雑なReactの流儀や巨大なフレームワークのお作法に振り回されず、純粋なJavaScript/TypeScriptでロジックを書く快感。これぞエンジニアリングの原点回帰です。

もちろん、大規模開発でNext.jsが必要な場面は多々あります。しかし、すべてのプロジェクトがそうではありません。

Amplifyの手軽さには、Honoの手軽さを。

このシンプルな組み合わせが、あなたのプロジェクトを無用な複雑さから解放し、開発速度をブーストさせる「銀の弾丸」になるかもしれないのです。

株式会社ソニックムーブ

Discussion