Written by Abhinav Anshul✏️
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
- Ensuring compatibility and stability across teams
- Setting up version control
- Enforcing code formatting rules
- Structuring your Next.js directory
- Setting up a custom
document
file - Setting up a custom layout
- Integrating Next.js with Storybook
- Deploying your Next.js application
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:
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 v16:
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:
engine-strict=true
Now, go to the package.json
file and add a new key-value
pair:
"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, 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:
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 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, 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:
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:
'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:
> 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, 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.
Setting up a 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:
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
If you are using Next.js v13 or newer, you already have out-of-the-box support for a layout file. Just create a file called layout.tsx
under the app
folder and write the following code:
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:
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"
}
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:
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.
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 is the most straightforward for Jamstack applications.
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.
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 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: 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.
LogRocket 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.
Top comments (0)