

Shahadat Robin
Software Developer, LemonHive
3 months ago
Next.js is a powerful React framework that makes it easy to build server-side rendered (SSR) and statically generated websites. In this article, I’ll walk you through how I setup every Next.js repository — not for beginners, not for experiments, but for production-ready work.
A repository should not begin with features. It should begin with discipline.
Before components grow, APIs multiply, contributors join — the foundation must be solid, predictable, and strict.
This is the exact baseline I use when starting any serious Next.js project.
This setup is not about trends. It is about consistency, scalability, and long-term maintainability.
I use pnpm for faster installs, efficient disk usage, and deterministic dependency management. Open the terminal and navigate where you’d like to store this project. Then run the following command:
pnpm create next-app@latest --typescript my-projectWhen prompted:
Then:
cd my-app
pnpm installNow we refined it.
The default ESLint config is good. But “good” is not enough, formatting must be unified.
Install Prettier
pnpm add -D prettier eslint-config-prettierCreate .prettierrc:
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100
"tabWidth": 2,
"arrowParens": "avoid"
}Create .prettierignore:
.next
out
node_modules
public
*.lockUpdate eslint.config.mjs:
const eslintConfig = defineConfig([
// ...previous Configs,
{
rules: {
'@next/next/no-img-element': 'error',
'react/no-unescaped-entities': 'off',
'no-console': 'error',
'import/no-anonymous-default-export': 'off',
},
},
])Added some eslint rules. Now formatting and linting are unified. They work together — not against each other.
Discipline should be automated.
Install Husky + lint-staged:
pnpm add -D husky lint-stagedEnable it:
pnpm dlx husky initThis command automatically creates a .husky/ folder and adds a prepare script to package.json
Then create .lintstagedrc.mjs:
export default {
'*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
'*.{json,md,css}': ['prettier --write'],
};Update `.husky/pre-commit`:
pnpm lint-stagedNow every commit runs linting and formatting automatically. You don’t rely on memory, You rely on process.
Every commit is guarded. That’s how professional codebases stay clean.
Manual installation ensures clarity and control.
pnpm add -D tailwindcss postcss autoprefixer
pnpm dlx tailwindcss init -pUpdate tailwind.config.ts:
import type { Config } from 'tailwindcss';
const config: Config = {
content: [
'./app/**/*.{ts,tsx}',
'./src/components/**/*.{ts,tsx}'
],
theme: {
extend: {}
},
plugins: []
};
export default config;Add to globals.css:
@tailwind base;
@tailwind components;
@tailwind utilities;Now styling is predictable, scalable, and utility-driven.
I prefer:
app/
src/
├── components/
├── hooks/
├── lib/
├── types/
├── utils/
A repository without structure becomes expensive. Intentional boundaries prevent long-term chaos.
Create app/not-found.tsx
export default function NotFound() {
return (
<div className="flex min-h-screen items-center justify-center">
<h1 className="text-3xl font-semibold">
404 | Page Not Found
</h1>
</div>
);
}This handles all unknown routes automatically.
Create app/error.tsx
'use client';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div className="flex min-h-screen flex-col items-center justify-center">
<h2 className="text-xl font-semibold">
Something went wrong.
</h2>
<button
onClick={() => reset()}
className="mt-4 rounded bg-black px-4 py-2 text-white"
>
Try again
</button>
</div>
);
}This catches runtime errors inside routes.
For catching errors across the entire application tree, create app/global-error.tsx
'use client';
export default function GlobalError({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<html>
<body>
<div className="flex min-h-screen flex-col items-center justify-center">
<h1 className="text-2xl font-semibold">
Application Error
</h1>
<p className="mt-2 text-gray-600">
Something critical happened.
</p>
<button
onClick={() => reset()}
className="mt-4 rounded bg-black px-4 py-2 text-white"
>
Reload
</button>
</div>
</body>
</html>
);
}
Difference:
error.tsx → Handles errors inside route segmentsglobal-error.tsx → Handles root-level rendering failuresBoth are important in production applications.
Update tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true
}
}Strict typing prevents silent bugs. Loose typing accumulates hidden debt.
In package.json:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"prepare": "husky install",
"format": "pnpm prettier --write .",
"type-check": "tsc --noEmit"
}
}Because velocity without structure collapses, scaling without guardrails breaks teams. This setup takes an extra 20–30 minutes, but it saves weeks over a project’s lifecycle.
A repository is not just code — it is a contract between your present and future self.
A great repository doesn’t shout. It should feel calm, Predictable, Reliable. That’s the goal. If you expect your project to grow — prepare it accordingly.