Brand logoobin
ProjectsInsightsContact
Resume

2020 © Shahadat Robin

How I Setup Next.js Repository for High-Velocity Projects

How I Setup Next.js Repository for High-Velocity Projects
Author Shahadat Robin's Image

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.

🧱 Stack & Tooling

This is the exact baseline I use when starting any serious Next.js project.

  • Next.js (App Router)
  • pnpm
  • TypeScript
  • ESLint + Prettier
  • Husky (pre-commit hooks)
  • Tailwind CSS
  • Custom 404 page
  • Custom error page
  • Custom global-error boundary
This setup is not about trends. It is about consistency, scalability, and long-term maintainability.

.✦ Initialize with pnpm + TypeScript

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-project

When prompted:

  • Which linter would you like to use? -> ESLint
  • Would you like to use React Compiler? -> No
  • Would you like to use Tailwind CSS? -> No (I prefer installing it manually for control)
  • Would you like your code inside a `src/` directory? -> No
  • Would you like to use App Router? -> Yes
  • Would you like to customize the import alias (`@/*` by default)? -> Yes

Then:

cd my-app
pnpm install

Now we refined it.

.✦ Strengthen ESLint + Prettier

The default ESLint config is good. But “good” is not enough, formatting must be unified.

Install Prettier

pnpm add -D prettier eslint-config-prettier

Create .prettierrc:

{
  "semi": true,
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 100
  "tabWidth": 2,
  "arrowParens": "avoid"
}

Create .prettierignore:

.next
out
node_modules
public
*.lock

Update 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.

.✦ Add Husky (Pre-commit Protection)

Discipline should be automated.

Install Husky + lint-staged:

pnpm add -D husky lint-staged

Enable it:

pnpm dlx husky init

This 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-staged

Now 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.

.✦ Install Tailwind CSS Manually

Manual installation ensures clarity and control.

pnpm add -D tailwindcss postcss autoprefixer
pnpm dlx tailwindcss init -p

Update 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.

.✦ Folder Structure

I prefer:

app/
src/
 ├── components/
 ├── hooks/
 ├── lib/
 ├── types/
 ├── utils/

A repository without structure becomes expensive. Intentional boundaries prevent long-term chaos.

.✦ Custom 404 Page

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.

.✦ Route-Level Error Page

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.

.✦ Global Error Boundary (Root-Level)

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 segments
  • global-error.tsx → Handles root-level rendering failures

Both are important in production applications.

.✦ Strengthen TypeScript Strictness

Update tsconfig.json:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "forceConsistentCasingInFileNames": true
  }
}

Strict typing prevents silent bugs. Loose typing accumulates hidden debt.

.✦ Essential Scripts

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"
  }
}

.✦ Why I Do This Every Time

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.

Related

NextJSSanity

Learn how to embed Sanity (v3) Studio inside Next.js project

NextJSSentry

Sentry in Next.js