🎉 I'm releasing 12 products in 12 months! If you love product, checkout my new blog workingoutloud.dev

Back to home

Next.js Enterprise Project Structure

During a course on Enterprise Architecture Patterns by Łukasz Ruebbelke, I was introduced to the idea of "The Iron Triangle of Programming".

The concept is around about three pillars of code that when left poorly managed tend towards programmer purgatory. These concepts are:

  1. Code volume: the larger the codebase, the more unmanageable it can become.
  2. State management: complex state management leads towards this aforementioned purgatory.
  3. Flow control: the less readable your conditional logic is, the more difficult it becomes to maintain.

It is undeniable that code bases will continue to grow, so we can set ourselves up for success with code volume by focusing on the project layout.

This post will display an overview of an opinionated project structure and how to set it up for Next.js. This code structure is one that has made it more manageable for me to separate application code from configuration as well as infrastructure code when maintaining a large, mono-repo.

Source code can be found on my okeeffed/nextjs-enterprise-project-structure GitHub repo.

Prerequisites

  1. Basic familiarity with Create Next App.
  2. Basic familiarity with Next.js.
  3. Basic familiarity with TypeScript.

Getting started

We will let create-next-app create the project directory nextjs-enterprise-project-structure for us:

$ npx create-next-app@latest --ts nextjs-enterprise-project-structure # ... creates Next.js app for us $ cd nextjs-enterprise-project-structure

The ts flag will enable TypeScript support.

At this stage, a working Next.js app is ready for us. We can check this by running npm run dev and checking the app runs on localhost.

Opting for a src directory

By default, using the tree command, we can see that our project structure looks like the following:

# Outputs the tree ignoring node_modules and .git. $ tree -I "node_modules|.git" . ├── README.md ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages │ ├── _app.tsx │ ├── api │ │ └── hello.ts │ └── index.tsx ├── public │ ├── favicon.ico │ └── vercel.svg ├── styles │ ├── Home.module.css │ └── globals.css └── tsconfig.json

The first change that we are aiming to make to opt to use a src folder to host our Next.js code.

Using src is documented under the Next.js src directory documentation.

The aim of this is to separate our application code from our top-level configuration.

If you have written a large Next.js project before, you'll notice that the top-level folders start to become unruly as you add more and more top-level folders to separate our application concerns.

Placing them within src enables us to keep our top-level folders clean and organized as well as have a mutual understanding across developers about where code for the application belongs.

For this project, we can move pages and styles into src.

# Create `src` folder $ mkdir src # Move desired folders into `src` $ mv pages styles src

If we now run npm run dev again, then you'll see that without changing our configuration more than moving the pages folder, then the Next.js config will know where to find our project entry point (as per the documentation).

Note: as the documentation says, configuration files should not be moved to src nor the public folder.

At this stage, if we re-run our tree command the project structure will look like the following:

# Output updated structure $ tree -I "node_modules|.git" . ├── README.md ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── vercel.svg ├── src │ ├── pages │ │ ├── _app.tsx │ │ ├── api │ │ │ └── hello.ts │ │ └── index.tsx │ └── styles │ ├── Home.module.css │ └── globals.css └── tsconfig.json

Breaking up our src folder

Now that we have our src folder, it is worth breaking things down even further into logical pieces.

In general, the folder structure that I generally opt for with larger projects follows the concept of Colocation but still using the design principle Separation of Concerns as a guideline.

For my larger projects, this normally means that my src folder is split into four folders:

  1. pages: This is still used for my pages layout (and is required for Next.js, so it ain't going no where).
  2. common: This folder consists of common code that I use across my application. I will speak more to this in a latter section but it generally contains most of the code.
  3. modules: "Modules" for my is synonymous to features or logical groupings of code that make up the larger pages.
  4. content: This itself can be optional or moved to elsewhere, but it is generally where I put my copy, internationalization and images etc. that I do not keep public.

The common folder itself generally becomes the most complex. Within that, I also define a number of different folders based on the project I am running:

For example, a current larger project I am working on currently has the following folders within common:

$ tree src/common -L 1 src/common ├── components ├── context ├── hooks ├── mocks ├── queries ├── styles ├── types └── utils

Within components again, I generally break down my components into three areas:

  1. application: components that pertain to only being used within the application. This includes things like login components, page shells, etc.
  2. marketing: any component that is generally used for marketing purposes like pricing tables, etc.
  3. ecommerce: all components related to e-commerce work. Think carts, checkout, etc.

It is entirely up to you how you structure your common folder, but the same principle applies of colocating when directly related or following the separation of concerns.

Once this has been set up (and if you are using TypeScript), the last change that I make to the initial setup is to use TS configuration paths.

Setting up TypeScript configuration paths

Within tsconfig.json, you can set the compiler option with baseUrl to be . and then set a paths array to help with the import statements.

My personal rule of thumb is to set a path for modules and content and then for each of the folders within common:

{ "compilerOptions": { "baseUrl": ".", "paths": { "@components/*": ["src/common/components/*"], "@styles/*": ["src/common/styles/*"], "@modules/*": ["src/modules/*"], "@content/*": ["src/content/*"] } // ... omitted for brevity } // ... omitted for brevity }

This now means that if we have a named export component available in src/common/components/application/my-component.tsx, we can import it as import { MyComponent } from '@components/application/my-component'.

To see some of this structure in action, let's update the default Next.js app to reflect some of the concepts we've spoken about.

Breaking the default app up

At this point, we want our src to have the following structure:

$ tree src src ├── common │   ├── components │   │   └── marketing │   │   ├── Card │   │   │   ├── Card.module.css │   │   │   ├── Card.tsx │   │   │   └── index.ts │   │   ├── Footer │   │   │   ├── Footer.module.css │   │   │   ├── Footer.tsx │   │   │   └── index.ts │   │   └── SimpleGrid │   │   ├── SimpleGrid.module.css │   │   ├── SimpleGrid.tsx │   │   └── index.ts │   └── styles │   └── globals.css ├── modules │   └── home │   ├── Home.module.css │   ├── Home.tsx │   └── index.ts └── pages ├── _app.tsx ├── api │   └── hello.ts └── index.tsx

Within common you will need to create a components folder and a styles folder and within modules we need to create a home folder.

Given that everything displayed is what I would consider "marketing" content, I will create a marketing folder within common and within that, I will create a Card, Footer and SimpleGrid component to reflect some of the "re-useable" components that I identified within the home page.

You could possibly go further with the "title" and "description" of the home page, but this will be enough for now.

Below will go into depth of each file we create and what to add to it.

Note that I personally opt to use barrels and enforce it with ESLint rules, but you may argue it goes against the concept of Code Volume that we spoke about earlier with the Iron Triangle of Programming.

src/common/components/marketing/Card

The following files and content were created.

Card.module.css:

.card { margin: 1rem; padding: 1.5rem; text-align: left; color: inherit; text-decoration: none; border: 1px solid #eaeaea; border-radius: 10px; transition: color 0.15s ease, border-color 0.15s ease; max-width: 300px; } .card:hover, .card:focus, .card:active { color: #0070f3; border-color: #0070f3; } .card h2 { margin: 0 0 1rem 0; font-size: 1.5rem; } .card p { margin: 0; font-size: 1.25rem; line-height: 1.5; }

Card.tsx:

import * as React from "react"; import styles from "./Card.module.css"; interface CardProps { href: string; title: string; body: string; } export const Card: React.FC<CardProps> = ({ href, title, body }) => { return ( <a href={href} className={styles.card}> <h2>{title}</h2> <p>{body}</p> </a> ); };

index.ts:

export { Card } from "./Card";

Footer.module.css:

.footer { display: flex; flex: 1; padding: 2rem 0; border-top: 1px solid #eaeaea; justify-content: center; align-items: center; } .footer a { display: flex; justify-content: center; align-items: center; flex-grow: 1; } .logo { height: 1em; margin-left: 0.5rem; }

Footer.tsx:

import * as React from "react"; import Image from "next/image"; import styles from "./Footer.module.css"; export const Footer: React.FC = () => { return ( <footer className={styles.footer}> <a href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app" target="_blank" rel="noopener noreferrer" > Powered by{" "} <span className={styles.logo}> <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} /> </span> </a> </footer> ); };

index.ts:

export { Footer } from "./Footer";

src/common/components/marketing/SimpleGrid

SimpleGrid.module.css:

.grid { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; max-width: 800px; } @media (max-width: 600px) { .grid { width: 100%; flex-direction: column; } }

SimpleGrid.tsx:

import * as React from "react"; import styles from "./SimpleGrid.module.css"; export const SimpleGrid: React.FC = ({ children }) => { return <div className={styles.grid}>{children}</div>; };

index.ts:

export { SimpleGrid } from "./SimpleGrid";

src/common/styles

globals.css:

html, body { padding: 0; margin: 0; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; } a { color: inherit; text-decoration: none; } * { box-sizing: border-box; }

src/common/components/modules/Home

Home.module.css:

.main { min-height: 100vh; padding: 4rem 0; flex: 1; display: flex; flex-direction: column; justify-content: center; align-items: center; } .title a { color: #0070f3; text-decoration: none; } .title a:hover, .title a:focus, .title a:active { text-decoration: underline; } .title { margin: 0; line-height: 1.15; font-size: 4rem; } .title, .description { text-align: center; } .description { margin: 4rem 0; line-height: 1.5; font-size: 1.5rem; } .code { background: #fafafa; border-radius: 5px; padding: 0.75rem; font-size: 1.1rem; font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace; }

Home.tsx:

import * as React from "react"; import { Card } from "@components/marketing/Card"; import { SimpleGrid } from "@components/marketing/SimpleGrid"; import styles from "./Home.module.css"; export const Home: React.FC = () => { return ( <main className={styles.main}> <h1 className={styles.title}> Welcome to <a href="https://nextjs.org">Next.js!</a> </h1> <p className={styles.description}> Get started by editing{" "} <code className={styles.code}>pages/index.tsx</code> </p> <SimpleGrid> <Card title="Documentation" body="Find in-depth information about Next.js features and API." href="https://nextjs.org/docs" /> <Card title="Learn &rarr;" body="Learn about Next.js in an interactive course with quizzes!" href="https://nextjs.org/learn" /> <Card title="Examples" body="Discover and deploy boilerplate example Next.js projects." href="https://github.com/vercel/next.js/tree/master/examples" /> <Card title="Deploy &rarr;" body="Instantly deploy your Next.js site to a public URL with Vercel." href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app" /> </SimpleGrid> </main> ); };

index.ts:

export { Home } from "./Home";

src/pages

index.tsx:

import type { NextPage } from "next"; import Head from "next/head"; import { Home } from "@modules/home"; import { Footer } from "@components/marketing/Footer"; const IndexPage: NextPage = () => { return ( <div style={{ padding: "0 2rem", }} > <Head> <title>Create Next App</title> <meta name="description" content="Generated by create next app" /> <link rel="icon" href="/favicon.ico" /> </Head> <Home /> <Footer /> </div> ); }; export default IndexPage;

As for the _app.tsx file, we need to update the styles import:

import "@styles/globals.css"; import type { AppProps } from "next/app"; function MyApp({ Component, pageProps }: AppProps) { return <Component {...pageProps} />; } export default MyApp;

Running the app one last time

At this stage, if you now run the app again, you will see no changes to the default app, but we now have an example of the default app working in a format that follows some of the opinionated conventions mentioned above.

Summary

Today's post demonstrated an opinionated approach to a Next.js project structure that can scale.

Although we used a contrived example with the default application, you can see how the files begin to break up and can be reused.

It is important to note that the example app did not break up things into pieces such as i18n for content, nor displayed how the configuration works at the top-level as you add more and more to the mono-repo (things such as infrastructure-as-code or dev-ops files).

The larger the app becomes, the more a layout like the one displayed becomes useful!

The final code can be found at okeeffed/nextjs-enterprise-project-structure GitHub repo.

Resources and further reading

  1. Basic familiarity with Create Next App.
  2. Basic familiarity with Next.js.
  3. Basic familiarity with TypeScript
  4. okeeffed/nextjs-enterprise-project-structure GitHub repo

Photo credit: lanceanderson

Personal image

Dennis O'Keeffe

@dennisokeeffe92
  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on Visibuild.

1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.

Next.js Enterprise Project Structure

Introduction

Share this post