Simon.
Get in touch

Setting Up a Modern Next.js Project.

17 Feb 2025

Building a modern Next.js application involves more than just throwing components together. A well-structured project with the right tooling can significantly boost productivity, maintainability, and collaboration. This guide walks you through setting up a robust Next.js project, incorporating essential tools and best practices.


Preparing the Environment:

Before diving in, ensure you have the following installed:

  • Node.js: An active LTS version is recommended. (We'll lock this down later.)
  • Yarn: My preferred package manager for this setup. You can use npm if you prefer, but the commands will differ slightly.
  • VS Code (or your IDE of choice): A good code editor is essential. I'll be referencing VS Code throughout this guide.

Project Initialization: Laying the Foundation

1. Create the Next.js app:

sh
npx create-next-app@latest

Answer the prompts. Here's a sample configuration:

sh
✔ What is your project named? … coffee-roasting-lms
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like your code inside a `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to use Turbopack for `next dev`? … Yes
✔ Would you like to customize the import alias (`@/*` by default)? … Yes
✔ What import alias would you like configured? … @/*

2. Integrate Shadcn UI:

Shadcn/UI is a set of beautifully designed, accessible components and a code distribution platform. You have full control to customize and extend the components to your needs.

sh
npx shadcn@latest init

Follow the prompts to configure Shadcn UI.

3. Component Organization:

For better organization, I recommend adjusting the components.json file generated by Shadcn UI to match your project's folder structure.

Locking your Node.js and package manager versions prevents inconsistencies across development environments.

  • .nvmrc:
lts/iron
  • .npmrc:
engine-strict=true
  • package.json: JSON
json
"engines": {
    "node": ">=v20.18.2",
    "yarn": ">=1.22.22",
    "npm": "please-use-yarn"
},

Git Setup: Version Control from the Start

Initialize a Git repository and connect it to a remote origin:

sh
git init
git remote add origin <your-repo-url>
git checkout -b Initial
git add .
git commit -m "Initial commit"

Code Quality: Keeping Things Clean

ESLint: Identifying and Fixing Issues

Install ESLint with Prettier support:

sh
yarn add -D eslint-config-prettier

Update eslint.config.mjs:

typescript
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";

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

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

const eslintConfig = [
  ...compat.config({
    extends: (["next/core-web-vitals", "next/typescript", "prettier"]),
    rules: {
      semi: ["error"],
      quotes: ["error", "double"]
    }
  }),
];

export default eslintConfig;

Run the linter, you should get a message like this:

✔ No ESLint warnings or errors
Done in 1.30s.

If you encounter any errors, ESLint will help you clearly understand what they are. If you come across a rule you don't agree with, you can easily disable it in the "rules" section by setting it to 1 (warning) or 0 (ignore), like this:

  "rules": {
    "no-unused-vars": 0, // As example: Will never bug you about unused variables again
  }

Prettier: Automatic Code Formatting

sh
yarn add --dev --exact prettier

Create .prettierrc:

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

Create .prettierignore:

.yarn
.next
dist
node_modules

Add Prettier script to package.json:

json
"scripts": {
  "prettier": "prettier --write ."
}

Run Prettier:

sh
yarn prettier

Git Hooks (Husky): Automating Checks

Install Husky:

sh
yarn add --dev husky
npx husky init

Add a pre-commit hook (.husky/pre-commit):

sh
echo "npm test" > .husky/pre-commit

Modify .husky/pre-commit:

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

echo '🏗️🚀 Pre-flight check: Formatting, linting, and building before liftoff!'

# Check Prettier standards
yarn check-format ||
(
    echo '🤢🤮 CODE CRIME ALERT! 🤮🤢
            Your formatting is an absolute disgrace. Run `yarn format`, stage the changes, and try again.';
    false;
)

# Check ESLint Standards
yarn check-lint ||
(
        echo '😤💀 LINT POLICE SAYS NO! 💀😤
                ESLint caught some bad code. Fix the errors above, commit again, and don’t make me do this twice!'
        false; 
)

# Check TypeScript standards
yarn check-types ||
(
    echo '🤡😂 TYPE CHECK FAIL DETECTED 😂🤡
            That’s not even JavaScript—it’s just chaos! Read the errors above and fix your mess.';
    false;
)

# If everything passes... Now we can build
echo '🤔🤔🤔 Hmm... Looks clean so far. Let’s see if it builds. 🚀'

yarn build ||
(
    echo '❌👷‍♂️ CALL THE ENGINEERS! ❌
            Build failed. Check the logs above and try again.';
    false;
)

# If everything passes... Now we can commit
echo '✅🎉 SHIP IT! 🎉✅ 
        Everything checks out. Committing now like a responsible dev.'

Commitlint: Enforcing Commit Message Standards

To enforce commit message conventions, install CommitLint:

sh
yarn add --dev @commitlint/{cli,config-conventional}

Configure CommitLint:

sh
echo "export default { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js

Add hook

To use commitlint you need to setup commit-msg hook (currently pre-commit hook is not supported)

sh
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg

Storybook Setup

Storybook is a free tool for building and testing UI components and pages in isolation.

sh
npx storybook@latest init

main.ts

typescript
import type { StorybookConfig } from "@storybook/nextjs";
import path from "path";

const config: StorybookConfig = {
  stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
  webpackFinal: async (config) => {
    if (config.resolve) {
      config.resolve.alias = {
        ...config.resolve.alias,
        "@": path.resolve(__dirname, "../src"),
      };
    }
    return config;
  },
  addons: [
    "@storybook/addon-onboarding",
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@chromatic-com/storybook",
    "@storybook/addon-interactions",
    "@storybook/addon-styling-webpack",
    "@storybook/addon-themes",
  ],
  framework: {
    name: "@storybook/nextjs",
    options: {},
  },
  docs: {
    autodocs: "tag",
  },
  staticDirs: ["../public"],
};
export default config;

preview.ts

typescript
import type { Preview } from "@storybook/react";
import "../src/styles/globals.css";

const preview: Preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
};

export default preview;

Open eslint.config.mjs and update it to the following:

json
import { FlatCompat } from "@eslint/eslintrc";
import { dirname } from "path";
import { fileURLToPath } from "url";

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

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

const eslintConfig = [
  ...compat.config({
    extends: [
      "next/core-web-vitals",
      // "next/typescript",
      "prettier",
      "plugin:storybook/recommended",
    ],
    globals: {
      React: "readonly",
    },
    overrides: [
      {
        files: ["*.stories.@(ts|tsx|js|jsx|mjs|cjs)"],
        rules: {
          "storybook/hierarchy-separator": "error",
        },
      },
    ],
    rules: {
      "no-unused-vars": [
        1,
        {
          args: "after-used",
          argsIgnorePattern: "^_",
        },
      ],
      semi: ["error"],
      quotes: ["warn", "double"],
      "no-unused-vars": [1, { args: "after-used", argsIgnorePattern: "^_" }],
    },
  }),
];

export default eslintConfig;

At this stage we will be making a commit with message build: implement storybook

VS Code Configuration

Now that we’ve implemented ESLint, Prettier, and Storybook, we can set up Visual Studio Code (VS Code) to run them automatically. Create a directory called `.vscode` in the root of your project, and inside it, add a file named `settings.json`. This file will contain project-specific settings that we can share with our team through the code repository. In `settings.json`, we will add the following values: `.vscode/settings.json`

json
{
  "editor.defaultFormatter": "esbenp.prettier-vscode",

  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "always",
    "source.fixAll.ts": "always",
    "source.organizeImports": "always"
  }
}
Debugging

Let's set up an environment for debugging our application in case we run into any issues during development.
Inside of your .vscode directory create a launch.json file:
launch.json

json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Next.js: debug server-side",
      "type": "node-terminal",
      "request": "launch",
      "command": "npm run dev"
    },
    {
      "name": "Next.js: debug client-side",
      "type": "chrome",
      "request": "launch",
      "url": "http://localhost:3000"
    },
    {
      "name": "Next.js: debug client-side (Firefox)",
      "type": "firefox",
      "request": "launch",
      "url": "http://localhost:3000",
      "reAttach": true,
      "pathMappings": [
        {
          "url": "webpack://_N_E",
          "path": "${workspaceFolder}"
        }
      ]
    },
    {
      "name": "Next.js: debug full stack",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/node_modules/.bin/next",
      "runtimeArgs": ["--inspect"],
      "skipFiles": ["<node_internals>/**"],
      "serverReadyAction": {
        "action": "debugWithEdge",
        "killOnServerStop": true,
        "pattern": "- Local:.+(https?://.+)",
        "uriFormat": "%s",
        "webRoot": "${workspaceFolder}"
      }
    }
  ]
}

First, we will install cross-env, which is necessary for setting environment variables when teammates are working in different environments.

sh
yarn add -D cross-env

With that package installed we can update our package.json dev script to look like the following:

package.json

json
{
  ...
  "scripts": {
    ...
    "dev": "cross-env NODE_OPTIONS='--inspect' next dev",
  },
}

This will allow you to log server data in the browser while working in dev mode, making it easier to debug issues.

At this stage I'll be making a new commit with message build: add debugging configuration

Following these steps ensures that your Next.js project is well-structured, properly formatted, and adheres to best practices. Happy coding! 🚀