Repo
Docs
Getting Started
Create a New Monorepo

Creating a new monorepo

This guide uses a global installation of turbo. Follow the installation guide to get this setup. Alternatively, you can use your package manager to run a locally installed turbo in the commands below.

Quickstart

To create a new monorepo, use our create-turbo (opens in a new tab) npm package:

npx create-turbo@latest

You can also clone a Turborepo starter repository to get a head start on your monorepo. To see Turborepo examples and starters, see the Turborepo examples directory on GitHub (opens in a new tab).

Full tutorial

This tutorial will walk you through setting up a basic example. By the end, you'll feel confident with using turbo, and know all the basic functionality.

During this tutorial, some lines of code are omitted from the code samples. For instance, when showing a package.json we won't show all of the keys - only the ones that matter.

1. Running create-turbo

First, run:

npx create-turbo@latest

This installs the create-turbo (opens in a new tab) CLI, and runs it. You'll be asked several questions:

Where would you like to create your turborepo?

Choose anywhere you like. The default is ./my-turborepo.

Which package manager do you want to use?

Turborepo doesn't handle installing packages, so you'll need to choose one of:

create-turbo will detect which package managers you have available on your system. If you're unsure which to choose, Turborepo recommends pnpm.

Installation

Once you've picked a package manager, create-turbo will create a bunch of new files inside the folder name you picked. It'll also install all the dependencies that come with the basic example by default.

2. Exploring your new repo

You might have noticed something in the terminal. create-turbo gave you a description of all of the things it was adding.

>>> Creating a new turborepo with the following:

 - apps/web: Next.js with TypeScript
 - apps/docs: Next.js with TypeScript
 - packages/ui: Shared React component library
 - packages/eslint-config-custom: Shared configuration (ESLint)
 - packages/tsconfig: Shared TypeScript `tsconfig.json`

Each of these is a workspace - a folder containing a package.json. Each workspace can declare its own dependencies, run its own scripts, and export code for other workspaces to use.

Open the root folder - ./my-turborepo - in your favourite code editor.

Understanding packages/ui

First, open ./packages/ui/package.json. You'll notice that the package's name is "name": "ui" - right at the top of the file.

Next, open ./apps/web/package.json. You'll notice that this package's name is "name": "web". But also - take a look in its dependencies.

You'll see that "web" depends on a package called "ui".

apps/web/package.json
{
  "dependencies": {
    "ui": "*"
  }
}

This means that our web app depends on our local ui package.

If you look inside apps/docs/package.json, you'll see the same thing. Both web and docs depend on ui - a shared component library.

This pattern of sharing code across applications is extremely common in monorepos - and means that multiple apps can share a single design system.

Understanding imports and exports

Take a look inside ./apps/docs/app/page.tsx. Both docs and web are Next.js (opens in a new tab) applications, and they both use the ui library in a similar way:

apps/docs/app/page.tsx
import { Button, Header } from "ui";
//       ^^^^^^^^^^^^^^         ^^
 
export default function Page() {
  return (
    <>
      <Header text="Docs">
      <Button />
    <>
  );
}

They're importing Button directly from a dependency called ui! How does that work? Where is Button coming from?

Open packages/ui/package.json. You'll notice these two attributes:

packages/ui/package.json
{
  "main": "./index.tsx",
  "types": "./index.tsx"
}

When workspaces import from ui, main tells them where to access the code they're importing. types tells them where the TypeScript types are located.

So, let's look inside packages/ui/index.tsx:

packages/ui/index.tsx
import * as React from "react";
export * from "./Button";

Everything inside this file will be able to be used by workspaces that depend on ui.

index.tsx is exporting everything from a file called ./Button, so let's go there:

packages/ui/Button.tsx
import * as React from "react";
 
export const Button = () => {
  return <button onClick={() => alert("boop")}>Boop</button>;
};

We've found our button! Any changes we make in this file will be shared across web and docs. Pretty cool!

Try experimenting with exporting a different function from this file. Perhaps add(a, b) for adding two numbers together.

This can then be imported by web and docs.

Understanding tsconfig

We have two more workspaces to look at, tsconfig and eslint-config-custom. Each of these allow for shared configuration across the monorepo. Let's look in tsconfig:

packages/tsconfig/package.json
{
  "name": "tsconfig",
  "files": ["base.json", "nextjs.json", "react-library.json"]
}

Here, we specify three files to be exported, inside files. Packages which depend on tsconfig can then import them directly.

For instance, packages/ui depends on tsconfig:

packages/ui/package.json
{
  "devDependencies": {
    "tsconfig": "*"
  }
}

And inside its tsconfig.json file, it imports it using extends:

packages/ui/tsconfig.json
{
  "extends": "tsconfig/react-library.json"
}

This pattern allows for a monorepo to share a single tsconfig.json across all its workspaces, reducing code duplication.

Understanding eslint-config-custom

Our final workspace is eslint-config-custom.

You'll notice that this is named slightly differently to the other workspaces. It's not as concise as ui or tsconfig. Let's take a look inside .eslintrc.js in the root of the monorepo to figure out why.

.eslintrc.js
module.exports = {
  // This tells ESLint to load the config from the package `eslint-config-custom`
  extends: ["custom"],
};

ESLint (opens in a new tab) resolves configuration files by looking for workspaces with the name eslint-config-*. This lets us write extends: ['custom'] and have ESLint find our local workspace.

But why is this in the root of the monorepo?

The way ESLint finds its configuration file is by looking at the closest .eslintrc.js. If it can't find one in the current directory, it'll look in the directory above until it finds one.

So that means that if we're working on code inside packages/ui (which doesn't have a .eslintrc.js) it'll refer to the root instead.

Apps that do have an .eslintrc.js can refer to custom in the same way. For instance, in docs:

apps/docs/.eslintrc.js
module.exports = {
  root: true,
  extends: ["custom"],
};

Just like tsconfig, eslint-config-custom lets us share ESLint configs across our entire monorepo, keeping things consistent no matter what project you're working on.

Summary

It's important to understand the dependencies between these workspaces. Let's map them out:

  • web - depends on ui, tsconfig and eslint-config-custom
  • docs - depends on ui, tsconfig and eslint-config-custom
  • ui - depends on tsconfig and eslint-config-custom
  • tsconfig - no dependencies
  • eslint-config-custom - no dependencies

Note that the Turborepo CLI is not responsible for managing these dependencies. All of the things above are handled by the package manager you chose (npm, pnpm or yarn).

3. Understanding turbo.json

We now understand our repository and its dependencies. How does Turborepo help?

Turborepo helps by making running tasks simpler and much more efficient.

Let's take a look inside turbo.json, at the root:

turbo.json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**"]
    },
    "lint": {},
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

What we're seeing here is that we've registered three tasks with turbo: lint, dev and build. Every task registered inside turbo.json can be run with turbo run <task> (or turbo <task> for short).

Before we move on, let's try running a task called hello that isn't registered in turbo.json:

turbo hello

You'll see an error in the terminal. Something resembling:

Could not find the following tasks in project: hello

That's worth remembering - in order for turbo to run a task, it must be in turbo.json.

Let's investigate the scripts we already have in place.

4. Linting with Turborepo

Try running our lint script:

turbo lint

You'll notice several things happen in the terminal.

  1. Several scripts will be run at the same time, each prefixed with either docs:lint, ui:lint or web:lint.
  2. They'll each succeed, and you'll see 3 successful in the terminal.
  3. You'll also see 0 cached, 3 total. We'll cover what this means later.

The scripts that each run come from each workspace's package.json. Each workspace can optionally specify its own lint script:

apps/web/package.json
{
  "scripts": {
    "lint": "next lint"
  }
}
apps/docs/package.json
{
  "scripts": {
    "lint": "next lint"
  }
}
packages/ui/package.json
{
  "scripts": {
    "lint": "eslint \"**/*.ts*\""
  }
}

When we run turbo lint, Turborepo looks at each lint script in each workspace and runs it. For more details, see our pipelines docs.

Using the cache

Let's run our lint script one more time. You'll notice a few new things appear in the terminal:

  1. cache hit, replaying logs appears for docs:lint, web:lint and ui:lint.
  2. You'll see 3 cached, 3 total.
  3. The total runtime should be under 100ms, and >>> FULL TURBO appears.

Something interesting just happened. Turborepo realised that our code hadn't changed since the last time we ran the lint script.

It had saved the logs from the previous run, so it just replayed them.

Let's try changing some code to see what happens. Make a change to a file inside apps/docs:

apps/docs/app/page.tsx
import { Button, Header } from "ui";
 
export default function Page() {
  return (
    <>
-     <Header text="Docs" />
+     <Header text="My great docs" />
      <Button />
    <>
  );
}

Now, run the lint script again. You'll notice that:

  1. docs:lint has a comment saying cache miss, executing. This means that docs is running its linting.
  2. 2 cached, 3 total appears at the bottom.

This means that the results of our previous tasks were still cached. Only the lint script inside docs actually ran - again, speeding things up. To learn more, check out our caching docs.

5. Building with Turborepo

Let's try running our build script:

turbo build

You'll see similar outputs to when we ran our lint script. Only apps/docs and apps/web specify a build script in their package.json, so only those are run.

Take a look inside build in turbo.json. There's some interesting config there.

turbo.json
{
  "pipeline": {
    "build": {
      "outputs": [".next/**", "!.next/cache/**"]
    }
  }
}

You'll notice that some outputs have been specified. Declaring outputs will mean that when turbo finishes running your task, it'll save the output you specify in its cache.

Both apps/docs and apps/web are Next.js apps, and they output builds to the ./.next folder.

Let's try something. Delete the apps/docs/.next build folder.

Run the build script again. You'll notice:

  1. We hit FULL TURBO - the builds complete in under 100ms.
  2. The .next folder re-appears!

Turborepo cached the result of our previous build. When we ran the build command again, it restored the entire .next/** folder from the cache. To learn more, check out our docs on cache outputs.

6. Running dev scripts

Let's now try running dev.

turbo dev

You'll notice some information in the terminal:

  1. Only two scripts will execute - docs:dev and web:dev. These are the only two workspaces which specify dev.
  2. Both dev scripts are run simultaneously, starting your Next.js apps on ports 3000 and 3001.
  3. In the terminal, you'll see cache bypass, force executing.

Try quitting out of the script, and re-running it. You'll notice we don't go FULL TURBO. Why is that?

Take a look at turbo.json:

turbo.json
{
  "pipeline": {
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

Inside dev, we've specified "cache": false. This means we're telling Turborepo not to cache the results of the dev script. dev runs a persistent dev server and produces no outputs, so there is nothing to cache. Learn more about it in our docs on turning off caching.

Additionally, we set "persistent": true, to let turbo know that this is a long-running dev server, so that turbo can ensure that no other tasks depend on it. You can read more in the docs for the persistent option.

Running dev on only one workspace at a time

By default, turbo dev will run dev on all workspaces at once. But sometimes, we might only want to choose one workspace.

To handle this, we can add a --filter flag to our command.

turbo dev --filter docs

You'll notice that it now only runs docs:dev. Learn more about filtering workspaces from our docs.

Summary

Well done! You've learned all about your new monorepo, and how Turborepo makes handling your tasks easier.

Next steps