Phase 0 — Project Setup¶
Goal: Create production-style foundations that every subsequent phase builds on.
Status: ✅ Complete
What Was Built¶
Infrastructure¶
- Docker Compose with PostgreSQL 17, Apache Kafka 3.9 (KRaft mode), and Redis 7
- Health checks on all containers (
pg_isready,redis-cli ping, Kafka broker API) - Named volumes for data persistence across restarts
- Credentials synchronized between Docker Compose and
.env
Database¶
- Prisma 7 with driver adapter pattern (
@prisma/adapter-pg) moduleFormat = "cjs"to resolve ESM/CJS mismatch with NestJS- First migration:
tenantstable with UUID primary keys, enum status, JSONB metadata, unique slug index - Idempotent seed script using
upsert(3 sample tenants) - Database connection URL configured in
prisma.config.ts(Prisma 7 requirement — not inschema.prisma)
Application¶
- NestJS 11 scaffolded via CLI with
--strictflag and pnpm - TypeScript strict mode (
strict: true— all 10 checks enabled) - Path aliases:
@common/*,@config/*,@modules/* - Global API prefix
/apiwith URI versioning (/api/v1/...) - Health endpoint excluded from prefix and versioning via
VERSION_NEUTRAL - Swagger/OpenAPI at
/api/docs(development only) - Helmet (security headers), compression (gzip), CORS
- Global validation pipe with whitelist, forbidNonWhitelisted, transform
- Graceful shutdown hooks for clean connection teardown
Observability¶
- Correlation ID middleware — every request gets a UUID in
x-correlation-id - Request logger middleware — logs method, path, status, duration for every request
- Global exception filter — consistent error JSON with correlationId, timestamp, path
- Health check endpoint at
/health— Postgres connectivity viaSELECT 1
Code Quality¶
- ESLint 9 + Prettier + EditorConfig
- Jest configured with path alias mapping
.env.exampledocumenting every required variable.gitignorecovering secrets, generated code, Docker data, IDE files- Bruno API collection started
Key Decisions¶
| Decision | Why |
|---|---|
| Modular monolith over microservices | Domain boundaries unproven. Extract modules when scale demands it |
| Prisma over TypeORM | Stronger types, better migration tooling, schema-first approach |
| pnpm over npm | Faster installs, strict dependency resolution, smaller disk usage |
| UUID primary keys | Safe for distributed systems, no sequential guessing |
strict: true in TypeScript |
Catches null errors, uninitialized properties, unsafe any usage at compile time |
| URI versioning over header versioning | Visible in URLs, works with Swagger, industry standard (Stripe, GitHub) |
VERSION_NEUTRAL on health controller |
Load balancers expect /health at a fixed path without version prefix |
moduleFormat = "cjs" in Prisma |
NestJS compiles to CommonJS; Prisma 7 defaults to ESM. Without this flag, the app crashes |
Driver adapter pattern (PrismaPg) |
Prisma 7 requirement — the client no longer manages its own connection |
tsx over ts-node for seed |
ts-node can't resolve .js extension imports in Prisma's generated TypeScript files |
Gotchas Encountered¶
- Prisma 7 removed
urlfromschema.prisma— connection URL moved toprisma.config.ts - Prisma 7 generates ESM by default — must set
moduleFormat = "cjs"for NestJS - Prisma 7 requires driver adapters — can't just
super()in PrismaService, must passPrismaPgadapter @prisma/clientmust be installed separately —prisma(CLI) and@prisma/client(runtime) are different packages- Local Postgres on port 5432 conflicts with Docker — stop Homebrew/Postgres.app before running containers
forRoutes('*')deprecated in NestJS 11 — useforRoutes('*path')for wildcard middleware routing- Health endpoint needs
VERSION_NEUTRAL—excludeinsetGlobalPrefixdoesn't bypass URI versioning - Prisma 7 seed config moved to
prisma.config.ts— no longer inpackage.jsonunder"prisma": { "seed": ... }
Files Created/Modified¶
meterplex/
├── .editorconfig NEW
├── .env MODIFIED (all app env vars)
├── .env.example MODIFIED (documented template)
├── .gitignore MODIFIED (Prisma generated/, Docker data/)
├── docker-compose.yml NEW
├── nest-cli.json UNCHANGED (from CLI)
├── package.json MODIFIED (scripts, metadata, engines, jest)
├── prisma/
│ ├── migrations/
│ │ └── 20260329_init_tenant/
│ │ └── migration.sql NEW (auto-generated)
│ ├── schema.prisma MODIFIED (Tenant model, moduleFormat)
│ └── seed.ts NEW
├── prisma.config.ts MODIFIED (seed command added)
├── src/
│ ├── app.module.ts MODIFIED (imports Config, Prisma, Health)
│ ├── common/
│ │ ├── filters/
│ │ │ ├── http-exception.filter.ts NEW
│ │ │ └── index.ts NEW
│ │ ├── middleware/
│ │ │ ├── correlation-id.middleware.ts NEW
│ │ │ ├── request-logger.middleware.ts NEW
│ │ │ └── index.ts NEW
│ │ └── index.ts NEW
│ ├── config/
│ │ ├── config.module.ts NEW
│ │ ├── env.validation.ts NEW
│ │ └── index.ts NEW
│ ├── health/
│ │ ├── health.controller.ts NEW
│ │ ├── health.module.ts NEW
│ │ ├── prisma.health.ts NEW
│ │ └── index.ts NEW
│ ├── main.ts MODIFIED (full production setup)
│ └── prisma/
│ ├── prisma.module.ts NEW
│ ├── prisma.service.ts NEW
│ └── index.ts NEW
├── tsconfig.json MODIFIED (strict, paths, include prisma)
└── bruno/
├── bruno.json NEW
├── environments/
│ └── local.bru NEW
└── health/
└── health-check.bru NEW
Phase 0 flow¶

Interview Talking Points¶
- "I chose a modular monolith because the domain boundaries weren't validated yet. Premature decomposition creates a distributed monolith — the worst of both worlds."
- "Every environment variable is validated at boot. If
DATABASE_URLis missing, the app refuses to start with a clear error — not a cryptic crash 30 seconds later." - "Every request gets a correlation ID that flows through logs, error responses, and eventually Kafka events. You can trace any issue end-to-end with one UUID."
- "The error response format is consistent across the entire API — the frontend team only needs one error interface."
- "Prisma 7 generates fully typed queries from the schema. If I mistype a field name, TypeScript catches it at compile time, not production."