How to structure scalable Next.js project architecture - LogRocket Blog (2024)

Editor’s note: This article was updated on 30 January 2024 to provide information related to the App Router introduced in Next.js v13, as well as to provide additional recommendations related to linting, Prettier, ESLint, engine locking, and pre-commit guidelines.

How to structure scalable Next.js project architecture - LogRocket Blog (1)

Next.js has become increasingly popular in the React ecosystem in recent years. It is now a de facto framework for building React web applications.

This development framework’s popularity is largely thanks to features like first-class support for a wide range of tools and libraries, the use of CSS modules for styling, the use of TypeScript for type safety, image optimization, and much more.

These features make it possible to build scalable applications — if you know how to structure your Next.js project strategically.

In this article, you will learn how to architect a Next.js application from scratch that will scale up without any issues as your project grows.

You can explore the final project on GitHub or simply follow along as we build it in this tutorial.

Setting up a Next.js project

Let’s begin by installing a fresh Next.js project. In the terminal, all you have to do is type the following command:

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

This will create a Next.js boilerplate template called nextjs-architecture. Make sure you choose --typescript as a flag. Using TypeScript in your Next.js app is always a good idea, as it allows better type safety — something you must have while building a modern, scalable, full-stack app.

In this example, I am using npm as a package manager. You can use yarn/pnpm according to your needs.

After a successful installation, open nextjs-architecture in VS Code or any IDE of your choice and run the following:

npm run start

This command will spin up your local development server for nextjs-architecture. By default, Next.js serves its app on port number 3000. Therefore, go to localhost:3000 to see your boilerplate code running successfully.

This Next.js project is using version 14.1.0 and will look like this on the local server:

How to structure scalable Next.js project architecture - LogRocket Blog (2)

Ensuring engine compatibility and stability at scale

If you are building an app for work, it’s likely that multiple team members are involved in your project. Therefore, you have to make sure all of them are on the same Node.js version. Using different versions would cause package version inconsistency and may later cause bugs.

To prevent any issues caused by inconsistencies between different versions of Node, you can add a .nvmrc file at the root level of your project and add the Node.js version number that you want this project to use. In this case, Node has been set to v18:

18.17.0

You should also configure your package manager — in this case, npm — to strictly manage dependency usage for team members. Create a new file called npmrc and add the following code:

engine-strict=true

Now, go to the package.json file and add a new key-value pair:

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

This will ensure your project requires Node.js version 16 and above to run, and in this case, also enforces the use of npm as a package manager. Installing packages using yarn/pnpm will throw an error in this project. If you are using Yarn, you can add please-use-yarn instead.

These measures will help ensure compatibility and stability at scale as your Next.js project grows and changes.

Setting up Git version control

At this point, you have set up some basic boilerplate code for Next.js along with some configuration. Now would be a good time to add a version control system to work with fellow team members on this project.

A version control repository is important for any project, but especially crucial for projects expected to scale. Developers can easily track changes, manage project versions, and revert to previous versions when needed, making it easier to collaborate with team members and maximize application uptime.

We will be using GitHub for this project, but you can also opt for GitLab, Bitbucket, or other platforms to host your project’s version control repository according to your needs.

First, make sure you have the GitHub SHA key added to your local machine. Then, create an empty repository on GitHub and name it nextjs-architecture.

Over 200k developers use LogRocket to create better digital experiencesLearn more →

Now, go to your VS Code terminal and push changes to that empty repo using the following command:

git add .git commit -m "initial commit"

For the very first commit, you might need to push it to the remote repository like so:

git remote add origin [emailprotected]:<YOUR_GITHUB_USERNAME>/nextjs-architecture.gitgit branch -M maingit push -u origin main

Before you push a commit, ensure you have the .gitignore file added at the root level in your project. Next.js by default provides one with the boilerplate code.

The .gitignore file ensures certain folders, such as node_modules or files containing your security keys or environment variables, don’t accidentally get pushed to the repo. These components get generated on the fly when the app is pushed and deployed to a hosting server in the production environment.

You can also copy the above three commands from GitHub itself once you initialize an empty repo. To check if changes have been pushed successfully, go to your GitHub repo and refresh the page. You should be able to see your changes there.

Engine locking

It’s always a good idea to implement engine locking to ensure your project uses the same version across different environments. This will make your project less error-prone and ensure that it works as expected in both the development and production environments.

Engine locking is more of an npm feature than a Next.js feature, and it works across all npm projects. To enable this feature, all you have to do is specify a key called engines in your package.json file:

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

"18.x" represents any Node version starting with 18.

You also have to create a config file for npm called .npmrc at the root level — the same level as package.json — and add this line:

engine-strict=true

This will make the Node project strict on the version that you specified earlier in the package.json file and will throw an error if there is a version mismatch on either the local or the production environment while installing.

Engine locking is a good practice to follow especially when working with a large team and having different environments for testing and production.

Enforcing code formatting rules

Code formatting is very important for maintaining code consistency in your project as it scales. You can enforce a strict set of code formatting rules for team members working on the same project, but on different branches or modules.

To achieve this, first, add eslint to your project. Luckily, in our case, Next.js comes with built-in support for ESLint, so you simply need to configure it.

Check for a .eslintrc.json file at the very root level of your project. This file allows you to write eslint rules in key-value pairs.

You can choose from hundreds of rules and add as many rule sets as you want or need in your project. For example, add the following code to this file:

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

In the example above, React has been added as a global package for this project. By doing so, you are ensuring that React is always defined in functional components and JSX code, even if you haven’t explicitly mentioned import React from 'react' at the top of the file.

The second rule added here — no-unused-vars — will warn you if you have a variable defined in your file that is not being used anywhere across the app. This rule can help you with removing unnecessary variable declaration, which happens a lot in team projects.

ESLint does its job pretty well, but when paired with Prettier, it can be even more powerful, providing a consistent coding format for all team members across the organization. You can achieve this by installing the prettier package to the project like so:

npm i prettier -D

Once the installation has been finished, create two files at the root level — the same level as the eslintrc.json file. These files should be named .prettierrc and .prettierignore.

The .prettierrc file will contain all the Prettier rules that you are introducing in the project. The following code demonstrates a few rules you can add as JSON key-value pairs:

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

The .prettierignore file will contain the names of those files and folders that you do not want Prettier to run and analyze. For example, you would never want to run Prettier on the node_modules folder, dist folder, package.json, and other such files. Therefore, add paths to these files in .prettierignore like so:

distnode_modulespackage.json

Now, try changing anything in your code from a single quotation mark to a double quotation mark, as in the following example:

'Hello' => "Hello"

If you run npm run prettier --write and everything runs correctly, you will see Prettier has formatted all your files — changing double quotation marks into single quotation marks again — because of the rule you defined earlier in the .prettierrc file.

Running this command every time can be cumbersome, so it’s better to put this in your package.json file as a script:

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

Now, all you have to do is type npm run prettier. You can also configure your VS Code to run Prettier whenever you hit Cmd + S.

With all this set up, now would be a good time to commit changes to your repo. Make sure to follow proper naming conventions while committing changes. Conventional Commits provides a helpful resource you can follow while handling Git naming conventions.

Structuring your Next.js directory

You can now dive into deciding how you want to architect your application code. Neither React nor Next.js have, in general, an opinion as to how you should structure your app. However, since Next.js has file-based routing, you should structure your app similarly.

You can begin by creating a directory structure as follows:

src > > app > components > utils > hooks

The app folder will be responsible for creating file-based routing in this application.

The components folder can be used to create React-based component files, such as card components, sliders, tabs, and more.

The utils folder can be used for a variety of things, such as reusable library instances, some dummy data, or reusable utility functions.

Finally, in the hooks folder, you can create any custom React Hooks that you might need and which React doesn’t provide out of the box.

Note that creating all the subfolders inside the src folder is optional. You can skip this altogether and instead add all the subfolders as folders in the root location. While installing a fresh Next.js app, it does ask you if you want a src based structure — this decision is up to your personal preference.

Setting up a custom layout

In the App Router in Next.js v13 or greater, you already have out-of-the-box support for a layout file. You might already have a file called layout.tsx under src > app > layout.tsx. This is similar to the _document.tsx that was present in the pages directory in earlier Next.js versions.

You can put any components that will be shared across the application — such as Navbar, Footer, and others — in this layout.tsx file. You can also put your global fonts or even wrap the entire app with a provider if you’re using a third-party state management library such as TanStack Query:

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> );}

If you are still on Next.js v12, then you can follow a trick to create a similar layout structure. Create a Layout.tsx file that wraps the children component:

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

Then, import the Layout /> component at the root level — i.e., index.tsx or _app.tsx — in your app:

<Layout> <App /></Layout>

This Layout file would behave similarly and import your shared components to all your routes.

Integrating Next.js with Storybook

Next.js pairs nicely with the Storybook library, an essential part of building a modern web application. It’s perfect for when you want to visualize a component based on different props and states.

In this project, you can install Storybook like so:

npx sb init --builder webpack5

Based on your project version, you might need to install webpack 5 as a dependency as well.
After a successful installation, you will see a storybook folder and a stories folder.

Before you run your stories, you need to tweak .eslintrc.json to allow it to read Storybook as a plugin:

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

There are a few known issues you might encounter with your Storybook and Next.js integration. You can add the following code in your package.json file as a workaround for these bugs:

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

Make sure to check the docs for any updates — depending on when you’re reading this tutorial, this bug might have been fixed already, so you might not need to do this.

Add the following to the main.js file under the .storybook folder as well:

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

Lastly, you can add npm commands to the package.json file to run your stories quickly:

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

If everything is correct, you can now run stories by running npm run storybook. This command will open port number 6006, where you should then see your demo stories:

How to structure scalable Next.js project architecture - LogRocket Blog (5)

You can now begin creating individual React components under the components folder, along with subsequent stories to visualize. For example:

> components > Button > Button.tsx > Button.stories.tsx > button.modules.css

React components such as these can be tested, visualized, and integrated further into your application under the pages folder.

Setting up a pre-commit hook

As your codebase grows, you may have many developers working on it simultaneously. In such cases, it’s of the utmost importance to enforce a set of rules such as code formatting, single quote vs double quote, linting rules, and more to ensure consistency, streamline collaboration, and prevent errors.

This is exactly what a pre-commit hook does: it sets up some rules that will run before you commit your changes to Git. You can have a set of config files that will format your codebase and then only you will be able to commit your changes.

To set up a pre-commit hook with Next.js, you need the following tools:

  1. ESLint
  2. Husky
  3. lint-staged
  4. Prettier

Next.js already comes with ESLint, and Prettier includes industry best practices. To override them, you can run the following command:

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

Make sure you have prettier mentioned in the .eslintrc config file as the last plugin in the array. Your eslint config file could be either .eslintrc or eslintrc.json based on your preference. The file could look something like this:

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

Here, you can see a couple of basic rules that will run against your codebase as a pre-commit hook and implement the required changes. Based on your team size and preference, you can create a config file for Prettier like so:

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

The sample rules are pretty self-explanatory — you can add your own based on your needs and preferences.

Next, you can go ahead and install Husky, which will manage the pre-commit hooks:

npm i --dev husky

Now you can go ahead and install the lint-staged file, which manages the linting for your changes. Create a .lintstagedrc.js file and add the following snippet:

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(' ')}`};

If you look closely, this module is looking for TypeScript and JavaScript files and prettifying them as mentioned in your .prettierrc.js configuration. This is the check that runs as a pre-commit — before committing to Git.

After setting up, there is one last piece that you need to do: set up a shell file that will run Husky. Create a file at the root as /.husky/pre-commit and add the following code:

#!/bin/sh. "$(dirname "$0")/_/husky.sh"npm run lint-staged

This sets up your pre-commit. Now, try changing anything in your app to something wrong and committing the changes. Your pre-commit should run and show you an error indicating the line number and file.

Pre-commit hooks are a great practice if you work with a big team. They help you manage formatting and linting rules, as well as make sure everybody is on the same page. Without pre-commit hooks, you may run into many merge conflicts, especially since linters and code formatters work differently on different machines.

Leveraging Git branching strategies

Git branching strategies are crucial for effective collaboration and a must for large-scale projects. There are different strategies that you can pick for your project, such as:

Gitflow is the most widely adopted branching strategy. It uses feature-driven branches and multiple primary branches. It’s perfect for projects that have scheduled releases. Here’s a diagram from the Atlassian docs showing how the Gitflow branching strategy works:

How to structure scalable Next.js project architecture - LogRocket Blog (6)

Here, the integration branches have the recorded history of the main branch. It serves as a feature branch, which can later be merged for releases.

The Gitflow strategy also uses the hotfix naming convention to further branch out from the feature branch for immediate or priority fixes. The flexibility of Gitflow has made it immensely popular among real-world projects.

In recent years, the trunk-based development workflow is gaining traction as well. Instead of having, for example, many different feature branches or hotfix branches, trunk flow focuses on one main branch only.

This strategy assumes the main branch is always stable and developers can pull from the main branch, work on smaller iterations, and eventually merge with the main branch. It reduces the pain point that comes with the Gitflow strategy of too many merge conflicts due to its exhausting branch strategy.

At the end of the day, the best branching strategy is the one that suits your project and the team in general, making Git collaboration more efficient.

Deploying your Next.js application

After you have pushed everything to GitHub, the final step would be deploying the Next.js application. You can choose any hosting provider, but we will be using Vercel in this demo project, as it’s the most straightforward option for Jamstack applications.

Sign up for Vercel and set up your account as a Hobby. Make sure to sign up using the GitHub account where you have pushed your project. Once that is done, choose your nextjs-architecture project from the Vercel dashboard.

Fill in all details as directed, such as:

  • Project name: nextjs-architecture
  • Build Command: npm run build
  • Install Command: npm install
  • Root Directory: ./

That concludes our demo project! You can check out the complete code on GitHub.

Conclusion

You have now set everything up in this Next.js app to make it easy to scale as your project continues to grow. You can further improve on this architecture by using Git workflow strategies for multiple teams, such as the Git branching strategy we discussed above.

As the Next.js team continues to ship a better version each year, you will likely see more interesting web architecture ideas.

LogRocket: Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next.js app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — start monitoring for free.

How to structure scalable Next.js project architecture - LogRocket Blog (2024)

References

Top Articles
Latest Posts
Article information

Author: Amb. Frankie Simonis

Last Updated:

Views: 5674

Rating: 4.6 / 5 (56 voted)

Reviews: 87% of readers found this page helpful

Author information

Name: Amb. Frankie Simonis

Birthday: 1998-02-19

Address: 64841 Delmar Isle, North Wiley, OR 74073

Phone: +17844167847676

Job: Forward IT Agent

Hobby: LARPing, Kitesurfing, Sewing, Digital arts, Sand art, Gardening, Dance

Introduction: My name is Amb. Frankie Simonis, I am a hilarious, enchanting, energetic, cooperative, innocent, cute, joyous person who loves writing and wants to share my knowledge and understanding with you.