engineering

Getting Started With Turborepo - Everything That You Need To Know

Getting Started With Turborepo - Everything That You Need To Know

Turborepo is a build system that addresses the challenges faced in managing monorepos. Learn about Turborepo, its features, how to use Turborepo, and more.

Kaushal Joshi

Kaushal Joshi

Dec 03, 2024 12 min read

As software companies grow, managing a cohesive ecosystem of applications, libraries, and utilities becomes increasingly challenging. Maintaining everything from core applications (like web and mobile apps) to APIs, documentation sites, UI components, and shared libraries, requires a balance of isolation and synchronization to maintain productivity and efficiency.

To solve this, many organizations adopt a monorepo structure. Monorepos can streamline development, improve collaboration, and simplify code sharing. However, they also come with challenges like slower build times and dependency management complexities. This is where Turborepo steps in.

Turborepo is designed to address the pain points of traditional monorepos. In this blog, we’ll explore what Turborepo is, why it’s essential, and how it works. We’ll also cover key features that set Turborepo apart and guide you on how to use Turborepo with Next.js and other projects to improve development workflows.

Before diving into Turborepo, let’s first establish a solid understanding of the monorepo architecture.

What is a Monorepo?

A monorepo is a single repository that contains multiple projects, libraries, and services. Companies that work on large interconnected parts of a system often prefer monorepos over polyrepos. In a polyrepo approach, each project is stored in its own repository.

In a monorepo architecture, all code for multiple apps, libraries, and shared components is stored in a single repository. Suppose your organization hosts two separate web apps that share code, a documentation site, and an API. It also maintains a few internal libraries, services, and UI components. Traditionally, you'd host everything in separate repositories and install dependencies as required. With a monorepo setup, you only need to import what you need because you have access to all of the codebase.

This approach offers significant benefits for teams. Because, as a team,

  • You don’t have to maintain multiple repositories.

  • You don’t need to install the same dependencies multiple times.

  • You can work on a package and instantly use it in your application, without needing to deploy it and installing a new version again.

This was just a surface. Let’s briefly understand what are the benefits of a monorepo.

Benefits of Using a Monorepo

Setting up a monorepo has three benefits over the traditional approach.

  • Shared code dependencies: Teams can easily share libraries, modules, or components between projects. If one project changes a shared library, those changes propagate to all dependent projects, making it simpler to maintain consistency.

  • Unified configuration: Common configurations like ESLint rules, TS config, and Tailwind config, can be shared across projects. This eliminates the need for duplicating and ensures consistency across an organization.

  • Improved CI/CD: With everything in one place, continuous integration and deployment workflows can be more streamlined, allowing for faster testing and deployment.

Challenges With a Monorepo

A monorepo is just one approach to solving these problems, and it brings its own set of challenges.

  • Build and test speed: As projects grow, builds can become slower. Testing and compiling large codebases can lead to lengthy build times, impacting productivity.

  • Dependency Management: Managing dependencies across different projects can lead to issues, particularly when different projects rely on incompatible versions of a dependency.

  • Scalability: With very large monorepos, tools like Git can struggle to handle the vast amount of code, resulting in slower checkouts and commits.

These challenges have led to the development of tools like Turborepo that specifically aim to improve the scalability and efficiency of monorepos.

What is a Turborepo?

Turborepo is a modern, high-performance build system built in Go. It is currently maintained and open-sourced by Vercel. It addresses the challenges faced in managing monorepos. Turborepo was developed to overcome the drawbacks of monorepos by providing an optimized way to manage, build, and deploy applications in monorepos efficiently.

Instead of requiring a full rebuild every time there’s a change, Turborepo speeds up development by using advanced caching, parallel task execution, and dependency tracking. It focuses on reducing redundant work and reusing previously built artifacts, which is particularly valuable in large codebases where builds can become resource-intensive and time-consuming.

Key Features of Turborepo

Turborepo is built with a focus on smart caching and parallel execution. Caching previous builds avoids redundant work and saves time and system resources. Additionally, Turborepo takes advantage of the computing power of multi-core processors, allowing it to run multiple tasks in parallel.

The core features of Turborepo include a caching layer, parallel task execution, and dependency graph tracking. Let’s discuss them in detail.

Caching layer

The caching layer in Turborepo is designed to save and reuse outputs from previous tasks. When a task runs, Turborepo saves its output to a local cache. If that task is re-run with the same inputs, Turborepo will skip the task and retrieve the output from the cache instead.

Turborepo’s caching depends on the inputs and outputs defined in turbo.json, so even minor changes in code or configuration prompt a re-run to maintain accuracy.

Parallel task execution

Turborepo is optimized to leverage multi-core processors by running tasks concurrently. When tasks don’t depend on each other, Turborepo executes them in parallel. This speeds up the build process, as multiple tasks can be completed simultaneously rather than sequentially.

For instance, if you have separate tasks for building the frontend and backend of an application, Turborepo can execute both tasks simultaneously, maximizing the efficiency of your build process. Turborepo’s scheduler optimizes task order, respecting dependencies to maximize efficiency.

This is how tasks are executed in traditional monorepo projects:

Turborepo executes tasks in parallel, so they finish executing way faster:

Dependency Graph Tracking

A key feature of Turborepo is its ability to track dependencies between tasks. When Turborepo begins a build process, it analyzes the relationships between tasks based on the dependencies defined in turbo.json. Using these relationships, Turborepo creates a dependency graph, a structure that maps out the order in which tasks should be executed.

This ensures that tasks are completed in the correct order. For example, if a task depends on the output of another task, Turborepo will wait until the dependency is complete before proceeding.

Remote Caching

Turborepo supports remote caching, which enables teams to share cached build outputs across different machines. When remote caching is enabled, Turborepo uploads completed task outputs to a shared cache storage. This means that when a developer or CI/CD pipeline runs a task that has already been completed on another machine, Turborepo can download the cached output rather than running the task again.

This is particularly useful in large teams or CI/CD pipelines, where each build can leverage previous outputs to speed up the process.

Command Line Interface (CLI)

Turborepo provides a CLI for developers to run tasks defined in turbo.json. Commands like npx turbo run build or npx turbo run lint allow developers to trigger specific tasks or groups of tasks. The CLI uses the configurations in turbo.json to manage dependencies, apply caching, and parallelize tasks as necessary.

How Does Turborepo Work?

Turborepo operates through a carefully structured process, using configurations, dependency tracking, and caching to ensure efficient and accurate builds. Here’s a closer look at how it works step-by-step:

Configuration with turbo.json

Turborepo is configured through a turbo.json file at the root of the repository. This file defines the tasks Turborepo should run, as well as dependencies between tasks and their expected outputs. For example, you might define tasks for building, testing, and linting each project or package within the monorepo. Here’s an example configuration:

{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"]
    },
    "lint": {
      "outputs": []
    }
  }
}

This configuration tells Turborepo how to run each task, what other tasks it depends on (e.g., test depends on build), and what outputs to expect from each task.

How to Use Turborepo?

As you are aware of what is Turborepo, its features, and how it works, it’s time to get our hands dirty! Turborepo provides a way to integrate it within your existing project or provides a boilerplate. For the article, we’d use a boilerplate.

Execute the following command inside the terminal

pnpm dlx create-turbo@latest monorepo-name

It’ll ask you to choose a package manager. I’d prefer pnpm as it’s faster and saves storage. You are free to choose NPM or Yarn.

This command generates a new monorepo with the following structure:

turborepo-demo/
├── apps/
│   ├── docs/                 # Frontend application
│   └── web/                  # A documentation site
├── packages/
│   ├── eslint-config/        # Shared ESLin configuration
│   ├── typescript-config/    # Shared TypeScript library
│   └── ui/                   # Shared UI components
├── turbo.json                # Turborepo configuration file
├── pnpm-workspace.yaml       # YAML configuration of workspaces
└── package.json              # Root package.json for managing workspaces
  • apps/ folder: Contains deployable applications, like the main frontend web app (web) and a documentation site (docs).

  • packages/ folder: Holds shared libraries and code modules (e.g., ui, eslint-config, and typescript-config that can be used across different apps within the monorepo.

  • turbo.json contains the configuration of the Turborepo. We’ll dive deep into this later.

  • pnpm-workspace.yaml holds workspace information of the monorepo. It defines the main apps that would be deployed.

  • package.json is the root package file of the monorepo. Every app/package might have its own package file.

Exploring turbo.json file

The turbo.json file is where you configure Turborepo’s caching and pipeline behaviors.

Normally in a monorepo, tasks are executed sequentially. First linting, then builds, then testing, and then deployments. With pipelines, you can explicitly tell Turbo what a task depends on.

{
  "$schema": "https://turbo.build/schema.json",
  "ui": "tui",
  "tasks": {
    "lint": {},
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["$TURBO_DEFAULT$", ".env*"],
      "outputs": [".next/**", "!.next/cache/**"]
    },
    "test": {
      "dependsOn": ["build"]
    },
    "deploy": {
      "dependsOn": ["lint", "build", "test"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

In the above snippet, the lint command has no dependency. However, to execute build, all the dependencies of build must be executed before (The caret (^) symbol refers to dependencies). To execute test, build must be executed successfully. And finally, all three commands must be executed successfully to execute the build command.

A pipeline like this allows you to perform more operations in less time, utilize more CPU resources, and eventually deliver faster builds.

The apps folder

Inside the apps folder, you’d see two directories: web and docs. These are the actual applications that you host on the web for users to interact. Each project is a standalone application. The default apps are built with Next.js, but you can use any JavaScript-based library or framework, backend or frontend.

Go to package.json in any of the two projects and you’d find something interesting.

{
  "name": "web",
  ...
  "dependencies": {
    "@repo/ui": "workspace:*",
    ...
  },
  "devDependencies": {
    "@repo/eslint-config": "workspace:*",
    "@repo/typescript-config": "workspace:*",
    ...
  }
}

There are some packages linked to workspace:. These are the shared packages that can be accessed by any app throughout the codebase. They are defined in packages/ directory, which we’ll discuss soon.

The packages/ directory

Now let’s focus our attention on the packages/ directory. There are three directories available here: eslint-config, typescript-config, and ui. Each folder is a package serving its own purpose. You can use code exported from these anywhere in the apps/ directory.

Take packages/ui/ for instance. It is the shared UI library built with React. If you open its package.json, you’ll see exports key. Here you must define the components that you want to export from here. Only components mentioned here can be imported in projects in apps/ directory.

Suppose you want to export a Button component from the UI library. Here’s how you can export it from the ui package:

{
    ...
    "exports": {
        "./button": "./src/button.tsx",
    }
    ...
}

Here’s how you’d use it in your project:

import { Button } from "@repo/ui/button";
  • @repo refers to the root of the packages folder, where all the shared packages are stored.

  • ui is the name of the package where UI components are stored

  • button is the name of the component we want to import. This is the same name that you used as a key inside the exports object previously.

Working on the monorepo apps

As mentioned earlier, projects inside apps/ are standalone projects that you can deploy. Let’s see that in action. Start the development server by executing the dev command at the root of your project.

pnpm dev

Both web and docs are now live in different localhost ports. You can see each project’s logs inside the same terminal.

You can toggle between projects by using up and down arrow keys.

Go to http://localhost:3000/ and click on the button that says Open alert. It’ll trigger an alert saying Hello from your web app! This code came from the shared UI component. Open packages/ui/src/button.tsx in your editor and make a change in the alert message. Once you save and check again, you’ll instantly see the new message pop up in the alert box.

You did not rebuild the package, restart the server, or import a newer version. Yet changes were made in real-time. Crazy, right?

There’s more to it!

We’ve just scratched the surface. Turborepo offers so many ways to make working with monorepos simpler and better. It’s impossible to cover everything in a single article. If you want to read articles on specific topics, feel free to let me know! I’d be happy to cover more interesting topics in the future.

For now, you can refer to Turborepo's official documentation to learn more about it.

Wrapping Up

In this blog, we learned what are monorepos, and the features and challenges of monorepos. We also saw what is Turborepo, what are Turborepo’s features, and how Turborepo works, and used Turborepo to create a monorepo with Next.js.

I hope you learned something from this article. If you did, make sure you share it with your friends and peers so that they’d also learn something valuable.

If you’re looking for a new job, or looking for a community to share a cool project you’ve built, check out Peerlist! It’s the professional network to show & tell what you are working on!

See you there! Until then, happy coding! 👩‍💻

Create Profile

or continue with email

By clicking "Create Profile“ you agree to our Code of Conduct, Terms of Service and Privacy Policy.