All posts

How I Setup My Monorepo

7 min read

If you’ve ever juggled multiple repositories for related projects, you know the pain of syncing dependencies, sharing code, and keeping tooling consistent. That’s where a monorepo shines. By housing all your apps, packages, and configurations under one roof, you get a single source of truth that simplifies dependency management and streamlines collaboration.

Now, why use pnpm and Turborepo 2.0? Simply put, pnpm makes managing dependencies across multiple projects way easier and faster. Turborepo then takes care of running your build and dev tasks efficiently, so you don’t waste time waiting.

Together, they keep your monorepo running smoothly without a lot of fuss—letting you focus on writing code instead of wrestling with tooling.

In this post, I'll share how I setup my monorepo with pnpm and Turborepo 2.0. We'll cover the following topics:

Monorepo Structure Overview

When working with a monorepo, organizing your codebase clearly is key to staying productive. My setup uses a straightforward folder structure that groups related projects and shared code into dedicated workspaces. This approach keeps everything tidy and makes it easy to find what you need without digging through a maze of files.

It obviously depends on the project, but here's an example:

app
app1
app2
app3
cms
cms1
cms2
config
eslint-config
prettier-config
typescript-config
packages
tailwind
types
ui-core
utils
prettier.rc
package.json
pnpm-workspace.yaml
turbo.json

apps/

The ⁠apps/ folder typically houses my frontend apps. Each app lives in its own subfolder, making it easy to develop, build, and deploy them independently while still benefiting from shared code and configurations elsewhere in the monorepo. If my project needs a more complex backend, I'll create a dedicated api/ workspace.

packages/

The ⁠packages/ folder contains shared code that can be used across multiple apps. This includes reusable components, utilities, and types.

config/

The ⁠config/ folder contains shared configuration files for ESLint, Prettier, and TypeScript. Usually, you'll find these files in the packages/ workspace, but I find it useful to have a dedicated folder for these files, as it makes it easier to manage them and avoid issues managing packages, particularly when some packages have to be published to npm.

cms/

The cms folder is not something I use in every project, but I had a project recently that needed two instances of a CMS (PayloadCMS in this case), so it came in handy to create a dedicated workspace for it.

Package Management & Dependency Alignment

Turborepo

Turborepo is a powerful build system designed specifically for monorepos. It efficiently manages and pipelines tasks like builds, linting, and testing, helping speed up your development workflow and keep everything running smoothly.

pnpm

pnpm is my package manager of choice because it installs dependencies quickly and uses disk space efficiently by sharing packages across workspaces. It enforces strict workspace dependency rules through the ⁠pnpm-workspace.yaml file, which defines your workspace boundaries and prevents accidental or undeclared dependencies from creeping in. Turborepo works seamlessly with pnpm, leveraging its workspace features to optimize builds and caching.

sherif

sherif is a tool that helps you manage your dependencies across your monorepo. It's a great choice for monorepos, as it makes managing dependencies across multiple projects way easier and faster.

Turborepo Configuration

Tasks

At the core of the monorepo’s automation is the ⁠turbo.json configuration file. It defines a set of tasks that cover everything from building and developing to linting and migrating databases. Here’s a quick look at the key tasks I typically use:

  • build: Builds all the apps and packages
  • dev: Starts all the apps in development mode
  • lint: Lints all the apps and packages
  • format: Formats all the apps and packages
  • format:check: Checks if the apps and packages are formatted correctly
  • migrate: Migrates the database
  • test: Runs all the tests
  • types:check: Runs the TypeScript type checker
  • types: Generates the TypeScript types (for Payload CMS or Supabase for example)

Sometimes, I'll also create a few root tasks, like //#lint:root or //#format:root to run the lint and format tasks at the root of the monorepo.

Environment Variables

Managing environment variables across multiple workspaces can get tricky, strict envMode requires all environment variables to be defined in the turbo.json file.

For this, I create a turbo.json file in each app/package and use the passThroughEnv option, which allows me to pass through environment variables to the tasks.

This strategy ensures environment variables are loaded correctly and consistently across all tasks and workspaces and while deploying on Vercel.

Here's an example of the turbo.json file in the app workspace:

apps/web/turbo.json
json
{ "extends": ["//"], "tasks": { "build": { "passThroughEnv": [ "SUPABASE_URL", "SUPABASE_ANON_KEY", "SUPABASE_SERVICE_ROLE_KEY", "NEXT_PUBLIC_SUPABASE_URL", "NEXT_PUBLIC_SUPABASE_ANON_KEY" ] } } }

Build Outputs

Organizing build outputs cleanly is essential for incremental builds and caching. Here’s how outputs are handled:

  • Next.js builds output to ⁠.next/**, excluding cache files to keep things lean.
  • Distribution files usually go into ⁠dist/**, making it easy to locate compiled packages.
  • Type definitions for CMS and Supabase packages are generated and stored separately, improving editor support and type safety.

This clear separation of build artifacts keeps your workspace tidy and helps Turborepo optimize build times by knowing exactly what changed.

Here are a few examples:

json
{ "build": { "dependsOn": ["^build"], "inputs": ["$TURBO_DEFAULT$", ".env.production.local", ".env.local", ".env.production", ".env"], "outputs": [".next/**", "!.next/cache/**", "dist/**"] } }

Tips

Utility tasks

A smooth developer experience is key to productivity, so this setup includes several handy utility tasks:

  • format: Formats all the apps and packages with Prettier
  • types:check: Runs the TypeScript type checker
  • lint: Lints all the apps and packages with ESLint
  • test: Runs all the tests (usually using Vitest or Jest)

Sometimes, I'll also create a webhooks task to listen to webhooks. For example, listen to Clerk's webhooks with ngrok to update the database when a user is created or updated.

Together, these tools reduce friction and let you focus on building features instead of fixing tooling problems.

Tailwind CSS

It is pretty common nowadays to use Tailwind CSS for styling. In a monorepo, it might make sense to have a shared Tailwind CSS configuration in the packages/tailwind workspace.

With v3 and below, the configuration file was tailwind.config.ts, which made it easy to share the configuration across the monorepo.

With v4, the configuration is now CSS-first (though backwards compatible with the old JavaScript configuration), so sharing the configuration is a bit different.

But you can create a package that will contain some shared CSS:

packages/tailwind/src/colors.css
css
:root { --color-primary: #000000; --color-secondary: #111111; }

Export it in your package.json:

packages/tailwind/package.json
json
"exports": { "./colors.css": "./src/colors.css" }

Then, you can import it in your app:

apps/app1/globals.css
css
@import 'tailwindcss'; @import '@repo/tailwind/colors.css';

Deployment on Vercel

Vercel can detect turborepo automatically, so you don't need to do anything special to deploy your app, apart from setting the Root Directory:

Image


This monorepo setup brings together clear structure, smart tooling, and smooth workflows to create a development environment that just works. By combining pnpm, Turborepo, sherif, and thoughtful config management, you get a scalable and maintainable codebase that keeps your team productive and your projects shipping faster. If you’re managing multiple apps or shared packages, this approach is definitely worth a try.


  • Monorepo
  • Turborepo
  • pnpm
  • Vercel
  • Tailwind CSS
Made with ❤️ in 🇨🇦 · Copyright © 2025 Valentin Prugnaud
Foxy seeing you here! Let's chat!
Logo