Why Monorepos Are Worth the Setup in 2026
If your engineering team is managing a frontend app, a backend API, a shared component library, and a set of utility packages across separate repositories, you already know the pain: version drift, duplicated configs, broken cross-package changes that only surface in production, and a CI pipeline that takes forever because nothing is cached.
Monorepos solve this — and in 2026, Turborepo has become the go-to tool for TypeScript-first monorepos in the JavaScript ecosystem. It ships with remote caching, incremental builds, and a pipeline model that integrates cleanly with GitHub Actions, Vercel, and most cloud providers used by teams in Australia, Singapore, Canada, and the US.
This guide walks you through setting up a production-ready TypeScript monorepo from scratch using Turborepo 2.x, with shared packages, proper path aliases, and a working build pipeline.
What You'll Need
- Node.js 20+ (LTS recommended)
- pnpm 9+ — Turborepo works best with pnpm workspaces
- Basic familiarity with TypeScript and npm packages
- A terminal and a code editor (VS Code or Cursor work great)
Why pnpm? pnpm's workspace protocol handles cross-package dependencies cleanly and its symlink strategy avoids the phantom dependency problems that plague npm and Yarn hoisting.
Step 1: Initialise the Monorepo
Start by creating a new directory and initialising a pnpm workspace.
mkdir my-monorepo
cd my-monorepo
pnpm initNow create a pnpm-workspace.yaml at the root to declare where your packages live:
packages:
- 'apps/*'
- 'packages/*'Then install Turborepo as a dev dependency at the root:
pnpm add -D turbo -wCreate the top-level directory structure:
mkdir -p apps/web apps/api packages/ui packages/utils packages/tsconfigPro tip: Keep your apps/ directory for deployable applications and packages/ for shared internal libraries. This separation makes it much easier to apply different deployment strategies to each.
Step 2: Configure the Root TypeScript and Turborepo Settings
Root tsconfig
Create a tsconfig.json at the root. This is a base config your packages will extend:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}Shared tsconfig package
Inside packages/tsconfig, create a package.json:
{
"name": "@repo/tsconfig",
"version": "0.0.1",
"private": true,
"files": ["*.json"]
}Then create packages/tsconfig/base.json with your shared compiler options, and packages/tsconfig/nextjs.json and packages/tsconfig/node.json for app-specific variants. This pattern eliminates repeated TypeScript config across every app and package in your repo.
turbo.json
Create turbo.json at the root to define your pipeline:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"]
},
"typecheck": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
}
}
}The "^build" syntax tells Turborepo: before running build on this package, build all its dependencies first. This is what enables correct build ordering automatically.
Common pitfall: Forgetting to declare outputs breaks caching. Turborepo uses output hashing to decide whether a task needs to re-run. If your dist/ folder isn't listed, the cache will never restore it.
Step 3: Set Up a Shared UI Package
Navigate to packages/ui and initialise it:
{
"name": "@repo/ui",
"version": "0.0.1",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"lint": "eslint src/",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@repo/tsconfig": "workspace:*",
"typescript": "^5.7.0"
}
}Add a simple packages/ui/src/index.ts to export your components. Because this is declared as a workspace package, any app can import from it using @repo/ui — and pnpm handles the symlink automatically.
Step 4: Create an App and Wire Up Internal Dependencies
Inside apps/web, set up a Next.js 15 app (or any framework you prefer). Add your internal packages as dependencies using the workspace protocol:
{
"name": "web",
"dependencies": {
"@repo/ui": "workspace:*",
"@repo/utils": "workspace:*"
}
}Run pnpm install from the root. pnpm will resolve the workspace packages and link them locally. You can now import directly:
import { Button } from '@repo/ui'Pro tip: Use TypeScript path aliases sparingly in a monorepo. Let the exports field in each package's package.json do the heavy lifting instead — it's more explicit and plays better with bundlers like Vite, esbuild, and the Next.js compiler.
Step 5: Add a Root-Level Dev Script and Test the Pipeline
Add the following to your root package.json:
{
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"typecheck": "turbo run typecheck",
"test": "turbo run test"
}
}Run pnpm build from the root. Turborepo will analyse the dependency graph, build shared packages first, then build the apps that depend on them — in parallel where possible.
On the second run, you'll see the magic: tasks that haven't changed are restored from cache instantly. For a mid-sized team managing three or four apps, this alone can cut CI time by 60–70%.
Step 6: Enable Remote Caching
Local caching is great, but remote caching is what makes Turborepo exceptional for teams. Link your repo to Vercel's remote cache (free for most teams):
pnpm dlx turbo login
pnpm dlx turbo linkOnce linked, every developer on your team — and your CI runner — shares the same cache. If a colleague already built the @repo/ui package on their machine, you won't have to rebuild it in CI.
If you prefer a self-hosted option, Turborepo 2.x supports any S3-compatible storage backend as a custom remote cache endpoint, which matters for teams with strict data residency requirements in Singapore or Australia.
Common pitfall: Don't commit your .turbo directory or Turbo token to version control. Add .turbo to your .gitignore and store the TURBO_TOKEN as a CI secret.
Step 7: Wire Up GitHub Actions CI
Create .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
- run: pnpm typecheck
- run: pnpm lint
- run: pnpm testWith remote caching wired in, pull requests that only touch the apps/web package won't rebuild @repo/ui or @repo/utils — Turborepo skips them automatically.
Common Pitfalls to Avoid
- Circular dependencies: If
@repo/uiimports from@repo/utilsand vice versa, Turborepo's pipeline will deadlock. Keep shared packages strictly leaf nodes in your dependency graph. - Missing build outputs in turbo.json: Any file that a downstream package depends on must be listed as an output — otherwise caching will serve stale or empty builds.
- Using npm or Yarn hoisting with Turborepo: Both can produce phantom dependency issues that are difficult to debug. Stick with pnpm workspaces.
- Forgetting
--frozen-lockfilein CI: Without it, CI might silently upgrade packages and produce builds that don't match local development.
Next Steps
Once your monorepo is running smoothly, the next natural steps are:
- Add Changesets for versioning and changelogs if you plan to publish any packages to a private npm registry
- Set up Vitest with a shared config package for consistent unit testing across all workspaces
- Introduce ESLint 9's flat config at the root level so every package inherits the same linting rules without duplication
- Explore Turborepo's task graph visualiser (
turbo run build --graph) to audit your pipeline and catch bottlenecks early
If your team is starting a new product or migrating an existing multi-repo setup, getting the monorepo architecture right from the beginning saves weeks of rework later. At Lenka Studio, we've set up Turborepo-based monorepos for SaaS products and ecommerce platforms across Australia and Southeast Asia — and the consistency gains across frontend, backend, and shared tooling are immediately felt by the whole team.
If you're not sure whether a monorepo is the right architecture for where your product is headed, or you want a second opinion on your current setup, get in touch with the Lenka Studio team. We're happy to talk through the trade-offs with you.




