The Real Monorepo Problem Small Teams Face

You started with one repo. Then your SaaS grew: a marketing site, a customer dashboard, an API, maybe a mobile app. Now you have four repositories, four separate package.json files, and a shared Button component that has drifted into four slightly different versions. Every cross-cutting change — updating an auth utility, bumping a shared type definition — becomes a multi-repo pull request rodeo.

The obvious fix is a monorepo. But every guide you find assumes you have a platform engineering team, a Nx Cloud subscription, and the patience to configure Turborepo's remote caching before you ship another feature. You don't. You need something that works today, scales to tomorrow, and doesn't require a dedicated DevOps hire to understand.

This guide is for that team.


When a Monorepo Actually Makes Sense

Before restructuring anything, be honest about whether you have the right problem. A monorepo pays dividends when:

  • You share code across multiple apps — types, UI components, utility functions, API clients.
  • Your apps deploy independently but change together frequently.
  • Onboarding friction is rising because new engineers have to clone and configure five separate repos.
  • Dependency drift between apps is causing bugs that are hard to trace.

If you have one app and a separate landing page, a monorepo is probably premature. But if you are running a frontend, a backend API, a background job worker, and a shared design system — you are already paying the multi-repo tax.


A Pragmatic Folder Structure

You do not need a build tool to enforce monorepo conventions. Start with workspace-native tooling — npm workspaces, Yarn workspaces, or pnpm workspaces — and a clean folder contract.

my-saas/
├── apps/
│   ├── web/          # Next.js customer dashboard
│   ├── api/          # Express / Fastify backend
│   ├── worker/       # Background job processor
│   └── marketing/    # Astro or Next.js landing site
├── packages/
│   ├── ui/           # Shared React component library
│   ├── types/        # Shared TypeScript interfaces & enums
│   ├── utils/        # Pure utility functions (no framework deps)
│   ├── config/       # Shared ESLint, tsconfig, Tailwind config
│   └── db/           # Prisma schema + generated client
├── tooling/
│   └── scripts/      # Repo-wide maintenance scripts
├── package.json       # Root workspace definition
└── pnpm-workspace.yaml

The key discipline here is the apps vs packages split. Apps are deployable artifacts — they produce a build output that ships to a server or CDN. Packages are internal libraries — they are consumed by apps but never deployed on their own. Keeping these concerns separated prevents the most common monorepo mistake: letting apps import directly from other apps, which creates invisible coupling.


Shared Package Strategy

Keep packages small and focused

A shared utils package that becomes a dumping ground for everything is just a distributed mess. Aim for packages with a single clear responsibility. @myapp/types should only contain TypeScript types. @myapp/ui should only contain React components. When a package starts importing from too many sibling packages, that is a signal to rethink the boundary.

Version packages internally with workspace references

Use workspace protocol references ("@myapp/ui": "workspace:*") rather than publishing packages to npm. For a small SaaS team, private npm publishing adds overhead without benefit. Every app always pulls from the latest local source, which is exactly what you want during fast iteration.

Treat packages/config as a first-class citizen

Shared tooling config is tedious to maintain across apps but critical for consistency. Create a packages/config package that exports base tsconfig.json, eslint configs, and Tailwind presets. Each app extends these rather than defining them from scratch.

// apps/web/tsconfig.json
{
  "extends": "@myapp/config/tsconfig.base.json",
  "compilerOptions": {
    "outDir": "dist"
  },
  "include": ["src"]
}

CI Configuration That Won't Punish You

The biggest CI mistake in a monorepo is running every test suite on every commit regardless of what changed. On a small team this is tolerable early on, but it becomes a velocity killer fast.

Affected-only pipelines without a build orchestrator

You do not need Turborepo or Nx to get basic change detection. Most CI platforms — GitHub Actions, GitLab CI, CircleCI — support path filters natively.

# .github/workflows/api.yml
on:
  push:
    paths:
      - 'apps/api/**'
      - 'packages/utils/**'
      - 'packages/types/**'

Maintain one workflow file per app, each listing the app folder and its direct package dependencies in the paths filter. It is manual, but it is auditable and requires zero new tooling.

When to graduate to Turborepo or Nx

Turborepo is the right next step when your CI times are climbing above 15–20 minutes and manual path filters are becoming hard to maintain. It introduces a task graph that understands dependencies between packages and caches outputs intelligently — both locally and remotely.

Nx is the right choice when your organisation grows to the point where you need code generation, module boundary enforcement, and a migration system. Its power comes with a steeper learning curve and a more opinionated project structure.

Neither tool is wrong. The mistake is reaching for them before you have felt the pain they solve.


Common Pitfalls to Avoid

  • Circular dependencies between packages. packages/ui should not import from packages/db. Draw a strict dependency graph and enforce it.
  • Storing secrets per-app without a root strategy. Define a single .env convention early. A root .env.example documenting all variables across all apps saves enormous onboarding time.
  • Ignoring lock file conflicts. In a pnpm workspace, a single lock file governs all dependencies. Dependency conflicts surface earlier and more loudly — treat that as a feature, not a bug.
  • Merging apps and packages into one directory. The moment a developer has to read a README to know whether something is deployable or a library, you have lost the structural signal that makes the monorepo legible.

Why This Matters for Your Project

A well-structured monorepo is not an academic exercise — it directly compresses the time between idea and deployment. When your shared types live in one place, a breaking API change surfaces as a TypeScript error across every affected app before it reaches production. When your CI only runs what changed, pull requests merge faster and engineers stay in flow. For SaaS founders and small engineering teams trying to ship fast without accumulating structural debt, getting this foundation right early is one of the highest-leverage investments you can make.