Setting up a Next.js Project with Essential Best practices

8 mins read

#nextjs

#developer-experience

#tools

Introduction

Setting up a Next.js project isn’t just about installing dependencies, it’s about creating a maintainable, high-quality foundation. In this guide, we’ll cover a proven setup for essential tools like Prettier, ESLint, Husky, and Commitlint to ensure consistency, catch issues early, and improve team workflows, helping you stay organized and productive.

Project Setup

Start by creating a new Next.js project:

Terminal window
pnpm create next-app@latest

Name your project when prompted, and select Yes for ESLint setup.

Terminal window
✔ What is your project named? … next-template
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like your code inside a `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to use Turbopack for next dev? … No / Yes
✔ Would you like to customize the import alias (@/* by default)? … No / Yes
Creating a new Next.js app in /home/ouassim/next-template.

After the prompts, create-next-app creates a folder named after your project and installs the required dependencies.

Engine locking

Ensuring consistent Node.js versions across environments is essential for predictable functionality in both development and production. Specify the supported Node.js version in your package.json:

At the time of writing this, Next.js requires Node.js version 18.18.x or later.

package.json
{
...
"engines": {
"node": ">18.8.x"
}
...
}

Second, add a .npmrc file next to package.json. This .npmrc configuration enforces the Node.js version specified in package.json, preventing incompatible Node.js versions from being used.

.npmrc
engine-strict=true

The engine-strict setting tells your package manager to stop with an error on unsupported versions. This looks like:

Terminal window
ERR_PNPM_UNSUPPORTED_ENGINE  Unsupported environment (bad pnpm and/or Node.js version)
Your Node version is incompatible with "[email protected]([email protected]([email protected]))([email protected])".
Expected version: ^18.18.0 || ^19.8.0 || >= 20.0.0
Got: v18.12.1
This is happening because the package's manifest has an engines.node field specified.
To fix this issue, install the required Node version.

Let’s create our first commit:

Terminal window
git commit -m 'build: added engine locking'

Ignore the commit message structure for now, since we will be talking about it later on in this guide. Or jump directly to it Commitlint.

ESLint

ESLint already comes installed and pre-configured when creating a new Next.js project.

Let’s add a bit of extra configuration to make it stricter than the default settings. If you disagree with any of the rules it sets, no need to worry, it’s very easy to disable any of them manually. Use .eslintrc.json in the root directory to configure these additional rules.

.eslintrc.json
{
"extends": ["next/core-web-vitals", "next/typescript"],
"rules": {
// don't allow console.log statements and only allow info, warn and error logs
"no-console": ["error", { "allow": ["info", "warn", "error"] }]
}
}

Next, we will add eslint-plugin-tailwindcss for linting our tailwind classes (check for correct order, check for contradicting classnames…).

Terminal window
pnpm add -D eslint-plugin-tailwindcss @typescript-eslint/parser

Then update your .eslintrc.json file:

.eslintrc.json
{
"extends": [
"next/core-web-vitals",
"next/typescript",
"plugin:tailwindcss/recommended"
],
"rules": {
"no-console": ["error", { "allow": ["info", "warn", "error"] }],
"tailwindcss/classnames-order": "error"
},
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js"],
"parser": "@typescript-eslint/parser"
}
]
}

Let’s commit our changes

Terminal window
git commit -m 'build: added eslint rule + tailwindcss eslint plugin'

Prettier

Prettier is an opinionated code formatter that automatically formats our files based on a predefined set of rules.

It is only used during development, so I’ll add it as a devDependency:

Terminal window
pnpm add --save-dev --save-exact prettier

Next, create a config file .prettierrc with the following content:

.prettierrc
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

For the full configuration reference, check out the official documentation.

Next, create a .prettierignore file that lists the different directories/files we don’t want prettier to format:

.prettierignore
node_modules
.next

Add the following scripts to format files manually or check formatting status in CI environments:

package.json
{
...
"scripts": {
...
// format all files
"format": "prettier --write .",
// check if files are formatted, this is useful in CI environments
"format:check": "prettier --check ."
},
...
}

You can now run:

Terminal window
# format files
pnpm run format
# check if files are formatted
pnpm run format:check

Tailwind CSS

In order to automatically sort tailwind classes following the class order recommended by the tailwind team, we will be adding a new plugin to prettier called prettier-plugin-tailwindcss:

Terminal window
pnpm add -D prettier-plugin-tailwindcss

And then update your .prettierrc file and add plugins property to it:

.prettierrc
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5",
"plugins": ["prettier-plugin-tailwindcss"]
}

Sort imports

Next up, we will be adding @ianvs/prettier-plugin-sort-imports to our prettier config , this will allow us to sort import declarations using RegEX order.

First, install it as a devDependency:

Terminal window
pnpm add -D @ianvs/prettier-plugin-sort-imports

Then, update your .prettierrc file to be as follows:

.prettierrc
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5",
"plugins": [
"prettier-plugin-tailwindcss",
"@ianvs/prettier-plugin-sort-imports"
],
"importOrder": [
"^(react/(.*)$)|^(react$)",
"^(next/(.*)$)|^(next$)",
"<THIRD_PARTY_MODULES>",
"",
"^types$",
"^@/types/(.*)$",
"^@/config/(.*)$",
"^@/lib/(.*)$",
"^@/hooks/(.*)$",
"^@/components/ui/(.*)$",
"^@/components/(.*)$",
"^@/styles/(.*)$",
"^@/app/(.*)$",
"",
"^[./]"
],
"importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"]
}

Let’s commit our changes

Terminal window
git commit -m 'build: added prettier setup + tailwindcss plugin + sort imports plugin'

Git Hooks

Git hooks are scripts triggered at various stages in the Git workflow, ideal for enforcing code quality checks.

Husky 🐶

We are going to use a tool called husky.

Husky is a tool that makes it easier to use Git hooks, it provides a unified interface for managing hooks.

Install husky:

Terminal window
pnpm add --save-dev husky

husky init command:

The init command is the new and recommended way of setting up husky in your project. It creates a pre-commit script in .husky/ and updates the prepare script in your package.json.

Terminal window
pnpm exec husky init

package.json
{
...
"scripts": {
...
"prepare": "husky"
},
...
}

Adding a New Hook:

Adding a new hook is as simple as creating a file, but first, delete the existing hook (.husky/pre-commit) since we will be adding our own later on.

Add a pre-push hook for building code:

Terminal window
echo "pnpm run build" > .husky/pre-push

Commitlint

As you may have noticed from the previous commit messages, they follow a specific standard we call Conventional Commits, which is a lightweight convention on top of commit messages.

For more details, see Why Use Conventional Commits.

Now to ensure that our commit messages follow this standard, we will use a tool called commitlint, which acts as a linter for commit messages.

Install commitlint:

Terminal window
pnpm add --save-dev @commitlint/{cli,config-conventional}

Configure commitlint to use conventional config:

Terminal window
echo '{ "extends": ["@commitlint/config-conventional"] }' > .commitlintrc

Since we want to lint our commit messages before they are created we will use Husky’s commit-msg hook.

Terminal window
echo "pnpm dlx commitlint --edit \$1" > .husky/commit-msg

Let’s test the hook by commiting something that doesn’t follow the rules, you should see something like this if everything works.

Terminal window
git commit -m "foo: this will fail"
input: foo: this will fail
type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] [type-enum]
found 1 problems, 0 warnings
Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint
husky - commit-msg script failed (code 1)

Lint staged

Lint staged optimizes Git workflows by running linters on only staged files, improving speed and ensuring relevant files are checked before commiting.

Install lint staged.

Terminal window
pnpm add -D lint-staged

Then create a new file in the root of your project and name it .lintstagedrc.js and paste the following code to it.

const path = require("path")
const buildEslintCommand = (filenames) =>
`next lint --fix --file ${filenames
.map((f) => path.relative(process.cwd(), f))
.join(" --file ")}`
const prettierCommand = "prettier --write"
module.exports = {
"*.{js,jsx,ts,tsx}": [prettierCommand, buildEslintCommand],
"*.{json,css,md}": [prettierCommand],
}

The configuration above tells lint-staged to run next lint --fix and prettier --write on files that match the specified patterns.

Finally, let’s add our pre-commit hook so that this runs everytime before creating a new commit.

Terminal window
echo "pnpm lint-staged" > .husky/pre-commit

Let’s commit our changes

Terminal window
git commit -m 'build: added husky + commitlint + lint-staged setup'

VS Code setup

I chose VS code for this guide since it is the most used editor out there, I use neovim btw…

Extensions

Make sure you have these extensions installed:

These VS Code extensions streamline code formatting, linting, and Tailwind utility usage directly in your editor.

Workspace Settings

After configuring ESLint and prettier, it is time to make VS Code use them automatically. To do this first create .vscode directory in the root of your project and then add settings.json file and paste the following content to it.

settings.json
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
}

This will tell VS Code to use the Prettier extension as the default formatter and automatically format your files every time you save.

Recap

To summarise this whole article, here are some keypoints to keep in mind:

That was it for me, PEACE ✌️.