Vercel's React & Next.js Best Practices: Every Dev Should Know

12 min read

Modern Web Development Tooling: ESLint, Prettier, Husky & CI/CD

Writing good code is only half the equation. The other half is the infrastructure around it — the tools that catch problems before they reach production, enforce consistency across a team, and automate the repetitive work so developers can focus on what matters. This guide walks through the essential tooling stack for a modern web project: linting, formatting, pre-commit hooks, and CI/CD pipelines.

Why Tooling Matters

Without tooling, even experienced teams end up with:

  • Inconsistent code style across contributors
  • Bugs that a linter would have caught in seconds
  • Broken builds discovered only after merging to main
  • Time wasted on style debates in code review Good tooling makes these problems disappear automatically.

1. ESLint — Catching Problems Before Runtime

ESLint statically analyses your code to find bugs, enforce best practices, and flag anti-patterns — before you run a single line.

Setup

bash
1npm install --save-dev eslint @eslint/js
2npx eslint --init
UTF-8 2 Lines

For a Next.js + TypeScript project, use the official config:

bash
1npm install --save-dev eslint-config-next
UTF-8 1 Lines

eslint.config.js (Flat Config format)

javascript
1import js from '@eslint/js';
2import nextPlugin from '@next/eslint-plugin-next';
3import tsPlugin from '@typescript-eslint/eslint-plugin';
4import tsParser from '@typescript-eslint/parser';
5
6export default [
7 js.configs.recommended,
8 {
9 files: ['**/*.{ts,tsx}'],
10 plugins: {
11 '@typescript-eslint': tsPlugin,
12 '@next/next': nextPlugin,
13 },
14 languageOptions: {
15 parser: tsParser,
16 parserOptions: {
17 project: './tsconfig.json',
18 },
19 },
20 rules: {
21 // Catch common React mistakes
22 'react-hooks/rules-of-hooks': 'error',
23 'react-hooks/exhaustive-deps': 'warn',
24
25 // TypeScript-specific
26 '@typescript-eslint/no-unused-vars': 'error',
27 '@typescript-eslint/no-explicit-any': 'warn',
28 '@typescript-eslint/prefer-nullish-coalescing': 'error',
29
30 // Next.js
31 '@next/next/no-img-element': 'error', // Enforce next/image
32 '@next/next/no-html-link-for-pages': 'error', // Enforce next/link
33 },
34 },
35];
UTF-8 35 Lines

Add to package.json

json
1{
2 "scripts": {
3 "lint": "eslint . --ext .ts,.tsx",
4 "lint:fix": "eslint . --ext .ts,.tsx --fix"
5 }
6}
UTF-8 6 Lines

Key Rules to Enable

RuleWhy
no-unused-varsDead code causes confusion and bloat
react-hooks/exhaustive-depsMissing deps in useEffect cause stale closures
@next/next/no-img-elementForces use of next/image for performance
prefer-constSignals immutability intent clearly
no-console (warn)Prevents accidental logs in production

2. Prettier — Consistent Formatting, Zero Arguments

Prettier is an opinionated code formatter. It reformats your code automatically so the whole team writes in the same style — no more debates about tabs vs spaces, semicolons, or quote style.

Setup

bash
1npm install --save-dev prettier eslint-config-prettier
UTF-8 1 Lines

eslint-config-prettier disables ESLint rules that would conflict with Prettier.

.prettierrc

json
1{
2 "semi": true,
3 "singleQuote": true,
4 "trailingComma": "es5",
5 "tabWidth": 2,
6 "printWidth": 100,
7 "plugins": ["prettier-plugin-tailwindcss"]
8}
UTF-8 8 Lines

💡 prettier-plugin-tailwindcss automatically sorts Tailwind class names — great if you're using Tailwind CSS.

.prettierignore

javascript
1.next
2node_modules
3public
4*.min.js
UTF-8 4 Lines

Add to package.json

json
1{
2 "scripts": {
3 "format": "prettier --write .",
4 "format:check": "prettier --check ."
5 }
6}
UTF-8 6 Lines

ESLint + Prettier together

Add prettier to the end of your ESLint extends array so it always wins on formatting conflicts:

json
1{
2 "extends": ["next/core-web-vitals", "prettier"]
3}
UTF-8 3 Lines

3. Husky + lint-staged — Pre-Commit Hooks

Husky runs scripts before git commits. Combined with lint-staged (which only lints files you've actually changed), this ensures nothing broken ever enters version control.

Setup

bash
1npm install --save-dev husky lint-staged
2npx husky init
UTF-8 2 Lines

.husky/pre-commit

bash
1#!/bin/sh
2npx lint-staged
UTF-8 2 Lines

lint-staged config in package.json

json
1{
2 "lint-staged": {
3 "*.{ts,tsx}": [
4 "eslint --fix",
5 "prettier --write"
6 ],
7 "*.{js,json,css,md}": [
8 "prettier --write"
9 ]
10 }
11}
UTF-8 11 Lines

Now whenever a developer tries to commit, their changed files are automatically linted and formatted. If ESLint finds an unfixable error, the commit is blocked until it's resolved.

4. TypeScript's tsc — Type Checking in CI

ESLint and Prettier catch style and some logic errors, but TypeScript's compiler catches type errors. Run it as a separate check — don't rely on the editor alone.

json
1{
2 "scripts": {
3 "type-check": "tsc --noEmit"
4 }
5}
UTF-8 5 Lines

The --noEmit flag runs the type checker without producing any output files — perfect for CI.

5. CI/CD with GitHub Actions

All the local tooling in the world is useless if it's easy to bypass. A CI pipeline enforces it for everyone on every pull request.

.github/workflows/ci.yml

yaml
1name: CI
2
3on:
4 push:
5 branches: [main]
6 pull_request:
7 branches: [main]
8
9jobs:
10 quality:
11 name: Code Quality
12 runs-on: ubuntu-latest
13
14 steps:
15 - name: Checkout code
16 uses: actions/checkout@v4
17
18 - name: Setup Node.js
19 uses: actions/setup-node@v4
20 with:
21 node-version: '20'
22 cache: 'npm'
23
24 - name: Install dependencies
25 run: npm ci
26
27 - name: Type check
28 run: npm run type-check
29
30 - name: Lint
31 run: npm run lint
32
33 - name: Format check
34 run: npm run format:check
35
36 - name: Build
37 run: npm run build
UTF-8 37 Lines

This workflow runs on every push and PR to main. A PR can't be merged if any step fails.

Caching Dependencies

For faster runs, cache your node_modules:

yaml
1- name: Cache node_modules
2 uses: actions/cache@v4
3 with:
4 path: ~/.npm
5 key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
6 restore-keys: |
7 ${{ runner.os }}-node-
UTF-8 7 Lines

Deployment Step (Vercel)

yaml
1deploy:
2 name: Deploy to Vercel
3 runs-on: ubuntu-latest
4 needs: quality # Only deploy if quality checks pass
5 if: github.ref == 'refs/heads/main'
6
7 steps:
8 - uses: actions/checkout@v4
9 - name: Deploy
10 run: npx vercel --prod --token=${{ secrets.VERCEL_TOKEN }}
UTF-8 10 Lines

6. .editorconfig — Cross-Editor Consistency

Even with Prettier, different editors can apply different defaults before Prettier runs. An .editorconfig file sets a baseline that most editors respect:

plain
1root = true
2
3[*]
4indent_style = space
5indent_size = 2
6end_of_line = lf
7charset = utf-8
8trim_trailing_whitespace = true
9insert_final_newline = true
10
11[*.md]
12trim_trailing_whitespace = false
UTF-8 12 Lines

Putting It All Together

Here's the complete package.json scripts block for a project using all of the above:

json
1{
2 "scripts": {
3 "dev": "next dev",
4 "build": "next build",
5 "start": "next start",
6 "lint": "eslint . --ext .ts,.tsx",
7 "lint:fix": "eslint . --ext .ts,.tsx --fix",
8 "format": "prettier --write .",
9 "format:check": "prettier --check .",
10 "type-check": "tsc --noEmit"
11 }
12}
UTF-8 12 Lines

And the development workflow:

javascript
1CodeSaveEditor auto-formats (Prettier)
2
3 git commit → Husky runs lint-staged
4ESLint fixes + Prettier formats changed files
5Blocks commit if errors remain
6
7 PushGitHub Actions runs
8 → type-check + lint + format:check + build
9Blocks PR merge if any step fails
10
11 Merge to main → Auto-deploy to Vercel
UTF-8 11 Lines

Tooling Checklist

  • ✅ ESLint configured with TypeScript and Next.js plugins
  • ✅ Prettier set up with eslint-config-prettier to avoid conflicts
  • ✅ Husky + lint-staged running on pre-commit
  • tsc --noEmit in your CI pipeline
  • ✅ GitHub Actions workflow running on every PR
  • ✅ Build step in CI — catch broken builds before merging
  • .editorconfig for cross-editor baseline consistency
  • npm ci in CI (not npm install) for reproducible installs