Introduction
Software development often involves managing multiple projects and dependencies, which can quickly become a complex task. This is where the concept of a monorepo comes in handy. A monorepo is a strategy that consolidates multiple projects into a single repository, streamlining workflows and enhancing code reuse. In this blog post, we'll explore the process of setting up a monorepo using Nx and PNPM Workspace.
What is Monorepo?
A monorepo is a way to contain multiple projects within a single Git repository, enabling code sharing between these projects and avoiding code duplication. However, a monorepo is not always necessary until you reach a scale where you need multiple projects and require what we call packages within the monorepo.
For example, let's imagine we want a marketing website and a Next.js application for an imaginary company called Acme. We want to share some UI components between the marketing website and the Next.js application. Traditionally, we would create a separate Git repository for the Acme UI Library, publish the code to npm, and have both the Next.js application and the marketing website download the package from npm. While this approach works, it can be inefficient when making frequent changes. If we want to change the color of a button in the UI Library, we need to make the change, publish it to npm, and then upgrade the package version in both applications individually. This process involves multiple steps for a simple change, which is when we should consider transitioning to a monorepo.
Folder Structure
Transitioning to a monorepo setup allows for streamlined code changes and shared dependencies. A common structure for a monorepo is to have an apps folder for applications and a packages folder for shared packages, like the shared UI, tsconfig, and more.
In this structure, each application and package has its own package.json
, but there's also a root package.json and pnpm-workspace.yaml
file. Package managers like PNPM support workspaces, which allow you to have multiple package.json files within a codebase. The root package.json and the workspace feature unlock the power of the monorepo.
With workspaces, common dependencies like React can be installed once at the root level instead of being duplicated in each project. Additionally, packages can be installed as dependencies within other packages or applications. For example, the UI package can be set as a dependency for the web and marketing apps, allowing them to import components from the UI package locally instead of downloading it from npm.
If we want to change the color of a button in the UI package, we can make the code change, and it will be immediately reflected in the dependent apps because the package manager typically symlinks the entire package into the app's node_modules folder. Furthermore, once we commit the button color change to the repository, both applications will use the same code and have a consistent appearance because they're using the same version of the UI package. Another significant advantage of the monorepo architecture is the ability to streamline tooling across different projects. For example, we can have a shared configuration folder packages/config that contains individual tool configurations for linting, TypeScript, Tailwind CSS, etc. By using shared configurations, all projects will adhere to the same code style and formatting rules, simplifying maintenance and collaboration.
Should I Move to a Monorepo?
The decision to move to a monorepo setup should be carefully considered. While monorepos offer advantages like code sharing and streamlined tooling, they also introduce challenges, especially as the codebase grows larger and more complex. One significant challenge is managing the various tool configurations (e.g., Jest, Playwright, ESLint, Storybook, Prettier) across the workspace. Upgrading packages and keeping the monorepo in good working order can become a pain point. Additionally, because a monorepo contains multiple projects, your CI pipeline will need to build every application and package, even when only a subset of the code has changed, leading to slower build times. However, tools like Nx and Turborepo can mitigate this issue by implementing smart build systems. These tools can analyze the code changes and selectively build only the affected applications and packages, skipping those that haven't changed, thus improving CI pipeline performance.
Nx and Turborepo
Both Nx and Turborepo are popular choices for managing monorepos. While they serve the same purpose and are generally well-regarded, there are some key differences:
Nx: Nx supports both integrated and package-based monorepo approaches. With the integrated approach, it uses TypeScript path aliases to allow packages to reference each other directly, without relying on the package manager's workspace feature. With the package-based approach, it leverages the package manager's workspace feature like traditional monorepos. Nx also offers a suite of plugins and code generators to handle configuration management and migrations, regardless of the approach used.
Turborepo: Turborepo follows a traditional package-based monorepo approach, leveraging the package manager's workspace feature. It does not offer an integrated monorepo option like Nx. Turborepo focuses more on providing a flexible and customizable package-based monorepo setup.
The key difference is that Nx provides both integrated and package-based monorepo options, along with its suite of plugins and tooling. Turborepo solely focuses on the package-based monorepo approach, allowing for more flexibility and customization within that structure.
The choice between Nx and Turborepo depends on your team's preferences and requirements. If you want to delegate monorepo configuration management to a tool and follow an opinionated architecture, Nx may be a better fit. If you prefer more control and customization over your monorepo setup, Turborepo could be a better option.
Conclusion
A monorepo architecture offers several benefits for managing a growing codebase with multiple projects and shared components. By consolidating projects into a single repository, you can streamline workflows, enhance code reuse, and ensure consistent versioning across dependencies.
The key advantages of a monorepo include:
- Shared Codebase: Enables code reuse across projects, reducing redundancy and maintenance efforts.
- Unified Versioning: Ensures all projects are synchronized with compatible versions of dependencies.
- Simplified Management: Centralizes the management of dependencies and build processes.
I've also implemented a monorepo with NX package based
in my project
While monorepos may not be necessary for small projects or individual developers, they become increasingly advantageous as your codebase scales and requires more collaboration and shared components across teams. Setting up a monorepo can be a rewarding experience, leading to increased efficiency and code sharing across projects. However, it's important to adopt a monorepo only when you truly need it, such as when working on a complex product with a team.
That's it for now! I hope this post has sparked your curiosity about monorepos and inspired you to explore setting up your own monorepo architecture. See you in the next post!