How to structure scalable Next.js project architecture
Tags: nextjs, webdev
Published: 2023-05-09 13:33:28

**Written by [Abhinav Anshul](https://blog.logrocket.com/author/abhinavanshul/)✏️**

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, use of CSS modules for styling, 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. 

Jump ahead:

*   [Setting up a Next.js project](#setting-up-next-js-project)
*   [Ensuring compatibility and stability across teams](#ensuring-engine-compatibility-stability-scale)
*   [Setting up version control](#setting-up-version-control)
*   [Enforcing code formatting rules](#enforcing-code-formatting-rules)
*   [Structuring your Next.js directory](#structuring-next-js-directory)
*   [Setting up a custom `document` file](#setting-up-custom-document-file)
*   [Setting up a custom layout](#setting-up-custom-layout)
*   [Integrating Next.js with Storybook](#integrating-next-js-storybook)
*   [Deploying your Next.js application](#deploying-next-js-application)

You can [explore the final project on GitHub](https://github.com/abhinav-anshul/nextjs-architecture) or simply follow along as we build it in this tutorial.

## Setting up a Next.js project <a name="setting-up-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:

```bash
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](https://blog.logrocket.com/using-next-js-with-typescript/) 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](https://blog.logrocket.com/javascript-package-managers-compared/). 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:

```bash
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: ![Next Js Boilerplate Application Open To Welcome Screen In Dark Mode With Instructions And Links To Resources](http://blog.logrocket.com/wp-content/uploads/2023/04/Next-js-boilerplate-application-welcome-screen.png)

## Ensuring engine compatibility and stability at scale <a name="ensuring-engine-compatibility-stability-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 v16:

```bash
16.0.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:

```bash
engine-strict=true
```

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

```json
"engines": {
    "node": ">=16.0.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 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](https://blog.logrocket.com/version-management-changesets/), 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`. 

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

```bash
git add .
git commit -m "initial commit"
```

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

```bash
git remote add origin git@github.com:<YOUR_GITHUB_USERNAME>/nextjs-architecture.git
git branch -M main
git 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.

## 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](https://blog.logrocket.com/troubleshooting-next-js-app-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:

```json
{
  "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](https://blog.logrocket.com/linting-typescript-eslint-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:

```bash
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:

```javascript
{
  "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:

```javascript
dist
node_modules
package.json
```

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

```javascript
'Hello' =&gt; "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:

```json
 "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](https://www.conventionalcommits.org/en/v1.0.0/) you can follow while handling Git naming conventions.

## Structuring your Next.js directory <a name="structuring-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](https://blog.logrocket.com/implementing-ssr-next-js-dynamic-routing-prefetching/#creating-next-js-routes), you should structure your app similarly. 

You can begin by creating a directory structure as follows:

```javascript
> pages
> components
> utils
> hooks
```

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

The `components` folder can be used to [create React-based component files](https://blog.logrocket.com/shoelace-build-web-components-react/), 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](https://blog.logrocket.com/create-your-own-custom-react-hooks/) that you might need and which React doesn't provide out of the box.

## Setting up a custom `document` file <a name="setting-up-custom-document-file">

When you generate a Next.js boilerplate code, it utilizes a `document` file that is responsible for the actual script to run. When your application grows more complex, it is a good idea to override the `document` file with your own custom `document` file. 

To do so, just create a file called `_document.tsx` — including the underscore — under your `pages` directory and paste the following code:

```typescript
import Document, { Head, Html, Main, NextScript } from 'next/document';
class MyDocument extends Document {
  render() {
    return (
      <Html>
        <Head>
          <link rel="preconnect" href="https://fonts.googleapis.com"/>
          <link rel="preconnect" href="https://fonts.gstatic.com" />
         <link href="https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;700&display=swap" rel="stylesheet"/>
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}
export default MyDocument;
```

This custom document file is a good place to include links to font files, analytics SDK links, or any script that you think should be available globally such as `<Main />` and `<NextScript />`.

## Setting up a custom layout <a name="setting-up-custom-layout">

If you are using Next.js v13 or newer, you already have [out-of-the-box support for a layout file](https://blog.logrocket.com/next-js-13-app-directory/#page-layout-file). Just create a file called `layout.tsx` under the `app` folder and write the following code:

```typescript
export default function RootLayout({ children }: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
	<Navbar />
		{children}
	<Footer />
	</body>
    </html>
  );
}
```

This file is on top of all your routes, so you can put shared components in it — such as `Navbar` or `Footer` — that would appear in every route. 

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:

```typescript
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:

```typescript
<Layout>
  <App />
</Layout>
```

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

## Integrating Next.js with Storybook <a name="integrating-next-js-storybook">

[Next.js pairs nicely with the Storybook library](https://blog.logrocket.com/building-next-js-app-tailwind-storybook/), 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:

```bash
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:

```json
{
  "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:

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

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

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

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

```json
"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: ![Storybook Dashboard For Next Js Project Showing Demo Button](http://blog.logrocket.com/wp-content/uploads/2023/04/Next-js-Storybook-setup.png) 

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

```javascript
> 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.

## Deploying your Next.js application <a name="deploying-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 is the most straightforward for [Jamstack applications](https://blog.logrocket.com/tag/jamstack/). 

Sign up for Vercel and set 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: `./`

You can [check out the complete code on GitHub](https://github.com/abhinav-anshul/nextjs-architecture).

## Conclusion

You have now set everything up in this Next.js app that would make it easy to scale as your project continues to grow. You can even further improve on this architecture by [using Git branching strategies](https://www.flagship.io/git-branching-strategies/) for multiple teams, different environments for local and production testing, and more. 

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

---

## [LogRocket](https://logrocket.com/signup): Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try [LogRocket](https://logrocket.com/signup). 

[![LogRocket signup](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://logrocket.com/signup)

[LogRocket](https://logrocket.com/signup) is like a DVR for web and mobile apps, recording literally everything that happens on your Next 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](https://logrocket.com/signup).