NashTech Blog

Monorepo Setup with Turborepo: The Complete Guide to Consistent Code Quality

Table of Contents

Introduction

Managing multiple packages in a monorepo is powerful, but keeping consistent linting rules across them can quickly get messy. In this guide, you’ll learn how to:

  • Set up a monorepo with Turborepo
  • Share and enforce consistent ESLint rules across all packages
  • Use pre-commit hooks with Husky and lint-staged
  • Automate with Turborepo pipelines

Step 1: Set Up the Turborepo Monorepo

npx create-turbo@latest my-turbo-monorepo
cd my-turbo-monorepo
pnpm install

This creates a structure like:

my-turbo-monorepo/
├── apps/
│ └── web/
├── packages/
│ └── ui/
├── turbo.json
├── package.json

Step 2: Create a Shared ESLint Config

Create a package to hold your shared ESLint rules:

mkdir -p packages/eslint-config
cd packages/eslint-config
pnpm init -y

Install dependencies:

pnpm add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-prettier eslint-plugin-react

Add the shared config in index.js:

// packages/eslint-config/index.js
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'react'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'prettier'
],
rules: {
semi: ['error', 'always'],
quotes: ['error', 'single'],
'no-console': 'warn',
'react/react-in-jsx-scope': 'off',
},
settings: {
react: {
version: 'detect',
},
},
};

Update the package.json of the eslint-config package:

{
"name": "@my-org/eslint-config",
"version": "1.0.0",
"main": "index.js"
}

Step 3: Use the Shared Config in Apps/Packages

Install your shared config in any app/package:

cd apps/web
pnpm add -D @my-org/eslint-config

Then create .eslintrc.js in apps/web:

module.exports = {
extends: ['@my-org/eslint-config'],
};

Step 4: Add ESLint, Prettier, Husky, and lint-staged to Root

Install at root:

pnpm add -D eslint prettier husky lint-staged

Add .prettierrc to enforce formatting:

{
"semi": true,
"singleQuote": true,
"printWidth": 100
}

Add a .lintstagedrc.json:

{
"**/*.{js,ts,tsx}": ["eslint --fix", "prettier --write"]
}

Set up Husky:

npx husky install
pnpm pkg set scripts.prepare="husky install"
npx husky add .husky/pre-commit "npx lint-staged"

Step 5: Setup Stylelint for Consistent Styling

Just like ESLint for JavaScript/TypeScript, Stylelint helps enforce consistent styling in your CSS, SCSS, or PostCSS files across the entire monorepo.

Create a Shared Stylelint Config Package

In your monorepo:

mkdir -p packages/stylelint-config
cd packages/stylelint-config
pnpm init -y

Install dependencies (adjust for your setup):

pnpm add -D stylelint stylelint-config-standard stylelint-config-prettier stylelint-order

Optional (for SCSS or Tailwind projects):

pnpm add -D stylelint-scss stylelint-config-recommended-scss stylelint-config-tailwindcss

Create index.js:

// packages/stylelint-config/index.js
module.exports = {
extends: [
'stylelint-config-standard',
'stylelint-config-prettier',
'stylelint-config-recommended-scss', // optional
'stylelint-config-tailwindcss', // optional
],
plugins: ['stylelint-order'],
rules: {
'color-hex-case': 'lower',
'declaration-empty-line-before': null,
'order/properties-alphabetical-order': true,
},
};

Add to package.json:

{
"name": "@my-org/stylelint-config",
"version": "1.0.0",
"main": "index.js"
}

Use the Shared Stylelint Config

In any app or package (e.g. apps/web):

pnpm add -D stylelint @my-org/stylelint-config

Create .stylelintrc.js:

module.exports = {
extends: ['@my-org/stylelint-config'],
};

Optional .stylelintignore:

node_modules
dist
build

Add to lint-staged for Pre-commit Checks

Update your .lintstagedrc.json:

{
"**/*.{js,ts,tsx}": ["eslint --fix", "prettier --write"],
"**/*.{css,scss}": ["stylelint --fix", "prettier --write"]
}


Step 6: Add Style lint to Turbo Pipeline (optional)

Add Style lint to Turbo Pipeline

In turbo.json:

{
"pipeline": {
"lint:style": {
"outputs": []
}
}
}

In your package scripts:

{
"scripts": {
"lint:style": "stylelint '**/*.{css,scss}'"
}
}

Add Lint to Turbo Pipelines

In turbo.json:

 {
"$schema": "https://turborepo.org/schema.json",
"pipeline": {
"lint": {
"outputs": []
},
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
}
}
}

And in package.json:

 {
"scripts": {
"lint": "turbo run lint"
}
}

Each app/package should now define its own lint script like:

 {
"scripts": {
"lint": "eslint . --ext .ts,.tsx"
}
}

Step 7 : Enforce Commit Message Convention with Commitlint (optional)

Maintaining consistent commit messages improves collaboration, release management, and enables tools like semantic-release.


Install Commitlint and Husky Hook

At the root of your monorepo:

pnpm add -D @commitlint/{cli,config-conventional}

Create a commitlint.config.js at the root:

// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
};

Add Commit-msg Hook with Husky

Make sure Husky is already initialized (.husky/ folder exists).

Add the commit-msg hook:

npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'

This hook will run every time a commit is made, and validate the commit message format.


Example Commit Messages (Using Conventional Commits)

  • feat: add new carousel component
  • fix: resolve crash on login
  • chore: update dependencies
  • refactor: simplify Redux logic
  • docs: update README
  • test: add coverage for auth utils

Optional: Create .commitlintrc.json Instead of commitlint.config.js

If you prefer JSON format:

// .commitlintrc.json
{
"extends": ["@commitlint/config-conventional"]
}

Final Monorepo Folder Structure

 my-turbo-monorepo/
├── apps/
│ └── web/ # App folder
│ ├── .eslintrc.js # Extends shared ESLint config
│ ├── .stylelintrc.js # Extends shared Stylelint config
│ └── package.json # Contains lint scripts

├── packages/
│ ├── ui/ # Reusable UI package
│ │ ├── .eslintrc.js
│ │ └── .stylelintrc.js
│ │
│ ├── eslint-config/ # Shared ESLint config
│ │ ├── index.js
│ │ └── package.json
│ │
│ └── stylelint-config/ # Shared Stylelint config
│ ├── index.js
│ └── package.json

├── .husky/ # Git hooks
│ ├── pre-commit # Runs lint-staged
│ └── commit-msg # Runs commitlint

├── .prettierrc # Prettier config
├── .lintstagedrc.json # lint-staged config
├── .eslintignore # ESLint ignore patterns
├── .stylelintignore # Stylelint ignore patterns
├── .gitignore
├── commitlint.config.js # Commitlint config
├── turbo.json # Turbo pipeline config
├── package.json # Root dependencies and lint scripts
└── pnpm-workspace.yaml # Defines workspace packages

Example Root package.json Scripts

 {
"scripts": {
"prepare": "husky install",
"lint": "turbo run lint",
"lint:style": "turbo run lint:style",
"format": "prettier --write ."
},
"lint-staged": {
"**/*.{js,ts,tsx}": ["eslint --fix", "prettier --write"],
"**/*.{css,scss}": ["stylelint --fix", "prettier --write"]
}
}

Summary

In this guide, you’ll learn how to set up a scalable monorepo using Turborepo with a strong focus on code quality and consistency. The article walks through configuring shared ESLint and Stylelint rules across apps and packages, integrating Prettier for formatting, and enforcing commit message standards using Commitlint and Husky. You’ll also learn how to automate linting with lint-staged and define Turbo pipelines for streamlined developer workflows. By the end, you’ll have a clean, production-ready monorepo setup that promotes maintainability and team efficiency.

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article

Scroll to Top