QuizApp Monorepo Skill
When to Use
Use this skill when:
- •Setting up the initial monorepo structure
- •Adding a new package to the workspace
- •Configuring cross-package dependencies
- •Setting up Turborepo task pipelines
- •Debugging workspace resolution issues
- •Running commands in specific packages
Critical Patterns
Workspace Structure
ALWAYS follow this structure:
quizapp/ ├── frontend/ # Next.js app ├── backend/ # Express API ├── shared/ # Shared contracts/schemas ├── infra/ # Docker configs (not a pnpm package) ├── pnpm-workspace.yaml # Workspace config ├── turbo.json # Turborepo config └── package.json # Root scripts
pnpm Workspace Configuration
File: pnpm-workspace.yaml
packages: - "frontend" - "backend" - "shared"
NEVER:
- •Include
infra/in packages (it's not a pnpm package) - •Use wildcards like
packages/*(be explicit) - •Include
node_modulesor.git
Root package.json
ALWAYS include these scripts:
{
"name": "quizapp",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint",
"format": "turbo run format",
"format:check": "turbo run format:check"
},
"devDependencies": {
"turbo": "^2.0.0"
}
}
Why:
- •Single entry point for all common tasks
- •Turborepo orchestrates execution across packages
- •
private: trueprevents accidental publishing
Package Naming Convention
ALWAYS use workspace protocol for internal dependencies:
// In frontend/package.json
{
"name": "frontend",
"dependencies": {
"shared": "workspace:*"
}
}
// In backend/package.json
{
"name": "backend",
"dependencies": {
"shared": "workspace:*"
}
}
Why workspace:*:
- •pnpm symlinks local packages instead of installing from registry
- •Changes in
sharedimmediately available infrontend/backend - •No need to rebuild/reinstall during development
Turborepo Configuration
File: turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**", "build/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"lint": {
"outputs": []
},
"format": {
"outputs": []
},
"format:check": {
"outputs": []
}
}
}
Key concepts:
- •
dependsOn: ["^build"]: Build dependencies first (e.g.,sharedbeforefrontend) - •
outputs: Cached artifacts for faster rebuilds - •
cache: false: Don't cache dev servers (they're stateful) - •
persistent: true: Keep dev servers running
Code Examples
Adding a New Package
- •
Create package directory:
bashmkdir shared cd shared pnpm init
- •
Configure package.json:
json{ "name": "shared", "version": "1.0.0", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "build": "tsc", "test": "vitest", "lint": "biome check ." }, "devDependencies": { "@biomejs/biome": "^1.4.0", "typescript": "^5.3.0", "vitest": "^1.0.0" } } - •
Add to workspace:
yaml# pnpm-workspace.yaml packages: - "frontend" - "backend" - "shared" # Added
- •
Install from root:
bashpnpm install
Using Shared Package
In frontend/package.json:
{
"name": "frontend",
"dependencies": {
"shared": "workspace:*"
}
}
Import in frontend code:
// frontend/app/page.tsx
import { QuizSchema } from 'shared/schemas/quiz'
const quiz = QuizSchema.parse(data)
CRITICAL: Shared package must be built before dependent packages can use it.
Running Commands
Run in all packages:
pnpm build # Runs turbo run build pnpm test # Runs turbo run test pnpm lint # Runs turbo run lint
Run in specific package:
pnpm --filter frontend dev pnpm --filter backend test pnpm --filter shared build
Run in multiple packages:
pnpm --filter frontend --filter backend dev
Why use filters:
- •Faster feedback when working on single package
- •Avoid running unnecessary tasks
- •Useful in CI/CD for partial builds
Task Dependencies
Scenario: Frontend depends on shared types
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"], // Build deps first
"outputs": [".next/**", "dist/**"]
}
}
}
Result:
pnpm build # 1. Builds shared # 2. Builds frontend (can import from shared) # 3. Builds backend (can import from shared)
Development Workflow
Parallel dev servers:
// Root package.json
{
"scripts": {
"dev": "turbo run dev"
}
}
// frontend/package.json
{
"scripts": {
"dev": "next dev"
}
}
// backend/package.json
{
"scripts": {
"dev": "tsx watch src/app.ts"
}
}
Run:
pnpm dev # Starts both frontend and backend in parallel
Why this works:
- •Turborepo runs
devtask in all packages - •
persistent: truekeeps servers running - •
cache: falseensures fresh state
Common Pitfalls
❌ Installing Packages in Wrong Location
cd frontend npm install zod # ❌ Using npm instead of pnpm
Fix:
cd frontend pnpm add zod # ✅ Use pnpm
❌ Not Building Shared Before Using
pnpm --filter frontend dev # ❌ Frontend can't import from unbuild shared
Fix:
pnpm --filter shared build # Build shared first pnpm --filter frontend dev # Now frontend can import
Better: Let Turborepo handle it:
pnpm dev # Turbo ensures correct order
❌ Circular Dependencies
// frontend/package.json
{
"dependencies": {
"backend": "workspace:*" // ❌ Frontend imports backend
}
}
// backend/package.json
{
"dependencies": {
"frontend": "workspace:*" // ❌ Backend imports frontend
}
}
Fix: Use shared for common code:
shared/ (types, schemas) ↑ ↑ | | frontend backend
❌ Forgetting to Add Package to Workspace
# pnpm-workspace.yaml packages: - "frontend" - "backend" # ❌ Forgot to add "shared"
Result: shared won't be linked, imports fail
Fix:
packages: - "frontend" - "backend" - "shared" # ✅ Added
Commands Reference
Installation
# Install all dependencies pnpm install # Install dependency in specific package pnpm --filter frontend add react-hook-form pnpm --filter backend add express # Install dev dependency pnpm --filter frontend add -D @types/node # Install at root pnpm add -D -w turbo # -w = workspace root
Running Scripts
# Run script in all packages (via Turbo) pnpm dev pnpm build pnpm test pnpm lint # Run script in specific package pnpm --filter frontend dev pnpm --filter backend build pnpm --filter shared test # Run multiple scripts sequentially pnpm build && pnpm test # Run in all packages without Turbo pnpm -r dev # -r = recursive (all packages)
Debugging
# Check workspace structure pnpm list --depth 0 # Verify workspace links pnpm why shared # Shows which packages use 'shared' # Clear cache rm -rf node_modules .turbo pnpm install # Turbo dry run (see execution plan) pnpm build --dry-run
Testing Checklist
When setting up monorepo, verify:
- •
pnpm installsucceeds without errors - • All packages listed in
pnpm-workspace.yaml - • Internal dependencies use
workspace:*protocol - •
pnpm devstarts all dev servers - •
pnpm buildbuilds in correct order - •
pnpm testruns all tests - •
pnpm lintchecks all packages - • Changes in
sharedreflect immediately infrontend/backend - • Turborepo cache works (second build is faster)
- • Can run commands in specific packages with
--filter
Resources
- •Architecture: See /docs/architecture.md
- •pnpm workspaces: https://pnpm.io/workspaces
- •Turborepo docs: https://turbo.build/repo/docs