The Monorepo Structure
project/
├── packages/
│ ├── web/ # Next.js frontend
│ │ └── src/
│ │ ├── app/
│ │ ├── components/
│ │ ├── hooks/
│ │ └── lib/
│ ├── api/ # Node.js backend
│ │ └── src/
│ │ ├── routes/
│ │ ├── controllers/
│ │ ├── services/
│ │ ├── middleware/
│ │ └── models/
│ └── shared/ # Shared TypeScript types
│ └── src/
│ ├── types.ts
│ ├── constants.ts
│ └── validators.ts
├── docker-compose.yml
├── turbo.json
└── package.json
Root package.json
{"workspaces":["packages/*"],"scripts":{"dev":"turbo run dev --parallel","build":"turbo run build","test":"turbo run test"}}
Environment Management
// packages/shared/src/env.ts
const requiredEnvVars = ['DATABASE_URL', 'JWT_SECRET', 'REDIS_URL'];
export const env = {
databaseUrl: process.env.DATABASE_URL!,
jwtSecret: process.env.JWT_SECRET!,
redisUrl: process.env.REDIS_URL!,
};
requiredEnvVars.forEach((key) => {
if (!process.env[key]) throw new Error(`Missing required env var: ${key}`);
});
Type Sharing
// packages/shared/src/types.ts
export type User = { id: string; email: string; name: string; createdAt: Date };
export type ApiResponse<T> = { success: boolean; data?: T; error?: string };
Backend:
import { User, ApiResponse } from '@shared/types';
router.post('/users', async (req, res) => {
const user: User = await db.users.create(req.body);
res.json<ApiResponse<User>>({ success: true, data: user });
});
Frontend uses same types — no mismatches, no runtime surprises.
Docker for Local Dev
version: '3'
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: dev
POSTGRES_PASSWORD: dev
POSTGRES_DB: mydb
ports: ["5432:5432"]
redis:
image: redis:7
ports: ["6379:6379"]
docker-compose up -d and the environment is ready.
Testing Structure
tests/
├── unit/
│ └── services/
├── integration/
│ └── routes/
└── fixtures/
CI/CD (GitHub Actions)
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npm run lint
- run: npm run test
- run: npm run build
Why This Stack
-
Monorepo: One repo, one CI/CD, type sharing, easier refactoring
-
Three packages:
web deploys to Vercel, api deploys anywhere, shared is the contract between them
-
TypeScript everywhere: Catches errors at build time, not runtime
-
Turbo: Parallel builds, smart caching
-
Docker locally: Dev matches prod
Day 1 setup: monorepo + docker + shared types + CI/CD + 5 integration tests. Costs one week. Saves months.
zunain.com