Monorepo Setups with ESLint and Prettier (for TypeScript Projects)

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 apps
  • packages/ → 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.