The Frontend Changed a Type and Production Broke: Monorepo for Vibe Projects
Three signals tell you it's time for a monorepo: one feature needs 3 PRs, type mismatches only explode in production, and the AI agent keeps missing spots. This guide gives you a copy-paste layout, Turborepo orchestration, shared-type practice, and deployment gotchas.

Scope first: what this guide does not cover
Three published guides touch repos and workflows — here's the division of labor:
- "Solo AI git workflow" answers how to branch — main-push vs PRs, reviewing AI-generated commit messages;
- "Dev environment standardization" answers how to unify local setups — devcontainers;
- "Minimal CI/CD pipeline" answers how code ships automatically — the 5 jobs.
This guide answers exactly one question: how many repos your code lives in, and how packages reference each other. Frontend, backend, shared components, DB schema — one repo managed with workspaces, or one repo per person? It's a structural choice that decides how development feels for the next year.
Three signals you need a monorepo
Don't adopt a monorepo because "big companies do it". Check these three signals — act when two hit:
- One feature needs 3 PRs across repos. A new field means one PR each in frontend, backend, shared types and docs, and the merge order matters — coordination cost has passed the monorepo learning cost;
- "Type mismatch" is a recurring cause of production bugs. The backend changed an API response, the frontend still has the old type, TypeScript passes in each repo — and it explodes on deploy;
- The AI agent keeps missing spots. Ask it to "make the username field optional" and it updates the frontend but forgets backend validation and the migration — because it can only see one repo at a time.
| Polyrepo | Monorepo | |
|---|---|---|
| Best for | Fully independent product lines, separate audit needs | Solo frontend + backend + shared packages |
| Atomic cross-package changes | No: N PRs + merge ordering | Yes: one commit changes everything |
| Type consistency | Synced via releases, always lagging | One tsc run checks all, fails on the spot |
| CI complexity | One pipeline per repo | One pipeline, but needs filters (below) |
| Mental overhead | Low (each repo is simple) | Medium (must understand workspace references) |
Directory layout: a skeleton you can copy
myapp/
├── apps/
│ ├── web/ # Next.js frontend
│ └── api/ # NestJS/FastAPI backend
├── packages/
│ ├── ui/ # shared React components
│ ├── db/ # Prisma schema + migrations + client
│ ├── schemas/ # zod validation schemas (shared FE/BE)
│ └── config/ # shared tsconfig / eslint
├── pnpm-workspace.yaml
├── turbo.json
└── package.json
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
Packages reference each other with the workspace protocol — the frontend's package.json declares "@myapp/ui": "workspace:*" and pnpm symlinks it. Edit packages/ui locally and apps/web picks it up instantly: no publishing, no npm-link voodoo.

Turborepo: run builds in dependency order
A monorepo build can't be cd apps/web && npm run build — when packages/db changes, api and web must rebuild too. Turborepo orchestrates exactly that:
// turbo.json
{
"$schema": "https://turborepo.dev/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"], // build dependencies first, then me
"outputs": ["dist/**", ".next/**"]
},
"db:migrate": {
"dependsOn": ["^db:generate"],
"cache": false // migrations always run for real
},
"dev": { "cache": false, "persistent": true },
"lint": { "dependsOn": ["^build"] }
}
}
^build means "run my dependencies' build first". From the root, turbo run build topologically sorts: config → db/schemas/ui → api/web, parallelizing independent tasks automatically. The bigger win is caching: untouched packages hit cache, so the second turbo run build takes seconds — priceless when an AI agent iterates on code all day.
Remote caching (Vercel Remote Cache) fits in the solo free tier; wire it into CI and halve build times again.
Shared types in practice: fail at compile time, not in production
This is the monorepo's biggest value for vibe projects. Three sharing points:
1. The DB schema is the type source. packages/db holds schema.prisma; the generated client is imported by both api and web. Change a column type:
// packages/db/package.json
{ "name": "@myapp/db", "exports": { ".": "./client.ts" } }
// packages/db/client.ts
import { PrismaClient } from '@prisma/client';
export const db = new PrismaClient();
export type { User, Post } from '@prisma/client';
2. One API contract for both sides. packages/schemas holds zod schemas — the backend validates input with them, the frontend derives form types:
// packages/schemas/src/post.ts
import { z } from 'zod';
export const CreatePostSchema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(1),
});
export type CreatePost = z.infer<typeof CreatePostSchema>;
// backend: app.post('/posts', zValidator(CreatePostSchema))
// frontend: const data: CreatePost = form.getValues() // types stay aligned
3. One tsc run at the root checks everything. turbo run typecheck runs tsc --noEmit across all packages. The backend changes a response field without updating the schema — the frontend's type errors surface alongside the backend's. Before commit, not after deploy.
Deployment: one gotcha per platform
Vercel (frontend apps/web): set Root Directory to apps/web, Build Command to cd ../.. && turbo run build --filter=web... (... includes dependencies). Add TURBO_REMOTE_CACHE_SIGNATURE_KEY for remote caching. Note: Vercel's monorepo support expects pnpm/npm/yarn workspaces — pin the version in the packageManager field.
Docker (backend apps/api): the biggest monorepo gotcha is the build context. The Dockerfile must live at the repo root (context = whole repo), then COPY precisely:
# Dockerfile.api at the repo root
FROM node:20-slim AS build
WORKDIR /app
# copy only api and its dependencies, keeping layer cache effective
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json turbo.json ./
COPY apps/api/package.json ./apps/api/
COPY packages/db/package.json ./packages/db/
COPY packages/schemas/package.json ./packages/schemas/
RUN npm i -g pnpm && pnpm install --frozen-lockfile \
--filter @myapp/api... # install api + deps only
COPY apps/api ./apps/api
COPY packages/db ./packages/db
COPY packages/schemas ./packages/schemas
RUN pnpm --filter @myapp/api build
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=build /app/apps/api/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER nonroot
ENTRYPOINT ["node", "dist/index.js"]
The key is --filter @myapp/api...: pnpm installs api and its dependencies only — not web's Next.js. COPY order follows the same layer-cache iron law: package.json first, source second (echoing this cycle's g1 Dockerfile guide).
CI: build only affected packages
A monorepo CI that builds everything on every push means 10 minutes for a one-line copy change. Turborepo filters fix that:
# .github/workflows/ci.yml excerpt
- run: pnpm turbo run build lint typecheck --filter=...[HEAD^]
# ...[HEAD^] = packages affected by this commit, plus dependents/dependencies
Only touched apps/web styles? CI runs web's build+lint and nothing else. Changed the schema in packages/db? api's and web's typechecks get pulled in automatically — the dependency graph decides the blast radius, not human memory. That's the monorepo's CI compounding: the more you change, the more you save… and the less you change, the faster it runs.
Releases: changesets lite for a team of one
Publishing packages to npm someday? Don't adopt the full changesets flow yet. The solo-sufficient setup: version: 0.0.0 placeholders everywhere, workspace-protocol references that don't care about versions. When the day comes to publish externally, run pnpm changeset init. Premature release process is the most expensive "process tax" one person can pay.
4 gotchas: forewarned is forearmed
- Phantom dependencies. pnpm isolates strictly — package A using package B's code without declaring it works under npm, explodes under pnpm. Fix: declare what's used where it's used; don't rely on hoisting luck.
- IDE jumps into .d.ts instead of source. packages'
exportspoint at dist, so Ctrl+click lands in build output. Fix: conditional exports viapublishConfig, or tsconfig paths pointing atsrcduring development. - AI agents get lost in the monorepo. A big repo means the agent guesses at file locations. Fix: a "package map" in the root
AGENTS.md— one line per package: what it is, when to touch it. A direct application of this site's AGENTS.md project-manual guide. - Secrets in shared packages. Someone puts a key-holding config file in packages/config and every app can read it. Fix: structure in the config package, secrets always via env vars (echoing g1's secret handling).
Pre-launch checklist
- ☐
pnpm installpasses in a clean environment; lockfile committed - ☐ Root
turbo run buildis green; second run hits cache (seconds) - ☐
turbo run typecheckcovers all packages, zero errors - ☐ Changing a packages/db field makes api's and web's typechecks fail (linkage verified)
- ☐ Vercel Root Directory / build filter configured; preview deploy succeeds
- ☐ Dockerfile at repo root;
--filterinstalls only what's needed - ☐ CI uses
...[HEAD^]; a web-only change doesn't build api - ☐ AGENTS.md has the package map so AI agents land in the right place
One line to close: a monorepo isn't "putting code together" — it's outsourcing cross-package consistency from human memory to the toolchain: tsc, turbo's dependency graph, workspace references. For a one-person-plus-AI team, that's structure traded for brainpower at a very good rate.
Comments (0)
Related articles

AI-generated Dockerfiles ship with five common defects: single-stage builds, root execution, no health checks, swallowed SIGTERM, and secrets baked into layers. This guide gives you multi-stage templates, a base-image decision table, production compose config, and a launch checklist.

Invoices turning into tofu on the customer's screen? This guide shows you how to pair Puppeteer Page.pdf for CJK layout with pdf-lib for merging and stamping — covering font subsetting, cross-page tables, headers and page numbers, chunked large-file generation, and sandbox hardening.

A user's 2,000 words must survive crashes, dead networks, and accidental tab closes. Three defensive layers — debounced input, localStorage safety net, versioned server persistence — plus optimistic-locking conflict UX (keep both, let the user choose), offline replay, and draft lifecycle.