Setting Up a Modern Next.js Project.
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! 🚀