DEV Community

Cover image for Getting Started with Monorepos - Vite, React and Shadcn
Francisco Luna
Francisco Luna

Posted on

Getting Started with Monorepos - Vite, React and Shadcn

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

Turborepo 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 .next folder 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-2 app behind the scenes.

  • /packages: This is the most important folder of a monorepo. Think about this as a global /shared folder for your application. We split folders here by their utility. By default, you'll only find the /ui folder. 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/*"
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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/*"],
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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&apos;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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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!

Sample Vite application using Turborepo

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:

Deploying a turborepo app

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 /web app. If your app is called web-2 you'd use --filter=web-2 instead.

  • 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!

Turborepo website 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)