End-to-end rules for building highly-modular TypeScript systems (backend & frontend) with NestJS, React and containerized deployment.
Your TypeScript applications don't have to become unmaintainable monsters. Stop wrestling with circular dependencies, unclear ownership, and deployment nightmares. This modular architecture rulebook transforms chaotic codebases into systems that actually work with your team, not against it.
You know the pain: that "quick feature" that requires touching 12 files across 5 different areas of your codebase. The deployment that breaks three unrelated features. The team friction when nobody knows who owns what code. The integration tests that take 45 minutes to run because everything depends on everything else.
Traditional monolithic TypeScript applications create these specific problems:
UserService imports AuthService which imports UserServiceThese Cursor Rules establish a battle-tested modular architecture that transforms your development workflow. Instead of fighting your codebase, you'll work with autonomous modules that can be developed, tested, and deployed independently.
Core architectural principles:
// Typical monolithic nightmare
import { UserService } from '../user/user.service';
import { AuthService } from '../auth/auth.service';
import { PaymentService } from '../payment/payment.service';
// All services tightly coupled, impossible to test independently
// Changes to one service break others
// Deployment requires coordinating multiple teams
// Clean modular approach
// user-auth package - owns authentication completely
export class UserRegisteredEvent {
constructor(readonly userId: UserId) {}
}
// payment package - listens to events, no direct coupling
@EventHandler(UserRegisteredEvent)
export class CreatePaymentProfileHandler {
async handle(event: UserRegisteredEvent) {
// Autonomous business logic
}
}
Development Velocity
Code Quality & Maintenance
Team Collaboration
Before (Monolithic)
# Understand how user profiles work
1. Read through 8 different service files
2. Understand 15 different database models
3. Figure out which tests need updating
4. Run 45-minute integration test suite
5. Coordinate deployment with 3 other teams
After (Modular)
# Work in isolated user-profile module
1. Focus on single domain: user-profile/
2. Run targeted tests in 30 seconds
3. Deploy independently to production
4. Zero coordination required
Before
After
Before
After
// Directory structure per business domain
user-auth/ # @acme/user-auth
├─ src/
│ ├─ application/ # commands, queries (CQRS)
│ ├─ domain/ # business logic, events
│ ├─ infrastructure/ # database, external APIs
│ └─ presentation/ # REST controllers, GraphQL
├─ tests/
├─ index.ts # public API only
└─ package.json
payment-processing/ # @acme/payment-processing
├─ src/
│ ├─ application/
│ ├─ domain/
│ ├─ infrastructure/
│ └─ presentation/
// ESLint configuration
{
"rules": {
"boundaries/element-types": [
"error",
{
"default": "disallow",
"rules": [
{
"from": "domain",
"disallow": ["infrastructure", "presentation"]
},
{
"from": "application",
"disallow": ["presentation"]
}
]
}
]
}
}
// Domain events replace direct coupling
export class UserRegisteredEvent {
constructor(
readonly userId: UserId,
readonly email: Email,
readonly timestamp: Date
) {}
}
// Publishers emit events
eventBus.publish(new UserRegisteredEvent(id, email, new Date()));
// Subscribers react independently
@EventHandler(UserRegisteredEvent)
export class SendWelcomeEmailHandler {
async handle(event: UserRegisteredEvent) {
await this.emailService.sendWelcome(event.email);
}
}
# GitHub Actions per module
name: Deploy User Auth Module
on:
push:
paths: ['packages/user-auth/**']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Test user-auth
- name: Build user-auth
- name: Deploy to Kubernetes
env:
NAMESPACE: user-auth
Your modular TypeScript architecture isn't just better code organization—it's a complete transformation of how your team builds, tests, and ships software. Stop fighting your codebase and start building systems that scale with your business.
You are an expert in TypeScript, Node.js, NestJS, React, Docker, Kubernetes, Nx/Lerna monorepos, Domain-Driven Design, event-driven systems and CI/CD automation.
Key Principles
- Design for single responsibility: one module ⇔ one well-defined business capability.
- Descriptive naming: UserAuthenticationModule ≫ AuthUtil. Avoid "misc", "common", "helper" buckets.
- Stable dependencies: higher-level layers must not import from lower-level layers. Enforce with ESLint import rules.
- No circular references: treat cycles as compile-time errors; use mediator pattern or domain events to break them.
- API-first & event-driven: every module owns its public API (REST/GraphQL) and emits domain events instead of synchronous coupling.
- Layered architecture inside a bounded context: core → domain → application/service → interface (controllers, resolvers, UI).
- Measurable value: create a new module only when it improves reuse, deployability, or team ownership (avoid over-modularization).
- Monorepo governance: one repo, many autonomous packages versioned by Nx / Lerna; each package declares explicit semver, deps and owners.md.
TypeScript
- `strict` compiler options **must** be enabled; forbid implicit any, implicit returns and unknown errors.
- File naming: kebab-case for directories, PascalCase for files exporting classes/types, camelCase for functions.
- Directory layout per package:
```
user-auth/ # npm scope @acme/user-auth
├─ src/
│ ├─ application/ # commands, queries (CQRS)
│ ├─ domain/ # aggregates, value objects, domain events
│ ├─ infrastructure/ # db, http, third-party
│ └─ presentation/ # controllers / GraphQL resolvers / React entry
├─ tests/
├─ index.ts # re-exports public API – no implementation
└─ package.json
```
- Barrel (`index.ts`) should only re-export types/functions – **never** contain logic.
- Prefer pure functions; when stateful, use injectable classes only (NestJS providers or React hooks).
- For shared types use `*.shared.ts` inside `/domain`, then import via path alias `@acme/user-auth/domain`.
- Enforce dependency direction with `dep-cruiser` or `eslint-plugin-boundaries`.
Error Handling & Validation
- Validate inputs at module boundary (controller or exported function) using Zod / class-validator.
- Return `Result<T, E>` or throw typed domain errors (extending `BaseDomainError`).
- Early return pattern:
```ts
if (!guard.isValid(cmd)) return err(Result.invalid(cmd));
```
- Domain layer never imports HTTP constructs; convert infrastructure exceptions to domain errors via mappers.
- Global NestJS `AllExceptionsFilter` maps domain errors → 4xx/5xx JSON; React uses an ErrorBoundary per feature.
NestJS Rules
- One NestJS Module file per npm package; name `<Feature>Module`.
- Expose only providers that form the public surface through `exports:`; hide internals.
- Use `@Module({imports:[...]})` instead of direct `import {Gateway} from 'other'` to keep dependency graph explicit.
- Communication across bounded contexts via `EventBus` (e.g., NATS) or `CommandBus`; **never** `import {Service}` from another context.
- Apply CQRS: `CommandHandler` mutates aggregates, `QueryHandler` reads projections.
- Configuration belongs to an isolated `config` package; inject via `ConfigService`.
React Rules
- Functional components + hooks only. No classes.
- Co-locate UI logic inside its domain package under `/presentation`.
- Each package owns its route segment; parent shell registers asynchronously using React.lazy + suspense.
- State management: React Query for server state, Zustand/Recoil for client state scoped to package.
- CSS: vanilla-extract or tailwind modules named `<feature>.css.ts`.
Testing
- Each package ships its own Jest config that extends root; run affected tests via Nx affected.
- Unit test domain logic (100 % lines) and application layer. Integration tests spin up TestContainers.
- Contracts: publish OpenAPI or GraphQL schema snapshots; break build when they change unexpectedly.
Performance
- Backend: scale horizontally per NestJS microservice (Docker image per npm package).
- Frontend: leverage webpack Module Federation or Vite’s `build.lib` to ship independently deployable bundles.
- Prefer asynchronous messaging over synchronous HTTP for non-critical paths to avoid cascade latency.
Security
- Static analysis: ESLint + SonarQube mandatory; fail pipeline on high severity.
- Secret management: `.env` only for local; production pulls from Vault/KMS.
- Each module defines its RBAC abilities; UI hides forbidden actions but backend enforces.
CI/CD & Tooling
- Git branch naming: `<package>/<short-desc>`.
- Every package has its own pipeline job:
1. lint ➜ test ➜ build ➜ docker-build ➜ push ➜ deploy channel (K8s namespace `<package>`)
- Conventional Commits with `scope=<package>`; release via semantic-release.
- Use Renovate to keep intra-repo dependencies up to date.
Common Pitfalls & Guards
- 🔴 Don’t place shared utils in `/common` without ownership ⇒ leads to god package.
- 🔴 Don’t expose concrete classes from modules; export interfaces.
- 🔴 Don’t let React import directly from NestJS code; share only typed SDK generated from OpenAPI.
- ✅ Set up ESLint rule `no-circular-dependencies` & unit test that forbidden imports fail.
- ✅ Run `docker scout` or Trivy in CI to catch CVEs in each module image.
Examples
```ts
// good: domain event
export class UserRegisteredEvent {
constructor(readonly userId: UserId) {}
}
// mediator to avoid cyclic dep
eventBus.publish(new UserRegisteredEvent(id));
```
```ts
// bad: leaking infrastructure
@Injectable()
export class AuthService {
constructor(private readonly http: AxiosInstance) {} // ❌ domain layer knows Axios
}
```
```tsx
// React feature entry (lazy-loaded)
export default function UserProfileRoute() {
const {data} = useUserProfileQuery();
return <UserProfileView {...data}/>;
}
```