I've been working on professional codebases for over 3 years and there's a pattern I find often.
The monorepository (monorepo). This pattern is frequently used in TypeScript codebases because it allows you to create shared types and utilities for all your projects.
But how do you deploy your projects and keep the monorepo maintainable? Using tools like pnpm workspaces and Turborepo.
Introducing Turborepo
Turborepo is a build system for TypeScript codebases used with pnpm workspaces to scale and manage monorepos. Its main benefit is the Remote Cache, meaning that your CI pipeline does not repeat tasks; saving computing hours and money as the codebase grows.
It also allows you to perform tasks scheduling easier, allowing you to lint, build and test each project separately.
Reasons to use a Monorepo
You might be wondering something similar to this:
"Cannot I just build everything with Next.js?"
For most small projects, that's a valid choice.
But what if:
- Your team does not want to use Vercel or OpenNext?
- They have multiple shared applications already?
- They prefer to separate the PWA from the landing page because of SEO?
- Your codebase is TypeScript heavy and you want to share Zod Schemas, interfaces and constants between your projects?
- You want more control over your infrastructure using S3 buckets?
There are reasons to use a monorepo with multiple clients. Understand the needs of your stakeholders, their budget and how each application will scale before considering introducing a tool like Turborepo.
With 2 client apps you're likely to duplicate dependencies, schemas and design systems. Let me tell you something, it won't scale well and you're introducing technical debt; because now you have to maintain the same thing in 2 different places! The monorepo solves this problem.
Benefits of a monorepo
- Prevents code duplication in your codebase
- Ensures consistency and clear CI/CD strategies
- Centralizes constants, design systems and schemas for validation
- Better Developer Expericence
- Makes it easier to work solo or with small teams
Creating your first Monorepo
We'll be building and deploying our first monorepo with Shadcn, Vite + React and Astro. The goal is simple, you'll be responsible for building a system with 2 layers:
- The React web application used by customers
- A shared design system built with Radix UI and Shadcn
This is a standard setup in modern startups where the the PWA serves a dashboard. With the shared design system, we can also create a static website attracts leads and eventually integrates blogs and legal pages for SEO.
Getting started
Let's first create a monorepo using Shadcn CLI. It'll initialize a Monorepo for us using Turborepo:
pnpm dlx shadcn@latest init --monorepo
Next, select a template, in this case I've selected Vite but feel free to use the one you prefer.
The Folder Structure
This is the folder structure the Shadcn CLI has created for us. Let me explain it to you the key folders and files because at first this was black magic to me:
/.turbo: Here we can find the cache from Turborepo builds. It's pretty similar to a
.nextfolder in Next.js applications./apps: This is the folder where your React and Astro applications and clients will be. I intentionally created the Astro
web-2app behind the scenes./packages: This is the most important folder of a monorepo. Think about this as a global
/sharedfolder for your application. We split folders here by their utility. By default, you'll only find the/uifolder. However, you can also add/constants,/utils,/schemas... You name it.turbo.json: The main configuration file of Turborepo. For this example, we don't need to make changes here.
pnpm-workspace.yaml: We let pnpm know that we've created a monorepo using workspaces. It ensures to orchestrate the dependencies of your projects accordingly. Here's how this file looks like:
packages:
- "apps/*"
- "packages/*"
Adding Components
Navigate to your application folder:
cd apps/web
Then install the components you need:
pnpm dlx shadcn@latest add
The components will be added automatically to your /packages/ui/components folder. But why?
If you're curious and analyze the components.json file in your /web app folder, you'll see something like this:
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "radix-nova",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "../../packages/ui/src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"hooks": "@/hooks",
"lib": "@/lib",
"utils": "@workspace/ui/lib/utils",
"ui": "@workspace/ui/components"
},
"rtl": false,
"menuColor": "default",
"menuAccent": "subtle"
}
It's telling the Shadcn CLI in what part of your workspace you are adding components and how.
As the documentation explicitly says, If you run npx shadcn@latest add button, the CLI will install the button component under packages/ui and update the import path for components in apps/web.
How does exporting components work?
In your /web project config, if you check your tsconfig file, you should have a file similar to this one after adding your components:
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@workspace/ui/*": ["../../packages/ui/src/*"],
}
}
}
Notice how it's using namespace for the /ui package to centralize the configuration. Yes, that's all the black magic; it's a custom alias for the package.
If your intellisense does not work well with this setup, verify your tsconfig.app.json, you can explicitly add the paths from tsconfig.json. It's a bit of code duplication but it's worth it.
Testing the monorepo
Let's go to our App.tsx file and add a button from our /ui package:
import { Button } from "@workspace/ui/components/button"
export function App() {
return (
<div className="flex min-h-svh p-6">
<div className="flex max-w-md min-w-0 flex-col gap-4 text-sm leading-loose">
<div>
<p>You may now add components and start building.</p>
<p>We've already added the button component for you.</p>
<Button className="mt-2">Button</Button>
</div>
<div className="text-muted-foreground font-mono text-xs">
(Press <kbd>d</kbd> to toggle dark mode)
</div>
</div>
</div>
)
}
As an important note, remember that your client application needs the dependencies from your packages. You cannot use Radix UI components in Astro if you have not added React support in your framework.
If you read the documentation of Shadcn monorepo and followed the steps properly, you should be able to see a screen like this!
Congratulations! You've opened a whole world of possibilities with monorepos. However, how do I deploy it?
Extra: Deploying your Monorepo
We can deploy it a Vite + React application using CloudFlare pages. You can use another provider if you wish.
Make sure you're deploying it on CloudFlare pages, not workers.
Your setup should look like this:
Here are two key things you need to consider:
Compilation command:
pnpm dlx turbo build --filter=web- It tells pnpm to use turborepo to build the/webapp. If your app is calledweb-2you'd use--filter=web-2instead.Build output directory:
/apps/web/dist- Where the output of the build is. Change this accordingly based on your project setup.
Framework preset is not so relevant because we're using custom commands for our monorepo.
Once we've waited for our basic CI/CD pipeline, our website has been deployed!
Conclusion
In this blog post you've learned what's a monorepo with TypeScript, why we use it and how to create one using Shadcn, Vite and React.
While this setup is basic, the main purpose was getting started with Turborepo.
Here are some ways to further improve this setup and challenge yourself:
- What if you could introduce a second app?
- What if you add constants and share them between multiple apps?
- Could you try implementing a staging environment in your deployments?
The only limit is your imagination. Now go and stop copying and pasting code. Build packages instead!




Top comments (0)