はじめに
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 を生成する処理を実装しました。
import { v4 as uuidv4 } from "uuid";
/**
* 新しいUUIDを生成する
* @returns 生成されたUUID文字列
*/
export function generateUuid(): string {
return uuidv4();
}
このファンクションを shared
パッケージ経由で import させるために、package.json や tsconfig 設定も修正しました。
export * from "./id";
... 省略
"main": "dist/index.js",
"types": "dist/index.d.ts",
... 省略
... 省略
{
"compilerOptions": {
"outDir": "./dist",
... 省略
また、共有パッケージを shared
パッケージ経由でアクセスさせる場合は、build しないと反映されないので、修正後は build するか、tsc --watch
のように修正後すぐに build されるようにしておく必要があります。
※ TypeScript の Project References など試してみたが、うまくいかず・・・。よりよいやり方があったら追記します。
infra パッケージの Lambda から参照する
実装した ID を生成する処理を Lambda function から参照します。
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 でリソースを作成します。
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 を実行して、正しく動くかを確認してみます。
意図していた通り実行できることを確認できました!
web パッケージの Next.js アプリから参照する
Next.js の Web ページに、共有パッケージの ID 生成処理を利用して、画面に表示する処理を実装します。
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>
);
}
"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>
);
}
ローカルで起動して、正しく実行できているかを試してみます。
意図した動きになってそうですね!
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 の選択肢に出ませんでした。
build 時は、参照できないなどの問題はなかったのですが、実装時に手入力は面倒なので、package.json に shared
パッケージの dependencies
を指定することで、解消することができました。
"dependencies": {
...,
"shared": "*"
}
なお、IntelliJ IDEA だと、問題なく参照できました。
CDK パッケージの型参照設定
cdk init
コマンドで初期化したプロジェクトの場合、以下のように tsconfig.json に typeRoots が設定されています。
"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 ビルドコマンド などをそのまま利用しようとすると、パスが異なるので、環境に合わせて修正する必要があります。
参考