PROJECT: Turborepo Monorepo (Next.js + Node API + Shared Packages)
Skill gerado a partir do pack templates-claude-code. Arquivo de origem: fullstack/02-turborepo-monorepo.md. Use como baseline e adapte ao projeto antes de mudancas grandes.
Conteudo do template
STACK
- Monorepo: Turborepo 2.x + pnpm workspaces
- Web App: Next.js 15 (App Router)
- API Server: Node.js 22 + Express 5 + TypeScript
- Shared packages:
@repo/ui,@repo/config,@repo/types,@repo/db - Database: PostgreSQL via Drizzle ORM (shared
@repo/db) - Language: TypeScript 5.x (strict, project references)
- CI: GitHub Actions with Turborepo remote cache
PROJECT STRUCTURE
apps/
├── web/ # Next.js frontend
│ ├── src/
│ ├── package.json # depends on @repo/ui, @repo/types
│ └── tsconfig.json
└── api/ # Express API server
├── src/
├── package.json # depends on @repo/db, @repo/types
└── tsconfig.json
packages/
├── ui/ # Shared React components (shadcn-based)
│ ├── src/
│ │ ├── button.tsx
│ │ └── index.ts
│ └── package.json
├── db/ # Drizzle schema + client
│ ├── src/
│ │ ├── schema.ts
│ │ ├── client.ts
│ │ └── index.ts
│ └── package.json
├── types/ # Shared TS interfaces, no runtime deps
│ ├── src/
│ │ └── index.ts
│ └── package.json
└── config/
├── eslint/
│ └── library.js # Shared ESLint config
└── typescript/
├── base.json # Base tsconfig
├── nextjs.json
└── node.json
turbo.json
pnpm-workspace.yaml
package.json # Root: dev tooling only
ARCHITECTURE RULES
packages/*are internal packages — no public npm publish; versioned via"version": "0.0.0".@repo/typeshas zero runtime dependencies — types only; any dependency is a TS dev dep.@repo/dbis the only place Drizzle is imported — apps use@repo/dbexclusively.- Shared packages export from
src/index.ts—package.json"exports"field points to dist orsrc(viatsup). turbo.jsondefines the full pipeline — no app-level ad-hoc scripts; all tasks flow through turbo.pnpm --filterfor isolated ops — never runnpmoryarncommands in workspaces.- CI uses remote cache —
TURBO_TOKEN+TURBO_TEAMenvironment variables set in GitHub Actions.
TURBO.JSON PIPELINE
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": [".env"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json", "package.json"],
"outputs": [".next/**", "dist/**", "!.next/cache/**"]
},
"dev": {
"dependsOn": ["^build"],
"persistent": true,
"cache": false
},
"test": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tests/**"],
"outputs": ["coverage/**"]
},
"lint": {
"inputs": ["src/**", ".eslintrc.*"]
},
"typecheck": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json"]
},
"db:migrate": {
"cache": false
}
}
}
SHARED PACKAGE SETUP
// packages/types/package.json
{
"name": "@repo/types",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./dist/index.js"
}
},
"devDependencies": { "typescript": "^5.0.0" }
}
// packages/types/src/index.ts
export interface User {
id: string
email: string
name: string
role: 'user' | 'admin'
createdAt: Date
}
export interface Post {
id: string
title: string
body: string
authorId: string
published: boolean
}
export type ApiResponse<T> = { data: T; error?: never } | { data?: never; error: string }
SHARED UI PACKAGE
// packages/ui/src/button.tsx
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from './utils'
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
outline: 'border border-input hover:bg-accent',
ghost: 'hover:bg-accent hover:text-accent-foreground',
destructive: 'bg-destructive text-destructive-foreground',
},
size: {
sm: 'h-8 px-3',
default: 'h-10 px-4 py-2',
lg: 'h-11 px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: { variant: 'default', size: 'default' },
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export const Button = ({ className, variant, size, ...props }: ButtonProps) => (
<button className={cn(buttonVariants({ variant, size }), className)} {...props} />
)
SHARED DB PACKAGE
// packages/db/src/schema.ts
import { pgTable, text, boolean, timestamp } from 'drizzle-orm/pg-core'
import { createId } from '@paralleldrive/cuid2'
export const users = pgTable('users', {
id: text('id').primaryKey().$defaultFn(createId),
email: text('email').notNull().unique(),
name: text('name').notNull(),
role: text('role', { enum: ['user', 'admin'] }).notNull().default('user'),
createdAt: timestamp('created_at').defaultNow(),
})
// packages/db/src/client.ts
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
import * as schema from './schema'
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
export const db = drizzle(pool, { schema })
export * from './schema'
ROUTING TABLE (turborepo task → app)
| Task | Scope | Cache | Action |
|---|---|---|---|
turbo build |
all | yes | Build all apps + packages |
turbo dev |
all | no | Start dev servers in parallel |
turbo test |
all | yes | Run all test suites |
turbo lint |
all | yes | ESLint all workspaces |
turbo typecheck |
all | yes | tsc --noEmit everywhere |
pnpm --filter web dev |
apps/web | no | Start Next.js only |
pnpm --filter api dev |
apps/api | no | Start Express only |
pnpm --filter @repo/db db:migrate |
packages/db | no | Run Drizzle migrations |
turbo build --filter=...web |
apps/web + deps | yes | Build web + its deps only |
QUALITY GATES
-
turbo typecheck— zero errors across all workspaces -
turbo lint— clean -
turbo test— all pass -
turbo build— all outputs produced - No cross-package imports via relative paths (
../../packages/ui) — use@repo/ui -
pnpm installfrom root only — nevercd packages/ui && npm install - Remote cache configured in CI (
TURBO_TOKENsecret set) -
packages/typeshas zero runtime deps
FORBIDDEN
- ❌
import { User } from '../../packages/types/src'— use@repo/types - ❌
npmoryarncommands — pnpm only - ❌ Individual
tsconfig.jsonextending different bases than@repo/config/typescript/* - ❌
devDependenciesinpackages/*that are also inpeerDependenciesof an app — resolve at root - ❌ Publishing internal packages to npm registry
- ❌ Circular dependencies between packages (packages/a → packages/b → packages/a)
- ❌ App-specific code in shared packages (
if (process.env.NEXT_PUBLIC_*)in@repo/db)