Learn how to build a monorepo in Next.js

mangelosanto

Matt Angelosanto

Posted on December 8, 2022

Learn how to build a monorepo in Next.js

Written by Oghenetega Denedo✏️

Monorepo architecture has become a very popular trend among devs creating modern web apps.

Although the practice isn’t new by any means, large companies like Google and Microsoft have utilized monorepos for a long time to manage software at scale, and it’s also used in popular open-source projects like React, Next.js, Jest, Babel, and many more.

In this article, we will discuss what monorepos are and the tools you need for working with them.

We’ll also explore how you can build a monorepo for a Next.js project with a sample use case to follow along with.

Jump ahead:

What is a monorepo?

A monorepo is a single version-controlled repository that contains several isolated projects with well-defined relationships.

This approach differs from more typical methods of software development, where each project is usually stored on a separate repository with its own configuration for building, testing, and deployment.

Monorepo Vs. Multirepo

Benefits of using a monorepo in Next.js

There are some key factors that have caused this shift in direction regarding how software projects are structured (particularly those with large codebases).

These factors are:

Simple code sharing

Applications built in a monorepo can easily share reusable code and configurations, since they share the same repository.

In contrast to a polyrepo, this means that there is reduced code duplication, ensuring faster development and ease of maintenance.

Furthermore, developers don’t have to go through the difficult process of publishing packages and resolving incompatibilities with projects that rely on them.

Atomic commits

Large-scale changes affecting multiple applications can be made in a single commit while ensuring the application works as expected before committing changes.

An excellent example of an atomic commit is when a breaking change is made to a shared library that’s used by several apps, thereby forcing the developer to ensure the apps that depend on it are updated to be compatible with the recent change.

Consistency

Monorepos offer better consistency than polyrepos, since the codebase is all in one place and each project can easily share the same coding style and tools for testing, deployment, and code maintenance.

Monorepo tools

Monorepos are very useful for managing projects, but to get the most out of them, you need to work with the right tools to ensure your development workflow is fast and effective.

Available monorepo tools vary based on their features, language support, and barrier to entry in terms of the expertise required to use them.

The following is a list of monorepo tools for working with JavaScript/TypeScript codebases:

  • Turborepo: Smart build system for JavaScript/TypeScript monorepos
  • Nx: Next-generation build system with first-class monorepo support and powerful integrations
  • Bazel: Fast, scalable, multi-language, and extensible build system
  • Lerna: Fast and modern build system for managing and publishing multiple JavaScript/TypeScript packages from the same repository

Turborepo is the tool of choice for this tutorial. It’s an easy-to-use, fast, and effective build system for TypeScript/JavaScript codebases.

N.B., if you want to see what else can be done with Turborepo, we have another tutorial focused on building a full-stack TypeScript monorepo

Turborepo is built on workspaces, a feature supported by Yarn, npm, and pnpm for managing multiple packages within a top-level root package.

Turborepo ships with the following features that make working with monorepos easy:

  • Incremental builds: This ensures builds are executed when there are changes in a workspace to prevent unnecessary computations
  • Parallel execution: Executes tasks in parallel while maximizing the use of every available core in the CPU to ensure quick executions
  • Remote caching: This is an impressive feature Turborepo supports that allows you to share the cache of task executions with your dev team and CI/CD server to cut down execution time
  • Dependency graph visualization: This supports generating a graph of a task execution plan to give a high-level view of the steps taken to run it

Building the monorepo

For this tutorial, we’ll build a monorepo for a sample ecommerce application that’s made up of two independent Next.js apps: an admin and a store.

We’ll also cover how to leverage the significant benefits monorepos provide. These are primarily the following:

  • Code sharing; to make a reusable component library that can be used by both Next.js apps
  • A shared configuration package for linting and formatting

Project setup

In your terminal, enter the following command to create a new directory for the project and set up the package.json:

mkdir nextjs-monorepo 
cd nextjs-monorepo 
yarn init -y
Enter fullscreen mode Exit fullscreen mode

This is the first step in building the monorepo; now we must set up the project’s workspaces.

Creating workspaces

Earlier in this article, I mentioned that Turborepo is built on workspaces — all packages and apps in the monorepo will be stored on a separate workspace of their own.

Open the package.json file at the root of the project and insert the code below:

{
  "name": "nextjs-monorepo",
  "private": true,
  "version": "1.0.0",
  "workspaces": [
    "apps/*",
    "packages/config/*",
    "packages/shared/*"
  ],
  "engines": {
    "node": ">=14.0.0"
  },
  "packageManager": "yarn@1.22.17"
}
Enter fullscreen mode Exit fullscreen mode

The workspaces field in the package.json file is an array of paths that tells the package manager where our workspaces are located.

apps/* is for all the independent Next.js applications; packages/config/* stores reusable packages for linting and formatting; and packages/shared/* contains reusable code that is used by projects in app/ — this is where the UI component library will be stored.

Setting up the Next.js applications

At the root of the project, create a new folder, apps/, to store the Next.js apps we’re going to set up:

mkdir apps
cd apps
Enter fullscreen mode Exit fullscreen mode

Next, let’s add the admin and store applications:

yarn create next-app admin 
yarn create next-app store
Enter fullscreen mode Exit fullscreen mode

When it's installed, open the package.json file of the admin application, located at apps/admin/package.json. Then, replace the value of the dev script with the next dev — port 3001 — so it can run on a different port.

Once that’s done, run the development server for both projects with yarn dev to ensure everything works properly.

In the apps/admin/pages/index.js file, insert the following code:

export default function Home() {
  return (
    <div>
      <h1>Admin</h1>
      <button>Click Me!</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

We will do the same in the apps/store/pages/index.js file, so insert the following code once again:

export default function Home() {
  return (
    <div>
      <h1>Store</h1>
      <button>Click Me!</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now, we’ve completed the basic setup necessary for both Next.js apps. In the next section, we’ll set up Turborepo for running our development tasks.

Setting up Turborepo

Workspaces and tasks are the building blocks of a monorepo.

Package managers like Yarn and npm work well for installing packages and configuring workspaces, but they aren’t optimized for running tasks in a complex project setup like a monorepo, and this is where Turborepo shines.

Installing Turborepo

Let’s start by installing Turborepo for our project. At the root of the monorepo, run the following script:

yarn add turborepo -DW
Enter fullscreen mode Exit fullscreen mode

Once the installation is complete, create a new file, turbo.json, at the root of the monorepo to store the configuration required for Turborepo to work. Then, enter the following code:

{
  "$schema": "https://turborepo.org/schema.json",
}
Enter fullscreen mode Exit fullscreen mode

Running tasks

Let’s configure Turborepo to run the Next.js applications in apps/. Open the turbo.json file and enter the code below:

{
  "$schema": "https://turborepo.org/schema.json",
  "pipeline": {
    "dev": {
      "cache": false
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Let’s take a moment to examine the contents of the turbo.json file:

  • The pipeline field defines the tasks that Turborepo will run on the monorepo; every property in the pipeline object is a task that corresponds to a script in the package.json file of a workspace
  • The dev field inside the pipeline object defines a workspace’s dev task; "cache": false tells Turborepo not to cache the results of this task

N.B., Turborepo will only run tasks that are defined in the scripts section of the workspace’s package.json file

We’ll need to define a script in the scripts field of the package.json file at the root of the monorepo to run the dev server of the Next.js applications.

Insert the following code in the package.json file at the root of the monorepo:

{
  "scripts": {
    "dev": "turbo run dev --parallel"
  }
}
Enter fullscreen mode Exit fullscreen mode

The --parallel flag tells Turborepo to run the dev task of the workspaces in parallel.

Enter yarn dev in your terminal at the root of the monorepo to start the development server for the Next.js applications.

If that was successful, you should have an output similar to the image below:

Turborepo Output

Now that Turborepo is up and running, the next step is to set up a reusable configuration package for linting and formatting.

Linting and formatting

Monorepos enable the use of a unified code standard for all projects within it to ensure consistency throughout the codebase.

An automated code linting and formatting tool like ESLint can be configured to extend a shared configuration that every workspace in the project can use.

Set up ESLint

We’ll need to create a new workspace for the shared ESLint config package that will be used across the workspaces in apps/.

Enter the following script to create a new workspace for the ESLint config package:

mkdir -p packages/config/eslint-config-custom 
cd packages/config/eslint-config-custom    
Enter fullscreen mode Exit fullscreen mode

Create a package.json file in packages/config/eslint-config-custom and insert the following code:

{
  "name": "eslint-config-custom",
  "version": "1.0.0",
  "main": "index.js",
}    
Enter fullscreen mode Exit fullscreen mode

"main": "index.js" specifies the entry point of this package and index.js contains the ESLint configuration that will be imported by the modules that will use it.

Install ESLint and the plugins relevant to this project with the following:

yarn add eslint eslint-config-next eslint-config-prettier eslint-config-react eslint-config-turbo
Enter fullscreen mode Exit fullscreen mode

Create a new file, index.js, in packages/config/eslint-config-custom and enter the following code:

module.exports = {
  extends: ["next", "turbo", "prettier"],
};
Enter fullscreen mode Exit fullscreen mode

Now we’re done with setting up the reusable ESLint configuration package for this project, the next step is using it in our Next.js apps.

To use the eslint-config-custom package in the admin and store workspaces, we’ll need to add it as a dependency.

In the package.json file of the admin and store workspaces, remove every ESLint package and plugin and insert the following code:

{
  "devDependencies": {
   "eslint-config-custom": "*"
  }    
}
Enter fullscreen mode Exit fullscreen mode

Update the .eslintrc.json file in the apps/store and apps/admin workspaces with the following code:

{
  "root": true,
  "extends": ["custom"] // Tells ESLint to use the "eslint-config-custom" package
}
Enter fullscreen mode Exit fullscreen mode

Finally, run yarn install at the root of the monorepo to update the dependencies in the node_modules folder.

If you followed the previous steps correctly, you should find the local eslint-config-custom package in the root node_modules folder.

Eslint-config-custom Package

Running linting and formatting tasks

Before we start running tasks for linting and formatting, we’ll need to add the necessary scripts in the package.json file of the admin and store apps.

Open the package.json file of the admin and store apps and insert the following in the scripts field:

{
  "lint": "eslint .",
  "format": "eslint --fix --ext .js,.jsx ."
}
Enter fullscreen mode Exit fullscreen mode

Next, we’ll need to create the tasks for linting and formatting the workspaces in the monorepo. In the turbo.json file at the root of the monorepo, add the following code in the pipeline field:

{
  "lint": {
    "outputs": []
  },
  "format": {
    "outputs": []
  }  
}
Enter fullscreen mode Exit fullscreen mode

The outputs field in the lint and format tasks stores an array of globs — any file that matches the pattern of the glob is treated as an artifact that will be cached.

The value of the output in the lint and format tasks is set to an empty array, which tells Turborepo to cache the logs to stdout and stderr of this task. As a result, whenever this task is re-run and there are no changes in the workspace, Turborepo replays the cached logs, which means the execution time of a task is very fast.

To run the new tasks, we’ll need to update the scripts field of the package.json file at the root of the monorepo with the following code:

{
  "lint": "turbo run lint",
  "format": "turbo run format"  
}
Enter fullscreen mode Exit fullscreen mode

Once that’s complete, you can now run the lint and format tasks by entering the following command:

yarn lint
yarn format
Enter fullscreen mode Exit fullscreen mode

Here’s a sample output of the execution of yarn lint:

Yarn-lint Output

Building a reusable component library

In modern frontend development, components are the building blocks of every application, irrespective of the size of the project.

The breaking down of complex UIs into reusable components and their abstraction to a shared component library is a standard development practice today — it makes codebases easier to maintain while still following software development best practices like DRY.

We will build our own reusable component library that projects in apps/ can make use of. To get started, we’ll need to create a new workspace.

Enter the following command at the root of the monorepo to create a new workspace for the component library:

mkdir -p packages/shared/ui
cd packages/shared/ui
Enter fullscreen mode Exit fullscreen mode

In packages/shared/ui, create a new package.json file and insert the following:

{
  "name": "ui",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "lint": "eslint .",
    "format": "eslint --fix --ext .js,.jsx ."
  },
  "devDependencies": {
    "eslint": "^7.32.0",
    "eslint-config-custom": "*",
    "react": "^18.2.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Next, let’s create a reusable button component that can be used by the Next.js applications. In the packages/shared/ui workspace, create a new file, Button.jsx, and enter the following code:

import * as React from "react";

export const Button = ({ children }) => {
  return <button>{children}</button>;
};
Enter fullscreen mode Exit fullscreen mode

Create a new file, index.js, that’ll serve as the entry point to this package and export the individual React components.

Add the following code to the index.js file:

import { Button } from "./Button.jsx";
export { Button };
Enter fullscreen mode Exit fullscreen mode

To use the Button component in our Next.js apps, we’ll need to add the ui package as a dependency in the workspace’s package.json file.

Add the ui package by inserting the following code in the dependencies field within the package.json file of the admin and store workspaces, respectively:

{
  "dependencies": {
    "ui": "*"
  }
}
Enter fullscreen mode Exit fullscreen mode

Once that’s complete, run yarn install to update the dependencies in the node_modules folder.

Next, in the pages/index.js file of the Next.js apps, replace the existing code with the following:

// apps/admin/pages/index.js
import { Button } from "ui";
export default function Home() {
  return (
    <div>
      <h1>Admin</h1>
      <Button>Click Me!</Button>
    </div>
  );
}

// apps/store/pages/index.js
import { Button } from "ui";
export default function Home() {
  return (
    <div>
      <h1>Store</h1>
      <Button>Click Me!</Button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Restart the development server, visit each application, and you will observe an error similar to the following image:

Error Signals

The reason for this error is that we haven’t configured our Next.js apps to handle the transpilation of local packages, like the ui package in packages/shared.

There’s a nice package on npm that solves this problem: next-transpile-modules. It enables the transpilation of local packages with a Next.js/Babel configuration.

Let’s install the next-transpile-modules package in the admin and store workspaces by entering the following command:

yarn workspace admin add -D next-transpile-modules
yarn workspace store add -D next-transpile-modules
Enter fullscreen mode Exit fullscreen mode

In the next.config.js file of the admin and store workspaces, enter the following code to use the next-transpile-modules package to transpile the component library package:

/** @type {import('next').NextConfig} */
const withTM = require("next-transpile-modules")(["ui"]);
module.exports = withTM({
  reactStrictMode: true,
  swcMinify: true,
});
Enter fullscreen mode Exit fullscreen mode

Since we’ve made changes to the next.config.js file, we’ll have to restart the development server for the changes to take effect. After restarting the server, navigate to localhost:3000 and the error should be resolved, with everything now working as expected.

Conclusion

Monorepos will continue to grow in popularity in the web development community because of their numerous benefits and the advancement of tools that make working with them easier for developers.

I hope you found this guide to building a monorepo in Next.js useful — let me know of your own experiences in the comments below.


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 signup

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.

💖 💪 🙅 🚩
mangelosanto
Matt Angelosanto

Posted on December 8, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related