Table of Contents
- What is a Monorepo?
- Why ESLint and Prettier Setup Needs Special Attention in Monorepos
- Common Structure of a TypeScript Monorepo
- Centralized ESLint and Prettier Setup
- Per-Package Overrides (When Needed)
- Step-by-Step: Setting up ESLint and Prettier in a Monorepo
- Handling Different TypeScript Configs
- Best Practices
- Conclusion
What is a Monorepo?
A monorepo is a single repository that stores code for multiple packages or projects.
Instead of separate repos, all related services, libraries, apps live together.
Example:
apps/
→ Frontend, Backend appspackages/
→ Shared libraries, utilities
Popular tools: Nx, Turborepo, Lerna, or even plain npm/yarn workspaces.
Why ESLint and Prettier Setup Needs Special Attention in Monorepos
- Different packages might have different settings (e.g., Node.js backend vs React frontend).
- TypeScript configs (
tsconfig.json
) may differ between packages. - We want shared rules but flexibility to override locally if needed.
- We want fast linting/formatting without unnecessary scanning.
Goal: Centralized config with optional per-package customization.
Common Structure of a TypeScript Monorepo
Example:
/monorepo-root
/apps
/backend
/frontend
/packages
/utils
/ui-components
package.json
tsconfig.base.json
.eslintrc.js
.prettierrc
Each subfolder (apps/
, packages/
) might be its own mini-project.
Centralized ESLint and Prettier Setup
Instead of copying .eslintrc.js
and .prettierrc
everywhere,
put them at the root and let packages inherit them.
Per-Package Overrides (When Needed)
Sometimes a backend might need Node.js-specific linting,
or frontend might need React-specific linting.
In that case:
- Root config remains default.
- Specific sub-projects override only small parts.
Step-by-Step: Setting up ESLint and Prettier in a Monorepo
1. Install ESLint and Prettier at Root
npm install --save-dev eslint prettier @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-prettier eslint-plugin-prettier
If using React, also install:
npm install --save-dev eslint-plugin-react eslint-plugin-react-hooks
2. Root .eslintrc.js
Configuration
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
project: ['./tsconfig.base.json'], // important
tsconfigRootDir: __dirname,
ecmaVersion: 2020,
sourceType: 'module',
},
plugins: ['@typescript-eslint', 'prettier'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
rules: {
'prettier/prettier': 'error',
},
ignorePatterns: ['node_modules/', 'dist/', 'build/'],
};
3. Root .prettierrc
{
"semi": true,
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"trailingComma": "all",
"arrowParens": "always"
}
Add a .prettierignore
:
node_modules
dist
build
coverage
4. TypeScript Configuration (tsconfig.base.json
)
Example:
{
"compilerOptions": {
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
}
}
Each sub-project will extend this.
Example in apps/frontend/tsconfig.json
:
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"]
}
5. If Needed: Specific ESLint Overrides per Package
Inside apps/frontend/.eslintrc.js
:
module.exports = {
extends: ['../../.eslintrc.js', 'plugin:react/recommended'],
settings: {
react: {
version: 'detect',
},
},
};
Inside apps/backend/.eslintrc.js
(for Node.js):
module.exports = {
extends: ['../../.eslintrc.js'],
env: {
node: true,
},
};
6. Add Workspace Lint and Format Scripts
Inside root package.json
:
{
"scripts": {
"lint": "eslint '**/*.{ts,tsx,js,jsx}' --fix",
"format": "prettier --write '**/*.{ts,tsx,js,jsx,json,md,yml,yaml}'"
}
}
Or better:
Target only packages you want:
eslint 'apps/**/*.{ts,tsx,js,jsx}' 'packages/**/*.{ts,tsx,js,jsx}' --fix
prettier --write 'apps/**/*.{ts,tsx,js,jsx,json,md}' 'packages/**/*.{ts,tsx,js,jsx,json,md}'
Handling Different TypeScript Configs
If you get this ESLint error:
Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser
It’s because each sub-project might need its own tsconfig.json
.
Solutions:
- Configure
parserOptions.project
per package inside.eslintrc.js
- Or loosen parsing rules at root if needed (
createDefaultProgram: true
) — not recommended for strict setups.
Best Practices
- Always put shared config at root.
- Use project-specific
.eslintrc.js
only when necessary (e.g., React vs Node). - Ignore build folders (
dist/
,build/
) properly. - Use VSCode extensions like ESLint, Prettier, and configure
formatOnSave
. - Automate via Husky + lint-staged: run linting/formatting only on changed files before commit.
Example:
npm install --save-dev husky lint-staged
Add to package.json
:
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"**/*.{ts,tsx,js,jsx}": [
"eslint --fix",
"prettier --write"
]
}
Conclusion
Setting up ESLint and Prettier in a monorepo properly ensures:
- Code quality is consistent across multiple apps/packages.
- Developers can focus more on building rather than formatting.
- Scaling your monorepo becomes much easier.