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:
- bun (opens in a new tab)
- npm (opens in a new tab)
- pnpm (opens in a new tab)
- yarn (opens in a new tab)
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"
.
{
"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:
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:
{
"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
:
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:
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
:
{
"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
:
{
"devDependencies": {
"tsconfig": "*"
}
}
And inside its tsconfig.json
file, it imports it using extends
:
{
"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.
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
:
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 onui
,tsconfig
andeslint-config-custom
docs
- depends onui
,tsconfig
andeslint-config-custom
ui
- depends ontsconfig
andeslint-config-custom
tsconfig
- no dependencieseslint-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:
{
"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.
- Several scripts will be run at the same time, each prefixed with either
docs:lint
,ui:lint
orweb:lint
. - They'll each succeed, and you'll see
3 successful
in the terminal. - 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:
{
"scripts": {
"lint": "next lint"
}
}
{
"scripts": {
"lint": "next lint"
}
}
{
"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:
cache hit, replaying logs
appears fordocs:lint
,web:lint
andui:lint
.- You'll see
3 cached, 3 total
. - 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
:
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:
docs:lint
has a comment sayingcache miss, executing
. This means thatdocs
is running its linting.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.
{
"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:
- We hit
FULL TURBO
- the builds complete in under100ms
. - 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:
- Only two scripts will execute -
docs:dev
andweb:dev
. These are the only two workspaces which specifydev
. - Both
dev
scripts are run simultaneously, starting your Next.js apps on ports3000
and3001
. - 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
:
{
"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
- Need to add more tasks? Learn more about using pipelines
- Want to speed up your CI? Set up remote caching.
- Want some inspiration? Take a look at our directory of examples (opens in a new tab)