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
- Open VS Code
File→Add Folder to Workspace...- Select the folders you want
File→Save Workspace As...→ save asmyproject.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-workspacefile 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.workingDirectoriesin 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 enormousnode_modulestrees. Watching them wastes CPU and battery. Exclude every build output directory.