← Blog
explainx / blog

What Are Monorepos? A 101 Guide for JavaScript, Python, Next.js, AI Agents, and MCP (2026)

Monorepos explained: one repo, many packages. Compare tools (npm, pnpm, Bun, Turborepo, Nx, Poetry, uv), with Next.js, Python, agent skills, and MCP server examples plus authoritative references.

15 min readYash Thakker
MonorepoJavaScriptPythonNext.jsTurborepoAI AgentsMCPDeveloper Tools

MDX restores the committed source plus an HTML comment attribution; plain text bundles the rendered markdown body with the explainx.ai attribution footer.

What Are Monorepos? A 101 Guide for JavaScript, Python, Next.js, AI Agents, and MCP (2026)

Most production software is not one folder and one package.json. It is a marketing site, a dashboard, shared database types, a Python MCP server, and a folder of agent skills—all shipping together. A monorepo (monolithic repository) keeps those pieces in one Git tree so you can change an API and every consumer in the same commit.

This guide is Monorepos 101: definitions, when they help, how JavaScript, Next.js, and Python setups differ, and how AI agents and MCP servers fit the layout—with references and copy-paste examples. Tooling stats and version notes below were checked against primary sources (Turborepo, Nx, MCP TypeScript SDK, monorepo.tools) in May–June 2026.


Key takeaways

  1. Monorepo ≠ monolith. You still deploy separate apps; you version them together.

  2. Workspaces (npm, pnpm, Yarn, Bun) wire local packages with workspace:* dependencies—foundation of JS monorepos.

  3. Orchestrators (Turborepo, Nx, Bazel) add caching and task graphs; optional until CI time hurts.

  4. Next.js apps are usually apps/web-style packages that depend on shared packages/* libraries.

  5. Python often lives in parallel (externals/, services/) with Poetry or uv workspaces—not merged into npm.

  6. Agent skills are repo-level instructions (.agents/skills); MCP servers are separate processes agents call at runtime.

  7. Trade-off: faster refactors and shared CI vs. repo size, permission complexity, and slow builds without caching.


TL;DR

QuestionAnswer
What is it?One repo, many packages/projects
Why use it?Atomic changes, shared types/tools, one CI pipeline
JS minimumRoot package.json + workspaces + internal packages
Next.js patternapps/web imports packages/db, packages/ui, etc.
Python patternpyproject.toml per service or uv workspace at root
AgentsSkills at repo root; harness reads them per session
MCPStandalone TS/Python package; configured in Cursor/Claude, not bundled into Next
Learn morepnpm workspaces, Turborepo docs, uv workspaces, monorepo.tools

What is a monorepo?

A monorepo is a version-control strategy: multiple software projects live in one repository, typically with:

  • A root that defines tooling (lint, test, CI)
  • Apps — deployable surfaces (apps/web, apps/api)
  • Packages — shared libraries (packages/db, packages/ui)
  • Sometimes tools, docs, infra, and externals (vendored or git-subtree code)

The term comes from contrast with polyrepo (many repos, one product per repo). Google’s public engineering literature and tools like Bazel popularized monorepos at extreme scale; startups and mid-size teams often use npm/pnpm workspaces + Turborepo for a lighter version of the same idea.

Nx’s monorepo.tools defines a monorepo as one repository with multiple distinct projects and well-defined relationships—not mere “code colocation” and not a single undivided giant app. That framing matters in 2026 because repo boundaries are walls for AI agents as much as for humans: polyrepos force you to re-explain APIs and types at every edge; monorepos let agents read implementations directly.

Citation is not deployment. Shipping apps/web to Vercel does not mean you ship externals/python-substack to the same host—you still build and deploy each artifact; the monorepo only guarantees they evolve together in Git.


Monorepo vs polyrepo

DimensionMonorepoPolyrepo
Cross-package refactorOne PR updates API + all callersCoordinate releases across repos
Shared codepackages/* imported via workspace protocolPublish to npm/PyPI or duplicate
CIOne pipeline; affected-only with Nx/TurboPer-repo pipelines
OwnershipNeeds CODEOWNERS / path rulesRepo boundary = team boundary
Clone sizeGrows with everythingSmaller per repo
Access controlFiner-grained in Git hostingRepo-level permissions

Good monorepo fit: one product family, shared types, agents + web + MCP maintained by overlapping teams.

Good polyrepo fit: legally separate products, wildly different release cycles, or open-source libraries with external contributors who should not see internal apps.


Anatomy of a modern product monorepo

A layout common in Next.js + agents + MCP products (including directories like ExplainX that combine web apps, Prisma packages, and Python tooling):

my-product/
├── apps/
│   ├── web/              # Next.js marketing / app
│   ├── newsletter/       # Second Next.js or API app
│   └── learn/            # Another surface
├── packages/
│   ├── db/               # Prisma client + schema
│   ├── ui/               # Shared React components
│   └── config-eslint/    # Shared lint config
├── externals/
│   └── python-substack/  # Python lib + MCP server
├── .agents/
│   └── skills/           # Agent SKILL.md bundles
├── .claude/
│   └── skills/           # Mirror for Claude Code
├── chrome-apps/          # Extension (optional)
├── scripts/              # Repo-wide automation
├── package.json          # JS workspace root
└── .github/workflows/    # CI

Rules of thumb:

  • apps/* — things users or customers run in production browsers/services.
  • packages/* — libraries apps depend on; never import apps/web from packages/db.
  • externals/* — Python, forks, or submodules that use their own pyproject.toml.
  • .agents/skills — not npm packages; consumed by Cursor, Claude Code, or compatible harnesses (agent skills guide).

JavaScript and TypeScript monorepos

npm / pnpm / Yarn / Bun workspaces

All major JS package managers support workspaces: multiple package.json files linked locally.

Root package.json (Bun-style workspaces):

{
  "name": "my-product",
  "private": true,
  "workspaces": ["apps/*", "packages/*"],
  "scripts": {
    "dev": "bun run --filter '@my-product/web' dev",
    "build": "bun run --filter '*' build"
  }
}

Consumer app (apps/web/package.json):

{
  "name": "web",
  "private": true,
  "dependencies": {
    "db": "workspace:*",
    "ui": "workspace:*"
  }
}

The workspace:* protocol resolves db to packages/db on disk—no publish step during development.

pnpm uses pnpm-workspace.yaml (required at the root—pnpm does not use only package.json workspaces):

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

# Optional: centralize dependency versions (pnpm 9+)
catalog:
  react: "19.2.4"
  next: "16.2.2"
  typescript: "^5.8.0"

Then reference catalogs from member package.json files with "react": "catalog:" to avoid version drift across apps.

Filter scripts without Turbo: pnpm --filter web dev runs one package; pnpm -r build runs build in all workspace members in dependency order.

References: npm workspaces, pnpm workspaces, pnpm catalogs, Bun workspaces.

Shared package example (packages/db)

{
  "name": "db",
  "version": "0.0.0",
  "type": "module",
  "exports": {
    ".": "./src/index.ts"
  },
  "scripts": {
    "db:generate": "prisma generate",
    "postinstall": "prisma generate"
  },
  "dependencies": {
    "@prisma/client": "^6.1.0",
    "prisma": "^6.1.0"
  }
}
// packages/db/src/index.ts
import { PrismaClient } from "../generated/client";

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };

export const prisma =
  globalForPrisma.prisma ?? new PrismaClient();

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}

export * from "../generated/client";
// apps/web/app/api/skills/route.ts
import { prisma } from "db";

export async function GET() {
  const skills = await prisma.skill.findMany({ take: 10 });
  return Response.json(skills);
}

One schema change in packages/db → regenerate client → TypeScript errors in every app that imports db in the same PR. That is the monorepo superpower.

Turborepo and Nx (orchestration)

When build, test, and lint run across ten packages, naive npm run build --workspaces rebuilds everything every time.

Turborepo (vercel/turborepo) adds a task graph, local/remote caching, and content-addressed hashing so unchanged packages skip work. Public signals as of May 2026: roughly 30,000+ GitHub stars, homepage at turborepo.dev, active 2.9.x release line. Practitioner write-ups report large CI time reductions after migrating to Turborepo 2.x—treat as directional, not a guarantee on your graph.

Important: Turborepo 2.x renamed the top-level config key from pipeline to tasks. Older tutorials still show pipeline; that fails schema validation on 2.x. Migrate with:

npx @turbo/codemod migrate
{
  "$schema": "https://turborepo.dev/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    },
    "lint": {},
    "test": { "dependsOn": ["^build"] }
  }
}

The ^build microsyntax means “run build in dependencies first”—exactly what you want when apps/web imports packages/db.

Nx (nrwl/nx) adds affected detection, generators, and optional Nx Cloud CI distribution. As of May 2026: roughly 28,500 GitHub stars, ~9M weekly npm downloads on the core nx package (per npm registry stats), latest stable around 22.7.x. Nx’s own positioning in 2026 explicitly includes AI agents—caching and affected runs matter when agents trigger full-repo builds more often.

Open-source agent workflow repos such as gstack (covered in our gstack deep dive) use Nx-style layouts for slash commands and shared tooling.

When to add orchestration: CI exceeds ~10 minutes, or developers routinely skip building packages they did not touch (and break main). Many small teams stay on pnpm -r alone until pain appears—that is a valid 2026 pattern (pnpm production monorepo notes).


Next.js in a monorepo

Next.js does not require a monorepo, but monorepos are the default for teams running multiple Next apps (marketing + logged-in product) plus shared UI.

Minimal Next app package

{
  "name": "web",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "next": "16.2.2",
    "react": "19.2.4",
    "react-dom": "19.2.4",
    "db": "workspace:*"
  }
}

transpilePackages for workspace libraries

Next.js can compile dependencies from local packages or node_modules via transpilePackages (stable since v13.0.0 per Next.js docs).

You strictly need it when:

  • The workspace package is resolved as a symlinked node_modules boundary and Next would otherwise skip transpilation
  • The package imports CSS or assets that trigger “Global CSS cannot be imported from within node_modules”
  • You export raw TS/TSX without a separate build step and the bundler treats the package as external

You may not need it when the bundler resolves the package like local source (common in some Turborepo/Nx setups with project references)—a Next.js discussion (#93542) documents both behaviors.

// apps/web/next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  transpilePackages: ["ui", "db"],
};

export default nextConfig;

Turbopack note (2026): monorepo + subpath exports in internal packages can behave differently under Turbopack vs Webpack; if internal packages fail to resolve, check Next.js issue #85315 and fall back to --webpack until your graph is supported.

Environment variables

Each app keeps its own .env.local. Database URLs usually live in apps/web and apps/learn, while packages/db reads process.env.DATABASE_URL at runtime—document which app owns which secret in README or AGENTS.md so coding agents do not guess wrong.

Anti-pattern: importing upward

// ❌ packages/ui importing from apps/web
import { Header } from "../../apps/web/components/Header";

// ✅ move shared component into packages/ui
import { Header } from "ui";

Dependency direction should always flow appspackages, never the reverse.


Python in a monorepo

Python does not use npm workspaces. Common patterns:

PatternToolsUse when
Per-service pyproject.tomlPoetry, setuptoolsOne MCP server or API per folder
uv workspaceuvMultiple Python packages, one lockfile
Requirements + venvpipScripts only; weakest for multi-package

Poetry package with optional MCP extras

Real-world MCP servers often declare MCP dependencies as an optional group so library users are not forced to install FastMCP:

# externals/python-substack/pyproject.toml
[tool.poetry]
name = "python-substack"
version = "0.1.22"
packages = [{ include = "substack" }]

[tool.poetry.dependencies]
python = "^3.10"
requests = "^2.32.0"
python-dotenv = "^1.2.1"

[tool.poetry.group.mcp]
optional = true

[tool.poetry.group.mcp.dependencies]
fastmcp = "^3.1.1"

Install for MCP development:

cd externals/python-substack
poetry install --with mcp

Minimal FastMCP server in the same tree

# externals/python-substack/substack_mcp/mcp_server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("substack")


@mcp.tool()
async def post_draft_from_markdown(title: str, body_markdown: str) -> dict:
    """Create a draft post from markdown."""
    # call into substack.api ...
    return {"status": "draft_created", "title": title}


if __name__ == "__main__":
    mcp.run()

Reference: Model Context Protocol Python SDK, FastMCP.

uv workspace (multi-package Python)

For several Python libraries in one repo:

# pyproject.toml (repo root)
[tool.uv.workspace]
members = ["packages/py-common", "services/ingest", "services/mcp-bridge"]
# services/mcp-bridge/pyproject.toml
[project]
name = "mcp-bridge"
dependencies = ["py-common", "mcp>=1.0"]

[tool.uv.sources]
py-common = { workspace = true }

Reference: uv workspaces.

Caveats from official uv docs: workspaces share one uv.lock and an intersected requires-python across members—if one service needs Python 3.14 and another caps at 3.11, use path dependencies with separate envs instead of one workspace. Large open-source examples (e.g. Apache Airflow’s multi-package tree, discussed on Talk Python #540) use uv workspaces for per-package uv sync so import boundaries match declared dependencies.

CI: two languages, one pipeline

# .github/workflows/ci.yml (illustrative)
jobs:
  js:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
      - run: bun install
      - run: bun run build
      - run: bun test

  python-mcp:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v5
      - run: uv sync --directory services/mcp-bridge
      - run: uv run pytest services/mcp-bridge

Path filters (paths: ['apps/**', 'packages/**']) avoid running Python jobs on doc-only commits.


AI agents in a monorepo

Agent skills are not npm packages. They are markdown instruction bundles (often SKILL.md + references) that Claude Code, Cursor, or compatible harnesses load when a task matches.

Typical layout:

.agents/skills/
├── blog-writing/
│   ├── SKILL.md
│   └── templates/
├── seo-geo/
│   └── SKILL.md
└── frontend-design/
    └── SKILL.md

Why monorepo + skills:

  • Same repo the agent edits gets the same conventions (blog frontmatter, Prisma patterns, MCP import scripts).
  • One PR can update a skill and the code it describes.
  • Discovery: npx skills add and registries (ExplainX skills) distribute skills; in-repo copies stay pinned to your branch.

Example skill frontmatter agents read:

---
name: blog-writing
description: Write SEO/GEO-optimized blog posts for explainx.ai following established patterns.
---

# Blog Writing Skill

Content location: `/apps/web/content/blog/`

Agent harness note: Terminal agents (OpenClaw, Claude Code, etc.) benefit from monorepos because search and grep span the whole product—but large repos need .cursorignore / context rules so agents do not load node_modules or generated Prisma clients into context. Tools like incremental indexes (CocoIndex) target exactly this “living monorepo” problem.

2026 industry framing: monorepo.tools and Nx’s “polyrepo tax” narrative emphasize atomic cross-project PRs, single-source shared libraries, and agent-visible context across frontend, backend, and tools—without claiming every company should merge all repos tomorrow. Synthetic monorepos (dependency graphs across separate Git repos) are an incremental option when full consolidation is blocked.


MCP servers in a monorepo

Model Context Protocol (MCP) standardizes how hosts (Cursor, Claude Desktop, IDE extensions) call tools and read resources from external processes.

MCP servers in a monorepo are usually:

  1. TypeScript — official SDK in packages/mcp-foo or tools/mcp-foo
  2. Python — FastMCP or the Python SDK in externals/* or services/*
  3. Not embedded in Next.js — they run as stdio/SSE processes configured in the IDE

TypeScript MCP SDK versions (mid-2026)

The modelcontextprotocol/typescript-sdk repo (~12,500+ stars as of May 2026) documents two generations:

TrackPackagesStatus
v1.x (v1.x branch)@modelcontextprotocol/sdk + zod peerRecommended for production; latest stable around v1.29.0 (Mar 2026)
v2 (main branch)Split: @modelcontextprotocol/core, client, server; Node 20+, ESM-onlyPre-stable; migration guide

Use v1 in monorepo MCP packages until v2 GA unless you are explicitly experimenting.

TypeScript MCP server package sketch (v1)

{
  "name": "@my-product/mcp-github",
  "type": "module",
  "bin": { "mcp-github": "./dist/index.js" },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.29.0",
    "zod": "^3.25.0"
  }
}
// packages/mcp-github/src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: "github", version: "1.0.0" });

server.tool(
  "list_prs",
  { repo: z.string().describe("owner/name") },
  async ({ repo }) => {
    const prs = await fetchPRs(repo);
    return { content: [{ type: "text", text: JSON.stringify(prs, null, 2) }] };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

Install per official README: npm install @modelcontextprotocol/sdk zod.

Cursor / Claude config at repo root

{
  "mcpServers": {
    "github": {
      "command": "bun",
      "args": ["run", "--filter", "@my-product/mcp-github", "start"]
    },
    "substack": {
      "command": "poetry",
      "args": ["run", "python", "-m", "substack_mcp.mcp_server"],
      "cwd": "${workspaceFolder}/externals/python-substack"
    }
  }
}

Separation of concerns:

LayerRole
Next.js apps/webHuman UI, public APIs, SEO
Agent skillsHow the model should write/review code
MCP serversLive tools (DB, GitHub, Substack, browsers)
packages/dbShared data access for apps—not a substitute for MCP unless exposed via a server

Browse real server patterns in the ExplainX MCP directory and introducing MCP on ExplainX.


Versioning and publishing internal packages

Not every package in a monorepo ships to npm. Apps stay private: true. Libraries you publish need explicit versioning.

@changesets/cli is the common JS flow:

  1. Developer runs changeset and describes bump (patch / minor / major).
  2. Changesets accumulate under .changeset/.
  3. On release, CI versions packages and updates changelogs.
# One-time setup at repo root
bun add -D @changesets/cli
bun changeset init
<!-- .changeset/brave-lions.md -->
---
"db": patch
---

Fix connection pool defaults for serverless builds.

Lerna and Beachball solve similar problems; greenfield repos in 2026 usually pick Changesets + pnpm or Changesets + Turborepo.

Python: publish packages/py-common to PyPI independently with uv publish or Poetry poetry publish; tag releases in Git matching the monorepo tag py-common-v0.4.0 if multiple artifacts leave one repo.


Deploying from a monorepo

Each app is still one deployable unit. Platforms read a root directory or filter:

PlatformPattern
VercelProject root = apps/web; install from repo root with cd ../.. && bun install
DockerMulti-stage build: copy root lockfile → install → bun run --filter web build
Fly.io / Railwaydockerfile per app or monorepo-aware buildpack

Illustrative Dockerfile (Next app in workspace):

FROM oven/bun:1 AS deps
WORKDIR /app
COPY package.json bun.lock ./
COPY apps/web/package.json apps/web/
COPY packages/db/package.json packages/db/
RUN bun install --frozen-lockfile

FROM deps AS builder
COPY . .
RUN bun run --filter web build

FROM node:22-slim AS runner
WORKDIR /app
COPY --from=builder /app/apps/web/.next/standalone ./
ENV NODE_ENV=production
CMD ["node", "server.js"]

MCP and Python services deploy as separate containers or run on developer machines—do not co-locate with Next unless you operate a process supervisor (usually unnecessary).

Reference: Vercel monorepo docs.


Security and boundaries in agent-heavy monorepos

Monorepos plus agents + MCP increase blast radius if secrets leak into model context.

PracticeWhy
Per-app .env.localLimits what a compromised apps/web route exposes
.cursorignore / .gitignore for .env*Stops agents indexing secrets
Scoped MCP toolsGitHub MCP token with read-only PR scope, not org admin
CODEOWNERS on packages/db/prismaSchema changes need data-team review
No production DB URLs in skillsSkills are markdown—treat as public

MCP security and agent skill verification apply unchanged; monorepos concentrate assets, so access control moves to path-based review.


Putting it together: one feature across the stack

Goal: Add “export skills to CSV” — touches DB schema, web API, docs, agent skill, optional MCP tool.

StepLocationAction
1packages/db/prisma/schema.prismaAdd SkillExport model
2packages/dbprisma migrate dev + commit generated client
3apps/web/app/api/...Route using prisma from db
4apps/web/content/blog/...Document feature (BLUF + FAQ)
5.agents/skills/...Update skill: “run export via API route X”
6packages/mcp-skills (optional)MCP tool wrapping same API for Claude

One pull request, one CI run, reviewers see the full vertical slice—impossible to coordinate that cleanly across five polyrepos without release choreography.


Tooling cheat sheet

ToolEcosystemPrimary benefit
npm/pnpm/Yarn/Bun workspacesJS/TSLink local packages
TurborepoJS/TSCache + task pipeline
NxJS/TS (+ plugins)Affected graph, generators
PoetryPythonLockfile + optional dependency groups
uvPythonFast installs + workspaces
Bazel / PantsPolyglotHermetic builds at huge scale
Changesets / BeachballJSVersioning + changelogs per package
Lerna (legacy)JSPublishing; less common for app monorepos

Ecosystem scale (verified May–June 2026):

ProjectSignal
vercel/turborepo~30,100 GitHub stars; docs at turborepo.dev
nrwl/nx~28,500 GitHub stars; ~9.1M/week npm downloads on nx
modelcontextprotocol/typescript-sdk~12,500 GitHub stars; v1.x for production MCP servers
monorepo.toolsCurated monorepo education (Nx-maintained)

Choose orchestration when pain appears, not on day one.


Common mistakes

MistakeFix
Circular deps between packagesEnforce apps → packages only; extract shared module
Checking in node_modulesRoot lockfile + CI cache
One global .env for all appsPer-app env files + documented ownership
Bundling MCP into Next serverRun MCP as separate process; use API if needed from web
Agents read whole repoIgnore generated output, .next, node_modules
Python + JS same package nameNamespace: @org/web vs org_py_common
No affected CIPath filters or Turbo/Nx affected

When to start (and when to split)

Start a monorepo when:

  • You have 2+ deployables sharing types, auth, or design system
  • You ship agents + product and want skills aligned with code
  • You maintain MCP tools beside the app that consumes their data

Split or stay polyrepo when:

  • Teams need independent version lines with no coordination
  • Open-source boundary must not leak private apps
  • Repo clone / CI cost exceeds team capacity to invest in caching

Related reading

External references: Google monorepo engineering (overview) · monorepo.tools · pnpm · Turborepo · Nx · uv · MCP specification · MCP TypeScript SDK v1.x


Summary

A monorepo is one Git repository holding many related packages—Next.js apps, shared TypeScript libraries, Python MCP servers, and agent skills—so you can refactor across boundaries in a single change. JavaScript workspaces link apps/* to packages/*; Python typically uses separate pyproject.toml files or uv workspaces under externals/ or services/. Next.js consumes shared packages via workspace:* and transpilePackages. Agents read repo-level skills; MCP runs as separate stdio servers configured in the IDE, not inside the Next bundle.

Pick workspaces first, add Turborepo or Nx when CI hurts, and keep dependency arrows pointing inward toward shared packages. That layout is how modern AI-native product teams ship web, data, tools, and coding agents from one place.

Tool versions (Next.js 16.x, Prisma 6.x, FastMCP 3.x, MCP SDK v1.29.x, Turborepo 2.9.x, Nx 22.7.x) and star/download figures were checked against upstream docs and registries in May–June 2026; re-verify before production upgrades.

Related posts