Best Practices for Creating a Next.js Project with TypeScript and Linting Tools

Best Practices for Creating a Next.js Project with TypeScript and Linting Tools

Creating Consistent, Error-Free Next.js Applications with TypeScript and Linting

As I delved into the best practices for using linting tools in React applications, I considered implementing some of these techniques in my latest Next.js TypeScript project to ensure the code I write is both scalable and efficient. I also believe it would be beneficial to share my findings in an article aimed at helping others improve their coding abilities.

This article will teach you how to set up ESLint to function with Prettier in a Next.js application. Additionally, it will cover how to combine this tooling with Visual Studio Code.

NB: It is important to note that in this article, we will use yarn in place of npm.

Setting up the Project

To start a Next.js application with Typescript, Begin by opening your command-line interface (CLI) and running the command below:

yarn create next-app --typescript

Running this command will generate a bunch of boilerplate files to assist you in getting started, including a basic .eslint configuration.

Navigate to the generated source code and open it up in VSCode to begin.

cd my-app
code .

Setting up Eslint

Eslint comes pre-installed by default (you should be able to locate the dependency in package.json). To execute eslint, use the command below:

yarn eslint .

To display eslint errors in VSCode, it's necessary to install the ESLint plugin. Additionally, I suggest configuring VSCode to auto-correct eslint errors upon saving. To accomplish this, create a file named .vscode/settings.json and insert the following content:

// .vscode/settings.json
{
    "editor.codeActionsOnSave": {
      "source.fixAll.eslint": true
    }
  }

To verify whether the changes were successful, let's add the prefer-const rule to the .eslintrc.json configuration file:

// .eslintrc.json
{
    "extends": "next/core-web-vitals",
    "rules": {
      "prefer-const": "error"
    }
  }

To proceed, create a file called test.ts and input the following content:

export const APP_VERSION = 'v1.0.0'

VSCode should indicate an error, as demonstrated here:

Attempt to save the file: VSCode should automatically modify the let statement to a const. If it doesn't work, try restarting VSCode. Additionally, verify that you've included the leading dot for the .vscode directory that houses the settings.json file.

For a comprehensive collection of eslint regulations, I propose installing the @typescript-eslint/eslint-plugin:

yarn add --dev @typescript-eslint/eslint-plugin

Afterward, you can substitute your .eslintrc.json file with the following:

{
    "plugins": ["@typescript-eslint"],
    "extends": [
      "next/core-web-vitals",
      "plugin:@typescript-eslint/recommended"
    ],
    "rules": {
      // I suggest you add those two rules:
      "@typescript-eslint/no-unused-vars": "error",
      "@typescript-eslint/no-explicit-any": "error"
    }
  }

If you're dissatisfied with a rule included by @typescript-eslint/recommended, remember that you can deactivate it in your .eslintrc.json file, similar to the example below:

{
    // ...
    "rules": {
      "prefer-const": "off" // Turn rule off
    }
  }

Setting up Prettier

Now that we have successfully installed and configured eslint, let's proceed by enabling auto-formatting using Prettier.

Begin by installing the Prettier dependencies:

yarn add --dev prettier eslint-config-prettier

The prettier package is the main tool that will format your files. Additionally, the eslint-config-prettier package prevents conflicts between prettier and eslint rules.

To customize Prettier to your liking, create a .prettierrc.json file and configure it based on your preferences. You can view all the available options at prettier.io/docs/en/options.html.

{
  "semi": false,
  "trailingComma": "es5",
  "singleQuote": true,
  "tabWidth": 2,
  "useTabs": false
}

Next, modify your .eslintrc.json file in the following manner:

// .eslintrc.json
{
    // ...
    "extends": [
      "next/core-web-vitals",
      "plugin:@typescript-eslint/recommended",
      "prettier" // Add "prettier" last. This will turn off eslint rules conflicting with prettier. This is not what will format our code.
    ],
    // ...
  }

Let's proceed with configuring VSCode to format our code automatically based on our prettier configuration file when we save the file. To achieve this, install the Prettier plugin for VSCode.

Subsequently, modify the .vscode/settings.json file to instruct VSCode to format the code on save:

{
    "editor.codeActionsOnSave": {
      "source.fixAll.eslint": true
    },
    // Add those two lines:
    "editor.formatOnSave": true, // Tell VSCode to format files on save
    "editor.defaultFormatter": "esbenp.prettier-vscode" // Tell VSCode to use Prettier as default file formatter
  }

Going forward, each time you save a file, Prettier will format it as per your configurations. You might have to restart VSCode for the configuration to take effect.

Note: In case you only want specific file extensions to be formatted using Prettier when you save them, implement the following instead:

{
    // OR If want only typescript files to be formatted on save
    "[typescript]": {
      "editor.formatOnSave": true,
      "editor.defaultFormatter": "esbenp.prettier-vscode"
    }
  }

Setting up Husky to Check for Errors, Lint, and Format Code on Commit

It is highly recommended that you not only rely on the "format on save" feature in VSCode but also add an extra layer of safety by running eslint and prettier on each commit. This ensures that each file committed adheres to proper formatting and has no ESLint error. Moreover, you can perform additional checks while committing, such as TypeScript type-checking.

Using Husky is one way to achieve this; it is a small program that executes scripts for a given Git command.

# Install Husky
yarn add --dev husky

To enable Husky, execute the following command:

yarn husky install

⚠️ Please note that in the future, after cloning the project, you will need to execute yarn husky install to enable Husky.

Next, we need to add a Git hook by running the following command:

yarn husky add .husky/pre-commit "yarn tsc --noEmit && yarn eslint . && yarn prettier --write ."

Let's break down what this command does. On each commit, Husky will:

  • Run the tsc command to make sure there are no TypeScript errors

  • Run the eslint command to make sure there are no ESLint errors

  • Format our code using Prettier

To test if it works, try introducing an error in the code, such as replacing export const APP_VERSION = 'v1.0.0' with export const APP_VERSION: number = 'v1.0.0' in the test.ts file we created earlier, and try to commit. Husky should prevent the commit and display an error message in the console.

% git add -A
% git commit 
-m "test"
✖ yarn tsc --noEmit:
warning ../../package.json: No license field
error Command failed with exit code 2.
$ /Users/user/Downloads/quill/node_modules/.bin/tsc --noEmit
test.ts(1,14): error TS2322: Type 'string' is not assignable to type 'number'.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
[STARTED] yarn prettier --write /Users/user/Downloads/quill/test.ts
[SUCCESS] yarn prettier --write /Users/user/Downloads/quill/test.ts
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
husky - pre-commit hook exited with code 1 (error)

If you need to skip the husky checks for any reason, you can add the --no-verify flag to your commit command. For example: git commit --no-verify -m "Update README.md". Keep in mind that this should only be used if necessary, and you should always aim to have all checks passed before committing changes.

Lint staged: Optimize Checking of your Code

As a project gets bigger, it takes more time to check the entire codebase every time you make changes. However, in many cases, you may only modify specific files that do not require checking. For example, you may edit markdown files or CI yaml files that do not need to be checked by TypeScript.

Enter Lint staged. Its purpose is to run your lint scripts only when necessary, on the required files, to save time and improve your productivity.

Installation of lint-staged

yarn add --dev lint-staged

Configure Lint-Staged

To configure Lint-Staged, create a dedicated configuration file called lint-staged.config.js. While some prefer to configure it directly in package.json, using a dedicated file gives more flexibility and options for customization.

// lint-staged.config.js
module.exports = {
  // Type check TypeScript files
  '**/*.(ts|tsx)': () => 'yarn tsc --noEmit',

  // Lint then format TypeScript and JavaScript files
  '**/*.(ts|tsx|js)': (filenames) => [
    `yarn eslint --fix ${filenames.join(' ')}`,
    `yarn prettier --write ${filenames.join(' ')}`,
  ],

  // Format MarkDown and JSON
  '**/*.(md|json)': (filenames) =>
    `yarn prettier --write ${filenames.join(' ')}`,
}

This code specifies the matchers for lint-staged. Each matcher consists of a file pattern to match and the command(s) to run against the matched files.

Matchers are run in parallel, but the commands within a matcher are run in sequence. To save time, TypeScript and ESLint checks are separated into two matchers and run in parallel. Additionally, Prettier formatting is separated for TypeScript files and other files to avoid conflicts.

For the TypeScript command, filenames are not passed because TypeScript cannot be run on isolated files.

After defining the matchers, we need to update the pre-commit hook in the .husky/pre-commit file.

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

yarn lint-staged # Replace the last line with "yarn lint-staged"

Let's test out the new configuration. Try to commit the test.ts file we created earlier that contains TypeScript errors. If it still contains errors, you won't be able to commit it. Here's how to do it:

git add test.ts # Add a file with TypeScript errors...
git commit -m "add test.ts" # ...and this will fail!

Now, try editing and committing the README.md file. On commit, you will notice that only the Prettier script will be run, and not the others. Here's how to do it:

git reset # un-add files
echo "Hello!" >> README.md # Edit README.md
git add README.md # Stage README.md
git commit -m "update readme" # ...and only prettier will run on commit!

In conclusion, it's important to have an organized and efficient workflow when developing software, especially in a team environment. By implementing tools such as ESLint, Prettier, Husky, and Lint-staged, developers can ensure code quality, consistency, and reliability. These tools provide automated checks for code errors, formatting, and run scripts on Git commit to ensure that code is always in a good state. Following best practices in coding not only improves productivity and teamwork but also results in better software products. Therefore, incorporating these tools in your development process is highly recommended for a smooth and hassle-free coding experience.

If you found this article useful, drop an emoji or leave a comment. I hope that you found these tips helpful in improving your code quality and development workflow. Don't hesitate to share this article with other developers who might benefit from it. Thank you for reading, and happy coding!