How to Organize Your Frontend Projects with Turborepo
Thinking about adopting a monorepo but overwhelmed by complexity? If you're juggling multiple apps, libraries, and config files across projects. You're not alone and there's a better way. This post dives into how Turborepo simplifies monorepo management with blazing-fast builds, smart caching, and streamlined developer workflows. Whether you're a solo dev or scaling a team, learn how to tame your codebase and ship faster with confidence.

Jordan Wu
8 min read·Posted

Table of Contents
Turborepo
Turborepo is a fast, modern build system and task runner designed for managing monorepos, particularly in JavaScript and TypeScript ecosystems. As a software engineer, I appreciate how it simplifies the complexity of working with multiple interconnected packages, like apps, shared UI components, and utilities. All within a single codebase. Turborepo intelligently caches task outputs (like builds, tests, and linting) both locally and remotely, allowing you to skip redundant work and speed up your development and CI workflows dramatically. With its declarative turbo.json
configuration, it handles task orchestration based on dependency graphs, making it easy to define efficient pipelines. Whether you're a solo developer or part of a team, Turborepo helps keep large codebases maintainable, fast, and scalable.
Workspaces
A workspace is an individual project or package within your monorepo, such as a frontend app, a shared component library, or a utility module. Each lives in its own directory with its own package.json
. Workspaces are the building blocks of a monorepo, and Turborepo uses them to understand project boundaries, manage task execution, and optimize build performance. By treating each workspace as an isolated unit that can depend on or be depended upon by others. Turborepo creates a dependency graph that allows tasks like build
, test
, or lint
to run intelligently. Only where changes have occurred. This structure makes it easy to scale large codebases, share code across projects, and keep everything in sync without the overhead of managing separate repositories.
Managing dependencies
Turborepo does not play a role in managing your dependencies, leaving that work up to your package manager of choice. There are best practices for dependency installation that you should follow after understanding the two types of dependencies.
- External dependencies come from the npm registry, allowing you to leverage valuable code from the ecosystem to build your applications and libraries faster.
- Internal dependencies let you share functionality within your repository, dramatically improving discoverability and usability of shared code. We will discuss how to build an Internal Package in the next guide.
Install dependencies where they're used. When you install a dependency in your repository, you should install it directly in the package that uses it. The package's package.json
will have every dependency that the package needs. This is true for both external and internal dependencies.
Few dependencies in the root. The only dependencies that belong in the workspace root are tools for managing the repository whereas dependencies for building applications and libraries are installed in their respective packages.
A monorepo makes it easy to create and use internal packages, enabling seamless code sharing across projects. This improves consistency, reduces duplication, and accelerates development by centralizing reusable logic in one place. Turborepo automatically understands the relationships between Internal Packages using the dependencies in package.json
, creating a Package Graph (structure of your monorepo created by your package manager) under the hood to optimize your repository's workflows.
Depending on your library’s needs, you can choose from three compilation strategies for your internal packages:
- Just-in-Time Packages: The package is compiled by the application as it’s used, requiring minimal configuration.
- Compiled Packages: A moderate amount of configuration is needed to compile the package using a build tool like
tsc
or a bundler. - Publishable Packages: The package is fully compiled and prepared for publishing to the npm registry, requiring the most configuration.
Remote Caching
Turborepo Remote Caching allows teams to share build artifacts across machines by syncing task results to a remote, cloud-based cache. By default, Turborepo only caches results locally, which means even if all task inputs are identical, the same commands (like turbo run build
) must be re-executed separately by you, your teammates, your CI, or your deployment platform. Wasting both time and compute resources. With Remote Caching enabled, Turborepo securely communicates with a shared cache server, letting everyone on your team instantly reuse previously computed results. This dramatically speeds up workflows, avoids duplicated work, and makes monorepo development at scale much more efficient.
Remote Caching is free and can be used with Vercel Remote Cache.
Tasks
The root turbo.json
file is where you define the tasks Turborepo can execute. Once these tasks are set up, you can run them using the turbo run
command. Each entry in the tasks object represents a task that Turborepo will look for in your workspace packages, specifically in their package.json
scripts with matching names.
To control execution order, use the dependsOn
key to declare task dependencies. This ensures, for example, that all libraries are built before the application that depends on them begins its build process. Turborepo's ^
microsyntax is particularly useful here: it signals that the dependent task should start from the bottom of the dependency graph. So, if your application depends on a ui library with a build task, Turborepo will execute ui's build script first, then proceed to the application’s build once it completes successfully.
{
"tasks": {
"build": {
"dependsOn": ["^build"]
}
}
}
You may need to ensure that two tasks in the same package run in a specific order. For example, you may need to run a build task in your library before running a test task in the same library. To do this, specify the script in the dependsOn
key as a plain string (without the ^
).
{
"tasks": {
"test": {
"dependsOn": ["build"]
}
}
}
You can also specify an individual task in a specific package to depend on. In the example below, the build task in utils must be run before any lint tasks.
{
"tasks": {
"lint": {
"dependsOn": ["utils#build"]
}
}
}
You can also be more specific about the dependent task, limiting it to a certain package:
{
"tasks": {
"web#lint": {
"dependsOn": ["utils#build"]
}
}
}
Turborepo optimizes the developer workflows in your repository by automatically parallelizing and caching tasks. Once a task is registered in turbo.json
, you can use scripts
in package.json
for tasks that you run frequently. You can write your turbo
commands directly into your root package.json
.
{
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint"
}
}
While caching ensures you stay fast by never doing the same work twice, you can also filter tasks to run only a subset of the Task Graph.
Filtering by package is a simple way to only run tasks for the packages you're currently working on:
turbo build --filter=@acme/web
You can also filter to a specific task for the package directly without needing to use --filter
:
# Run the `build` task for the `web` package
turbo run web#build
# Run the `build` task for the `web` package, and the `lint` task for the `docs` package
turbo run web#build docs#lint
Structuring a Turborepo repository
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. First, 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.
packages:
- "apps/*"
- "packages/*"
In my Turborepo setup, I’ve organized the repository to separate core applications from shared libraries, making it easier to scale and maintain over time.
📁 apps/
— Executable Applications
These are runnable applications that serve as frontends or tools:
web
: The main Next.js web application used by end users.ui-storybook
: A standalone Storybook app for developing and showcasing UI components from the design system.
📁 packages/
— Shared Code and Tests
Reusable libraries and test suites shared across applications:
@repo/ui
: The core UI component library (design system).@repo/ui-tests
: Component-level tests for the UI library.@repo/web-tests
: End-to-end or integration tests specific to the web app.
📁 configs/
— Shared Tooling Configurations
Centralized configurations to enforce consistency across the repo:
@repo/eslint-config
: Shared ESLint config.@repo/playwright-config
: Shared Playwright setup for browser testing.@repo/tailwind-config
: Shared Tailwind CSS configuration.@repo/typescript-config
: Shared TypeScript base config.@repo/vitest-config
: Shared Vitest test configuration.
📁 infra/
— Infrastructure as Code
Terraform, provisioning scripts, and infrastructure definitions live here
- SST: an open-source framework that helps you build and deploy serverless applications on AWS.
Turborepo includes tools for understanding your repository structure, that can help you use and optimize your codebase. For example turbo ls
> turbo ls
@repo/eslint-config configs/eslint-config
@repo/playwright-config configs/playwright-config
@repo/tailwind-config configs/tailwind-config
@repo/typescript-config configs/typescript-config
@repo/ui packages/ui
@repo/ui-tests packages/ui-tests
@repo/vitest-config configs/vitest-config
infra infra
ui-storybook apps/ui-storybook
web apps/web
web-tests packages/web-tests
Summary
Turborepo is a high-performance build system and task runner designed to simplify managing monorepos in JavaScript and TypeScript ecosystems. It enables efficient development by caching tasks locally and remotely, orchestrating builds through a dependency graph, and supporting scalable project structures using isolated workspaces. With a declarative turbo.json configuration, Turborepo makes it easy to define and run tasks like builds, tests, and linting across interconnected packages. By embracing best practices for internal and external dependencies, leveraging remote caching for faster CI/CD, and organizing repositories with a clear apps, packages, configs structure. Teams can streamline collaboration, reduce duplication, and dramatically improve performance in large codebases.