Next.js CLI generates a really nice, minimal app scaffold with the production features that it is famous for, including a production-grade Webpack config, static site generation and support for SSR and SEO. However, the scaffold lacks a proper linting and code formatting setup.
In this post I'll show you how I configure linting and formatting in my Next.js projects. Setup shown is for a TypeScript app. However all of the steps would remain the same for a Javascript app except for one change that I'll point out in due course.
I set up the following:
Prettier for code formatting This ensures that your codebase is consistently formatted. Prettier can format pretty much any file (
.html
,.json
,.js
,.ts
,.css
,.scss
etc.).ESLint for linting code files: ESLint lints code files (Javascript and Typescript) for errors and problems and enforces coding style.
Stylelint for linting stylesheets Stylelint lints stylesheets (CSS and SCSS etc.) for errors and problems, including use of deprecated syntax and enforces coding style.
Scripts in
package.json
The most imporant thing here is to ensure that linters are part the"build"
script that is used to build the app locally and in CI/CD pipelines (including when deploying to Vercel, the company behind Next.js).-
lint-staged to run linters and code formatter on staged files in a Git pre-commit hook so that linters and formatter automatically run whenever we execute
git commit
.This is a fast failsafe to ensure code doesn't get committed if it fails linting rules and that when it does get committed, it is consistently formatted.
Running linters only on staged files (those that have been added to Git index using
git add
) is much faster than processing all files in the working directory. VS Code extensions to run Prettier, ESLint and Stylelint as you edit files. You can see any linting errors in real time via syntax highlighting and the code gets formatted properly as soon as you have made your changes. This is very useful if you use VS Code.
To follow along, generate a Next.js app using npx create-next-app myapp --eslint --typescript
. Then open up the new app in your code editor and proceed as follows.
Prettier setup
Prettier is an opinionated code formatter that can format pretty much any file (.html
, .json
, .js
, .ts
, .css
, .scss
etc.).
Set it up as follows:
-
Install Prettier:
npm install --save-dev prettier
-
Create
.prettierrc.json
in project root. Mine has the following content (the defaults for all other settings work fine for me):
{ "singleQuote": true, "jsxSingleQuote": true }
-
Create
.prettierignore
node_modules .next .husky coverage .prettierignore .stylelintignore .eslintignore
This file ensure that files which are not app code (i.e. which are not
.js
,.ts
,.css
files etc.) do not get formatted, otherwise Prettier would end up spending too much time processing files whose formatting you don't really care about.'prettierignore
(the file we just created),.eslintignore
and.stylelintignore
have been ignored because these are plain text files with no structure so Prettier would complain that it cannot format them.
ESLint setup
ESLint is the linter for JavaScript/TypeScript code.
This is the ESLint config that is generated by Next.js CLI:
{
"extends": "next/core-web-vitals"
}
If your app uses JavaScript then the default ESLint config is fine and you can skip the rest of this section.
Before we set up ESLint for use in a TypeScript app, note the following:
-
In
tsconfig.json
in app root,compilerOptions.strict
is already set totrue
:
{ "compilerOptions": { "target": "es5", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, strict: true, ...
This enables TypeScript's strict code checking. This means that if we run
npm run build
(the "build" script is defined asnext build
inpackage.json
), the command would fail if there's an error in the TypeScript code under a strict enforcement of TypeScript's type safety rules.However,
npm run lint
(the "lint" script is defined asnext lint
inpackage.json
, which in turn runs ESLint) would still succeed. This happens because as configured by Next.js CLI, ESLint does not enforce TypeScript's strict type safety rules.This is an issue, not least because linting staged files with
lint-staged
when committing code to Git would not fail even if there are strict type safety errors in it, and so the code would get checked in.Therefore we need to modify ESLint config to enforce TypeScript's strict checking during linting also.
-
Since Next v13, scaffolded projects are configured with a TypeScript Plugin demonstrated in this video. However, at least as of 05/05/2023, contrary to what their docs state, this provides edit time intellisense and syntax highlighting only. If the rules that the plugin looks out for are violated, neither linting (command
next lint
) nor building (next build
) fails.Therefore this plugin does not remove the need to set up ESLint properly for TypeScript.
To set up ESLint, proceed as follows:
-
On terminal in the app directory, run:
npm install --save-dev eslint eslint-config-prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser
-
If you already have a
.eslintrc.json
in app's root folder, rename it to.eslintrc.js
.Otherwise create a file named
.eslintrc.js
in the root. -
Replace contents of
eslintrc.js
with the following:
/* eslint-env node */ module.exports = { root: true, parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint'], extends: ['eslint:recommended', 'next/core-web-vitals', 'prettier'], overrides: [ { files: ['*.ts', '*.tsx'], parserOptions: { project: ['./tsconfig.json'], tsconfigRootDir: __dirname, }, extends: [ 'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended-requiring-type-checking', //declaring 'next/core-web-vitals' and 'prettier' again in case //the two plugin:... configs above overrode any of their rules //Also, 'prettier' needs to be last in any extends array 'next/core-web-vitals', 'prettier', ], }, ], };
This is what the various lines of this file do:
/* eslint-env node */
stops ESLint from complaining that this is a CommonJS module. We have had to put this in because ESLint, as we have configured it here, does not allow CommonJS modules (which.eslintrc.js
is, seemodule.exports = ...
at the top) in a TypeScript app and expects modules in the project to be ES6.root: true
says this is the topmost eslint configuration file although there may be nested eslint configs in subfolders.parser: '@typescript-eslint/parser'
specifies the ESLint Typescript parser to be used instead of the default Espree parser.parserOptions:
tells the parser where to find thetsconfig.json
file.extends:
specifies a number of ESLint configurations, each of which is a bundle of linting rules. A rule in a config further down the array (further to the right) overrides the same rule if declared in any config to the left.Config
'next/core-web-vitals'
specifies Next.js-specific rules (this was the only config in the original .eslintrc.json generated by the Next.js CLI).Config
prettier
is provided by the package eslint-config-prettier that we installed earlier and switches off those rules in ESLint that conflict with the code formatting done by Prettier. This must be the last config inextends:
.plugins: ['@typescript-eslint'],
specifies the plugin which provides the parser as well as two of the configs that provide Typescript-specific linting rules.The
overrides
section ensures that strict Typescript linting rules only apply to files with.ts
and.tsx
extensions (from this excellent StackOverflow answer). Otherwise, if we had put the configs inextends
that are currently in the override in the rootextends
instead, they would apply to.js
files also. Then running ESLint on the project would throw errors on.js
files if they don't meet strict Typescript rules. This would be a problem as we have several.js
config files including the.eslintrc.js
itself, so there would be linting errors. These errors are avoided by the use of the override. -
Create
.eslintignore
in the project root. It doesn't need to have any content for now:
but would come in handy in the future if ever you need to add folders or files that should be ignored by ESLint (see the final section of this post for an example).
Stylelint setup
Stylelint is a linter for CSS and SCSS stylesheets (as well as for stylesheets in other formats).
The setup given below would work for both CSS and SCSS files:
-
On the terminal in project root:
npm install --save-dev sass
Next.js has built-in SASS/SCSS support (so the webpack config knows how to handle
.scss
and.sass
files). However, you still need to install a version ofsass
package yourself, which is what we did above. -
Install packages for Stylelint and its rule configs:
npm install --save-dev stylelint stylelint-config-standard-scss stylelint-config-prettier-scss
Of these three packages:
stylelint
is the linter.stylelint-config-standard-scss
is a Stylelint config that provides linting rules. It uses the Stylelint plugin stylelint-css and extends configs stylelint-config-standard which defines rules for vanilla CSS, and stylelint-config-recommended-scss which defines SCSS specific rules. As a result extending from this one config is enough to get linting support for both CSS and SCSS files.stylelint-config-prettier-scss
extends stylelint-config-prettier and turns off those Stylint rules that conflict with Prettier's code formatting. This should be declared last inextends:
array in.stylelintrc.json
(as shown below). -
Create
.stylelintrc.json
in project root with the following contents:
{ "extends": [ "stylelint-config-standard-scss", "stylelint-config-prettier-scss" ], "rules": { "selector-class-pattern": null } }
"extends"
section declares the two Stylelint configs whose NPM packages we installed in the previous step."rules"
section is used to configure stylints rules. Here you can turn on or off, or configure behavior of, individual Stylelint rules. A rule can be turned off by setting it tonull
, as I have done for"selector-class-pattern"
. I turned it off because it insists on having CSS classes in the so called kebab case e.g..panel-quiz
instead of.panelQuiz
. I find it inconvenient for various reasons so I turned it off. -
Create
.stylelintignore
in the project root with the following contents:
styles/globals.css styles/Home.module.css coverage
I create this file so that the two stylesheets generated by Next.js CLI which do not comply with the linting rules can get ignored (there might be a better way of doing this but this works for me). Also, files in
coverage
folder do not need to be linted and would likely throw up errors.
Set up package.json
scripts
-
The most important script is
"build"
. The default command for this script,next build
, runs ESLint but not Stylelint. So modify it inpackage.json
file as follows:
{ "scripts": { "build": "prettier --check . && stylelint --allow-empty-input \"**/*.{css,scss}\" && next build", ...
Using this tweak, we can run
npm run build
locally or in a CI/CD pipeline and it would fail not only on ESLint failure but now also on Stylelint failure. Indeed if you deploy your app to Vercel, the default build logic there also callsnpm run build
. So when I introduced an error in one of my stylesheets, then deployed to Vercel, I got the following error during deployment:Notice we have added
prettier --check .
at the front also. This means if a file is not formatted as per our Prettier setup then too thebuild
script would fail. This should not happen ordinarily (see lint-staged setup below) but is a failsafe to prevent inconsistently formatted files from creeping into the codebase. If there are exceptions you want to allow, you can always mark those using special comments that Prettier understands (discussed below). -
Set up
"lint"
and"format"
scripts inpackage.json
. These allow formatting and linting on the command line and come in handy every now and then:
{ "scripts": { ... "lint": "prettier --check . && stylelint --allow-empty-input \"**/*.{css,scss}\" && next lint", "format": "prettier --write ."
-
I recommend setting up a
build:local
script as follows:
"build:local": "prettier --write . && stylelint --allow-empty-input \"**/*.{css,scss}\" && next build",
The only difference between this and the
build
script above is that this formats the code before linting and building it instead of just--check
ing the formatting. This is really useful when coding because tools such as command line tools and VS Code extensions frequently generate or alter code so that formatting is no longer consistent the Prettier ruleset. When this happens, it is quite frustrating to runnpm run build
only to find out that Prettier has failed because some files require formatting.If locally, during development, we run
npm run build:local
instead ofnpm run build
, any files that need formatting would first get formatted before lint and build takes place. Therefore the build will never fail because of a formatting issue in a file that we did not (manually) introduce.The reason why I have separated this from
build
script is becausebuild
is used on Vercel during remote build and deployment process. For this reason I usenpm run build
in my own CI/CD pipelines on GitHub Actions and Azure DevOps also. Hence the need to define another command for local use only that actually formats files instead of only checking the formatting.
lint-staged setup
lint-staged is a package that can be used to run formatting and linting commands on staged files in a Git repo. Staged files are those that have been added to the Git index using git add .
, these are the files that have changed since last commit and will get committed when you next run git commit
.
Husky is the typical choice in Node.js packages for registering commands to run in Git hooks. For example, registering npx lint-staged
with Husky to run in the Git pre-commit hook means lint-staged will run automatically whenever we execute git commit
to commit our code. At that time, the formatter (Prettier) and linters (ESLint and Stylelint) that have been configured in lint-staged will run on the staged files. If there are any errors, the commit would fail.
Whenever git commit
fails due to linting errors, we can fix those, then run git add .
and git commit
again. Thus code only ever gets into the repo after it has been consistently formatted and verified to be free of linting errors. This a particularly big advantage in a team setting.
I prefer to run formatting and linting only on staged files using lint-staged rather than on the entire codebase. This is both much faster (a much smaller number of files needs to be processed) and more useful (there's no point running formatter on files that are in the local directory but which haven't been staged, because any changes introduced by the formatter won't get committed).
Setup for lint-staged and Husky is as follows:
-
Install lint-staged package:
npm install --save-dev lint-staged
-
Create
lint-staged.config.js
in project root with the following contents:
/* eslint-env node */ const path = require('path'); const eslintCommand = (filenames) => `next lint --file ${filenames .map((f) => path.relative(process.cwd(), f)) .join(' --file ')}`; const formatCommand = 'prettier --write'; const stylelintCommand = 'stylelint --allow-empty-input "**/*.{css,scss}"'; module.exports = { '*.{js,jsx,ts,tsx}': [formatCommand, eslintCommand], '*.{css,scss}': [formatCommand, stylelintCommand], '!*.{js,jsx,ts,tsx,css,scss}': [formatCommand], };
The above file configures lint-staged to run Prettier, Stylelint and ESLint (
next lint
command invokes ESLint).lint-staged runs all three commands (see the object assigned to
module.exports
) in parallel. Therefore the globs for the commands need to select sets of files that are disjoint (do not overlap). Otherwise there could be a race condition where mutiple commands run on the same file at the same time with indeterminate results. -
Install Husky NPM package.
npm install --save-dev husky
-
Create and run a
prepare
script to run Husky's ownhusky install
command:
npm pkg set scripts.prepare="husky install" npm run prepare
The
"prepare"
script is a lifecycle script and one of the conditions under which runs automatically is whennpm install
is run without arguments in the project folder. Defining this script as we have done ensures that if you or a teammategit fetch
es the repo and then runsnpm install
in the project folder to fetch all its NPM dependencies so that you may start work on the project, then Husky's ownhusky install
command would also run. This would set Husky up to run any defined Git hooks when Git operations are performed in the project folder.Since the
npm prepare
script hasn't run yet, we run it for the first time to register Husky with the local Git repo, hence the commandnpm run prepare
at the end of the snippet above. -
Register lint-staged with Husky to run at Git pre-commit:
npx husky add .husky/pre-commit "npx lint-staged"
VS Code extensions setup
If you use VS Code as your code editor:
-
Install the following VS Code extensions to provide linting and formatting on file save and syntax highlight on linting errors:
-
Put the following in a
settings.json
file in.vscode
folder in the project (you can of course put these settings in you User Preferences file also).
{ "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[javascriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[scss]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "stylelint.validate": ["css", "scss"], "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll": true }, }
As set up, the extensions would lint and format on Save.
Final Checks and Troubleshooting
Build and commit:
npm run build
git add .
git commit -m "fix: set up linting and formatting"
Building and committing is a good sanity check for the setup we just did:
If anything had not been set up correctly, you would likely get errors either during build or at commit.
-
If you already had some code in the project, then there might be a few errors when you commit. Typically, these can be resolved by:
-
adding folders or file to one of the
*ignore
files.For example, I already had some code in my project with Storybook installed. So I had to add folders
.storybook
andstorybook-static
to each of.stylelintignore
,.eslintignore
and.prettierignore
as all three tools complained about them.
stories storybook-static
-
Adding plugins for specific file types.
For example, I had Gherkin
.feature
files in my project to describe integration tests. Prettier couldn't format these. So I added the prettier-plugin-gherkin by simply running:
npm install prettier-plugin-gherkin --save-dev
Note that usually it is enough to install the package for a Prettier plugin for Prettier to locate it and additional configuration is not required.
Likewise, ESLint complained when it encountered
.cy.ts
files containing Cypress interaction tests for my app. To resolve this linting error, I installed the NPM package for Cypress ESLint plugin and configured it as described here (unlike Prettier, to get this ESLint package to work, some configuration was required). -
Sometimes it is safe to turn off a linting rule at a specific line or for a whole file. While I am always wary of doing this, in a (deliberately bad) experimental code file, I had many instances of an error that VS Code ESLint extension pointed. This was not caught before but was now being pointed out because strict TypeScript linting rules had been enabled:
So I pressed Ctrl + . to Show Code Actions (I could instead have clicked the yellow lighbulb icon shown next to the issue), then selected Disable
@typescript/no-non-null-assertion
for the entire file.This placed the comment
/* eslint-disable @typescript-eslint/no-non-null-assertion */
on top of my file to disable all instances of that particular error within the file:
-
Top comments (2)
Really useful article with instructions. I followed line by line and setup configuration for our enterprise app. Everything is working OK. Thank you!
Thank you. It was very helpfull for me!