はじめに
こんにちは、梅雨です。
最近ではバックエンドに TypeScript を採用し、フロントエンドとE2Eで型安全に開発を行うことがきでるようになってきています。
今回は、npm を用いたモノレポ構成のプロジェクトでアプリケーション同士を上手く連携する方法について解説していこうと思います。
プロジェクト構成
この記事では以下のようなプロジェクト構成を例にして説明を進めていきます。各スタックは基本的になんでも構いませんが、今回はサーバに Express 、クライアントには Next.js を使用します。
monorepo-workspace-demo
├── apps
│ ├── client
│ └── server
└── packages
└── ui
ルートの npm init
まずはプロジェクトのルートディレクトリで npm init
を実行します。
$ npm init
package.json
の中身は以下の内容で充分です。ポイントとしては、各ワークスペースの package.json
のあるディレクトを workspaces
として指定しているところです。
これによって、複数のワークスペースを一元的に管理できるようになります。
{
"name": "monorepo-workspace-demo",
"workspaces": [
"apps/client",
"apps/server",
"packages/*"
]
}
packages/
以下には今後いろいろなパッケージが作成される可能性があり、その度に workspaces
を編集するのは効率が悪いため、ワイルドカードを用いて packages/*
のように指定しています。
各ワークスペースの設定
各ワークスペースには、package.json
を配置する必要があります。管理される側の package.json
では、特別な設定は必要ありません。
例として、サーバの package.json
は以下のようにしました。
{
"name": "server",
"scripts": {
"build": "tsc",
"dev": "nodemon src/index.ts",
"start": "node dist/index.js"
},
"dependencies": {
"express": "^4.21.2"
},
"devDependencies": {
"@types/express": "^5.0.0",
"nodemon": "^3.1.9",
"ts-node": "^10.9.2",
"typescript": "^5.7.2"
}
}
依存パッケージのインストール
各パッケージ(またはアプリケーション)で必要なライブラリをインストールするには、
- 各ディレクトリに移動して
npm i
- ルートディレクトリで
npm i -w client
(ワークスペースを指定)
の2通りの方法があります。せっかく npm workspace を使用しているので、自分は後者をおすすめします。
ここで一つ注意点があり、npm workspace で管理されている各ワークスペースには node_modules
はインストールされず、代わりにルートディレクトにインストールされます。また、package-lock.json
もルートディレクトのみに作成されます。
そのため、npx create-next-app@latest
コマンドなどでアプリケーションを初期化している場合は、そのプロジェクト内の node_modules
および package-lock.json
は手動で消してあげる必要があります。
ルートディレクトリで npm i
および npm list
を実行してみましょう。以下のように各ワークスペースでの依存関係が表示されるはずです。
$ npm i
$ npm list
monorepo-workspace-demo@ /Users/meiyu/Documents/qiita/monorepo-workspace-demo
├─┬ client@ -> ./apps/client
│ ├── @eslint/eslintrc@3.2.0
│ ├── @types/node@20.17.10
│ ├── @types/react-dom@19.0.2
│ ├── @types/react@19.0.1
│ ├── eslint-config-next@15.1.0
│ ├── eslint@9.17.0
│ ├── next@15.1.0
│ ├── postcss@8.4.49
│ ├── react-dom@19.0.0
│ ├── react@19.0.0
│ ├── tailwindcss@3.4.16
│ └── typescript@5.7.2
├─┬ server@ -> ./apps/server
│ ├── @types/express@5.0.0
│ ├── express@4.21.2
│ ├── nodemon@3.1.9
│ ├── ts-node@10.9.2
│ └── typescript@5.7.2 deduped
└─┬ ui@1.0.0 -> ./packages/ui
├── @types/react@19.0.1 deduped
└── react@19.0.0 deduped
パッケージ間でコードを呼び出す
最後に、パッケージ間でコードの呼び出しを行ってみましょう。
まずは packages/ui
に以下のようなシンプルなボタンコンポーネントを作成してみます。
import { ComponentPropsWithoutRef } from "react";
type ButtonProps = {
title: string;
} & ComponentPropsWithoutRef<"button">;
export const Button = ({ title, ...buttonProps }: ButtonProps) => {
return <button {...buttonProps}>{title}</button>;
};
tsconfig.json
は以下のように指定しています。
{
"compilerOptions": {
"target": "es2016",
"module": "CommonJS",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"outDir": "./dist",
"declaration": true
}
}
また、package.json
は以下のようになっています。
{
"name": "ui",
"main": "./dist/index.ts",
"scripts": {
"build": "tsc"
},
"devDependencies": {
"@types/react": "^19.0.1",
"react": "^19.0.0"
}
}
配布用のエンドポイントとして、./dist/index.ts
を指定しました。このファイルは、外部にエクスポートするコンポーネントを統合する役割があります。
export * from "./button";
以上で ui
パッケージの準備ができました。npm run build
コマンドで配布用にビルドしましょう。
$ npm run build -w ui
クライアント(Next.js)でこのコンポーネントを呼び出しましょう。
import { Button } from "ui";
const Home = () => {
return (
<div>
<Button title="Click" />
</div>
);
};
export default Home;
これで上手くいくはず(少なくとも VSCode 上では上手くいっている)のですが、残念ながらクライアントで npm run dev
を実行したところエラーが発生しました。
⨯ ./src/app/page.tsx:1:1
Module not found: Can't resolve 'ui'
> 1 | import { Button } from "ui";
| ^
2 |
3 | const Home = () => {
4 | return (
仕方ないので、クライアントの tsconfig.json
に以下の設定を追記しました。
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"],
+ "ui": ["../../packages/ui/src"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
これでは元も子もないような気がしますが、一旦これで上手くいきました。
別の解決策があればコメントで教えてください。
おわりに
今回の記事では、npm を用いたモノレポ構成のプロジェクトでアプリケーション同士を上手く連携する方法について解説しました。
TypeScript の強みである型安全性を最大限享受するためには各ワークスペース間の連携が必要不可欠であるため、ぜひ以上の内容を参考にしてみてください。