3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js と AWS CDK を npm workspaces を使って Monorepo 構成にしてみた

Posted at

はじめに

Monorepo 構成を試して見るべく、npm workspaces を使って、以下のような 3 パッケージ構成を試してみました。

[ルートディレクトリ]
├── [web パッケージ]: Next.js を使った web アプリ
├── [infra パッケージ]: AWS CDK を使った IaC コード(Lambda アプリ含め)
└── [共有パッケージ]: web, infra パッケージから利用される共有コード

web パッケージと infra パッケージで利用する共通的な処理やオブジェクトなどを、共有パッケージに配置して利用する構成を作成します。各パッケージのセットアップ方法、共有パッケージへの参照方法を実際に試してみます。

作成したソース一式は以下に配置していますので、詳細はリポジトリのソースを確認してください。

実行環境

  • Node.js: v22.14.0
  • npm: 10.9.2

セットアップ

セットアップは空の状態から、まずはルートパッケージを作成し、web パッケージ、infra パッケージ、共有パッケージをセットアップします。

ルートパッケージを作成

npm init
% tree
.
└── package.json

web パッケージを作成

npm init -w ./packages/web
% tree                      
.
├── node_modules
│   └── web -> ../packages/web
├── package-lock.json
├── package.json
└── packages
    └── web
        └── package.json

Next.js プロジェクトをセットアップ

作成した web パッケージに Next.js のプロジェクトをセットアップします。
create-next-app コマンドは空のディレクトリにする必要があったので、packages/web/package.json ファイルを削除してからセットアップコマンドを実行しました。

npx create-next-app@latest ./packages/web/.

セットアップが完了したら、起動することを確認します。

npm run -w web dev

> web@0.1.0 dev
> next dev --turbopack

   ▲ Next.js 15.3.1 (Turbopack)
   - Local:        http://localhost:3000
   - Network:      http://192.168.0.136:3000

 ✓ Starting...
 ✓ Ready in 562ms

問題なく起動することを確認できました。ルートディレクトリからパッケージ内の npm scripts を実行する際は、-w コマンドでワークスペースを指定して実行することができます。--workspace=web でも同様に実行することができます。

infra パッケージを作成

npm init -w ./packages/infra
% tree
.
├── node_modules
│   ├── infra -> ../packages/infra
│   └── web -> ../packages/web
├── package-lock.json
├── package.json
└── packages
    ├── infra
    │   └── package.json
    └── web
        └── package.json

AWS CDK プロジェクトをセットアップ

作成した infra パッケージに AWS CDK をセットアップします。

cd ./packages/infra
cdk init app --language typescript

デプロイする前に、bootstrap します。

npm run -w infra cdk -- bootstrap

> infra@0.1.0 cdk
> cdk bootstrap

...
 ✅  Environment aws://xxxxxxxx/ap-northeast-1 bootstrapped.

成功しましたので、AWS CDK のセットアップは問題なさそうですね。

共有(shared)パッケージを作成

npm init -w ./packages/shared
% tree                         
.
├── node_modules
│   ├── infra -> ../packages/infra
│   ├── shared -> ../packages/shared
│   └── web -> ../packages/web
├── package-lock.json
├── package.json
└── packages
    ├── infra
    │   └── package.json
    ├── shared
    │   └── package.json
    └── web
        └── package.json

共有パッケージに実装した処理を各パッケージから参照する

共有パッケージに、ID を生成する処理を実装しました。

packages/shared/src/id.ts
import { v4 as uuidv4 } from "uuid";

/**
 * 新しいUUIDを生成する
 * @returns 生成されたUUID文字列
 */
export function generateUuid(): string {
  return uuidv4();
}

このファンクションを shared パッケージ経由で import させるために、package.json や tsconfig 設定も修正しました。

packages/shared/src/index.ts
export * from "./id";
packages/shared/package.json
... 省略
"main": "dist/index.js",
"types": "dist/index.d.ts",
... 省略
packages/shared/tsconfig.json
... 省略
{
  "compilerOptions": {
    "outDir": "./dist",
... 省略

また、共有パッケージを shared パッケージ経由でアクセスさせる場合は、build しないと反映されないので、修正後は build するか、tsc --watch のように修正後すぐに build されるようにしておく必要があります。
※ TypeScript の Project References など試してみたが、うまくいかず・・・。よりよいやり方があったら追記します。

infra パッケージの Lambda から参照する

実装した ID を生成する処理を Lambda function から参照します。

packages/infra/lambda/uuid-generator.ts
import { generateUuid } from "shared";  // ここで共有パッケージの処理を import

export const handler = async () => {
  const id = generateUuid();
  console.log("Generated UUID:", id);

  return {
    statusCode: 200,
    body: JSON.stringify({
      id,
    }),
  };
};

実装した Lambda を CDK でデプロイできるように、NodejsFunction でリソースを作成します。

packages/infra/lib/infra-stack.ts
export class InfraStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    new nodejs.NodejsFunction(this, "UuidGeneratorFunction", {
      entry: path.join(__dirname, "../lambda/uuid-generator.ts"),
      handler: "handler",
      functionName: "uuid-generator",
    });
  }
}

これでデプロイした Lambda を実行して、正しく動くかを確認してみます。

image.png

意図していた通り実行できることを確認できました!

web パッケージの Next.js アプリから参照する

Next.js の Web ページに、共有パッケージの ID 生成処理を利用して、画面に表示する処理を実装します。

packages/web/app/page.tsx
import { generateUuid } from "shared";  // ここで共有パッケージの処理を import
import { UuidClient } from "./components/UuidClient";

export default function Home() {
  // サーバーサイドで初期UUIDを生成
  const uuid = generateUuid();
  return (
    <main className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-blue-50 to-blue-200 dark:from-gray-900 dark:to-blue-950">
      <UuidClient initialUuid={uuid} />
    </main>
  );
}
packages/web/app/components/UuidClient.tsx
"use client";
import { useState } from "react";

export function UuidClient({ initialUuid }: { initialUuid: string }) {
  const [currentUuid, setCurrentUuid] = useState(initialUuid);
  const [loading, setLoading] = useState(false);
  const handleRegenerate = async () => {
    setLoading(true);
    const res = await fetch("/api/generate-uuid");
    const data = await res.json();
    setCurrentUuid(data.uuid);
    setLoading(false);
  };
  return (
    <div className="flex flex-col items-center gap-8">
      <div className="text-xl font-bold text-black dark:text-black p-10 rounded-xl shadow-lg bg-white/90 dark:bg-black/70 border-2 border-blue-200 dark:border-blue-700 transition-all flex flex-col items-center gap-4">
        <span>生成したIDは</span>
        <span className="text-blue-700 dark:text-blue-400 text-4xl font-mono font-extrabold select-all break-all tracking-wide">
          {currentUuid}
        </span>
        <span>です</span>
      </div>
      <button
        className="px-8 py-3 rounded-lg bg-blue-600 text-white font-semibold text-lg shadow hover:bg-blue-700 transition disabled:opacity-50"
        onClick={handleRegenerate}
        disabled={loading}
      >
        {loading ? "再生成中..." : "IDを再生成"}
      </button>
    </div>
  );
}

ローカルで起動して、正しく実行できているかを試してみます。

image.png

意図した動きになってそうですね!

Tips

さいごに、環境構築時や動作確認時に知っておくと便利な Tips を紹介します。

依存関係を追加する

すべてのパッケージで利用可能な依存関係をインストール

npm install prettier

個別のパッケージに依存関係をインストール(オプションでワークスペース情報を指定する)

npm install prettier -w web

単一パッケージの時のようにディレクトリに移動してもOK

cd packages/web && npm install prettier

なお、node_modules は、ルートパッケージに作成され、各パッケージのディレクトリには、バージョン違いなど異なるパッケージが存在する場合に作成されます。

scripts を実行する

npm run コマンドで実行する scripts も、依存関係を追加する場合と同様にワークスペースを指定して実行することができます。

npm run build -w shared

VSCode のクイックフィックスから参照できない

環境や設定の問題かもしれませんが、VSCode で shared パッケージの関数を利用しようとした場合のサジェストの選択肢やクイックフィックスでの import の選択肢に出ませんでした。

image.png

build 時は、参照できないなどの問題はなかったのですが、実装時に手入力は面倒なので、package.json に shared パッケージの dependencies を指定することで、解消することができました。

packages/infra/package.json
"dependencies": {
    ...,
    "shared": "*"
}

image.png

なお、IntelliJ IDEA だと、問題なく参照できました。

CDK パッケージの型参照設定

cdk init コマンドで初期化したプロジェクトの場合、以下のように tsconfig.json に typeRoots が設定されています。

tsconfig.json
    "typeRoots": [
      "./node_modules/@types"
    ]

この設定の場合、指定されたパスの配下のパッケージのみが参照の対象となります。(../node_modules/@types/../../node_modules/@types/ が含まれない)
Monorepo 構成の場合は、巻き上げでモジュールを解決するため、typeRoot を指定しないほうが良さそうですね。

Next.js を standalone でビルドする際の出力パス

Next.js アプリを standalone オプションでビルドする場合、通常のプロジェクトとは異なり、今回作成した Monorepo 環境では出力パスが変わるようでした。

.next/standalone
├── node_modules
│   ├── ...
├── package.json
└── packages
    ├── shared
    │   └── package.json
    └── web
        ├── package.json
        └── server.js    // packages/web 配下に出力されている

わかっていれば特に問題はありませんが、公式サンプルの Docker ビルドコマンド などをそのまま利用しようとすると、パスが異なるので、環境に合わせて修正する必要があります。

参考

3
1
0

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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?