DEV Community

Cover image for How to Set Up Prettier in Next.js (With Every Error Fixed Step by Step)
Muhammad Hamid Raza
Muhammad Hamid Raza

Posted on

How to Set Up Prettier in Next.js (With Every Error Fixed Step by Step)

You just started a fresh Next.js project. Your code looks messy. Your tabs and spaces are at war. A teammate opens the same file and now there are 47 formatting "changes" in the diff — even though nothing actually changed.

Sound familiar? šŸ˜…

That's exactly why Prettier exists. It's a code formatter that takes your messy code and makes it clean, consistent, and predictable. Every time. Automatically.

But setting it up in a modern Next.js project isn't always smooth. Especially if you're using ESLint Flat Config (eslint.config.mjs), TypeScript, Tailwind CSS, and pnpm. The internet is full of outdated tutorials that either miss steps or don't tell you what to do when things break.

This guide covers the full setup and every real error you might run into — with exact fixes for each one.

Let's get into it. šŸš€


What Is Prettier?

Prettier is an opinionated code formatter. You give it your code, it gives it back — clean, consistent, and formatted exactly the way you configured it.

Think of it like autocorrect for your code. You type fast and messy, and Prettier quietly cleans up after you before you even notice.

It handles things like:

  • Consistent use of single or double quotes
  • Trailing commas
  • Semicolons at the end of lines
  • Line length limits
  • Indentation width

Prettier doesn't care about logic or bugs — that's ESLint's job. Prettier only cares about how your code looks.


Why This Matters

Inconsistent formatting is a silent killer of team productivity. One developer uses tabs, another uses spaces. One prefers single quotes, the other uses double quotes. The result? Git shows hundreds of "changes" in every pull request that are just formatting noise — not actual code changes.

Prettier solves this completely. Once it's set up:

  • Every file gets formatted the same way, every time
  • Your team stops fighting over style preferences
  • Code reviews focus on logic, not formatting
  • Tailwind class orders stay consistent with prettier-plugin-tailwindcss
  • VS Code can format your file automatically every time you save

The setup is a one-time cost that saves you time forever.


Full Setup Guide (Step by Step)

Step 1: Install Prettier and Its Plugins

Open your terminal in your project root and run:

pnpm add -D prettier eslint-config-prettier prettier-plugin-tailwindcss
Enter fullscreen mode Exit fullscreen mode

Here's what each package does:

  • prettier — The core formatter
  • eslint-config-prettier — Turns off ESLint formatting rules that conflict with Prettier
  • prettier-plugin-tailwindcss — Automatically sorts your Tailwind CSS classes in the correct order

Step 2: Create Your Prettier Config

In your project root, create a file called .prettierrc:

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100,
  "plugins": ["prettier-plugin-tailwindcss"]
}
Enter fullscreen mode Exit fullscreen mode

What each option does:

  • semi — Adds semicolons at the end of statements
  • singleQuote — Uses ' instead of " for strings
  • tabWidth — Uses 2 spaces for indentation
  • trailingComma: "es5" — Adds trailing commas where valid in ES5 (arrays, objects)
  • printWidth — Wraps lines longer than 100 characters
  • plugins — Activates the Tailwind class sorting plugin

Step 3: Create a .prettierignore File

You don't want Prettier touching generated files, build outputs, or lock files. Create .prettierignore in your project root:

.next
node_modules
public
pnpm-lock.yaml
docs
Enter fullscreen mode Exit fullscreen mode

Step 4: Add npm Scripts

Open your package.json and add these scripts:

"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start",
  "lint": "next lint",
  "format": "prettier --write .",
  "format:check": "prettier --check ."
}
Enter fullscreen mode Exit fullscreen mode
  • pnpm format — Formats every file in your project
  • pnpm format:check — Checks formatting without changing files (great for CI)

Step 5: Update Your ESLint Config

Open your eslint.config.mjs and add eslint-config-prettier at the end of your config array:

import eslintConfigPrettier from 'eslint-config-prettier';

export default [
  // ... your existing ESLint rules
  eslintConfigPrettier,
];
Enter fullscreen mode Exit fullscreen mode

This must go last in the array. It disables ESLint rules that would conflict with Prettier's formatting decisions.


Step 6: VS Code Auto-Format on Save (Optional but Highly Recommended)

Create or update .vscode/settings.json in your project root:

{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  }
}
Enter fullscreen mode Exit fullscreen mode

Make sure you have the Prettier - Code formatter extension installed in VS Code (esbenp.prettier-vscode).

Now every time you hit Ctrl + S, your file gets formatted automatically. āœ…


Debugging Every Error (The Real Journey)

Here's where most tutorials stop — and where developers get stuck. Let's fix every error you might hit.


šŸ”“ Error 1: Invalid project directory provided, no such directory: portfolio\lint

Full error:

$ next lint
Invalid project directory provided, no such directory: H:\...\portfolio\lint
[ELIFECYCLE] Command failed with exit code 1.
Enter fullscreen mode Exit fullscreen mode

Why it happens:

In Next.js 15/16 (and sometimes 14.2+), the CLI argument parser has a known bug where it misreads lint as a directory path instead of a subcommand — especially when using ESLint Flat Config (eslint.config.mjs). So it looks for a folder called lint and fails.

The fix:

Change your lint script in package.json to call ESLint directly:

"scripts": {
  "lint": "eslint ."
}
Enter fullscreen mode Exit fullscreen mode

If ESLint isn't already installed locally, run:

pnpm add -D eslint
Enter fullscreen mode Exit fullscreen mode

Then run:

pnpm run lint
Enter fullscreen mode Exit fullscreen mode

This bypasses Next's CLI argument parser entirely. It's faster, cleaner, and fully compatible with ESLint v9+ and Flat Config.


šŸ”“ Error 2: TypeError: Converting circular structure to JSON

Full error:

TypeError: Converting circular structure to JSON
  --> starting at object with constructor 'Object'
  |   property 'configs' -> object with constructor 'Object'
  |   property 'flat' -> object with constructor 'Object'
  |   ...
  |   property 'plugins' -> object with constructor 'Object'
  --- property 'react' closes the circle
Enter fullscreen mode Exit fullscreen mode

Why it happens:

Your eslint.config.mjs is using FlatCompat to extend "next/core-web-vitals". Under the hood, FlatCompat from @eslint/eslintrc tries to serialize the config using JSON.stringify() to validate it. But eslint-plugin-react (included inside Next's config) contains circular object references — objects that point back to themselves. JSON.stringify() can't handle circular references, so it crashes.

The fix:

Drop FlatCompat entirely. Import @next/eslint-plugin-next directly.

First, install it:

pnpm add -D @next/eslint-plugin-next
Enter fullscreen mode Exit fullscreen mode

Note for pnpm users: pnpm uses strict package isolation. Even if @next/eslint-plugin-next is used internally by eslint-config-next, pnpm won't hoist it to your root node_modules unless you explicitly install it. That's why you need to add it manually.

Then replace your eslint.config.mjs with this:

import nextPlugin from '@next/eslint-plugin-next';
import eslintConfigPrettier from 'eslint-config-prettier';

/** @type {import('eslint').Linter.Config[]} */
const eslintConfig = [
  {
    files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'],
    plugins: {
      '@next/next': nextPlugin,
    },
    rules: {
      ...nextPlugin.configs.recommended.rules,
      ...nextPlugin.configs['core-web-vitals'].rules,
    },
  },
  eslintConfigPrettier,
  {
    ignores: ['.next/**', 'node_modules/**', 'out/**', 'docs/**', 'public/**'],
  },
];

export default eslintConfig;
Enter fullscreen mode Exit fullscreen mode

No FlatCompat. No circular JSON crash. Clean and native. āœ…


šŸ”“ Error 3: Cannot find package '@next/eslint-plugin-next'

Full error:

Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@next/eslint-plugin-next'
imported from H:\...\portfolio\eslint.config.mjs
Did you mean to import "@next/eslint-plugin-next/dist/index.js"?
Enter fullscreen mode Exit fullscreen mode

Why it happens:

pnpm uses strict package isolation by design. It doesn't hoist nested dependencies to your root node_modules the way npm does. So even though @next/eslint-plugin-next is used internally by the next package, your code can't import it unless it's explicitly listed as your own dependency.

The fix:

Install it directly:

pnpm add -D @next/eslint-plugin-next
Enter fullscreen mode Exit fullscreen mode

Run lint again:

pnpm run lint
Enter fullscreen mode Exit fullscreen mode

šŸ”“ Error 4: 155 TypeScript and JSX Parsing Errors

Sample errors:

app/_error.tsx  28:1  error  Parsing error: The keyword 'interface' is reserved
app/about/loading.tsx  5:9  error  Parsing error: Unexpected token <
app/api/ask/route.ts  15:52  error  Parsing error: Unexpected token ModelId
Enter fullscreen mode Exit fullscreen mode

Why it happens:

ESLint's default parser is espree, which only understands plain JavaScript. It doesn't know TypeScript keywords like interface, type, or generics. It also doesn't know JSX syntax like <Component />. You need to tell ESLint to use @typescript-eslint/parser for your .ts and .tsx files.

The fix:

Install the TypeScript ESLint parser and plugin:

pnpm add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
Enter fullscreen mode Exit fullscreen mode

Then update your eslint.config.mjs to use the TypeScript parser:

import nextPlugin from '@next/eslint-plugin-next';
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import eslintConfigPrettier from 'eslint-config-prettier';

/** @type {import('eslint').Linter.Config[]} */
const eslintConfig = [
  {
    ignores: ['.next/**', 'node_modules/**', 'out/**', 'build/**', 'docs/**', 'public/**'],
  },
  {
    files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
    plugins: {
      '@next/next': nextPlugin,
      '@typescript-eslint': tsPlugin,
    },
    languageOptions: {
      parser: tsParser,
      parserOptions: {
        ecmaFeatures: {
          jsx: true,
        },
      },
    },
    rules: {
      ...nextPlugin.configs.recommended.rules,
      ...nextPlugin.configs['core-web-vitals'].rules,
    },
  },
  eslintConfigPrettier,
];

export default eslintConfig;
Enter fullscreen mode Exit fullscreen mode

Run lint again:

pnpm run lint
Enter fullscreen mode Exit fullscreen mode

All 155 parsing errors disappear. āœ…


šŸ”“ Error 5: Circular JSON Error Returns After Re-Adding FlatCompat

Why it happens:

If at any point you re-introduce FlatCompat to extend "next/core-web-vitals" or "next/typescript", the circular JSON error comes back. FlatCompat is fundamentally incompatible with the current version of eslint-plugin-react bundled inside eslint-config-next and ESLint v9+.

The fix:

Do not use FlatCompat at all. The final working eslint.config.mjs below is the definitive version that handles everything — Next.js rules, TypeScript parsing, JSX, and Prettier compatibility — without touching FlatCompat or @eslint/eslintrc:

import nextPlugin from '@next/eslint-plugin-next';
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import eslintConfigPrettier from 'eslint-config-prettier';

/** @type {import('eslint').Linter.Config[]} */
const eslintConfig = [
  {
    ignores: ['.next/**', 'node_modules/**', 'out/**', 'build/**', 'docs/**', 'public/**'],
  },
  {
    files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
    plugins: {
      '@next/next': nextPlugin,
      '@typescript-eslint': tsPlugin,
    },
    languageOptions: {
      parser: tsParser,
      parserOptions: {
        ecmaFeatures: {
          jsx: true,
        },
      },
    },
    rules: {
      ...nextPlugin.configs.recommended.rules,
      ...nextPlugin.configs['core-web-vitals'].rules,
    },
  },
  eslintConfigPrettier,
];

export default eslintConfig;
Enter fullscreen mode Exit fullscreen mode

This is the config that works. Keep it and don't look back.


Pros and Cons: FlatCompat vs Native Flat Config

FlatCompat Native Flat Config
Works with ESLint v9+ āŒ Often breaks āœ… Fully supported
TypeScript support Needs extra setup āœ… With @typescript-eslint/parser
pnpm compatibility āŒ Circular JSON crash āœ… Works perfectly
Code complexity More boilerplate āœ… Cleaner and simpler
Recommended for new projects āŒ No āœ… Yes

If you're starting a Next.js project today, go native Flat Config from day one. FlatCompat was a bridge for legacy configs — it's not meant to be the destination.


Final File Checklist

Here's everything you should have set up:

.prettierrc (project root)

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100,
  "plugins": ["prettier-plugin-tailwindcss"]
}
Enter fullscreen mode Exit fullscreen mode

.prettierignore (project root)

.next
node_modules
public
pnpm-lock.yaml
docs
Enter fullscreen mode Exit fullscreen mode

eslint.config.mjs (project root)

import nextPlugin from '@next/eslint-plugin-next';
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import eslintConfigPrettier from 'eslint-config-prettier';

const eslintConfig = [
  {
    ignores: ['.next/**', 'node_modules/**', 'out/**', 'build/**', 'docs/**', 'public/**'],
  },
  {
    files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
    plugins: {
      '@next/next': nextPlugin,
      '@typescript-eslint': tsPlugin,
    },
    languageOptions: {
      parser: tsParser,
      parserOptions: {
        ecmaFeatures: { jsx: true },
      },
    },
    rules: {
      ...nextPlugin.configs.recommended.rules,
      ...nextPlugin.configs['core-web-vitals'].rules,
    },
  },
  eslintConfigPrettier,
];

export default eslintConfig;
Enter fullscreen mode Exit fullscreen mode

package.json scripts

"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start",
  "lint": "eslint .",
  "format": "prettier --write .",
  "format:check": "prettier --check ."
}
Enter fullscreen mode Exit fullscreen mode

.vscode/settings.json (optional but recommended)

{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  }
}
Enter fullscreen mode Exit fullscreen mode

Best Tips

  • Always put eslintConfigPrettier last in your ESLint config array. It needs to override any formatting rules that come before it.
  • Commit your .prettierrc to version control so every teammate uses the same formatting rules automatically.
  • Use pnpm format:check in your CI pipeline to catch unformatted code before it gets merged.
  • Install the Prettier VS Code extension (esbenp.prettier-vscode) — it makes the whole workflow seamless.
  • Never mix FlatCompat with native Flat Config imports in the same file. Pick one approach and stick with it.

Common Mistakes to Avoid

Mistake 1: Keeping next lint as your lint script

next lint uses Next.js's CLI parser, which has a known argument parsing bug in newer versions. Switching to eslint . directly is more stable and faster.

Mistake 2: Using FlatCompat in ESLint v9+ projects

FlatCompat tries to convert legacy ESLint configs. But eslint-plugin-react (bundled in eslint-config-next) now contains circular object references that crash JSON.stringify(). The fix is native Flat Config — no FlatCompat needed.

Mistake 3: Forgetting @next/eslint-plugin-next with pnpm

pnpm doesn't hoist nested packages. Even if next uses @next/eslint-plugin-next internally, you need to install it explicitly in your own package.json or the import will fail.

Mistake 4: Missing the TypeScript parser

ESLint's default parser doesn't understand TypeScript or JSX. If you don't set @typescript-eslint/parser as your languageOptions.parser, you'll get hundreds of "Unexpected token" errors on perfectly valid code.

Mistake 5: Not ignoring .next/ and node_modules/

ESLint will try to lint every file it can find unless you tell it otherwise. Always add ignores in your config and a .prettierignore file to skip build artifacts.


Wrapping Up

Setting up Prettier in a modern Next.js project isn't as simple as the docs make it look — especially once you add TypeScript, Tailwind CSS, ESLint Flat Config, and pnpm into the mix. But once it's working, it's one of the best quality-of-life improvements you can make to your workflow.

Here's a quick recap of what you did:

  • āœ… Installed Prettier with Tailwind sorting and ESLint compatibility
  • āœ… Created .prettierrc and .prettierignore
  • āœ… Fixed the next lint path bug by calling eslint . directly
  • āœ… Fixed the circular JSON crash by removing FlatCompat
  • āœ… Fixed pnpm's strict isolation with a direct @next/eslint-plugin-next install
  • āœ… Fixed 155 TypeScript parsing errors with @typescript-eslint/parser
  • āœ… Set up VS Code to auto-format on every save

Your codebase is cleaner, your team stays consistent, and you never have to argue about tabs vs spaces again. 😊

If you found this guide helpful, share it with your team — chances are they've hit the same errors too. And for more practical dev guides, visit hamidrazadev.com where I write about real-world frontend development, Next.js, TypeScript, and more.

Top comments (0)