TurborepoMonorepoNext.jsArchitectureTypeScript

Why I Always Reach for Turborepo on Client Projects

Most freelance projects start small. Then they don't. Here's why I set up a Turborepo monorepo from day one — and why it's saved every client I've worked with.

L

Lazar Kapsarov

April 12, 2025 · 10 min read

Every client project starts the same way.

A founder with a clear idea. A tight scope. "We just need the core product first — we can worry about the marketing site later. And the admin panel. And the component library. And the email templates."

Three months in, "just the core product" is four interconnected applications sharing business logic, and someone is copy-pasting TypeScript types between repos.

I've seen this pattern enough times that I now set up Turborepo on day one, even when the project doesn't obviously need it yet. Especially when the project doesn't obviously need it yet.

Here's why.

What Turborepo Actually Is

Turborepo is a high-performance build system for JavaScript and TypeScript monorepos. That's the official description. What it means in practice: you keep multiple apps and packages in a single repository, and Turborepo figures out what needs to be rebuilt when something changes — and what doesn't.

It's not a framework. It doesn't touch your code. It sits above your toolchain and makes it fast and coordinated.

What it's not: a way to combine apps that should be separate products. Turborepo is for code that belongs together — shared components, shared types, shared configuration, shared utilities — living in one place without the friction of multiple repos.

The Project That Convinced Me

Early in my freelance work I was brought in to help with a startup that had started as a single Next.js app. By the time I joined, the codebase had three separate repos:

  • The customer-facing app
  • An internal admin panel built by a different contractor
  • A shared component library that was copy-pasted into both with minor variations

The Button component existed in three forms across the codebase. The TypeScript interfaces for the core data models were duplicated everywhere, slowly diverging. Updating the design system meant making changes in multiple places, publishing packages, bumping versions, and praying nothing broke downstream.

The team spent more time on coordination overhead than on building features.

The fix was a monorepo migration. It took two weeks. After that, a design system change was one PR. A shared type update propagated everywhere automatically. The admin panel and the customer app could share utilities without a package registry in between.

I've set up Turborepo from scratch on every client project since.

The Structure I Use

Here's the base structure I reach for:

apps/
  web/          ← Next.js customer-facing app
  admin/        ← Next.js admin panel
  docs/         ← Documentation site (optional)

packages/
  ui/           ← Shared component library
  typescript-config/  ← Shared tsconfig
  eslint-config/      ← Shared ESLint rules
  database/     ← Drizzle schema + client
  utils/        ← Shared business logic utilities
  emails/       ← React Email templates

turbo.json
package.json

Not every project gets every folder. A project that's genuinely just one app for now gets apps/web and packages/ui and packages/typescript-config. The structure is ready to grow — it doesn't force growth.

The turbo.json That Works

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^lint"]
    },
    "type-check": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"]
    }
  }
}

The ^ prefix on dependsOn means "run this task in all dependencies first." So when you build the web app, Turborepo knows to build packages/ui first. You don't think about it — it just works.

The Shared UI Package Setup

This is the part that pays dividends fastest. In packages/ui/package.json:

{
  "name": "@company/ui",
  "exports": {
    ".": "./src/index.ts"
  },
  "scripts": {
    "dev": "tsc --watch",
    "build": "tsc",
    "lint": "eslint ."
  }
}

Export everything from a single index.ts:

// packages/ui/src/index.ts
export { Button } from "./components/button";
export { Card } from "./components/card";
export { Input } from "./components/input";
// ...

In any app:

import { Button, Card } from "@company/ui";

One component. One source of truth. Change it once, it updates everywhere. No publishing, no version bumps, no copy-paste drift.

The Shared Database Package

This is the other one that earns its keep immediately on multi-app projects. Instead of each app maintaining its own database connection and schema:

// packages/database/src/index.ts
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import * as schema from "./schema";

const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });

export * from "./schema";
export * from "./types";

Both the customer app and the admin panel import from @company/database. The schema is defined once. Types flow from the schema automatically via Drizzle's inference. If you add a column, both apps see it immediately after a turbo build.

The Caching That Makes It Fast

This is Turborepo's main value proposition beyond organization: it only rebuilds what changed.

After your first full build, Turborepo hashes every input file for every task. On the next run, if the inputs haven't changed, it restores the output from cache and skips the build entirely. In a large codebase this is the difference between a 45-second CI run and a 4-minute one.

With remote caching enabled (Vercel's remote cache is free for Turborepo):

npx turbo link

Cache hits are shared across your whole team and your CI pipeline. Developer A builds packages/ui. Developer B pulls the same commit — Turborepo restores Developer A's build artifact from the remote cache. Developer B never runs that build locally.

On client projects with CI/CD pipelines, this compounds quickly. I've seen CI times drop by 60–70% after enabling remote caching on projects that were already reasonably optimized.

What I Tell Clients

When I propose a Turborepo setup, clients sometimes push back. "We're just one app right now, isn't this over-engineering?"

My answer: the monorepo structure costs you almost nothing to set up at the start. It costs you a lot to migrate to later. The question isn't whether you'll eventually want shared code — you will. The question is whether you want to pay the migration cost when your codebase is small and simple, or when it's large and under pressure.

Every client I've made this argument to has thanked me for it. None of them have ever wished they'd waited.

The One Downside Worth Mentioning

Local development setup is slightly more complex. New developers on a project need to understand the workspace structure. The pnpm workspace setup, the internal package references, the turbo dev command instead of next dev — these are all learnable, but there's a ramp.

I mitigate this by writing a solid README in every monorepo I set up. One turbo dev command starts every app and package watcher simultaneously. One turbo build builds everything in the correct order. Most developers onboard in under an hour.

The complexity is real but bounded. The payoff is unbounded.

The Bottom Line

Turborepo is not a solution looking for a problem. Every project that grows — and client projects always grow — will eventually need shared code, shared types, shared configuration. Setting up the infrastructure for that from day one is a decision that looks like overhead in week one and looks like foresight in month six.

I reach for it every time. I've never regretted it.


Lazar Kapsarov is a frontend engineer at PrismaFlux Media. If you're starting a new project and want to get the architecture right from day one, book a free strategy call.

:: NEXT STEP

Have a frontend architecture problem?

Book a free 30-minute strategy call. I'll audit your current setup and show you exactly where you're losing time and money.

Book Free Strategy Call →