# API Security in NestJS Production: The Controls I Actually Deploy

> Practical NestJS API security — rate limiting, input validation, security headers, audit logging, and the specific controls I configure for every production API deployment.

URL: https://www.matthewswong.com/en/blog/api-security-nestjs-production/

- Author: Matthews Wong (https://www.matthewswong.com/en/about/)
- Published: 2025-06-10
- Updated: 2025-06-10
- Category: DevOps

Building an API that works is table stakes. Building one that is secure under real-world attack conditions is where most production systems fall short. I run NestJS APIs serving ERP data for business clients — financial records, employee data, inventory levels. An API security failure on these systems has direct business consequences. After hardening multiple NestJS APIs and dealing with real attack attempts (brute force, injection probes, enumeration attacks), I have a security configuration checklist I apply to every deployment. This post covers the specific controls, code patterns, and monitoring that I use in production.

## Validation and Sanitization at the Boundary

The first security control in any NestJS API is input validation using class-validator and class-transformer. Every DTO (Data Transfer Object) should have explicit validation decorators — @IsString(), @IsEmail(), @IsInt('{ min: 0 }'), @MaxLength(255), etc. Enable the global ValidationPipe with whitelist: true (strips unknown properties) and forbidNonWhitelisted: true (throws on unknown properties rather than silently ignoring them). This prevents mass assignment attacks and ensures only expected properties reach your service layer.

### Global Validation and Security Middleware

Configure the global ValidationPipe in main.ts with strict settings. Add Helmet for security headers (X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, etc.) via the @nestjs/helmet package. Add compression middleware. Set a request body size limit to prevent large payload DoS attacks. These four lines of configuration — ValidationPipe, Helmet, compression, body limit — cover a significant surface area of common API vulnerabilities with minimal code.

> From my experience securing ERP APIs: add a custom ExceptionFilter that normalizes error responses. By default, NestJS returns different error response shapes for validation errors, HTTP exceptions, and unhandled exceptions. A uniform error response format prevents information leakage (stack traces, internal paths, database error messages) and makes security scanning harder. Never return raw database error messages — they reveal schema information and query structure.

## Rate Limiting and Brute Force Protection

Every public API endpoint needs rate limiting. NestJS has the @nestjs/throttler package which integrates cleanly. Configure three tiers: global limits (100 requests per minute per IP), auth endpoints (10 requests per minute per IP — much stricter), and authenticated endpoints (500 requests per minute per user). For auth endpoint rate limiting, also implement account-level lockout (not just IP-based) because attackers route through multiple IPs. Store rate limit counters in Redis for horizontal scaling — the in-memory default does not work with multiple API instances.

### IP Blocking and Fail2Ban Integration

At the infrastructure level, integrate with fail2ban to block persistent attackers at the nginx layer before they reach your NestJS application. Log structured JSON from your API (timestamp, IP, endpoint, status code, user_id) and write a fail2ban filter that triggers on patterns like: 20+ auth failures from one IP in 5 minutes, 50+ 422 validation errors from one IP in 10 minutes, or any SQLmap signature patterns in the request. Block for 24 hours on first trigger, 7 days on second, permanent on third.

> **Exposing Internal Errors Warning**

> I reviewed a production NestJS API for a client that was returning Prisma error objects verbatim in API responses. These responses included the SQL query that failed, the database schema field names, and in one case the connection string error mentioning the database hostname. All of this was visible to anyone who sent a malformed request. An attacker learns your database schema, query patterns, and infrastructure details for free. Implement a global ExceptionFilter that catches all unhandled exceptions, logs the full error server-side, and returns a generic error response to the client. Never trust NestJS's default error handling for production.

## Authentication Guards and Authorization Decorators

NestJS guards are the right place to implement authentication. A JWT guard validates the token on every request and attaches the user to the request context. Then use custom decorators for authorization: @Roles('admin') checked by a RolesGuard, @CheckOwnership() verified by an ownership guard that queries the resource. The critical rule: authentication (who are you?) and authorization (what can you do?) are separate concerns handled by separate guards. Do not mix them in service methods — keep authorization at the controller/guard layer where it is systematic and auditable.

## Audit Logging for Security Events

Security-relevant events must be logged with enough context to investigate incidents. Use a structured logging library (Winston or Pino) with JSON output. Log: every authentication event (success/failure, IP, user agent), every authorization failure (who, what resource, why denied), every admin action (who, what action, on what resource, before/after state), every input validation failure (endpoint, fields, IP). Ship logs to a centralized log service (ELK stack, Loki, CloudWatch) — logs on the same server as the application can be deleted by an attacker. Retain security logs for 90 days minimum, 1 year for compliance.

## My NestJS Security Checklist

Every production NestJS API I deploy gets: global ValidationPipe (whitelist, forbidNonWhitelisted, transform), Helmet security headers, rate limiting (throttler + Redis), CORS allowlist, JWT authentication guard, RBAC authorization guard, global ExceptionFilter (no stack traces in responses), structured audit logging (Winston + JSON), SQL injection prevention (Prisma ORM, validated raw queries only), input sanitization for XSS (strip HTML from string inputs), API versioning to allow security patches without breaking clients, and health check endpoint that does not leak system information. This covers the OWASP Top 10 controls most relevant to API-first applications.

## Sources & Further Reading

- NestJS — Security Documentation — https://docs.nestjs.com/security/authentication
- OWASP — REST Security Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html
- OWASP — Top 10 API Security Risks 2023 — https://owasp.org/API-Security/editions/2023/en/0x11-t10/

## Source links

- https://docs.nestjs.com/security/authentication
- https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html
- https://owasp.org/API-Security/editions/2023/en/0x11-t10/

## FAQ

### Why should NestJS's default error handling never be trusted in production?

By default, NestJS returns different error shapes for validation errors, HTTP exceptions, and unhandled exceptions — which can leak stack traces, internal file paths, and raw database error messages to the caller. A real example from a client audit showed Prisma error objects being returned verbatim, exposing the failed SQL query, schema field names, and even the database hostname. A global ExceptionFilter should catch all unhandled exceptions, log the full detail server-side, and return only a generic error to the client.

### What three rate-limiting tiers does this post recommend, and why does Redis matter for them?

The post recommends global limits (100 requests/minute per IP), strict auth-endpoint limits (10 requests/minute per IP), and higher authenticated-user limits (500 requests/minute per user). Redis is required for storing rate-limit counters because the default in-memory store does not survive multiple API instances — horizontal scaling would let attackers bypass limits by hitting different instances.

### How do NestJS guards separate authentication from authorization, and why does that matter for security?

Authentication (verifying who the caller is) is handled by a JWT guard that validates the token and attaches the user to the request context, while authorization (determining what the caller can do) is handled by separate guards like RolesGuard and an ownership guard using decorators such as @Roles('admin') and @CheckOwnership(). Keeping the two concerns in separate guards ensures authorization is applied systematically at the controller layer and is auditable, rather than scattered inconsistently through service methods.

### What events must be captured in audit logs, and where should those logs be stored?

Security-relevant events to log include every authentication attempt (success or failure, with IP and user agent), every authorization failure, every admin action with before/after state, and every input validation failure. Logs must be shipped to a centralized service such as an ELK stack, Loki, or CloudWatch — keeping logs on the same server as the application means an attacker who gains access can delete them. The post recommends retaining security logs for at least 90 days, or one year for compliance.

### What does enabling whitelist and forbidNonWhitelisted on the global ValidationPipe actually protect against?

Setting whitelist: true strips any request properties that are not declared in the DTO, while forbidNonWhitelisted: true goes further and throws an error rather than silently dropping the unexpected fields. Together these two options prevent mass assignment attacks — where an attacker adds extra fields (such as isAdmin or role) hoping the application will persist them — and ensure only explicitly defined properties ever reach the service layer.

---

- HTML version: https://www.matthewswong.com/en/blog/api-security-nestjs-production/
- Bahasa Indonesia: https://www.matthewswong.com/id/blog/api-security-nestjs-production/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
