apps/docs/content/docs/crafting-your-repository/structuring-a-repository.mdx
turbo is built on top of Workspaces, a feature of package managers in the JavaScript ecosystem that allows you to group multiple packages in one repository.
Following these conventions is important because it allows you to:
In this guide, we'll walk through setting up a multi-package workspace (monorepo) so we can set the groundwork for turbo.
Setting up a workspace's structure can be tedious to do by hand. If you're new to monorepos, we recommend using create-turbo to get started with a valid workspace structure right away.
You can then review the repository for the characteristics described in this guide.
In JavaScript, a workspace can either be a single package or a collection of packages. In these guides, we'll be focusing on a multi-package workspace, often called a "monorepo".
Below, the structural elements of create-turbo that make it a valid workspace are highlighted.
package.jsonturbo.jsonpackage.json in each packageFirst, your package manager needs to describe the locations of your packages. We recommend starting with splitting your packages into apps/ for applications and services and packages/ for everything else, like libraries and tooling.
Using this configuration, every directory with a package.json in the apps or packages directories will be considered a package.
If you'd like to group packages by directory, you can do this using globs like packages/* and packages/group/* and not creating a packages/group/package.json file.
package.json in each packageIn the directory of the package, there must be a package.json to make the package discoverable to your package manager and turbo. The requirements for the package.json of a package are below.
package.jsonThe root package.json is the base for your workspace. Below is a common example of what you would find in a root package.json:
{
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint"
},
"devDependencies": {
"turbo": "latest"
},
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": "9.0.0"
}
}
}
{
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint"
},
"devDependencies": {
"turbo": "latest"
},
"devEngines": {
"packageManager": {
"name": "yarn",
"version": "1.22.19"
}
},
"workspaces": ["apps/*", "packages/*"]
}
{
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint"
},
"devDependencies": {
"turbo": "latest"
},
"devEngines": {
"packageManager": {
"name": "npm",
"version": "10.0.0"
}
},
"workspaces": ["apps/*", "packages/*"]
}
{
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint"
},
"devDependencies": {
"turbo": "latest"
},
"devEngines": {
"packageManager": {
"name": "bun",
"version": "1.2.0"
}
},
"workspaces": ["apps/*", "packages/*"]
}
turbo.jsonturbo.json is used to configure the behavior of turbo. To learn more about how to configure your tasks, visit the Configuring tasks page.
A lockfile is key to reproducible behavior for both your package manager and turbo. Additionally, Turborepo uses the lockfile to understand the dependencies between your Internal Packages within your Workspace.
It's often best to start thinking about designing a package as its own unit within the Workspace. At a high-level, each package is almost like its own small "project", with its own package.json, tooling configuration, and source code. There are limits to this idea—but its a good mental model to start from.
Additionally, a package has specific entrypoints that other packages in your Workspace can use to access the package, specified by exports.
package.json for a packagenameThe name field is used to identify the package. It should be unique within your workspace.
We use @repo in our docs and examples because it is an unused, unclaimable namespace on the npm registry. You can choose to keep it or use your own prefix.
scriptsThe scripts field is used to define scripts that can be run in the package's context. Turborepo will use the name of these scripts to identify what scripts to run (if any) in a package. We talk more about these scripts on the Running Tasks page.
exportsThe exports field is used to specify the entrypoints for other packages that want to use the package. When you want to use code from one package in another package, you'll import from that entrypoint.
For example, if you had a @repo/math package, you might have the following exports field:
{
"exports": {
".": "./src/constants.ts",
"./add": "./src/add.ts",
"./subtract": "./src/subtract.ts"
}
}
Note that this example uses the Just-in-Time Package pattern for simplicity. It exports TypeScript directly, but you might choose to use the Compiled Package pattern instead.
<Callout type="info"> The `exports` field in this example requires modern versions of Node.js and TypeScript. </Callout>This would allow you to import add and subtract functions from the @repo/math package like so:
import { GRAVITATIONAL_CONSTANT, SPEED_OF_LIGHT } from "@repo/math";
import { add } from "@repo/math/add";
import { subtract } from "@repo/math/subtract";
Using exports this way provides three major benefits:
exports also has other powerful features compared to the main field like Conditional Exports. In general, we recommend using exports over main whenever possible as it is the more modern option.exports, you can ensure that your code editor can provide auto-completion for the package's exports.imports (optional)The imports field gives you a way to create subpaths to other modules within your package. You can think of these like "shortcuts" to write simpler import paths that are more resilient to refactors that move files. To learn how, visit the TypeScript page.
Of course, you'll want some source code in your package. Packages commonly use a src directory to store their source code and compile to a dist directory (that should also be located within the package), although this is not a requirement.
tsconfig.json in the root of your workspace. Packages should independently specify their own configurations, usually building off of a shared tsconfig.json from a separate package in the workspace. For more information, visit the TypeScript guide.../ to get from one package to another, you likely have an opportunity to re-think your approach by installing the package where it's needed and importing it into your code.With your Workspace configured, you can now use your package manager to install dependencies into your packages.