Skip to content

Managing ESLint and Prettier in a Monorepo Without the Drama

Managing a single project is draining enough; adding a monorepo to the mix can make it much worse.

3 min read
ESLint
Prettier
Monorepo
Linting
Tooling

Managing a single project is draining enough; adding a monorepo to the mix can make it much worse. I see the same pattern all the time: the first app has a clean .eslintrc, the second app just copies it, and the third app modifies a single rule. Fast forward three months, and you're stuck debugging why a build failed in the admin app while the others passed, hunting for configuration errors that shouldn't even be there.

Personally, I like to treat ESLint and Prettier like actual products. They have their own versions, they are consumed explicitly, and when something needs to change, you change it in one place.

My strategy is to wrap these configurations into their own packages within the workspace—for example, @acme/config-prettier. The contents are very thin, mostly just exporting base settings. The apps in the monorepo then just import this package. I do the same for ESLint: create one package that exports a configuration object (or a flat config array) that other apps can extend.

Of course, you shouldn't throw everything into a shared package. There has to be a limit.

In my view, only the fundamentals belong in a shared package—things like ecmaVersion, import rules, or TypeScript baselines. Highly specific framework stuff, like Next.js rules or .next folder ignore patterns, should stay at the individual app level.

There is one rule I won't compromise on: if you're using both, "prettier" must be in your ESLint extends (via eslint-config-prettier). I don't want two different tools fighting over a semicolon. Let Prettier handle the aesthetics and formatting, and let ESLint focus on the logic and code correctness.

Another common headache is sloppy internal imports. Sometimes an app will import directly from a deep path (like @acme/ui/src/button.tsx) instead of using the public exports. This usually breaks our linting assumptions. Setting up import/no-extraneous-dependencies with the correct packageDir helps a lot here to avoid a flood of false positives.

When it comes to maintenance, I prefer simple consistency over a massive list of rules. If your configuration is too rigid with styling rules, people will eventually ignore them or start modifying them secretly. I’d rather focus on rules that actually prevent bugs. If you really need an exception, use targeted overrides at the app level instead of creating a whole new branch of configuration.

One more crucial thing: keep your local environment and CI in sync. The configuration package running on my laptop must be the same one running on the server. Using different ESLint versions within the same monorepo is a guaranteed way to make "but it works on my machine" a regular topic in your weekly meetings.

This approach definitely has its downsides, mostly because it requires extra time upfront to set up the package structure. But for me, it makes much more sense than constantly fighting configuration drift across every single app.

That's the shape of it for me.