js

How to Build Scalable TypeScript Monorepos with Turborepo and Changesets

Learn how to streamline development with TypeScript monorepos using Turborepo and Changesets for faster builds and smarter versioning.

How to Build Scalable TypeScript Monorepos with Turborepo and Changesets

I’ve been thinking about this a lot lately. Every time I start a new project, I face the same problem: how do I manage multiple related applications and packages without losing my mind? If you’ve ever tried to coordinate updates between a frontend app, a backend API, and a shared component library, you know exactly what I mean. The constant context switching between repositories, the version conflicts, the duplicated code—it all adds up to frustration and wasted time.

That’s why I want to talk about building TypeScript monorepos with Turborepo and Changesets. This isn’t just another technical tutorial. This is about creating a development environment that actually works for you, not against you. Stick with me, and I’ll show you how to set up a system that handles the complexity so you can focus on building features.

Have you ever wondered why some teams seem to ship features faster while maintaining better code quality? Often, it comes down to their tooling and workflow. A well-structured monorepo can be that difference-maker.

Let’s start with the basics. A monorepo is simply a single repository that contains multiple packages or applications. Think about it: instead of having separate repos for your React app, your Node.js API, your shared utilities, and your documentation, you keep them all together. They share the same version control history, the same linting rules, and the same development workflow.

But here’s the catch: managing dependencies between these internal packages can get messy. That’s where Turborepo comes in. It’s a build system that understands how your packages relate to each other. It only rebuilds what’s changed, runs tasks in parallel, and caches results so you’re not constantly rebuilding from scratch.

Here’s a simple example of what your workspace configuration might look like:

# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'

This tells your package manager that anything in the apps or packages directories should be treated as a separate package. Each gets its own package.json, but they can all reference each other using workspace references.

Now, let’s talk about versioning. When you make a change to a shared package, how do you ensure all the apps that depend on it get updated correctly? This is where Changesets saves the day. It helps you manage version bumps and changelogs across multiple packages, without forcing everything to version in lockstep.

Consider this scenario: you update your shared UI components. With Changesets, you can specify that this is a “minor” change for the UI package, while keeping your API package at its current version. The system handles the version bumps and generates clear changelogs automatically.

Here’s how you might set up a shared TypeScript configuration package:

// packages/config/tsconfig.base.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "strict": true,
    "skipLibCheck": true,
    "esModuleInterop": true
  }
}

Every other package in your monorepo can extend this base configuration, ensuring consistency across your entire codebase. No more wondering if different packages are using different TypeScript settings.

What about actually building things? Turborepo’s configuration is surprisingly straightforward:

// turbo.json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": []
    },
    "lint": {
      "outputs": []
    }
  }
}

This configuration tells Turborepo that the build task depends on all dependencies being built first (that’s what ^build means). The test task depends on build completing first. And lint can run independently. When you run pnpm build from the root, Turborepo figures out the optimal order to build everything.

Let me share something I learned the hard way: always define clear boundaries between your packages. Each package should have a single, well-defined responsibility. Your UI package should only contain components. Your API package should only handle server logic. Your shared types package should only contain TypeScript interfaces.

Here’s an example of a well-structured types package:

// packages/types/src/user.ts
export interface User {
  id: string;
  email: string;
  name: string;
}

export interface CreateUserInput {
  email: string;
  name: string;
  password: string;
}

Every other package that needs user types can import them from @repo/types. No duplication, no synchronization issues, and type safety across your entire codebase.

Did you know that Turborepo can cache build outputs both locally and remotely? This means that if a teammate has already built a package with the same inputs, you can skip building it entirely. The time savings add up quickly, especially in CI/CD pipelines.

Setting up Changesets is equally straightforward. After installing it, you run pnpm changeset init to create a .changeset directory. When you’re ready to create a new version, you run pnpm changeset, which guides you through selecting which packages to version and what type of change you’re making (major, minor, or patch).

The real magic happens when you run pnpm changeset version. This command reads all the pending changesets, updates package versions, and generates changelogs. Then pnpm changeset publish publishes all the updated packages to your registry.

Here’s a practical tip: set up your root package.json scripts to handle common workflows:

{
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build",
    "test": "turbo run test",
    "changeset": "changeset",
    "version-packages": "changeset version",
    "release": "turbo run build && changeset publish"
  }
}

Now, developing across multiple packages feels natural. You run pnpm dev and all your apps start in development mode. You run pnpm build and everything builds in the correct order. You run pnpm release and your packages are versioned and published.

What happens when you need to add a new package? It’s simple. Create a new directory in packages/, run pnpm init inside it, and update its package.json to use workspace dependencies:

{
  "name": "@repo/new-package",
  "dependencies": {
    "@repo/types": "workspace:*",
    "@repo/ui": "workspace:*"
  }
}

The workspace:* syntax tells your package manager to use the local version of these packages during development, but it will be replaced with actual version numbers when published.

One question I often get: how do you handle environment variables in a monorepo? The answer is to keep environment-specific configuration at the app level, not the package level. Your shared packages should be environment-agnostic. If they need configuration, pass it in as parameters.

As your monorepo grows, you might wonder about performance. Turborepo handles this beautifully through its caching system. It hashes your source files, dependencies, and environment variables to determine if a task needs to be rerun. If nothing has changed, it uses the cached output.

The beauty of this setup is how it scales. Whether you have three packages or thirty, the workflow remains the same. New team members can get started quickly because everything is in one place. Code reviews are easier because related changes are grouped together. Refactoring is safer because you can update multiple packages in a single commit.

I want to leave you with this thought: the tools we choose shape how we work. A well-structured monorepo with Turborepo and Changesets isn’t just about technical convenience. It’s about creating a development experience where you can move quickly without breaking things, where you can share code safely, and where you can focus on solving business problems instead of tooling problems.

Give this approach a try in your next project. Start small with just two or three packages, and grow from there. The initial setup might take a couple of hours, but the time you’ll save in the long run is substantial.

What has your experience been with managing multiple related projects? Have you tried monorepos before, or are you considering them for future projects? I’d love to hear about your challenges and successes. Share your thoughts in the comments below, and if you found this helpful, please pass it along to someone who might benefit from it too.


As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!


📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!


Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Keywords: typescript monorepo,turborepo,changesets,monorepo setup,developer productivity



Similar Posts
Blog Image
Build High-Performance GraphQL APIs: Apollo Server, TypeScript & DataLoader Complete Tutorial 2024

Learn to build high-performance GraphQL APIs with Apollo Server 4, TypeScript & DataLoader. Master type-safe schemas, solve N+1 problems & optimize queries.

Blog Image
Build High-Performance GraphQL API with NestJS, TypeORM and Redis Caching

Learn to build a high-performance GraphQL API with NestJS, TypeORM & Redis. Master caching, DataLoader optimization, auth & monitoring. Click to start!

Blog Image
Build High-Performance GraphQL APIs with NestJS, Prisma, and Redis Cache - Complete Tutorial

Learn to build production-ready GraphQL APIs with NestJS, Prisma ORM, and Redis caching. Master authentication, DataLoader patterns, and real-time subscriptions for optimal performance.

Blog Image
Building Event-Driven Microservices with NestJS RabbitMQ and TypeScript Complete Guide

Learn to build scalable event-driven microservices using NestJS, RabbitMQ & TypeScript. Master sagas, error handling, monitoring & best practices for distributed systems.

Blog Image
How to Build a Scalable Video Conferencing App with WebRTC and Node.js

Learn how to go from a simple peer-to-peer video call to a full-featured, scalable conferencing system using WebRTC and Mediasoup.

Blog Image
How to Combine Fastify and Joi for Fast, Reliable API Validation

Learn how to integrate Joi with Fastify to create high-performance APIs with powerful, flexible validation rules. Boost speed and safety today.