Setting up ESLint in my Next.js + TypeScript project to keep code clean, consistent, and bug-free—a must for writing like a pro!

I guess you're reading this in the same shoes I found myself in before writing this article. I decided to document my findings because of how hard it was. Still, it was fun to set up ESLint for my next project through its documentation and understanding what is going on, especially with the new changes. For those who do not know what ESLint is, I've got you. Eslint If you have ever used a configured linters file, and how it works, you will understand better will I mean by ESLint is an advanced linting configuration that helps you to write cleaner and clearer code. It identifies and reports bugs, making your code more consistent and helping you follow best practices. In short, you use ESLint to set rules for your code. Rules Being the core concept in ESLint, rules agree on what you have done that is valid and also let you know what needs to be done, providing suggestions without changing the application's logic, known as Rule fixes. You can create custom rules. Also note that ESLint has built-in rules, some of which come as Plugins. These plugins include rules, configurations, processors, and languages Configuration file ESLint configuration files contain your project's ESLint configurations. NPM can also share these configurations. Parser A parser in computer science is part of the compiler; it is responsible for breaking down code into human human-readable language for the computer. In ESLint, a parser converts code into an abstract syntax tree. With all these, there is the Eslint Custom Processors that draws JavaScript from other files for Eslint to lint the code. Also, not forgetting the Formatter in charge of the appearance With all these in mind, let's move to real business. Firstly, I will be using pnpm as my installer Start or bootstrap the next project. I will be using pnpm as my package manager npx create-next-app@latest next, pnpm add --save-dev eslint @eslint/js typescript typescript-eslint @next/eslint-plugin-next Your package.json will look something like this, where the dev dependencies are { "name": "mytest", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", }, "dependencies": { "@next/eslint-plugin-next": "^15.3.2", "lucide-react": "^0.508.0", "next": "15.3.2", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@eslint/eslintrc": "^3", "@eslint/js": "^9.26.0", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9.26.0", "eslint-config-next": "15.3.2", "globals": "^16.1.0", "typescript": "^5", "typescript-eslint": "^8.32.0" } } Next, in your generated eslint.config.mjs, do the following configurations. We will start our configuration, but first, you have to note that Next has an existing plugin in the configuration that tracks common issues, which we installed above. Construct the __dirname. This is because in Commonjs, Node gives you the dirname since it is not done in modules, we have to recreate it by converting the import.meta.url into an absolute file path by using. const __filename = fileURLToPath(import.meta.url); // → turns this file’s URL into the real file path on your disk const __dirname = dirname(__filename); // → strips off the filename, leaving just the folder path // Tell FlatCompat where to look for configs const compat = new FlatCompat({ baseDirectory: __dirname }); // → now compat knows your project’s root folder and can find shareable configs there With it done, in the defineConfig method, pull in Next.js’s built-in best-practice rules add rules from plugin:@next/next/core-web-vitals and plugin:@next/next/typescript …compat.extends(“next/core-web-vitals”, “next/typescript”) Next, define our global ignores and environments in the config file { ignores: [ ".next/**", ".env", "node_modules", "public/**", "next.config.js", "postcss.config.js" ] }, { languageOptions: { globals: { ...globals.browser, ...globals.node } } } Now we can set up rules and plugins { plugins: { js, "@next/next": pluginNext, "@typescript-eslint": tsPlugin, tailwindcss: tailwind, }, files: ["**/*.{js,mjs,cjs,jsx,ts,tsx}"], rules: { ...js.configs.recommended.rules, ...pluginNext.configs.recommended.rules, ...pluginNext.configs["core-web-vitals"].rules, ...tsPlugin.configs.recommended.rules, ...tailwind.configs["flat/recommended"].rules, "quotes": ["warn", "double", { avoidEscape: true }], "semi": ["warn", "always"], "indent": ["warn", 2], "no-multiple-empty-lines": ["error", { max: 1 }], "eol-last": ["warn", "always"], "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": ["warn"], "no-undef": "off", },

May 13, 2025 - 12:28
 0
Setting up ESLint in my Next.js + TypeScript project to keep code clean, consistent, and bug-free—a must for writing like a pro!

Image description

I guess you're reading this in the same shoes I found myself in before writing this article. I decided to document my findings because of how hard it was. Still, it was fun to set up ESLint for my next project through its documentation and understanding what is going on, especially with the new changes. For those who do not know what ESLint is, I've got you.

Eslint
If you have ever used a configured linters file, and how it works, you will understand better will I mean by ESLint is an advanced linting configuration that helps you to write cleaner and clearer code. It identifies and reports bugs, making your code more consistent and helping you follow best practices. In short, you use ESLint to set rules for your code.

Rules
Being the core concept in ESLint, rules agree on what you have done that is valid and also let you know what needs to be done, providing suggestions without changing the application's logic, known as Rule fixes. You can create custom rules. Also note that ESLint has built-in rules, some of which come as Plugins. These plugins include rules, configurations, processors, and languages

Configuration file
ESLint configuration files contain your project's ESLint configurations. NPM can also share these configurations.

Parser
A parser in computer science is part of the compiler; it is responsible for breaking down code into human human-readable language for the computer. In ESLint, a parser converts code into an abstract syntax tree.

With all these, there is the Eslint Custom Processors that draws JavaScript from other files for Eslint to lint the code. Also, not forgetting the Formatter in charge of the appearance

With all these in mind, let's move to real business. Firstly, I will be using pnpm as my installer

Start or bootstrap the next project.

I will be using pnpm as my package manager

npx create-next-app@latest

next,

pnpm add --save-dev eslint @eslint/js typescript typescript-eslint @next/eslint-plugin-next

Your package.json will look something like this, where the dev dependencies are

{
  "name": "mytest",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
  },
  "dependencies": {
    "@next/eslint-plugin-next": "^15.3.2",
    "lucide-react": "^0.508.0",
    "next": "15.3.2",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3",
    "@eslint/js": "^9.26.0",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9.26.0",
    "eslint-config-next": "15.3.2",
    "globals": "^16.1.0",
    "typescript": "^5",
    "typescript-eslint": "^8.32.0"
  }
}

Next, in your generated eslint.config.mjs, do the following configurations. We will start our configuration, but first, you have to note that Next has an existing plugin in the configuration that tracks common issues, which we installed above.

Construct the __dirname. This is because in Commonjs, Node gives you the dirname since it is not done in modules, we have to recreate it by converting the

import.meta.url

into an absolute file path by using.

const __filename = fileURLToPath(import.meta.url);  
// → turns this file’s URL into the real file path on your disk  
const __dirname  = dirname(__filename);  
// → strips off the filename, leaving just the folder path  

// Tell FlatCompat where to look for configs
const compat = new FlatCompat({  
  baseDirectory: __dirname  
});  
// → now compat knows your project’s root folder and can find 

shareable configs there
With it done, in the defineConfig method, pull in Next.js’s built-in best-practice rules add rules from plugin:@next/next/core-web-vitals
and plugin:@next/next/typescript

 …compat.extends(“next/core-web-vitals”, “next/typescript”)

Next, define our global ignores and environments in the config file

 {
    ignores:
      [
        ".next/**",
        ".env",
        "node_modules",
        "public/**",
        "next.config.js",
        "postcss.config.js"
      ]
  },
  {
    languageOptions: { globals: { ...globals.browser, ...globals.node } }
  } 

Now we can set up rules and plugins

{
    plugins: {
      js,
      "@next/next": pluginNext,
      "@typescript-eslint": tsPlugin,
      tailwindcss: tailwind,
    },
    files: ["**/*.{js,mjs,cjs,jsx,ts,tsx}"],
    rules: {
      ...js.configs.recommended.rules,
      ...pluginNext.configs.recommended.rules,
      ...pluginNext.configs["core-web-vitals"].rules,
      ...tsPlugin.configs.recommended.rules,
      ...tailwind.configs["flat/recommended"].rules,

      "quotes": ["warn", "double", { avoidEscape: true }],
      "semi": ["warn", "always"],
      "indent": ["warn", 2],
      "no-multiple-empty-lines": ["error", { max: 1 }],
      "eol-last": ["warn", "always"],

      "no-unused-vars": "off",
      "@typescript-eslint/no-unused-vars": ["warn"],
      "no-undef": "off",
    },
  },

This is how the complete file will look

import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import { FlatCompat } from '@eslint/eslintrc'
import { defineConfig } from "eslint/config";
import { fileURLToPath } from "url";
import { dirname } from "path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename)

const compat = new FlatCompat({
  baseDirectory: __dirname
})

export default defineConfig([
  ...compat.extends("next/core-web-vitals", "next/typescript"),
  {
    ignores:
      [
        ".next/**",
        ".env",
        "node_modules",
        "public/**",
        "next.config.js",
        "postcss.config.js"
      ]
  },
  {
    languageOptions: { globals: { ...globals.browser, ...globals.node } }
  },
  { files: ["**/*.{js,mjs,cjs,ts}"], plugins: { js }, extends: ["js/recommended"] },
  { files: ["**/*.{js,mjs,cjs,ts}"], languageOptions: { globals: globals.browser } },
  {
    rules: {
      "no-unused-vars": ["warn"],
      "no-undef": ["warn"],
      "quotes": ["warn", "double", { "avoidEscape": true }],
      "semi": ["warn", "always"],
      "indent": ["warn", 2],
      "class-methods-use-this": "warn",
      "eol-last": ["warn", "always"],
      "no-unused-expressions": ["warn"],
      "no-multiple-empty-lines": ["error", { "max": 1 }],
      "no-trailing-spaces": ["warn"],
      "no-useless-constructor": 0,
      "no-loop-func": 0,
    }
  },

  js.configs.recommended,
  tseslint.configs.recommended,
]);

With this, you can run your script, and it works.

Conclusion
I would say the are many ways to configure ESLint in projects, and you might not find my approach as great as it may be to me, but I would love to hear from you. Every configuration depends on your project requirements.

References
Next.js documentation on the ESLint plugin, TypeScript ESLint, Eslint official documentation