DevToolBox無料
ブログ

pnpm Workspace Monorepo セットアップガイド

12分by DevToolBox

pnpm Workspace Monorepo Setup: The Complete Guide

pnpm workspaces provide the fastest, most disk-efficient way to manage a JavaScript/TypeScript monorepo. Unlike npm and yarn, pnpm uses a content-addressable store and hard links, meaning shared dependencies are stored only once on disk regardless of how many packages use them. This guide walks through setting up a production-ready pnpm monorepo from scratch, including shared configs, internal packages, and CI/CD pipelines.

Why pnpm for Monorepos?

Featurenpm workspacesyarn workspacespnpm workspaces
Disk usage (500 deps)~800 MB~700 MB~400 MB
Install speed (cold)~25s~18s~12s
Install speed (warm)~10s~6s~2s
Strict node_modulesNo (hoisted)Partial (PnP)Yes (isolated by default)
Phantom deps preventionNoWith PnPYes (strict by default)
Filtering commandsBasic (-w flag)Good (foreach)Excellent (--filter)
Content-addressable storeNoNoYes
Side effects cacheNoNoYes

Initial Setup

# Install pnpm globally
npm install -g pnpm@latest
# Or use corepack (Node.js 16+)
corepack enable
corepack prepare pnpm@latest --activate

# Create project directory
mkdir my-monorepo && cd my-monorepo

# Initialize root package.json
pnpm init

# Create the workspace configuration
touch pnpm-workspace.yaml

pnpm-workspace.yaml

# pnpm-workspace.yaml
packages:
  - 'apps/*'        # Applications (web, api, admin, etc.)
  - 'packages/*'    # Shared libraries and configs
  - 'tools/*'       # Build tools, scripts, generators

Root package.json

{
  "name": "my-monorepo",
  "private": true,
  "packageManager": "pnpm@9.5.0",
  "engines": {
    "node": ">=20.0.0",
    "pnpm": ">=9.0.0"
  },
  "scripts": {
    "dev": "pnpm -r --parallel run dev",
    "build": "pnpm -r run build",
    "test": "pnpm -r run test",
    "lint": "pnpm -r run lint",
    "type-check": "pnpm -r run type-check",
    "clean": "pnpm -r run clean && rm -rf node_modules",
    "format": "prettier --write '**/*.{ts,tsx,js,json,md,yaml}' --ignore-path .gitignore",
    "prepare": "husky"
  },
  "devDependencies": {
    "prettier": "^3.3.0",
    "husky": "^9.0.0",
    "lint-staged": "^15.0.0",
    "typescript": "^5.5.0"
  },
  "lint-staged": {
    "*.{ts,tsx,js}": ["prettier --write", "eslint --fix"],
    "*.{json,md,yaml}": ["prettier --write"]
  }
}

.npmrc Configuration

# .npmrc — pnpm-specific settings

# Hoist only these packages to the root (needed for some tools)
public-hoist-pattern[]=*prettier*
public-hoist-pattern[]=*eslint*

# Strict mode — prevent importing undeclared dependencies
strict-peer-dependencies=false
auto-install-peers=true

# Performance
prefer-frozen-lockfile=true

# Save exact versions
save-exact=true

Project Structure

my-monorepo/
├── apps/
│   ├── web/                       # Next.js web application
│   │   ├── package.json           # name: "@acme/web"
│   │   ├── tsconfig.json
│   │   ├── next.config.ts
│   │   └── src/
│   ├── api/                       # Express/Fastify API server
│   │   ├── package.json           # name: "@acme/api"
│   │   ├── tsconfig.json
│   │   └── src/
│   └── admin/                     # Admin dashboard
│       ├── package.json           # name: "@acme/admin"
│       └── src/
├── packages/
│   ├── ui/                        # Shared React component library
│   │   ├── package.json           # name: "@acme/ui"
│   │   ├── tsconfig.json
│   │   └── src/
│   │       ├── components/
│   │       │   ├── Button.tsx
│   │       │   ├── Input.tsx
│   │       │   └── Modal.tsx
│   │       └── index.ts
│   ├── shared/                    # Shared types and utilities
│   │   ├── package.json           # name: "@acme/shared"
│   │   └── src/
│   │       ├── types.ts
│   │       ├── validators.ts
│   │       └── index.ts
│   ├── config-eslint/             # Shared ESLint config
│   │   ├── package.json           # name: "@acme/eslint-config"
│   │   └── index.js
│   └── config-typescript/         # Shared TypeScript configs
│       ├── package.json           # name: "@acme/typescript-config"
│       ├── base.json
│       ├── nextjs.json
│       └── node.json
├── pnpm-workspace.yaml
├── pnpm-lock.yaml
├── package.json
├── .npmrc
├── .gitignore
└── turbo.json                     # Optional: Turborepo for task caching

Creating Internal Packages

Shared UI Library

// packages/ui/package.json
{
  "name": "@acme/ui",
  "version": "0.0.1",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "import": "./src/index.ts"
    },
    "./components/*": {
      "types": "./src/components/*.tsx",
      "import": "./src/components/*.tsx"
    }
  },
  "scripts": {
    "lint": "eslint src/",
    "type-check": "tsc --noEmit"
  },
  "dependencies": {
    "clsx": "^2.1.0"
  },
  "devDependencies": {
    "@acme/typescript-config": "workspace:*",
    "@types/react": "^18.3.0",
    "typescript": "^5.5.0"
  },
  "peerDependencies": {
    "react": "^18.0.0 || ^19.0.0",
    "react-dom": "^18.0.0 || ^19.0.0"
  }
}
// packages/ui/src/components/Button.tsx
import { type ButtonHTMLAttributes, forwardRef } from 'react';
import { clsx } from 'clsx';

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  isLoading?: boolean;
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ variant = 'primary', size = 'md', isLoading, children, className, disabled, ...props }, ref) => {
    return (
      <button
        ref={ref}
        disabled={disabled || isLoading}
        className={clsx(
          'inline-flex items-center justify-center rounded-lg font-medium transition-colors',
          {
            'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
            'bg-gray-100 text-gray-900 hover:bg-gray-200': variant === 'secondary',
            'bg-transparent hover:bg-gray-100': variant === 'ghost',
            'bg-red-600 text-white hover:bg-red-700': variant === 'danger',
            'px-3 py-1.5 text-sm': size === 'sm',
            'px-4 py-2 text-base': size === 'md',
            'px-6 py-3 text-lg': size === 'lg',
            'opacity-50 cursor-not-allowed': disabled || isLoading,
          },
          className
        )}
        {...props}
      >
        {isLoading ? 'Loading...' : children}
      </button>
    );
  }
);

Button.displayName = 'Button';

// packages/ui/src/index.ts
export { Button, type ButtonProps } from './components/Button';
export { Input, type InputProps } from './components/Input';
export { Modal, type ModalProps } from './components/Modal';

Shared TypeScript Config

// packages/config-typescript/base.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true
  },
  "exclude": ["node_modules", "dist", ".next"]
}

// packages/config-typescript/nextjs.json
{
  "extends": "./base.json",
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "ES2022"],
    "allowJs": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [{ "name": "next" }],
    "paths": { "@/*": ["./src/*"] }
  }
}

Essential pnpm Commands

# Install all dependencies
pnpm install

# Add a dependency to a specific package
pnpm add zod --filter @acme/api
pnpm add -D vitest --filter @acme/shared

# Add a workspace dependency (internal package)
pnpm add @acme/shared --filter @acme/api --workspace
pnpm add @acme/ui --filter @acme/web --workspace

# Run a script in a specific package
pnpm --filter @acme/web dev
pnpm --filter @acme/api test

# Run across all packages
pnpm -r run build          # Run build in all packages
pnpm -r --parallel run dev # Run dev in parallel

# Filter by directory
pnpm --filter ./apps/* run build
pnpm --filter ./packages/* run type-check

# Filter by dependency graph
pnpm --filter @acme/web... run build    # web and all its deps
pnpm --filter ...@acme/shared run test  # shared and all dependents

# Filter by changed files (great for CI)
pnpm --filter "...[origin/main]" run test  # Test changed packages

# List all workspace packages
pnpm ls -r --depth -1

# Why is a package installed?
pnpm why react --filter @acme/web

# Update dependencies across workspace
pnpm -r update typescript
pnpm -r update --latest     # Update to latest major versions

Consuming Internal Packages

// apps/web/package.json
{
  "name": "@acme/web",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "type-check": "tsc --noEmit"
  },
  "dependencies": {
    "@acme/ui": "workspace:*",
    "@acme/shared": "workspace:*",
    "next": "^15.0.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "@acme/eslint-config": "workspace:*",
    "@acme/typescript-config": "workspace:*",
    "@types/react": "^18.3.0",
    "typescript": "^5.5.0"
  }
}
// apps/web/src/app/page.tsx
import { Button } from '@acme/ui';
import { formatDate, type User } from '@acme/shared';

export default function Home() {
  return (
    <main>
      <h1>Welcome</h1>
      <Button variant="primary" size="lg">
        Get Started
      </Button>
    </main>
  );
}

CI/CD with pnpm

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Type check
        run: pnpm -r run type-check

      - name: Lint
        run: pnpm -r run lint

      - name: Test changed packages
        run: pnpm --filter "...[origin/main]" run test

      - name: Build
        run: pnpm -r run build

Integrating with Turborepo

// turbo.json — optional but recommended for caching
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    },
    "dev": {
      "dependsOn": ["^build"],
      "cache": false,
      "persistent": true
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"]
    },
    "lint": { "outputs": [] },
    "type-check": { "dependsOn": ["^build"], "outputs": [] }
  }
}
# Install Turborepo as a dev dependency
pnpm add -D turbo -w

# Use turbo instead of pnpm -r for task running
pnpm turbo build      # Builds with caching and correct order
pnpm turbo test       # Only re-tests changed packages
pnpm turbo dev        # Starts all dev servers in parallel

Best Practices

  • Use workspace:* protocol — always reference internal packages with workspace:*
  • Pin pnpm version — set packageManager in root package.json for reproducible installs
  • Enable strict mode — pnpm prevents phantom dependencies by default; do not disable this
  • Use --frozen-lockfile in CI — never let CI modify the lockfile
  • Scope dev dependencies — shared tools (prettier, husky) go in root; package-specific tools go in the package
  • Add Turborepo for caching — pnpm handles dependencies, Turborepo handles task orchestration
  • Use filter for CI — only build/test packages affected by the PR

Validate your package.json and turbo.json files with our JSON Formatter. For a broader look at monorepo tooling, read our Monorepo Guide 2026 covering Turborepo and Nx. For comparing pnpm with other package managers, see our npm vs yarn vs pnpm vs Bun comparison.

𝕏 Twitterin LinkedIn
この記事は役に立ちましたか?

最新情報を受け取る

毎週の開発ヒントと新ツール情報。

スパムなし。いつでも解除可能。

Try These Related Tools

{ }JSON Formatter📋YAML Formatter

Related Articles

Monorepo ガイド 2026:Turborepo vs Nx

Turborepo と Nx での monorepo セットアップ完全ガイド。

npm vs yarn vs pnpm vs bun:2026年どのパッケージマネージャーを使うべき?

npm、yarn、pnpm、bunをベンチマークで比較。速度、ディスク使用量、monorepoサポートを解説。

CI/CDパイプラインベストプラクティス:GitHub Actions、テスト、デプロイ

GitHub Actionsで堅牢なCI/CDパイプラインを構築 — テスト戦略とデプロイメントパターン。