Software Studio
Back to Articles

TypeScript Patterns for NestJS APIs

TypeScriptNestJSNode.jsAPI Design

NestJS is the closest thing Node.js has to a mature, opinionated backend framework. It draws heavily from Angular's architecture, modules, providers, decorators, but applied to server-side development. After building several production APIs with it, here are the patterns that matter.

The module system is NestJS's greatest strength. Each feature gets its own module with its controllers, services, and data access layer. This enforces separation of concerns at the framework level. A UserModule contains everything related to users. An AuthModule handles authentication. They communicate through well-defined interfaces.

Dependency injection makes testing straightforward. Every service declares its dependencies in the constructor, and NestJS resolves them automatically. In tests, you swap real dependencies for mocks. This pattern eliminates the need for monkey-patching or import hacking that plagues other Node.js frameworks.

Type-safe validation with class-validator and class-transformer is non-negotiable. Define DTOs (Data Transfer Objects) for every endpoint's input. Decorate properties with validation rules. The ValidationPipe applies these rules globally, rejecting malformed requests before they reach your business logic.

Error handling follows a consistent pattern: custom exception classes that extend HttpException. A BadRequestException for validation errors, a NotFoundException for missing resources, a ForbiddenException for authorization failures. The global exception filter catches these and returns consistent error responses.

For database access, I pair NestJS with Prisma. The PrismaService extends PrismaClient and implements OnModuleInit to handle connection lifecycle. Each feature module gets a repository service that wraps Prisma queries, keeping database concerns out of the business logic layer.

Guards handle authorization cleanly. An AuthGuard validates JWT tokens. A RolesGuard checks user permissions. These compose declaratively with decorators, @UseGuards(AuthGuard, RolesGuard) on a controller or route. The guard pattern keeps authorization logic centralized and testable.

Interceptors are underused but powerful. A LoggingInterceptor records request/response times. A TransformInterceptor wraps responses in a consistent envelope. A CacheInterceptor adds response caching. These cross-cutting concerns stay separate from business logic.

The pattern I wish I'd adopted sooner: command/query separation. Commands (create, update, delete) return simple acknowledgments. Queries return data. This maps cleanly to REST semantics and makes caching trivial, cache all GET responses, invalidate on mutations.