Software ArchitectureInfrastructure as Code
Monorepo Structure
Setting up a Turborepo/Nx workspace with apps, packages, and infrastructure in a unified repository.
Monorepo Structure
A monorepo consolidates multiple applications, packages, and infrastructure configurations into a single repository. This enables code sharing, atomic changes, and coordinated releases across the entire organization.
We use Turborepo for its simplicity and speed, but the same concepts apply to Nx, Lerna, or Rush.
Workspace Configuration
Initialize Turborepo Workspace
# Create new turborepo
pnpm dlx create-turbo@latest organization-platform
# Or add to existing project
cd existing-project
pnpm add turbo -D -wConfigure pnpm Workspaces
packages:
# Applications
- "apps/*"
# Shared packages/SDKs
- "packages/*"
# Infrastructure as code
- "infrastructure/*"
# Tools and scripts
- "tools/*"Configure Turborepo Pipeline
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": [
".env",
".env.local"
],
"globalEnv": [
"NODE_ENV",
"CI"
],
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "package.json", "tsconfig.json"],
"outputs": ["dist/**", ".next/**", "build/**"],
"cache": true
},
"test": {
"dependsOn": ["^build"],
"inputs": ["src/**", "test/**", "**/*.test.ts"],
"outputs": ["coverage/**"],
"cache": true
},
"test:e2e": {
"dependsOn": ["build"],
"inputs": ["src/**", "e2e/**"],
"outputs": [],
"cache": false
},
"lint": {
"dependsOn": ["^build"],
"inputs": ["src/**", ".eslintrc.*", "biome.json"],
"outputs": [],
"cache": true
},
"typecheck": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json"],
"outputs": [],
"cache": true
},
"dev": {
"dependsOn": ["^build"],
"persistent": true,
"cache": false
},
"deploy": {
"dependsOn": ["build", "test"],
"inputs": ["dist/**", "Dockerfile", "kubernetes/**"],
"outputs": [],
"cache": false
},
"db:migrate": {
"cache": false
},
"db:generate": {
"cache": false
}
}
}Root Package Configuration
{
"name": "organization-platform",
"private": true,
"packageManager": "pnpm@9.0.0",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"test": "turbo run test",
"test:e2e": "turbo run test:e2e",
"lint": "turbo run lint",
"typecheck": "turbo run typecheck",
"format": "biome format --write .",
"clean": "turbo run clean && rm -rf node_modules",
"changeset": "changeset",
"version-packages": "changeset version",
"release": "turbo run build --filter='./packages/*' && changeset publish"
},
"devDependencies": {
"@changesets/cli": "^2.27.0",
"@org/eslint-config": "workspace:*",
"@org/typescript-config": "workspace:*",
"turbo": "^2.0.0",
"typescript": "^5.4.0"
},
"engines": {
"node": ">=20.0.0",
"pnpm": ">=9.0.0"
}
}Complete Directory Structure
package.json
tsconfig.json
Dockerfile
docker-compose.yml
package.json
tsconfig.json
package.json
pnpm-workspace.yaml
turbo.json
biome.json
.env.example
docker-compose.yml
Package Configurations
{
"name": "@org/order-service",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsup src/index.ts --format esm --dts",
"start": "node dist/index.js",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"lint": "biome lint src/",
"typecheck": "tsc --noEmit",
"db:migrate": "drizzle-kit migrate",
"db:generate": "drizzle-kit generate",
"docker:build": "docker build -t order-service .",
"docker:push": "docker push $REGISTRY/order-service:$TAG"
},
"dependencies": {
"@org/core-sdk": "workspace:*",
"@org/events-sdk": "workspace:*",
"@org/database-sdk": "workspace:*",
"@org/auth-sdk": "workspace:*",
"@hono/node-server": "^1.11.0",
"hono": "^4.4.0",
"zod": "^3.23.0"
},
"devDependencies": {
"@org/typescript-config": "workspace:*",
"@org/eslint-config": "workspace:*",
"@types/node": "^20.14.0",
"tsup": "^8.1.0",
"tsx": "^4.15.0",
"typescript": "^5.4.0",
"vitest": "^1.6.0"
}
}{
"extends": "@org/typescript-config/node.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test"]
}# Build stage
FROM node:20-alpine AS builder
RUN corepack enable && corepack prepare pnpm@9.0.0 --activate
WORKDIR /app
# Copy workspace files
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json turbo.json ./
COPY packages/ ./packages/
COPY apps/order-service/ ./apps/order-service/
# Install dependencies
RUN pnpm install --frozen-lockfile
# Build with turbo (builds dependencies first)
RUN pnpm turbo run build --filter=@org/order-service
# Production stage
FROM node:20-alpine AS runner
WORKDIR /app
# Copy only production files
COPY --from=builder /app/apps/order-service/dist ./dist
COPY --from=builder /app/apps/order-service/package.json ./
# Install production dependencies only
RUN corepack enable && corepack prepare pnpm@9.0.0 --activate
RUN pnpm install --prod --frozen-lockfile
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]{
"name": "@org/core-sdk",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./domain": {
"import": "./dist/domain/index.js",
"types": "./dist/domain/index.d.ts"
},
"./application": {
"import": "./dist/application/index.js",
"types": "./dist/application/index.d.ts"
}
},
"files": [
"dist"
],
"scripts": {
"dev": "tsup --watch",
"build": "tsup",
"test": "vitest run",
"lint": "biome lint src/",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist"
},
"dependencies": {
"zod": "^3.23.0"
},
"devDependencies": {
"@org/typescript-config": "workspace:*",
"tsup": "^8.1.0",
"typescript": "^5.4.0",
"vitest": "^1.6.0"
},
"publishConfig": {
"access": "restricted",
"registry": "https://npm.pkg.github.com"
}
}import { defineConfig } from 'tsup';
export default defineConfig({
entry: {
index: 'src/index.ts',
'domain/index': 'src/domain/index.ts',
'application/index': 'src/application/index.ts',
},
format: ['esm'],
dts: true,
splitting: false,
sourcemap: true,
clean: true,
treeshake: true,
external: ['zod'],
});{
"extends": "@org/typescript-config/node.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}{
"name": "@org/typescript-config",
"version": "1.0.0",
"private": true,
"files": [
"*.json"
],
"exports": {
"./base.json": "./base.json",
"./node.json": "./node.json",
"./react.json": "./react.json"
}
}{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"esModuleInterop": true,
"strict": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"lib": ["ES2022"],
"module": "ESNext",
"target": "ES2022",
"moduleResolution": "bundler",
"moduleDetection": "force",
"allowSyntheticDefaultImports": true,
"noEmit": true
}
}{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"module": "ESNext",
"target": "ES2022",
"moduleResolution": "bundler"
}
}Dependency Management
// Script to validate internal dependency versions
import { readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
interface PackageJson {
name: string;
version: string;
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
}
const getWorkspacePackages = (rootDir: string): PackageJson[] => {
const packages: PackageJson[] = [];
const dirs = ['apps', 'packages'];
for (const dir of dirs) {
const fullPath = join(rootDir, dir);
const subdirs = readdirSync(fullPath, { withFileTypes: true })
.filter(d => d.isDirectory())
.map(d => d.name);
for (const subdir of subdirs) {
const pkgPath = join(fullPath, subdir, 'package.json');
try {
const content = readFileSync(pkgPath, 'utf-8');
packages.push(JSON.parse(content));
} catch {
// Skip if no package.json
}
}
}
return packages;
};
const validateWorkspaceDependencies = (packages: PackageJson[]) => {
const errors: string[] = [];
const packageNames = new Set(packages.map(p => p.name));
for (const pkg of packages) {
const allDeps = {
...pkg.dependencies,
...pkg.devDependencies,
};
for (const [dep, version] of Object.entries(allDeps)) {
// Check internal packages use workspace protocol
if (packageNames.has(dep) && version !== 'workspace:*') {
errors.push(
`${pkg.name}: Internal package ${dep} should use "workspace:*" instead of "${version}"`
);
}
}
}
return errors;
};
// Run validation
const packages = getWorkspacePackages(process.cwd());
const errors = validateWorkspaceDependencies(packages);
if (errors.length > 0) {
console.error('Dependency validation failed:');
errors.forEach(e => console.error(` - ${e}`));
process.exit(1);
}
console.log('✓ All dependencies valid');{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": [
"@changesets/changelog-github",
{ "repo": "organization/platform" }
],
"commit": false,
"fixed": [
["@org/core-sdk", "@org/events-sdk", "@org/database-sdk", "@org/auth-sdk"]
],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [
"@org/order-service",
"@org/payment-service",
"@org/notification-service"
],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
}
}// Sync versions across all packages
import { execSync } from 'node:child_process';
import { readFileSync, writeFileSync } from 'node:fs';
import { glob } from 'glob';
const syncSharedDependencies = async () => {
// Dependencies that should be the same version everywhere
const sharedDeps = [
'typescript',
'zod',
'hono',
'drizzle-orm',
'vitest',
];
// Get all package.json files
const packageFiles = await glob('**/package.json', {
ignore: ['**/node_modules/**', '**/dist/**'],
});
// Read root package.json for canonical versions
const rootPkg = JSON.parse(readFileSync('package.json', 'utf-8'));
const rootDeps = {
...rootPkg.dependencies,
...rootPkg.devDependencies,
};
for (const file of packageFiles) {
const content = JSON.parse(readFileSync(file, 'utf-8'));
let modified = false;
for (const dep of sharedDeps) {
if (content.dependencies?.[dep] && rootDeps[dep]) {
if (content.dependencies[dep] !== rootDeps[dep]) {
content.dependencies[dep] = rootDeps[dep];
modified = true;
}
}
if (content.devDependencies?.[dep] && rootDeps[dep]) {
if (content.devDependencies[dep] !== rootDeps[dep]) {
content.devDependencies[dep] = rootDeps[dep];
modified = true;
}
}
}
if (modified) {
writeFileSync(file, JSON.stringify(content, null, 2) + '\n');
console.log(`Updated ${file}`);
}
}
};
syncSharedDependencies();// For more complex constraints, use syncpack
// syncpack.config.js
module.exports = {
// Ensure same versions across workspace
versionGroups: [
{
label: 'TypeScript should be the same everywhere',
packages: ['**'],
dependencies: ['typescript'],
policy: 'sameRange',
},
{
label: 'Internal packages use workspace protocol',
packages: ['**'],
dependencies: ['@org/*'],
dependencyTypes: ['prod', 'dev'],
pinVersion: 'workspace:*',
},
],
// Enforce dependency types
semverGroups: [
{
label: 'Production deps use exact versions',
packages: ['apps/**'],
dependencyTypes: ['prod'],
range: '',
},
{
label: 'Dev deps can use caret ranges',
packages: ['**'],
dependencyTypes: ['dev'],
range: '^',
},
],
};{
"scripts": {
"deps:check": "syncpack list-mismatches",
"deps:fix": "syncpack fix-mismatches",
"deps:lint": "syncpack lint",
"deps:update": "pnpm update -r --interactive"
}
}Turbo Remote Caching
{
"$schema": "https://turbo.build/schema.json",
"remoteCache": {
"signature": true
},
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"],
"cache": true
}
}
}name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
# Turbo will use remote cache automatically
- run: pnpm turbo run build test lint --summarizeFiltering and Running Tasks
# Build everything
pnpm turbo run build
# Build specific app and its dependencies
pnpm turbo run build --filter=@org/order-service
# Build only packages (SDKs)
pnpm turbo run build --filter='./packages/*'
# Build everything except admin dashboard
pnpm turbo run build --filter='!@org/admin-dashboard'
# Build only changed packages since main
pnpm turbo run build --filter='...[origin/main]'
# Run tests for packages that depend on core-sdk
pnpm turbo run test --filter='...@org/core-sdk'
# Dev mode for specific services
pnpm turbo run dev --filter=@org/order-service --filter=@org/api-gatewayCode Generators
import { mkdir, writeFile, readFile } from 'node:fs/promises';
import { join } from 'node:path';
interface ServiceConfig {
name: string;
port: number;
dependencies: string[];
}
const generateService = async (config: ServiceConfig) => {
const servicePath = join(process.cwd(), 'apps', config.name);
// Create directory structure
await mkdir(join(servicePath, 'src', 'routes'), { recursive: true });
await mkdir(join(servicePath, 'src', 'handlers'), { recursive: true });
await mkdir(join(servicePath, 'test'), { recursive: true });
// Generate package.json
const packageJson = {
name: `@org/${config.name}`,
version: '1.0.0',
private: true,
type: 'module',
scripts: {
dev: 'tsx watch src/index.ts',
build: 'tsup src/index.ts --format esm --dts',
start: 'node dist/index.js',
test: 'vitest run',
lint: 'biome lint src/',
typecheck: 'tsc --noEmit',
},
dependencies: {
...Object.fromEntries(
config.dependencies.map(dep => [`@org/${dep}`, 'workspace:*'])
),
'@hono/node-server': '^1.11.0',
hono: '^4.4.0',
},
devDependencies: {
'@org/typescript-config': 'workspace:*',
'@types/node': '^20.14.0',
tsup: '^8.1.0',
tsx: '^4.15.0',
typescript: '^5.4.0',
vitest: '^1.6.0',
},
};
await writeFile(
join(servicePath, 'package.json'),
JSON.stringify(packageJson, null, 2)
);
// Generate index.ts
const indexTs = `
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
import { logger } from 'hono/logger';
import { cors } from 'hono/cors';
const app = new Hono();
app.use('*', logger());
app.use('*', cors());
app.get('/health', (c) => c.json({ status: 'healthy' }));
const port = process.env.PORT ?? ${config.port};
console.log(\`${config.name} running on port \${port}\`);
serve({ fetch: app.fetch, port: Number(port) });
export default app;
`.trim();
await writeFile(join(servicePath, 'src', 'index.ts'), indexTs);
// Generate tsconfig.json
const tsconfig = {
extends: '@org/typescript-config/node.json',
compilerOptions: {
rootDir: 'src',
outDir: 'dist',
},
include: ['src/**/*'],
exclude: ['node_modules', 'dist'],
};
await writeFile(
join(servicePath, 'tsconfig.json'),
JSON.stringify(tsconfig, null, 2)
);
// Generate Dockerfile
const dockerfile = `
FROM node:20-alpine AS builder
RUN corepack enable && corepack prepare pnpm@9.0.0 --activate
WORKDIR /app
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json turbo.json ./
COPY packages/ ./packages/
COPY apps/${config.name}/ ./apps/${config.name}/
RUN pnpm install --frozen-lockfile
RUN pnpm turbo run build --filter=@org/${config.name}
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/apps/${config.name}/dist ./dist
COPY --from=builder /app/apps/${config.name}/package.json ./
RUN corepack enable && pnpm install --prod
USER node
EXPOSE ${config.port}
CMD ["node", "dist/index.js"]
`.trim();
await writeFile(join(servicePath, 'Dockerfile'), dockerfile);
console.log(`✓ Generated service: ${config.name}`);
console.log(` Run: pnpm install && pnpm turbo run dev --filter=@org/${config.name}`);
};
// CLI usage
const args = process.argv.slice(2);
if (args.length < 1) {
console.error('Usage: pnpm generate:service <name> [--deps core-sdk,events-sdk]');
process.exit(1);
}
const name = args[0];
const depsArg = args.find(a => a.startsWith('--deps='));
const dependencies = depsArg
? depsArg.replace('--deps=', '').split(',')
: ['core-sdk'];
generateService({ name, port: 3000, dependencies });When to Use Monorepo
Next Steps
- SDK Modules - Creating and publishing internal packages
- Pipeline Orchestration - CI/CD workflows for monorepos
Infrastructure as Code
Modular monorepo architecture combining microservices, CQRS, event sourcing, and serverless patterns with shared SDKs and orchestrated CI/CD pipelines.
SDK Modules
Creating internal packages with protocol-based communication that work across any language - TypeScript, Go, .NET, or any other runtime.