Ides Editors

Multi-Root Workspaces for Monorepos

A practical guide to VS Code multi-root workspaces for managing monorepos, with folder-specific settings, task scoping, extension configuration, and debugging across packages.

Multi-Root Workspaces for Monorepos

A monorepo with five packages and a shared config directory is painful to work with in a single VS Code window. The file explorer becomes a deep tree of nested folders. Search results include irrelevant matches from packages you are not working on. Linters fight over conflicting configurations. Multi-root workspaces solve this by treating each package as a separate root folder within one window, each with its own settings, tasks, and debug configurations.

I switched to multi-root workspaces for every monorepo I maintain. The immediate benefit is a cleaner file explorer and scoped search. The deeper benefit is per-folder settings — different linting rules, different formatters, different test runners — all coexisting in one window.

Prerequisites

  • VS Code installed (v1.18+ for multi-root support)
  • A project with multiple related directories or a monorepo
  • Basic familiarity with VS Code settings and tasks
  • Understanding of your project's package structure

Creating a Multi-Root Workspace

From the UI

  1. Open VS Code
  2. FileAdd Folder to Workspace...
  3. Select the folders you want
  4. FileSave Workspace As... → save as myproject.code-workspace

From the Command Line

code myproject.code-workspace

Workspace File Structure

The .code-workspace file is JSON:

{
  "folders": [
    {
      "name": "API Server",
      "path": "packages/api"
    },
    {
      "name": "Web Client",
      "path": "packages/web"
    },
    {
      "name": "Shared Library",
      "path": "packages/shared"
    },
    {
      "name": "Root Config",
      "path": "."
    }
  ],
  "settings": {},
  "extensions": {
    "recommendations": []
  }
}

The name field controls what appears in the file explorer sidebar. Without it, VS Code uses the folder name. Custom names make navigation clearer when folder names are generic like src or lib.

Folder Ordering

Folders appear in the explorer in the order listed. Put the folders you work with most at the top:

{
  "folders": [
    { "name": "API", "path": "packages/api" },
    { "name": "Web", "path": "packages/web" },
    { "name": "Shared", "path": "packages/shared" },
    { "name": "E2E Tests", "path": "tests/e2e" },
    { "name": "Infrastructure", "path": "infra" },
    { "name": "Root", "path": "." }
  ]
}

Folder-Specific Settings

The real power of multi-root workspaces is per-folder settings. Each folder can have different formatting, linting, and editor behavior.

Workspace-Level Settings

Settings in the .code-workspace file apply to all folders:

{
  "folders": [...],
  "settings": {
    "editor.tabSize": 2,
    "files.trimTrailingWhitespace": true,
    "search.exclude": {
      "**/node_modules": true,
      "**/dist": true
    }
  }
}

Per-Folder Settings

Each folder can have its own .vscode/settings.json that overrides workspace settings:

// packages/api/.vscode/settings.json
{
  "editor.tabSize": 4,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "[javascript]": {
    "editor.formatOnSave": true
  },
  "eslint.workingDirectories": ["."],
  "jest.rootPath": "."
}
// packages/web/.vscode/settings.json
{
  "editor.tabSize": 2,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "emmet.includeLanguages": {
    "javascript": "javascriptreact"
  },
  "css.validate": false,
  "tailwindCSS.includeLanguages": {
    "plaintext": "html"
  }
}

The settings hierarchy for multi-root workspaces:

1. Default Settings
2. User Settings
3. Workspace Settings (.code-workspace file)
4. Folder Settings (.vscode/settings.json in each folder)

Folder settings always win for their folder's files.

Scoped Search

Search in multi-root workspaces can target specific folders:

Ctrl+Shift+F → Click "files to include" field

Use folder name prefixes to scope searches:

# Search only in the API folder
./API/**

# Search in API and Shared
./API/**,./Shared/**

# Exclude test files in all folders
!**/*.test.js

Or right-click a folder in the explorer → "Find in Folder" to scope automatically.

Search Exclusions Per Folder

// packages/api/.vscode/settings.json
{
  "search.exclude": {
    "**/node_modules": true,
    "**/dist": true,
    "**/coverage": true,
    "**/*.generated.js": true
  }
}
// packages/web/.vscode/settings.json
{
  "search.exclude": {
    "**/node_modules": true,
    "**/.next": true,
    "**/build": true
  }
}

Tasks in Multi-Root Workspaces

Workspace-Level Tasks

Define tasks in the .code-workspace file that apply globally:

{
  "folders": [...],
  "settings": {},
  "tasks": {
    "version": "2.0.0",
    "tasks": [
      {
        "label": "Install All",
        "type": "shell",
        "command": "npm install",
        "options": {
          "cwd": "${workspaceFolder:Root}"
        },
        "problemMatcher": []
      },
      {
        "label": "Build All",
        "type": "shell",
        "command": "npm run build --workspaces",
        "options": {
          "cwd": "${workspaceFolder:Root}"
        },
        "problemMatcher": "$tsc"
      }
    ]
  }
}

The ${workspaceFolder:FolderName} variable resolves to the path of the named folder.

Per-Folder Tasks

Each folder can have its own .vscode/tasks.json:

// packages/api/.vscode/tasks.json
{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "API: Start Dev",
      "type": "shell",
      "command": "npm run dev",
      "isBackground": true,
      "problemMatcher": []
    },
    {
      "label": "API: Test",
      "type": "shell",
      "command": "npm test",
      "group": {
        "kind": "test",
        "isDefault": true
      },
      "problemMatcher": []
    }
  ]
}
// packages/web/.vscode/tasks.json
{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "Web: Start Dev",
      "type": "shell",
      "command": "npm run dev",
      "isBackground": true,
      "problemMatcher": []
    },
    {
      "label": "Web: Build",
      "type": "shell",
      "command": "npm run build",
      "group": {
        "kind": "build",
        "isDefault": true
      },
      "problemMatcher": []
    }
  ]
}

When you run Ctrl+Shift+B or select "Run Task," VS Code shows tasks from all folders, prefixed with the folder name.

Compound Tasks Across Folders

Start multiple services from a workspace-level compound task:

// In .code-workspace
{
  "tasks": {
    "version": "2.0.0",
    "tasks": [
      {
        "label": "Start All Services",
        "dependsOn": ["API: Start Dev", "Web: Start Dev"],
        "dependsOrder": "parallel",
        "problemMatcher": []
      }
    ]
  }
}

Debugging Across Folders

Per-Folder Launch Configurations

Each folder has its own .vscode/launch.json:

// packages/api/.vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "API: Debug Server",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/src/server.js",
      "envFile": "${workspaceFolder}/.env",
      "console": "integratedTerminal",
      "skipFiles": ["<node_internals>/**"]
    }
  ]
}
// packages/web/.vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Web: Debug",
      "type": "node",
      "request": "launch",
      "runtimeExecutable": "npm",
      "runtimeArgs": ["run", "dev"],
      "console": "integratedTerminal"
    }
  ]
}

Workspace-Level Compounds

Debug multiple packages simultaneously:

// In .code-workspace or a workspace-level launch.json
{
  "launch": {
    "version": "0.2.0",
    "configurations": [],
    "compounds": [
      {
        "name": "Full Stack Debug",
        "configurations": ["API: Debug Server", "Web: Debug"],
        "stopAll": true
      }
    ]
  }
}

Extension Management

Per-Folder Extension Recommendations

// packages/api/.vscode/extensions.json
{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "humao.rest-client",
    "mtxr.sqltools"
  ]
}
// packages/web/.vscode/extensions.json
{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "bradlc.vscode-tailwindcss",
    "dsznajder.es7-react-js-snippets"
  ]
}

Workspace-Level Recommendations

// In .code-workspace
{
  "extensions": {
    "recommendations": [
      "dbaeumer.vscode-eslint",
      "esbenp.prettier-vscode",
      "eamodio.gitlens"
    ],
    "unwantedRecommendations": [
      "hookyqr.beautify"
    ]
  }
}

Disabling Extensions Per Folder

Some extensions should only run in specific folders. Use the eslint.workingDirectories pattern for ESLint, or disable extensions for specific file types:

// packages/api/.vscode/settings.json
{
  "eslint.workingDirectories": ["."],
  "tailwindCSS.enable": false
}

Complete Working Example: Monorepo Workspace

my-monorepo/
  packages/
    api/
      .vscode/
        settings.json
        tasks.json
        launch.json
      src/
      package.json
    web/
      .vscode/
        settings.json
        tasks.json
      src/
      package.json
    shared/
      src/
      package.json
  package.json
  myproject.code-workspace
// myproject.code-workspace
{
  "folders": [
    { "name": "API Server", "path": "packages/api" },
    { "name": "Web Client", "path": "packages/web" },
    { "name": "Shared Lib", "path": "packages/shared" },
    { "name": "Root", "path": "." }
  ],
  "settings": {
    "editor.tabSize": 2,
    "editor.formatOnSave": true,
    "files.trimTrailingWhitespace": true,
    "files.insertFinalNewline": true,
    "search.exclude": {
      "**/node_modules": true,
      "**/dist": true,
      "**/coverage": true,
      "**/.turbo": true
    },
    "files.watcherExclude": {
      "**/node_modules/**": true,
      "**/.git/objects/**": true,
      "**/dist/**": true
    },
    "git.autoRepositoryDetection": "openEditors",
    "typescript.tsdk": "node_modules/typescript/lib"
  },
  "tasks": {
    "version": "2.0.0",
    "tasks": [
      {
        "label": "Install All Dependencies",
        "type": "shell",
        "command": "npm install",
        "options": { "cwd": "${workspaceFolder:Root}" },
        "problemMatcher": []
      },
      {
        "label": "Build All Packages",
        "type": "shell",
        "command": "npm run build --workspaces",
        "options": { "cwd": "${workspaceFolder:Root}" },
        "group": { "kind": "build", "isDefault": true },
        "problemMatcher": "$tsc"
      },
      {
        "label": "Test All Packages",
        "type": "shell",
        "command": "npm test --workspaces",
        "options": { "cwd": "${workspaceFolder:Root}" },
        "group": { "kind": "test", "isDefault": true },
        "problemMatcher": []
      },
      {
        "label": "Start Full Stack",
        "dependsOn": ["API: Dev Server", "Web: Dev Server"],
        "dependsOrder": "parallel",
        "problemMatcher": []
      }
    ]
  },
  "launch": {
    "version": "0.2.0",
    "compounds": [
      {
        "name": "Debug Full Stack",
        "configurations": ["API: Debug", "Web: Debug"],
        "stopAll": true
      }
    ]
  },
  "extensions": {
    "recommendations": [
      "dbaeumer.vscode-eslint",
      "esbenp.prettier-vscode",
      "eamodio.gitlens",
      "christian-kohler.npm-intellisense"
    ]
  }
}

Common Issues and Troubleshooting

ESLint reports wrong errors or does not work

ESLint cannot find the correct config because it resolves relative to the workspace root, not the folder root:

Fix: Add "eslint.workingDirectories": ["."] in each folder's .vscode/settings.json. This tells the ESLint extension to use the folder root as the working directory.

TypeScript cannot resolve imports from other packages

The TypeScript language server does not see project references across folders:

Fix: Use a root tsconfig.json with project references and set "typescript.tsdk": "node_modules/typescript/lib" in workspace settings. Each package needs a tsconfig.json with "composite": true and "references" pointing to dependencies.

Search includes results from all folders

You want to search only in the package you are editing:

Fix: Right-click the folder in the explorer → "Find in Folder." Or in the search panel, type ./FolderName/** in the "files to include" field. You can also use the folder filter button in the search results.

Tasks from all folders create a cluttered task list

With 5 folders and 5 tasks each, the task picker has 25 entries:

Fix: Prefix task labels with the folder name ("API: Build", "Web: Test"). This groups them logically in the picker. Use compound tasks at the workspace level for common multi-folder operations.

Git shows changes from all packages mixed together

The Source Control panel shows a single list of changes across all folders:

Fix: Set "git.autoRepositoryDetection": "openEditors" or "subFolders" in workspace settings. If each package has its own git repo, VS Code shows separate SCM panels. For a single repo, use the file path to identify which package a change belongs to.

Best Practices

  • Use descriptive folder names in the workspace file. "API Server" is better than "api". The names appear throughout VS Code — in the explorer, task picker, debug configurations, and search scoping.
  • Commit the .code-workspace file to version control. New team members can open the workspace immediately instead of manually adding folders. Include per-folder settings and tasks.
  • Keep workspace-level settings minimal. Put formatting and linting rules in per-folder settings. Workspace settings should cover cross-cutting concerns like search exclusions and file watchers.
  • Prefix task and debug names with the folder. This prevents confusion when the same logical task exists in multiple packages. "API: Test" and "Web: Test" are instantly clear.
  • Set up compound tasks for full-stack workflows. Starting all services should be one action, not three. Same for running all tests or building all packages.
  • Use eslint.workingDirectories in every folder. This is the single most common source of confusion in multi-root monorepos. Set it proactively.
  • Exclude aggressively with files.watcherExclude. Monorepos have enormous node_modules trees. Watching them wastes CPU and battery. Exclude every build output directory.

References

Powered by Contentful