Back

How to structure scalable Next.js project architecture

January 30, 2024

Setting up a Next.js project

Begin by installing a fresh Next.js project using TypeScript:

npx create-next-app --typescript nextjs-architecture

This creates a Next.js boilerplate template. Using TypeScript is recommended as "it allows better type safety — something you must have while building a modern, scalable, full-stack app."

After installation, start the development server:

npm run start

The app will be available at localhost:3000 by default.

Ensuring engine compatibility and stability at scale

When multiple team members work on a project, consistency across Node.js versions is critical. Add a .nvmrc file at the root level:

18.17.0

Create an .npmrc file with:

engine-strict=true

Update package.json to include:

"engines": {
    "node": ">=18.17.0",
    "npm": "please-use-npm"
}

This ensures all team members use compatible versions and prevents installation inconsistencies.

Setting up Git version control

A version control system is crucial for projects expected to scale. Initialize Git and push to a repository:

git add .
git commit -m "initial commit"
git remote add origin [email protected]:<YOUR_GITHUB_USERNAME>/nextjs-architecture.git
git branch -M main
git push -u origin main

Ensure a .gitignore file exists at the root level to prevent sensitive folders like node_modules and environment variable files from being committed.

Engine locking

Engine locking ensures consistent versions across environments. Specify the Node version in package.json:

"engines": {
  "node": "18.x"
}

Create .npmrc with:

engine-strict=true

This practice is especially important when working with large teams across different environments.

Enforcing code formatting rules

Code formatting maintains consistency as projects scale. Next.js includes built-in ESLint support. Configure it by creating or updating .eslintrc.json:

{
  "extends": ["next", "next/core-web-vitals", "eslint:recommended"],
  "globals": {
    "React": "readonly"
  },
  "rules": {
    "no-unused-vars": "warn"
  }
}

The React global ensures React is available in functional components without explicit imports. The no-unused-vars rule warns about unused variables.

Adding Prettier for consistent formatting

Install Prettier:

npm i prettier -D

Create .prettierrc:

{
  "tabWidth": 2,
  "semi": true,
  "singleQuote": true
}

Create .prettierignore:

dist
node_modules
package.json

Add to package.json:

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

Run formatting with npm run prettier. Configure VS Code to run Prettier on save for automatic formatting.

Structuring your Next.js directory

Create a directory structure:

src/
  ├── app/
  ├── components/
  ├── utils/
  └── hooks/
  • app: File-based routing
  • components: Reusable React components (cards, sliders, tabs, etc.)
  • utils: Reusable library instances, dummy data, and utility functions
  • hooks: Custom React hooks

Using a src folder is optional; the structure can exist at the root level instead.

Setting up a custom layout

In Next.js v13+, use the built-in layout file at src/app/layout.tsx:

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  );
}

This layout file is ideal for components shared across the application like Navbar and Footer, or for wrapping the entire app with providers.

For Next.js v12, create a Layout.tsx component:

function Layout({ children }) {
  return (
    <div>
      <Navbar />
      {children}
      <Footer />
    </div>
  );
}

Import it at the root level in _app.tsx.

Integrating Next.js with Storybook

Install Storybook:

npx sb init --builder webpack5

Update .eslintrc.json to include Storybook:

{
  "extends": [
    "plugin:storybook/recommended",
    "next",
    "next/core-web-vitals",
    "eslint:recommended"
  ],
  "globals": {
    "React": "readonly"
  },
  "overrides": [
    {
      "files": ["*.stories.@(ts|tsx|js|jsx|mjs|cjs)"]
    }
  ],
  "rules": {
    "no-unused-vars": "warn"
  }
}

Add to package.json:

"resolutions": {
  "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.cd77847.0"
}

Update .storybook/main.js:

module.exports = {
  typescript: { reactDocgen: "react-docgen" },
  // ... other config
}

Add scripts to package.json:

"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"

Run npm run storybook to access Storybook at port 6006.

Create component structures like:

components/
  └── Button/
      ├── Button.tsx
      ├── Button.stories.tsx
      └── button.modules.css

Setting up a pre-commit hook

Pre-commit hooks enforce rules before code is committed, ensuring consistency across the team. Install required tools:

npm i --dev prettier eslint-plugin-prettier eslint-config-prettier husky

Update .eslintrc:

{
  "extends": ["next", "next/core-web-vitals", "prettier"],
  "plugins": ["prettier"],
  "rules": {
    "prettier/prettier": "warn",
    "no-console": "warn"
  }
}

Create .prettierrc:

{
  "singleQuote": true,
  "trailingComma": "none",
  "quoteProps": "as-needed",
  "bracketSameLine": false,
  "bracketSpacing": true,
  "tabWidth": 2
}

Install Husky:

npm i --dev husky

Install and configure lint-staged by creating .lintstagedrc.js:

module.exports = {
  // Type check TypeScript files
  '**/*(ts|tsx)': () => 'yarn tsc --noEmit',

  // Lint & Prettify TS and JS files
  '**/*(ts|tsx|js)': filenames => [
    `yarn eslint ${filenames.join(' ')}`,
    `yarn prettier --write ${filenames.join(' ')}`
  ],

  // Prettify only Markdown and JSON files
  '**/*(md|json)': filenames => `yarn prettier --write ${filenames.join(' ')}`
};

Create /.husky/pre-commit:

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

npm run lint-staged

Pre-commit hooks help ensure "everybody is on the same page" and prevent merge conflicts from different formatting approaches across machines.

Leveraging Git branching strategies

Several popular branching strategies exist:

  • Gitflow: Feature-driven branches with multiple primary branches, ideal for projects with scheduled releases
  • GitHub Flow: Simpler approach focused on continuous delivery
  • GitLab Flow: Combines features of Gitflow and GitHub Flow
  • Trunk-based Development: Single main branch that stays stable; developers work in smaller iterations and merge frequently

Gitflow is widely adopted and uses feature branches with hotfix naming conventions for priority fixes. Trunk-based development has gained traction recently, reducing merge conflicts by focusing on a single stable main branch.

"The best branching strategy is the one that suits your project and the team in general."

Deploying your Next.js application

Use Vercel for straightforward deployment of Jamstack applications:

  1. Sign up for Vercel using your GitHub account
  2. Select the nextjs-architecture project from the dashboard
  3. Configure:
    • Project name: nextjs-architecture
    • Build Command: npm run build
    • Install Command: npm install
    • Root Directory: ./

Complete the deployment process following Vercel's instructions.

Conclusion

This architecture provides the foundation for scaling a Next.js application. Further improvements can be made by implementing Git workflow strategies for multiple teams and exploring new features as the Next.js team releases updates.