The concept is around about three pillars of code that when left poorly managed tend towards programmer purgatory. These concepts are:
Code volume: the larger the codebase, the more unmanageable it can become.
State management: complex state management leads towards this aforementioned purgatory.
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.
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.
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:
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:
pages: This is still used for my pages layout (and is required for Next.js, so it ain't going no where).
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.
modules: "Modules" for my is synonymous to features or logical groupings of code that make up the larger pages.
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:
Within components again, I generally break down my components into three areas:
application: components that pertain to only being used within the application. This includes things like login components, page shells, etc.
marketing: any component that is generally used for marketing purposes like pricing tables, etc.
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:
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:
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.
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 →"
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 →"
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!