Monorepoでの設定例 – NestJS, Next.js

はじめに

monorepoは設定が決まるまで、かなり大変な作業ですが、一度うまくいけば、まとめて管理できるメリットがあり、少人数のプロジェクトにはむいていると思います。

うまくいった設定を以下にメモし、今後の設定の見直しなどに利用しようと思います。

アプリ全体の設定

  • ルートで、pnpm build, pnpm devで開発できるように設定

./package.json

{
  "name": "my-app",
  "private": true,
  "packageManager": "pnpm@9.4.0",
  "scripts": {
    "dev": "turbo run dev --parallel",
    "build": "turbo run build",
    "lint": "turbo run lint",
    "test": "turbo run test"
  },
  "devDependencies": {
    "turbo": "latest",
    "typescript": "^5.9.2"
  }
}

./pnpm-workspace.yaml

packages:
  - 'apps/*'
  - 'libs/adapters'

./tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "baseUrl": ".",
    "paths": {
      "@app/adapters": ["libs/adapters/src/index.ts"],
      "@app/adapters/*": ["libs/adapters/src/*"]
    },
    "declaration": true,
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "files": [],
  "references": [
    { "path": "./libs/adapters" },
    { "path": "./apps/api" }
  ]  
}

./turbo.json

{
  "$schema": "https://turborepo.org/schema.json",
  "tasks": {
    "dev": {
      "cache": false,
      "persistent": true
    },
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "lint": {
      "outputs": []
    },
    "test": {
      "outputs": []
    }
  }
}

apiアプリ

./apps/api/package.json

{
  "name": "api",
  "version": "0.0.1",
  "description": "",
  "author": "",
  "private": true,
  "license": "UNLICENSED",
  "scripts": {
    "build": "tsc -b",
    "format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"",
    "start": "nest start",
    "dev": "nest start --watch",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "node dist/apps/ai-avatar-backend/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./apps/ai-avatar-backend/test/jest-e2e.json"
  },
  "dependencies": {
    "@app/adapters": "workspace:*",
    "@nestjs/common": "^11.0.1",
    "@nestjs/config": "^4.0.2",
    "@nestjs/core": "^11.0.1",
    "@nestjs/platform-express": "^11.0.1",
    "firebase-admin": "^13.5.0",
    "openai": "^5.19.1",
    "reflect-metadata": "^0.2.2",
    "rxjs": "^7.8.1"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3.2.0",
    "@eslint/js": "^9.18.0",
    "@nestjs/cli": "^11.0.0",
    "@nestjs/schematics": "^11.0.0",
    "@nestjs/testing": "^11.0.1",
    "@types/express": "^5.0.0",
    "@types/jest": "^30.0.0",
    "@types/node": "^22.10.7",
    "@types/supertest": "^6.0.2",
    "eslint": "^9.18.0",
    "eslint-config-prettier": "^10.0.1",
    "eslint-plugin-prettier": "^5.2.2",
    "globals": "^16.0.0",
    "jest": "^30.0.0",
    "prettier": "^3.4.2",
    "source-map-support": "^0.5.21",
    "supertest": "^7.0.0",
    "ts-jest": "^29.2.5",
    "ts-loader": "^9.5.2",
    "ts-node": "^10.9.2",
    "tsconfig-paths": "^4.2.0",
    "turbo": "^2.5.6",
    "typescript": "^5.7.3",
    "typescript-eslint": "^8.20.0"
  },
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      "ts"
    ],
    "rootDir": ".",
    "testRegex": ".*\\.spec\\.ts$",
    "transform": {
      "^.+\\.(t|j)s$": "ts-jest"
    },
    "collectCoverageFrom": [
      "**/*.(t|j)s"
    ],
    "coverageDirectory": "./coverage",
    "testEnvironment": "node",
    "roots": [
      "<rootDir>/apps/",
      "<rootDir>/libs/"
    ]
  }
}

./apps/api/tsconfig.json

{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src",
    "baseUrl": "./",
    "paths": {
      "@app/adapters/*": ["../../libs/adapters/dist/*"],
      "@app/adapters": ["../../libs/adapters/dist"]
    },
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "target": "es2017",
    "lib": ["es2017", "dom"],
    "declaration": true,
    "composite": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "sourceMap": true,
    "noEmit": false,
    "incremental": true,
    "tsBuildInfoFile": "./dist/.tsbuildinfo"
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules",
    "dist",
    "**/*.spec.ts"
  ],
  "references": [
    { "path": "../../libs/adapters" }
  ]
}

./apps/api/nest-cli.json

{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "tsConfigPath": "tsconfig.json"
  }
}

webアプリ

./app/web/package.json

{
  "name": "web",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build --turbopack",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "firebase": "^12.2.1",
    "next": "15.5.2",
    "react": "19.1.0",
    "react-dom": "19.1.0"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3",
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "15.5.2",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

./apps/web/tsconfig.json

// apps/web/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "jsx": "preserve",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "incremental": true,
    "resolveJsonModule": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "target": "ES2017",
    "skipLibCheck": true,
    "strict": false,
    "noEmit": true,
    "module": "esnext",
    "esModuleInterop": true,
    "moduleResolution": "node",
    "isolatedModules": true
  },
  "include": [
    "**/*.ts",
    "**/*.tsx",
    "next-env.d.ts",
    ".next/types/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ],
  "references": [
    {
      "path": "../../libs/adapters"
    }
  ]
}

@app/adaptersアプリ

./lib/adapters/package.json

{
  "name": "@app/adapters",
  "version": "1.0.0",
  "description": "Shared adapters library for the monorepo.",
  "private": true,
  "files": ["dist"],
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc -b",
    "clean": "rimraf dist"
  },
  "dependencies": {
    "@nestjs/common": "^11.1.6",
    "firebase-admin": "^13.5.0",
    "openai": "^5.19.1"
  },
  "devDependencies": {
    "@nestjs/testing": "^11.1.6",
    "@types/jest": "^30.0.0",
    "rimraf": "^6.0.1",
    "typescript": "^5.9.2"
  }
}

./lib/adapters/tsconfig.json

{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "composite": true,               // プロジェクト参照用
    "declaration": true,             // .d.ts も出力
    "emitDeclarationOnly": false,    // .js も出す
    "outDir": "./dist",
    "rootDir": "./src",
    "noEmit": false,                 // emit 禁止を無効化
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

関連記事

カテゴリー

アーカイブ

Lang »