A NestJS microservices monorepo for managing personal trainers, trainees, training packages, appointments, and trainee health history — built around Bounded Context (BC) boundaries, one database per context, and inter-service communication over RabbitMQ / TCP.
Each Bounded Context is an independently deployable NestJS application that owns its data
exclusively; nothing reaches across a context boundary except through a well-defined
@MessagePattern contract. This keeps the system safe to scale, split, or hand off to
separate teams without a rewrite.
- Architecture
- Tech Stack
- Monorepo Structure
- Bounded Contexts
- Getting Started
- Running the Backend
- Running the Frontend
- Production Deployment
- API Documentation
- Database & Migrations
- Testing & Code Quality
- Contributing
- License
graph TB
subgraph Client
WEB["Angular Web App<br/>:4200"]
end
subgraph Services["HTTP APIs"]
AUTH["auth<br/>:3001"]
IAM["iam<br/>:3006"]
TRAINER["trainer-bc<br/>:3002"]
TRAINEE["trainee-bc<br/>:3003"]
PACKAGE["package-bc<br/>:3004"]
APPT["appointment-bc<br/>:3005"]
end
MQ[["RabbitMQ<br/>(inter-service transport)"]]
REDIS[("Redis<br/>sessions + CSRF")]
WEB --> AUTH & IAM & TRAINER & TRAINEE & PACKAGE & APPT
AUTH -- "validate credentials" --> IAM
AUTH --> REDIS
TRAINER -. TCP/RMQ .-> IAM
TRAINEE -. TCP/RMQ .-> IAM
PACKAGE -. TCP/RMQ .-> IAM
APPT -. TCP/RMQ .-> IAM
AUTH & IAM & TRAINER & TRAINEE & PACKAGE & APPT -.-> MQ
IAM --> DBIAM[("fit_binder_iam")]
TRAINER --> DBTRAINER[("fit_binder_trainer")]
TRAINEE --> DBTRAINEE[("fit_binder_trainee")]
PACKAGE --> DBPACKAGE[("fit_binder_package")]
APPT --> DBAPPT[("fit_binder_appointment")]
Key design decisions
- Database-per-context — every BC owns a physical PostgreSQL database; there are no cross-database joins. Cross-context data is referenced by UUID and resolved over the network.
authis stateless and DB-less — it holds no data of its own. It issues/validates JWTs, manages CSRF, and keeps session state in Redis; all identity data (users, password hashes) lives iniam, reached over TCP.- Pluggable transport — services talk to each other over RabbitMQ by default
(
TRANSPORT=rmq), with a TCP fallback (TRANSPORT=tcp) for local development without a broker. - JSON:API responses — every HTTP response is normalized by a global
TransformInterceptor. - Shared contracts, not shared runtime —
libs/contractsexposes framework-agnostic TypeScript types consumed by both the Angular frontend and the NestJS backend, whilelibs/common(which pulls in Nest/TypeORM) never leaks into the browser bundle.
| Layer | Technology |
|---|---|
| Backend framework | NestJS 11 on Fastify |
| Language | TypeScript 5.7 |
| Database / ORM | PostgreSQL + TypeORM (one schema per Bounded Context) |
| Inter-service comm. | RabbitMQ (amqp-connection-manager) with TCP fallback via @nestjs/microservices |
| Cache / sessions | Redis (ioredis) — auth sessions, CSRF tokens |
| Auth | JWT (@nestjs/jwt, passport-jwt), bcrypt password hashing |
| Validation | class-validator / class-transformer |
| API docs | OpenAPI via @nestjs/swagger, rendered with Scalar + Swagger UI |
| Security hardening | @fastify/helmet, @fastify/rate-limit, CORS, CSRF |
| Frontend | Angular 22 (standalone APIs) + Tailwind CSS 4 |
| Testing | Jest (backend), Vitest (frontend) |
| Monorepo tooling | Nx + pnpm workspaces (backend), Nest CLI monorepo mode, ESLint 9 (flat config), Prettier, dependency-cruiser |
| Process management | PM2 (ecosystem.config.js) for production |
fit-binder/
├── apps/
│ ├── auth/ # Stateless auth: JWT, sessions (Redis), CSRF HTTP :3001 | TCP :4001
│ │ ├── package.json # Scoped runtime deps for this app
│ │ ├── project.json # Nx targets: build / serve / test / test:e2e
│ │ └── src/
│ ├── iam/ # Identity system-of-record: users, credentials HTTP :3006 | TCP :4006
│ ├── trainer-bc/ # Trainer Bounded Context HTTP :3002 | TCP :4002
│ ├── trainee-bc/ # Trainee + health history Bounded Context HTTP :3003 | TCP :4003
│ ├── package-bc/ # Training package + purchase Bounded Context HTTP :3004 | TCP :4004
│ ├── appointment-bc/ # Appointment + session Bounded Context HTTP :3005 | TCP :4005
│ └── web/ # Angular 22 standalone frontend (NOT an Nx project) HTTP :4200
│
├── libs/
│ ├── common/ # BaseEntity, base CRUD operations, decorators, guards (backend only)
│ ├── config/ # Centralized, validated configuration (backend only)
│ ├── database/ # TypeORM data sources + migrations (backend only)
│ └── contracts/ # Framework-agnostic TS types shared by web + backend
│ (each lib also carries its own package.json + project.json)
│
├── nx.json # Nx workspace config: target defaults, plugins, caching
├── pnpm-workspace.yaml # apps/* + libs/* as pnpm workspace packages (apps/web excluded)
├── ecosystem.config.js # PM2 process definitions for production
├── CLAUDE.md # AI/agent contribution guide (architecture + conventions)
└── CODE_OF_CONDUCT.md # Full coding conventions reference
FitBinder's backend (apps/* except web, plus libs/*) is an Nx workspace on top of
a pnpm workspace — the same pattern used across our other NestJS monorepos. Every backend
app and lib is its own workspace package with its own package.json (scoped dependencies)
and project.json (Nx project targets), instead of one monolithic root manifest:
apps/trainer-bc/
├── package.json # "trainer-bc" — only the deps this BC actually imports, plus @lib/* as workspace:*
├── project.json # Nx targets: build, serve, test, test:e2e (lint is auto-inferred)
└── src/
pnpm-workspace.yamldeclaresapps/*+libs/*as workspace packages, explicitly excludingapps/web(see below).nx.jsonwires up@nx/eslintand@nx/jest, which auto-inferlintandtesttargets for every project from the existing root ESLint flat config and each project'sjest.config.js— no per-project ESLint config duplication needed.buildandservetargets are thinnx:run-commandswrappers around the existing Nest CLI monorepo build (nest-cli.json,"monorepo": true— unchanged), so Nx adds task orchestration, dependency-aware ordering, and local caching on top of the exact samenest build <app>/nest start <app> --watchcommands used before.- Cross-project awareness means
nx run-many --target=build --allbuilds every BC (in the right order) in one command, andnx affected --target=testonly re-tests what actually changed — this is what makes the workspace "ready to scale": adding BC #7 means adding one moreapps/<name>project, not touching a shared config file every other team depends on.
pnpm exec nx build trainer-bc # build a single project
pnpm exec nx run-many --target=build --all # build everything, respecting the dependency graph
pnpm run affected:test # only test what changed since the base branch
pnpm run graph # visualize the project graphAll the original pnpm start:dev:<app> / pnpm build:<app> scripts still work exactly as
before — they call the same underlying Nest CLI commands the Nx targets wrap, so nothing
about day-to-day development changes unless you opt into the nx … commands above.
apps/web is deliberately excluded from the Nx/pnpm workspace graph. There is no
project.json, no shared root tsconfig reference — it is a plain, default Angular
CLI app with its own package.json, angular.json, and dependency tree, wired in only
through convenience scripts at the root (pnpm start:web → pnpm --dir apps/web start).
This keeps the Angular toolchain (and its release cadence) fully decoupled from the
NestJS/TypeORM backend; the only thing the two sides share is libs/contracts, consumed
by apps/web via a TypeScript path mapping in apps/web/tsconfig.json, not a package
dependency.
| Service | Owns | HTTP | TCP | Database |
|---|---|---|---|---|
auth |
Login/refresh/logout, CSRF (no DB — Redis only) | 3001 | 4001 | — |
iam |
Users, credentials, (planned) roles/permissions | 3006 | 4006 | fit_binder_iam |
trainer-bc |
Trainers | 3002 | 4002 | fit_binder_trainer |
trainee-bc |
Trainees + body composition / health records | 3003 | 4003 | fit_binder_trainee |
package-bc |
Training packages + purchases | 3004 | 4004 | fit_binder_package |
appointment-bc |
Appointments + training sessions | 3005 | 4005 | fit_binder_appointment |
Every BC's data is private — no service reads another BC's database directly. Cross-BC
lookups go through @MessagePattern handlers exposed on that BC's TCP/RabbitMQ client,
injected via the AppMicroservice enum from @lib/common.
- Node.js ≥ 24 (see
.nvmrc→nvm use) - pnpm
- PostgreSQL (one database per Bounded Context)
- RabbitMQ (default inter-service transport) — or set
TRANSPORT=tcpto run without one - Redis (required by
authfor sessions/CSRF)
# Install root (backend) dependencies
pnpm install
# Install frontend dependencies
pnpm --dir apps/web installcp .env.example .envFill in database credentials, RabbitMQ/Redis connection details, and JWT secrets. Each
service reads its own *_DB_*, *_MODULE_HTTP_PORT, and *_MODULE_MICROSERVICE_PORT
variables — see .env.example for the full reference.
# Run a single service
pnpm start:dev:auth
pnpm start:dev:iam
pnpm start:dev:trainer-bc
pnpm start:dev:trainee-bc
pnpm start:dev:package-bc
pnpm start:dev:appointment-bc
# Run every backend service concurrently
pnpm start:dev:all# Build a single service
pnpm build:auth # or build:iam / build:trainer-bc / build:trainee-bc / ...
# Build all services
pnpm build:all# Angular dev server on :4200
pnpm start:web
# equivalently, from apps/web directly
pnpm --dir apps/web start
# Production build
pnpm build:web
# Unit tests (Vitest)
pnpm test:webThe Angular dev server proxies
/api/<bc>/*to each BC's HTTP port automatically viaapps/web/proxy.conf.json— no manual CORS setup needed in local development.
Backend services are built with nest build and run under PM2 using
ecosystem.config.js, which defines one process per BC plus auth, each loading its
config from .env:
pnpm build:all
pm2 start ecosystem.config.jsOr run a single compiled service directly:
node dist/apps/auth/main.jsEvery service exposes OpenAPI docs at two endpoints once it is running:
| Service | Scalar (recommended) | Swagger UI |
|---|---|---|
auth |
http://localhost:3001/auth/v1/api-docs |
/auth/v1/classic-docs |
iam |
http://localhost:3006/iam/v1/api-docs |
/iam/v1/classic-docs |
trainer-bc |
http://localhost:3002/trainer/v1/api-docs |
/trainer/v1/classic-docs |
trainee-bc |
http://localhost:3003/trainee/v1/api-docs |
/trainee/v1/classic-docs |
package-bc |
http://localhost:3004/package/v1/api-docs |
/package/v1/classic-docs |
appointment-bc |
http://localhost:3005/appointment/v1/api-docs |
/appointment/v1/classic-docs |
Each BC owns a dedicated PostgreSQL database. Migrations always go through the TypeORM CLI — timestamps are never hand-written, to keep ordering correct.
# Generate a migration (example: trainer-bc)
pnpm migration:generate:trainer --name=AddTrainerStatus
# Run migrations
pnpm migration:run:iam
pnpm migration:run:trainer
pnpm migration:run:trainee
pnpm migration:run:package
pnpm migration:run:appointment
# Run all pending migrations across every BC
pnpm migration:run:all
# Revert the last migration for a BC
pnpm migration:revert:trainer| BC | Database | Env prefix |
|---|---|---|
| iam | fit_binder_iam |
IAM_DB_* |
| trainer-bc | fit_binder_trainer |
TRAINER_DB_* |
| trainee-bc | fit_binder_trainee |
TRAINEE_DB_* |
| package-bc | fit_binder_package |
PACKAGE_DB_* |
| appointment-bc | fit_binder_appointment |
APPOINTMENT_DB_* |
authhas no row in this table — it is intentionally database-less; its state lives in Redis.
# Unit tests (backend, Jest)
pnpm test
pnpm test:watch
pnpm test:cov
# Lint (auto-fix, strict — see eslint.config.mjs)
pnpm lint
# Format
pnpm formatThe project also ships a dependency-cruiser configuration to enforce architectural
boundaries (e.g. no BC importing another BC's internals) and a .coderabbit.yaml for
automated PR review.
This codebase follows a strict, documented set of conventions so that every Bounded Context stays structurally identical and easy to onboard into:
CLAUDE.md— architecture overview and AI/agent contribution guide.CODE_OF_CONDUCT.md— the full convention reference: naming, entity/DTO/service/controller layering, Swagger documentation rules, and file structure.
Adding a new entity or CRUD resource? Always scaffold it with the implement-entity
workflow rather than hand-rolling the layers:
/implement-entity
Pull requests are welcome — please make sure pnpm lint and pnpm test pass before
submitting, and follow the conventions above so reviews stay fast.
Licensed under the MIT License.