Next.js & Monorepos: A Beginner’s Guide to Scalable Development

In the ever-evolving world of web development, managing large-scale projects can quickly become a complex challenge. As your application grows, so does the complexity of your codebase, making it harder to maintain, scale, and collaborate effectively. This is where monorepos come into play, offering a structured approach to managing multiple projects within a single repository. This guide will walk you through the fundamentals of using monorepos in Next.js, empowering you to build more organized, scalable, and maintainable applications.

What is a Monorepo?

A monorepo, short for “monolithic repository,” is a software development strategy where you store multiple projects, libraries, or packages within a single repository. Instead of having separate repositories for each project, everything lives together. This approach offers several advantages, especially when working with modern JavaScript frameworks like Next.js.

Why Use a Monorepo?

Monorepos provide several key benefits:

  • Simplified Dependency Management: All projects share a single set of dependencies, reducing version conflicts and making it easier to manage dependencies across your entire codebase.
  • Code Reusability: Easily share code and components between different projects within the monorepo, reducing code duplication and promoting consistency.
  • Improved Collaboration: Easier for teams to collaborate, as all code is in a central location, making it easier to track changes, review code, and contribute to different parts of the application.
  • Atomic Changes: Make changes across multiple projects in a single commit, ensuring that related changes are always deployed together, which is crucial for maintaining consistency.
  • Simplified Tooling: Tools like linting, testing, and build processes can be configured at the root level and applied to all projects, reducing configuration overhead.

Setting Up a Monorepo with Next.js and Yarn/npm Workspaces

Let’s dive into creating a monorepo structure with Next.js. We’ll use either Yarn or npm workspaces to manage our projects. Both are excellent choices, and the setup is quite similar. For this example, we’ll use Yarn, but the concepts apply to npm as well.

Step 1: Initialize the Monorepo

First, create a new directory for your monorepo and initialize a Yarn project:

mkdir nextjs-monorepo
cd nextjs-monorepo
yarn init -y

This will create a package.json file at the root of your project.

Step 2: Enable Yarn Workspaces

In your package.json, add the following configuration to enable Yarn workspaces:

{
  "name": "nextjs-monorepo",
  "private": true, // Important: Prevents publishing the root package
  "workspaces": [
    "packages/*"
  ],
  "devDependencies": {
    "typescript": "^5.0.0"
  }
}

The "private": true field prevents you from accidentally publishing the root package to npm. The "workspaces": ["packages/*"] tells Yarn to look for packages in the packages directory. We also added a typescript dev dependency at the root. This allows us to manage TypeScript versions across all projects.

Step 3: Create Your Projects

Create a packages directory in your project root. Inside packages, you’ll create individual Next.js applications and/or shared libraries. Let’s create two example projects: a Next.js application called web-app and a shared UI component library called ui-components.

First, create the directories:

mkdir packages/web-app
mkdir packages/ui-components

Then, navigate into the web-app directory and create a new Next.js app. Note the use of yarn create next-app which automatically installs dependencies and sets up the project structure:

cd packages/web-app
yarn create next-app . --typescript

Now, navigate into the ui-components directory and initialize a basic package. We’ll create a simple button component:

cd ../ui-components
yarn init -y

Create a simple button component (packages/ui-components/src/Button.tsx):

import React from 'react';

interface ButtonProps {
  children: React.ReactNode;
  onClick?: () => void;
  className?: string;
}

const Button: React.FC = ({ children, onClick, className }: ButtonProps) => {
  return (
    <button>
      {children}
    </button>
  );
};

export default Button;

Finally, we need to create a tsconfig.json file in packages/ui-components:

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "baseUrl": ".",
    "paths": {
      "*": ["./src/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

This configures TypeScript for our component library.

Step 4: Linking Packages

To use ui-components in web-app, you need to link them. This is automatically handled by Yarn workspaces:

  1. Install Dependencies: In the root of your project (nextjs-monorepo), run yarn. This will install all dependencies for all projects in your monorepo and link them together.
  2. Import in web-app: In your web-app project (e.g., in packages/web-app/pages/index.tsx), import the button component:
import React from 'react';
import Button from 'ui-components/src/Button';

const Home: React.FC = () => {
  return (
    <div>
      <Button> alert('Button clicked!')}>Click Me</Button>
    </div>
  );
};

export default Home;

Now, when you run your web-app, the button component from ui-components will be rendered, and you can click on it.

Step 5: Running Your Projects

To start your Next.js application, navigate to the web-app directory and run the development server:

cd packages/web-app
yarn dev

Your Next.js application will start, and you can see the button component from your shared library. You can also add scripts to the root package.json to run commands across all packages. For example, to run the dev server for the web app, you could add this script:

{
  "name": "nextjs-monorepo",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "dev:web-app": "yarn workspace web-app dev"
  },
  "devDependencies": {
    "typescript": "^5.0.0"
  }
}

And then run yarn dev:web-app from the root of your project.

Advanced Monorepo Techniques

1. Shared Configuration

One of the biggest advantages of a monorepo is the ability to share configuration across all your projects. This includes things like:

  • Linters and Formatters: Use tools like ESLint and Prettier at the root level to ensure consistent code style across all your projects.
  • Testing Frameworks: Configure a testing framework (e.g., Jest or React Testing Library) at the root and share test configurations.
  • Build Tools: Define build scripts and configurations at the root, making it easy to build and deploy all your projects consistently.

Here’s how you might set up ESLint and Prettier:

  1. Install Dependencies at the Root: Install ESLint, Prettier, and any necessary plugins (e.g., for TypeScript and React) at the root of your project.
yarn add -D eslint prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-plugin-prettier eslint-plugin-react
  1. Create Configuration Files: Create .eslintrc.js and .prettierrc.js files at the root of your project. Here’s an example .eslintrc.js:
module.exports = {
  root: true,
  parser: '@typescript-eslint/parser',
  plugins: [
    '@typescript-eslint',
    'react',
    'prettier'
  ],
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
    'prettier',
    'plugin:prettier/recommended'
  ],
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true
    }
  },
  rules: {
    'prettier/prettier': 'error',
    'react/prop-types': 'off'
  },
  settings: {
    react: {
      version: 'detect'
    }
  }
}

And a sample .prettierrc.js:

module.exports = {
  printWidth: 120,
  tabWidth: 2,
  useTabs: false,
  semi: true,
  singleQuote: true,
  trailingComma: 'all',
  bracketSpacing: true,
  jsxBracketSameLine: false,
  arrowParens: 'always',
  parser: 'typescript',
}
  1. Configure Scripts: Add scripts to your root package.json to run ESLint and Prettier across all projects. For example:
{
  "name": "nextjs-monorepo",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "dev:web-app": "yarn workspace web-app dev",
    "lint": "eslint --ext .js,.jsx,.ts,.tsx packages/",
    "lint:fix": "eslint --fix --ext .js,.jsx,.ts,.tsx packages/",
    "format": "prettier --write "packages/**/*.{js,jsx,ts,tsx}""
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "eslint": "^8.0.0",
    "prettier": "^2.0.0",
    "@typescript-eslint/eslint-plugin": "^5.0.0",
    "@typescript-eslint/parser": "^5.0.0",
    "eslint-config-prettier": "^8.0.0",
    "eslint-plugin-prettier": "^4.0.0",
    "eslint-plugin-react": "^7.0.0"
  }
}

Now, you can run yarn lint to check for linting errors, yarn lint:fix to automatically fix them, and yarn format to format your code across all your projects.

2. Versioning

In a monorepo, managing the versions of your packages is crucial. You have several options:

  • Manual Versioning: Manually update the version numbers in each package’s package.json file. This is the simplest approach but can become tedious as your project grows.
  • Automated Versioning with Tools: Use tools like changesets or lerna to automate the versioning and publishing process. These tools can automatically calculate the version bumps based on your commits and generate changelogs.

Let’s briefly look at using changesets:

  1. Install Changesets:
yarn add -D @changesets/cli
  1. Configure Changesets: Run yarn changeset init to initialize changesets in your project.
  2. Make Changes: Make changes to your packages.
  3. Create a Changeset: Run yarn changeset. This will prompt you to describe the changes you made and select the appropriate version bump (major, minor, or patch) for each package. This will create a changeset file in the .changeset directory.
  4. Publish: When you’re ready to publish, run yarn changeset publish. Changesets will automatically bump the versions of the packages, generate changelogs, and publish them to npm (or your preferred registry). You’ll typically need to configure this with a CI/CD pipeline for automated publishing.

3. Building and Deploying

Monorepos require a well-defined build and deployment strategy. You’ll likely want to:

  • Independent Builds: Each package should be built independently, so changes in one package don’t require rebuilding the entire monorepo.
  • Dependency Resolution: Ensure that dependencies are resolved correctly during the build process, especially when using shared packages.
  • Deployment Pipelines: Set up CI/CD pipelines to automate the build, testing, and deployment of your projects. Tools like Vercel, Netlify, and GitHub Actions are excellent choices.

Here’s a simplified example using Vercel:

  1. Connect Your Repository: Connect your monorepo’s Git repository to Vercel.
  2. Configure Build Commands: Vercel will automatically detect your Next.js projects. You might need to configure the build command in your Vercel project settings. For example, you might need to specify the build command as yarn install && yarn build. If you have multiple Next.js apps in your monorepo, you’ll need separate Vercel projects for each one. Vercel will automatically detect the Next.js apps in your monorepo.
  3. Deployment: When you push changes to your repository, Vercel will automatically build and deploy your projects.

Common Mistakes and How to Avoid Them

  • Ignoring Dependency Conflicts: Carefully manage dependencies to avoid conflicts. Use Yarn or npm workspaces to ensure consistent versions across all packages. Regularly update your dependencies and resolve any conflicts promptly.
  • Over-Complicating the Setup: Start with a simple monorepo structure and gradually add complexity as needed. Don’t try to implement every advanced feature at once.
  • Poor Documentation: Maintain clear documentation for your monorepo structure, build processes, and shared components. This will help new team members understand and contribute to the project.
  • Ignoring Package Visibility: Be mindful of the visibility of your packages. If a package is only used internally, make sure it’s not accidentally published to a public registry. Use the "private": true flag in your package.json for packages that are not meant to be published.
  • Not Using Versioning Tools: As your project grows, manually managing versions becomes cumbersome. Employ tools like changesets or lerna early on to automate versioning and publishing.

Key Takeaways

  • Monorepos offer a structured approach to managing multiple projects within a single repository, leading to improved code organization, reusability, and collaboration.
  • Yarn and npm workspaces are excellent choices for setting up a monorepo in a Next.js project.
  • Shared configuration, versioning, and a well-defined build and deployment strategy are crucial for successful monorepo management.
  • Start simple and gradually add complexity as needed.

FAQ

1. What are the main benefits of using a monorepo?

The main benefits include simplified dependency management, code reusability, improved collaboration, atomic changes, and simplified tooling.

2. How do I choose between Yarn and npm workspaces?

Both Yarn and npm workspaces are viable options. Yarn often provides slightly better performance and a more streamlined developer experience. However, npm workspaces are built-in, so you don’t need to install any extra packages. The choice often comes down to personal preference.

3. How can I share code between different projects in my monorepo?

You can share code by creating shared packages (like the ui-components example) and importing them into your other projects. Yarn/npm workspaces automatically handle the linking of these packages, making it easy to reference code across the monorepo.

4. How do I manage versions in a monorepo?

You can manage versions manually by editing the package.json files or use tools like changesets or lerna to automate the process.

5. How do I deploy a monorepo with multiple Next.js apps?

You’ll typically need to deploy each Next.js app in your monorepo as a separate project. Platforms like Vercel and Netlify can often detect and deploy these applications automatically. You may need to configure build commands and deployment settings for each individual project within your deployment platform.

Monorepos are a powerful technique for managing and scaling complex Next.js projects. By organizing your codebase into a single repository, you can enhance collaboration, promote code reuse, and streamline your development workflow. While setting up a monorepo might seem daunting at first, the benefits in terms of maintainability, scalability, and developer experience are well worth the effort. By following the steps outlined in this guide, you can confidently set up a monorepo for your Next.js projects and unlock the advantages of this modern development approach. The ability to create a consistent coding style, manage dependencies effectively, and simplify the deployment process becomes invaluable as your projects evolve. Embrace the monorepo and watch your development process become more efficient, your code more maintainable, and your team more productive.