🧭

Next.js × Hono × モノレポで実践する「リアルオプション戦略」:初期の開発速度と将来の柔軟性を両立させるWebシステム設計

に公開

はじめに

ASSIGN CTOの須永と申します。

弊社では転職エージェントの支援品質の高度化・顧客価値の最大化を目指し、面談・選考管理、
各種書類作成、求人マッチング、データ分析などの機能を持つ業務管理システムを内製で開発しています。

昨年度からは新たに新卒就活支援事業を開始し、転職支援事業と同様に業務管理システムの開発プロジェクトを本格的に推進することになりました。

この記事では、上記システムの設計で採用した「リアルオプション戦略」に基づく技術選定の背景と、その具体的な内容についてご紹介します。

プロジェクトにおける不確実性

このプロジェクトには、いくつかの大きな制約や考慮すべき不確実性が存在しました。

  • スケジュールと人的リソースの制約
    • 2025年6月にプロジェクトを開始し、10月にはテストリリースを実施。業務プロセスとの
      フィット&ギャップを検証しながら、2026年1月の正式リリースを目指しています。
    • 開発体制は、メイン開発者1名、サポートメンバー2名と少数です。
  • 進化し続ける業務プロセス
    • 新卒就活支援は年度単位で支援のプロセスが進み、時期によって顧客ニーズも変動します。そのため、学びや顧客価値の再現性を確実に次年度の改善に活かすサイクルが不可欠です。
    • また、事業自体がまだ新しく、ベストな業務プロセスや施策を模索している段階であるため、システムの仕様が変わり続けることを前提に置く必要がありました。
  • 将来のシステム連携
    • さらに将来的には、新卒向けモバイルアプリ「ASSIGN 新卒」や、既存の中途採用支援システム、統合顧客管理基盤とのシームレスなデータ連携が求められます。

これらの課題を前に、私たちは初期段階で完璧なシステムを設計し切るのは非現実的だと判断しました。

そこで掲げたのが、将来の変更可能性を確保し、その時々の最適解を選択できる状態を維持するという、リアルオプション戦略の考え方を取り入れた設計思想です。

リアルオプション戦略とは

リアルオプション戦略は、元々は金融工学の用語ですが、プロジェクト管理においては
「将来の不確実性に対して、すぐに最適な選択を取ろうとはせず、状況がより明確になった時点で
意思決定できるように、選択肢(オプション)を保持しておく」アプローチを指します。

今回のシステム開発に当てはめると、以下の3点が重要だと考えています。

  • 決め打ちを避ける
    技術や設計を「今のベスト」に固定せず、あえて判断を先送りする。将来的にサーバー環境の変更やマイクロサービス化といった新しい道が出てきた際に、選択肢を狭めないようにする。

  • 変更容易性を高める
    モジュール同士が密に結びついた構造ではなく、独立して修正・交換できる疎結合なアーキテクチャを採用する。選択肢を残せるようにすることと、実際に行動に移す際の実行可能性を担保する。

  • 後戻りできる設計にする
    万が一ある技術選定が想定どおりに機能しなかった場合でも、別の道に切り替えられるようにしておく。撤退コストを低く保つことで、不確実性の中での意思決定のハードルを下げる。

この思想をもとに、今回のシステムの設計を紹介します。

全体方針:モノレポで速度と品質を両立する開発基盤を構築する

私たちのプロジェクトの根幹をなすのが、モノレポ構成です。
最初にこの構成を決定したことで、以降の技術選定の自由度と開発スピードが大きく向上しました。

もちろん、最初からモノレポ一択だったわけではなく、チームの状況とプロジェクトの不確実性を踏まえ、以下の3つの選択肢を比較検討しました。

構成案 メリット デメリット
A. Next.js単体
(Route HandlersでAPI実装)
・最もシンプルで学習コストが低い
・Vercelへのデプロイが容易
・APIの独立性が低い(UIと密結合しやすい)
・将来のマイクロサービス化など、拡張性に乏しい
・APIのランタイムがNode.jsに固定される
B. リポジトリ分割
(フロントとバックで別々)
・責務が完全に分離される
・各リポジトリで最適な技術スタックを選べる
・少人数チームでは管理業務のオーバーヘッドが大きい
・環境構築が煩雑になりがち
C. モノレポ(今回採用) ・フロントとバックで型を完全に共有可能
・環境構築が一度で済む
・責務分離と開発効率を両立できる
・ビルドやCI/CDの仕組みがやや複雑になる

この比較から、私たちは少人数チームで責務を分離しつつも、最大限の開発効率を発揮するという目的を達成するために、モノレポが最適だと判断しました。

今回採用した技術スタック

このプロジェクトでは、以下の技術スタックを主に採用しています。

  • フロントエンド: Next.js
    Reactベースのフレームワークです。品質の高いUI/UXと、App Routerによるサーバー/クライアントの明確な責務分離を実現します。
  • バックエンド: Hono
    Node.js、Deno、Bun等の実行環境に依存しない、超軽量なWebフレームワークです。
  • ORM: Prisma
    TypeScriptとの親和性が非常に高いORMです。スキーマ定義から型安全なクライアントを自動生成し、データベースアクセスを効率化します。
  • 型定義・バリデーション: Zod
    TypeScriptファーストなスキーマ定義・バリデーションライブラリです。APIリクエストの検証からフロントエンドの型まで、一貫した定義を共有するために利用します。
  • モノレポ管理: pnpm & Turborepo
    効率的なパッケージ管理と高速なビルドシステムを提供します。
  • インフラ: AWS CDK
    TypeScriptでインフラを定義できるIaC(Infrastructure as Code)フレームワークです。

ディレクトリ構成

大まかなディレクトリ構成を以下のように定義しています。

/
├─ apps/
│   ├─ web/             # Next.js フロントエンド
│   └─ api/             # Hono バックエンド
├─ packages/
│   ├─ database/        # Prisma スキーマ・クライアント
│   ├─ shared/          # 共通型定義・ユーティリティ
│   └─ config/          # 共通設定(ESLint, TypeScriptなど)
├─ infrastructure/
│   └─ cdk/             # AWS CDK スタック定義
├── docker-compose.yml  # ローカル開発用のデータベースなど
├── package.json
├── pnpm-workspace.yaml # モノレポ管理対象のディレクトリ設定
├── turbo.json          # Turborepo設定
└── docs/               # プロジェクトの設計書や説明書

主な選定理由①:型定義の共有による開発効率と品質の向上

モノレポ最大の恩恵は、packages/sharedによるフロントエンドとバックエンドの型共有です。例えば、packages/sharedで候補者の型をZodで定義します。

import { z } from "zod";

export const CandidateSchema = z.object({
  candidateId: z.string(),
  name: z.string(),
  // ...
  interviewPriority: z.enum(["high", "medium", "low"]).optional(), // 新しい項目
});

export type Candidate = z.infer<typeof CandidateSchema>;

このCandidate型は、バックエンドのAPI実装とフロントエンドのUIコンポーネントの両方から直接importして利用できます。

import { Candidate } from '@app/shared'; // ← 型をインポート

// DBから取得したデータをCandidate型として扱う
const candidate: Candidate = await this.repo.findDetail(candidateId);

import { Candidate } from '@app/shared'; // ← 同じ型をインポート

type Props = {
  candidate: Candidate;
};

export default function CandidateCard({ candidate }: Props) {
  // candidate.interviewPriority が型安全に補完される
  return <div>{candidate.name} - 優先度: {candidate.interviewPriority}</div>;
}

もしAPIがinterviewPriorityを返し忘れたり、フロントエンドがtypoしたりすれば、ビルドや型チェックの段階で即座にエラーが検知されます。
この型の一貫性が、仕様変更時の手戻りを防ぎ、スピーディーな開発を支えています。

主な選定理由②:AIとの協業効率の最大化

開発を進めてまだ2ヶ月程度ですが、モノレポはAIコーディングエージェントと非常に相性が良い構成だと感じています。
この相性の良さを支えているのが Everything as Code という思想です。
https://docs.aws.amazon.com/ja_jp/wellarchitected/latest/devops-guidance/everything-as-code.html

これは、アプリケーションのコードだけでなく、インフラ定義(AWS CDK)、データベーススキーマ(Prisma)、テストコード、仕様書(Markdown)など、プロジェクトに関するあらゆるものをコードとしてリポジトリで一元管理するアプローチです。

プロジェクトの全体構造をコンテキストとしてAIに与えることで、例えば「候補者情報にファイル添付機能を追加する」といった横断的な修正依頼も迅速に完結します。

  1. Prismaスキーマの更新(ファイルパスを保存するカラムの追加)
  2. 共有型定義の修正
  3. Honoエンドポイントへのロジック追加(ファイルアップロード処理)
  4. Next.js画面へのフォーム実装(ファイル選択コンポーネントの追加)
  5. AWS CDKによるインフラ定義の更新(ファイルを保存するS3バケットと、APIからのアクセス権限設定)
  6. E2Eテスト(Playwrightなど)へのテストケース追加(ファイル添付と保存を検証するシナリオ)

このような一連の流れをAIが自動で提案し、それをレビューするだけで対応を完了させることができています。
ビジネスサイドからのフィードバックを即座にシステムに反映できるこのサイクルは、不確実性の高いプロジェクトにおいて強力な推進力となります。

将来的には、Everything as Codeを推し進め、設計からデプロイまでをAIで効率化することも見据えています。

リアルオプション戦略としてのポイント

現時点では一体で開発するのが最も効率的ですが、将来的にシステムの特定機能の負荷が高まったり、チームが拡大したりした際には、リポジトリの分割やマイクロサービスへの切り出しが必要になるかもしれません。

今回の構成でも、こうした将来のアーキテクチャ変更に対する選択肢を確保しつつ、現在の開発速度を最大化することができます。最初から完璧な分割戦略を予測するのではなく、必要になったタイミングで最適なアーキテクチャへ移行する権利を保持しておくということです。

例えば、モノレポからマイクロサービスや独立リポジトリへの移行も、段階的に進めることが可能です。

  • マイクロサービス化(モノレポ内での切り出し)
    • 対象機能の境界を整理し、APIスキーマや型定義を packages に集約
    • apps 配下に新サービス(例: apps/matching-api)を追加
    • API Gatewayなどを利用して、一部のリクエストを新サービスへ振り分け
    • 段階的に処理を移管し、移行リスクを抑える
  • リポジトリ分割(完全な独立化)
    • 共有型定義やユーティリティをプライベートnpmパッケージ化して依存関係を明示化
    • git filter-repo で対象ディレクトリを抽出し、新リポジトリを作成
    • 新リポジトリにCI/CDパイプラインを整備
    • インフラ接続や依存サービスを更新し、単独運用可能な状態へ移行

もちろん、この移行をスムーズに行うためには、共有ライブラリの肥大化を避ける、サービス間の契約をAPIで明示する、DBの境界を意識するなどの疎結合を保つための規律が重要になります。

モノレポ管理ツールの選定:シンプルさで初期フェーズの開発速度を最大化する

モノレポ構成の採用を決めた私たちが次に向き合ったのが、「このモノレポをどう効率的に管理するか」というツール選定です。特に以下の2つの選択肢を重点的に比較しました。

  • Projen + Nx: 規律と自動化を重視する、高機能な組み合わせ。
    • Projen: .projenrc.jsというコードファイルでpackage.jsonなどの設定ファイルを自動生成・管理し、規律を強制する仕組みです。
    • Nx: その上で高度なキャッシュ機能、依存関係グラフの可視化、コードジェネレータといった高機能なツールを提供します。
  • Turborepo: シンプルさと速度を追求する、軽量なビルドシステム。
    • turbo.jsonというシンプルなJSONファイルにタスク間の依存関係を定義するだけで、ビルドやテストを劇的に高速化できるシンプルな仕組みが特徴です。

2つの選択肢の比較を簡単にまとめると、以下のようになります。

比較観点 Projen + Nx Turborepo (今回採用)
設定方法 コードによる設定 (.projenrc.js)
package.json等を自動生成・管理。手動編集は非推奨。
JSONによる設定 (turbo.json)
既存のpackage.jsonはそのままに、タスク間の依存関係を定義。
思想・役割 プロジェクト全体の管理者
Projenが規律(設定)を、Nxが実行(タスク、キャッシュ、コード生成)を担当。
高速なビルドシステム
ビルドやテストの高速化に特化。プロジェクト構造には関与しない。
学習コスト 高い
Projenの思想とNxの豊富な機能を理解する必要がある。
低い
数個のキーを覚えるだけで、すぐにキャッシュの恩恵を受けられる。
柔軟性 vs 規律 規律を重視
一貫性を強制するため、自由な設定変更はしにくい。
柔軟性を重視
既存のツールや設定をそのまま活かせる。導入・撤退が容易。
エコシステム リッチ
Nxのプラグインによるコード生成、依存関係グラフの可視化など。
シンプル
キャッシュとタスクパイプラインに特化。他の機能は他のツールと組み合わせる思想。

Turborepoを選択した理由

上記の比較表の通り、Projen + Nxは、大規模で長期的なプロジェクトにおいて一貫性を保つための非常に強力なソリューションになると考えています。

しかし、仕様が頻繁に変わる初期フェーズにおいて、その厳格な規律は過剰であり、むしろ足枷になる可能性があると判断しました。

私たちの最優先事項は、短い期間で変化に対応しながら開発を進めるスピードと柔軟性です。

Turborepoは、アプリケーションのコードや各パッケージの設定に干渉することなく、turbo.jsonへのわずかな記述で導入できる手軽さがあります。
このシンプルさと柔軟性が、私たちの要求に合致すると考えました。

また、開発初期は最小限の規約で開発速度を最大化し、もし将来プロジェクトが巨大化し、より厳格な規律が必要になった際には、その時点でNxのような高機能なツールを追加で導入する選択肢も残されています。

バックエンドの選定:軽量なHonoで身軽さを保つ

私たちは特定のフレームワークや環境に過度に依存することを避け、将来にわたって柔軟性と移行容易性を確保したいと考えました。
この思想に基づき、私たちはいくつかの選択肢を比較検討しました。

フレームワーク 特徴 選定時に考慮したこと
NestJS
(リッチフレームワーク)
・DI、AOPなどエンタープライズ向け機能が豊富
・規約が強く、コードの属人性が減る
・学習コストが高く、規約の量が認知負荷に繋がる
・小規模なAPIにはオーバースペック気味
・フレームワークへの依存度が高く、他への移行が困難
Fastify
(シンプルフレームワーク)
・非常に高速でパフォーマンスが高い
・プラグイン機構による拡張性が特徴
・TypeScriptとの親和性やエコシステムがHonoに比べてやや発展途上
・Zodバリデーション等の連携で一手間かかる場合がある
Hono今回採用
(超軽量フレームワーク)
・ランタイム非依存(Node.js, Bun, Deno, CF Workers等)
・フレームワークの規約が最小限で、学習コストが低い
・Zodとの親和性が極めて高い
・DIなど高度な機能は自前で組み合わせる必要がある
※今回はTsyringeを組み合わせることで解決

最終的に、Honoの軽量さとポータビリティが、私たちのリアルオプション戦略に最も合致すると判断しました。

主な選定理由①:ビジネスロジックそのものに集中可能であること

Honoの魅力は、フレームワークの存在をほとんど意識させない点にあります。
規約は最小限で、純粋なTypeScriptの関数を組み合わせるようにAPIを構築できます。
特に@hono/zod-openapiライブラリとの連携は素晴らしく、Zodスキーマを定義するだけで、リクエストのバリデーション、TypeScriptの型定義、そしてOpenAPIドキュメントの生成までが完了します。

import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';
import { CandidateSchema } from '@app/shared/types/candidates';

const app = new OpenAPIHono();

const getCandidateRoute = createRoute({
  method: 'get',
  path: '/v1/candidates/{id}',
  request: { params: z.object({ id: z.string() }) },
  responses: {
    200: {
      description: '候補者詳細',
      content: { 'application/json': { schema: CandidateSchema } },
    },
  },
});

app.openapi(getCandidateRoute, async (c) => {
  const { id } = c.req.valid('param'); // ← バリデーション済み
  const candidate = await candidateService.getById(id);
  return c.json(candidate);
});

NestJSのように@Controller, @ParamといったデコレーターやDIコンテナを強く意識する必要がなく、開発もシンプルに進められています。

主な選定理由②:DIによる見通しの良いアーキテクチャ

Honoは軽量ですが、拡張性も備えています。
私たちは依存性注入(DI)ライブラリのTsyringeを組み合わせることで、テスト可能で疎結合なアーキテクチャを実現しました。

import { CandidateService } from './candidate.service';
import { TOKENS } from '../../app/di/tokens';

export class CandidateController {
  static async detail(c: Context) {
    const di = c.get('di'); // リクエストスコープのDIコンテナを取得
    const candidateService = di.resolve<CandidateService>(TOKENS.CandidateService); // Service層をDIコンテナから解決

    const candidate = await candidateService.detail(c.req.param('id'));
    return c.json(candidate);
  }
}

Controllerは具体的なServiceの実装を知ることなく、抽象(TOKENS)に依存します。これにより、各層の責務が明確になり、メンテナンス性とテスト性が向上します。

リアルオプション戦略としてのポイント

Honoを採用した最大の理由は、その柔軟な移行可能性にあります。

Honoは特定のJavaScriptランタイムに依存しません。例えば、ビジネスロジックはそのままに、それを動かす実行環境だけを後から最適化できる権利をHonoは与えてくれます。

  • 現時点では、安定性を重視してNode.jsで動かす。
  • 将来、パフォーマンスが求められればBunDenoへの移行を検討する。
  • AWS LambdaからAWS FargateCloudflare Workersに移行する際も、コードの変更は最小限で済む。

不確実性の高い事業においてシステムを長期的に運用する際に、この柔軟性は鍵になると考えています。

フロントエンドの選定:Next.jsで既存知見を活用し、品質と開発効率を担保する

本プロジェクトのフロントエンドは、Next.js App Routerを採用しました。
理由はシンプルで、既存プロダクトで蓄積した知見を最大限に活用し、短期で品質の安定と学習コストを最小化し開発スピードを両立するためです。

Webフロントは技術進化が速く、キャッチアップやデザイン/アクセシビリティ維持にコストがかかりやすい領域です。
また、新卒支援の業務画面は「表示中心のページ」と「操作が密なフォーム/ワークフロー」が混在します。このように性質の異なるUIを少人数で効率よく開発するうえで、既存のノウハウが活かせるNext.jsを選び、組織全体の開発速度と品質を担保することを優先しました。

主な選定理由:関心分離とキャッシュ制御

今となっては一般的ですが、App Routerの設計に沿って、データ取得・描画をServer Componentsに、ユーザー操作をClient Componentsに明確に分離できる点は、変更耐性と初期表示性能を損なわずに開発を進める上でやはり大きなメリットとなりました。

さらに、Next.js 15からfetch リクエストや GET Route Handlers、クライアントナビゲーションがデフォルトでキャッシュされなくなり、明示的にオプトインする方式へ変更されたことも追い風となりました。
業務システムのように動的なデータ更新・表示が多いアプリケーションでは、キャッシュの挙動を細かく制御しやすくなったこともメリットであると感じています。
https://nextjs.org/blog/next-15?utm_source=chatgpt.com#caching-semantics

リアルオプション戦略としてのポイント

今回の選択は技術的な軸によるものというより、将来の統合・共通化の余地を最大化するためのオプションを保持するという意思決定と言えます。

中途/新卒エージェントの業務支援システムや、候補者向けWebサイトを含む複数プロダクトの基盤をNext.jsで揃えることで、以下のような後から効く権利を持つことができます。

  • UIパターンやコンポーネントの横断共有によるデザインシステムの標準化
  • 計測・アクセシビリティ・エラーハンドリングの統一による品質改善
  • 人材流動性の向上によるチーム間の立ち上がりコスト削減

結果として、「今は既存知見で最速に価値を届ける」ことと「将来プロダクト群をまとめて進化させる自由度を残す」ことの両立を実現できています。

おわりに

この記事では、不確実性の高い新規事業において、私たちが「リアルオプション戦略」を軸にどのように技術選定を行ったかをご紹介しました。
今回選択したアーキテクチャは、現時点での最適解というよりも、将来の最適解を選ぶ権利を確保するための選択です。

重要なのは、事業の成長と共に学び、改善を続けていくことだと考えています。
実際、私たちのチームにとってモノレポ開発やHonoの採用は初めての挑戦であり、このプロジェクトを通じて組織全体の技術的な引き出しを増やしていくという目的も含まれています。

また、記事中でも触れましたが、今回のシステム開発については、今後さらに具体的なテーマで深掘りしていく予定です。(以下例)

  • Webフロント編: Tanstack Queryによる複雑な状態管理
  • バックエンド編: HonoとTsyringeによるDIアーキテクチャ実践
  • インフラ編: AWS CDKを用いた再現可能な環境構築
  • AI駆動開発編: モノレポ環境でのAIとの最適な協業スタイル
  • ポストモーテム編: 一定期間の運用を経て分かった今回の設計のPros and Cons

「このプロジェクトがどう進んでいくのか気になる」と思っていただいた方には、今後の記事も楽しみにしていただけると励みになります。

今回の私たちの技術選定が、同じように不確実性の高いプロジェクトに取り組む開発者の皆さんにとって、何かしらのヒントや議論のきっかけになれば幸いです。

最後までお読みいただき、ありがとうございました。

参考文献

ASSIGN

Discussion