How to structure scalable Next.js project architecture
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:
- Sign up for Vercel using your GitHub account
- Select the
nextjs-architectureproject from the dashboard - Configure:
- Project name:
nextjs-architecture - Build Command:
npm run build - Install Command:
npm install - Root Directory:
./
- Project name:
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.