DEV Community

Ahad Nawaz
Ahad Nawaz

Posted on

NestJS Architecture Patterns I Use in Every Production Backend

I have built production backends for ERP platforms, AI tools, real estate applications, e-commerce systems, and online marketplaces. They run on different infrastructure, serve different users, and solve different problems.

They all share the same architectural patterns.

Not because I am inflexible. Because these patterns solve the problems that every production backend eventually faces. Here is what I use and why.

Global Exception Filter

The first thing I add to every NestJS project is a global exception filter. Every unhandled error in the application flows through it.

It does three things: logs the full error with context, maps the error to an appropriate HTTP status code, and returns a consistent error response structure to the client.

Why this matters: without a global exception filter, error responses are inconsistent. Some endpoints return stack traces in development. Some return empty 500s in production. Frontend engineers cannot write reliable error handling code because the shape of the error response varies by endpoint.

A consistent error structure like { success: false, message: string, code: string } across every endpoint makes the entire API predictable and makes frontend error handling trivial.

Request Logging Interceptor

Every incoming request and outgoing response gets logged with: timestamp, method, path, status code, duration, and user ID if the request is authenticated.

This is not optional for a production system. When something goes wrong in production, the first question is always what did the user do and what did the system respond. Without request logs you are guessing.

I use a NestJS interceptor for this, not middleware, because interceptors have access to the response after it has been processed, which lets you log the status code and duration alongside the request details.

Repository Pattern for Data Access

I do not put database queries directly in service classes. Every entity has a repository class that owns all database interactions for that entity.

The service layer calls the repository. The controller calls the service. The controller knows nothing about the database. The service knows nothing about the HTTP layer.

This separation matters for three reasons. It makes unit testing possible without spinning up a real database. It makes it easy to swap out the underlying ORM or query builder without touching business logic. And it forces you to think about data access patterns explicitly rather than scattering ad hoc queries throughout the codebase.

DTOs and Class Validator for Every Input

Every endpoint that accepts input has a DTO class decorated with class-validator constraints. Validation runs before the handler ever executes.

This is not just about security, though it is that too. It is about trust. If your handler receives a DTO, you know it is valid. You do not need to check if email is a valid email address inside your service. That check already happened.

I use class-transformer alongside class-validator to automatically strip unknown properties from incoming requests. Users cannot send extra fields that your DTO does not define. This prevents a class of injection bugs that are otherwise easy to miss.

Standardized API Response Wrapper

All successful API responses follow the same structure: { success: true, data: T, meta?: PaginationMeta }.

I implement this with a response interceptor that wraps the handler return value automatically. Controllers return plain objects or arrays. The interceptor wraps them.

The meta field carries pagination information for list endpoints: total count, page number, page size, and total pages. This is always calculated server side and always included when returning a list, even if the list is not paginated. It makes adding pagination later a non breaking change.

Authentication and Authorization Architecture

Authentication is handled by a JWT guard that validates the token, decodes the payload, and attaches the user to the request object.

Authorization is handled separately through a roles guard and a permissions guard. The roles guard checks coarse grained access: is this user an admin, a manager, a standard user. The permissions guard checks fine grained access: does this user have permission to perform this specific action on this specific resource.

Separating authentication from authorization like this keeps the code clean and makes it possible to update the authorization logic without touching authentication, and vice versa.

I decorate routes with both the required role and the required permission. If either check fails, the request gets a 403. If authentication fails, it gets a 401. The HTTP status codes are semantically correct, which matters for frontend error handling.

Configuration Module

All configuration comes from environment variables, validated at startup using a schema. If a required environment variable is missing or malformed, the application refuses to start.

This is a forcing function for good deployment hygiene. You cannot accidentally deploy to production with a missing database URL because the application will not start without one. Every configuration error surfaces at startup, not at the moment a user triggers the code path that needs it.

I use NestJS ConfigModule with a Joi validation schema. The schema documents exactly what environment variables the application requires, which is also useful onboarding documentation for new engineers.

Health Check Endpoint

Every backend I ship has a /health endpoint that returns the status of the application and its critical dependencies: database connectivity, cache connectivity, and any external APIs that the application depends on.

This is used by load balancers and uptime monitoring. It is also the first place to look when something is wrong. If the database check fails on the health endpoint, you know where to look before you start digging through logs.

Database Migration Strategy

All schema changes go through migration files, never direct database modifications. Migrations run automatically on deployment before the new application code starts.

Every migration is written to be reversible where possible. I test rollbacks before shipping to production. This means that if a deployment causes issues, you can roll back the code and roll back the schema change without manual intervention.

For large tables, I use additive migrations first: add the new column, deploy the code that handles both old and new states, then run the migration to drop the old column. This avoids locking production tables on write heavy systems.

Why These Patterns and Not Others

These are not the only valid patterns. They are the ones that have consistently made my codebases easier to maintain, debug, and extend across multiple projects and multiple engineers.

The common thread is explicitness. Each pattern makes something explicit that would otherwise be implicit: what errors can happen, what inputs are valid, who has access to what, what the application requires to run. Explicit systems are easier to reason about and easier to fix when something goes wrong.

Start with the global exception filter and the request logger. Add the rest as your application grows. These are investments in maintainability that pay back quickly.


I am Ahad, Founder of REIVEX Technologies. I write about backend architecture, full stack engineering, and shipping production software fast. Find me at ahadnawaz.dev.

Top comments (0)