2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【npm workspace】モノレポ構成のプロジェクトでアプリケーションを相互に連携する

Last updated at Posted at 2024-12-15

はじめに

こんにちは、梅雨です。

最近ではバックエンドに 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 として指定しているところです。

これによって、複数のワークスペースを一元的に管理できるようになります。

package.json
{
  "name": "monorepo-workspace-demo",
  "workspaces": [
    "apps/client",
    "apps/server",
    "packages/*"
  ]
}

packages/ 以下には今後いろいろなパッケージが作成される可能性があり、その度に workspaces を編集するのは効率が悪いため、ワイルドカードを用いて packages/* のように指定しています。

各ワークスペースの設定

各ワークスペースには、package.json を配置する必要があります。管理される側の package.json では、特別な設定は必要ありません。

例として、サーバの package.json は以下のようにしました。

apps/server/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"
  }
}

依存パッケージのインストール

各パッケージ(またはアプリケーション)で必要なライブラリをインストールするには、

  1. 各ディレクトリに移動して npm i
  2. ルートディレクトリで 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 に以下のようなシンプルなボタンコンポーネントを作成してみます。

packages/ui/button.tsx
import { ComponentPropsWithoutRef } from "react";

type ButtonProps = {
  title: string;
} & ComponentPropsWithoutRef<"button">;

export const Button = ({ title, ...buttonProps }: ButtonProps) => {
  return <button {...buttonProps}>{title}</button>;
};

tsconfig.json は以下のように指定しています。

packages/ui/tsconfig.json
{
  "compilerOptions": {
    "target": "es2016",
    "module": "CommonJS",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "jsx": "react-jsx",
    "outDir": "./dist",
    "declaration": true
  }
}

また、package.json は以下のようになっています。

packages/ui/package.json
{
  "name": "ui",
  "main": "./dist/index.ts",
  "scripts": {
    "build": "tsc"
  },
  "devDependencies": {
    "@types/react": "^19.0.1",
    "react": "^19.0.0"
  }
}

配布用のエンドポイントとして、./dist/index.ts を指定しました。このファイルは、外部にエクスポートするコンポーネントを統合する役割があります。

packages/ui/src/index.ts
export * from "./button";

以上で ui パッケージの準備ができました。npm run build コマンドで配布用にビルドしましょう。

$ npm run build -w ui

クライアント(Next.js)でこのコンポーネントを呼び出しましょう。

apps/client/src/page.tsx
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 に以下の設定を追記しました。

apps/client/src/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 の強みである型安全性を最大限享受するためには各ワークスペース間の連携が必要不可欠であるため、ぜひ以上の内容を参考にしてみてください。

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?