Skip to content
Fernando Valler

Security Checklist for NestJS Backend Applications

A practical 30-point security checklist for NestJS backend applications, covering authentication, authorization, input validation, rate limiting, secrets, CORS, CSRF, file uploads, logging, dependencies, and infrastructure.

Security Checklist for NestJS Backend Applications

Building an API with NestJS is relatively straightforward. Building an API that remains secure when exposed to the internet, handling untrusted input, authentication, file uploads, external integrations, and sensitive information requires additional care.

NestJS provides several mechanisms for implementing security, including Guards, Pipes, interceptors, authentication, authorization, rate limiting, and security headers. However, the framework cannot decide which business rules need to be protected in your specific application.

This checklist brings together practical security checks that can be performed before deploying a NestJS application to production.


1. Is authentication implemented correctly?

Every application with users or private data needs to clearly define how the identity of the client will be validated.

Check that:

  • Private endpoints require authentication.
  • Invalid tokens are rejected.
  • Expired tokens are rejected.
  • The JWT validation algorithm is explicitly configured.
  • The application does not accept unexpected algorithms.
  • Keys and secrets are not hardcoded.
  • Refresh tokens are protected against improper reuse.
  • Tokens have an expiration time appropriate to the application’s risk level.

NestJS provides Guards to protect routes and also allows an authentication Guard to be registered globally, leaving only explicitly marked routes public.

A useful strategy is:

By default, a route should be private. Public access should be an explicit exception.


2. Authentication is not authorization

An authenticated user is not automatically allowed to access every resource.

Consider an API such as:

http
GET /users/123/orders

The fact that the user is authenticated does not mean they are allowed to view the orders belonging to user 123.

This type of issue is related to Broken Object Level Authorization (BOLA), listed as API1 in the OWASP API Security Top 10.

Check that:

  • Each resource verifies who is allowed to access it.
  • IDs sent by the client are not treated as authorization.
  • A user cannot access another user’s resources simply by changing an ID.
  • Administrative operations require additional authorization.
  • Update, delete, export, and similar operations require the appropriate permissions.

In NestJS, Guards can be used to structure mechanisms such as RBAC, but resource-level authorization still needs to reflect the actual business rules of the application.


3. Do not trust data provided by the client

Never consider data safe simply because there is a DTO or because TypeScript defines a type.

The client controls the request.

It can send:

json
{
  "name": "Fernando",
  "role": "admin",
  "isActive": true,
  "balance": 999999
}

If the application blindly persists everything it receives, this can lead to mass assignment or authorization issues at the property level.

The API should explicitly define which fields can be accepted for each operation.

For example, prefer specific DTOs:

typescript
export class CreateUserDto {
  name: string;
  email: string;
}

Instead of directly using a database entity as an API input model.


4. Use ValidationPipe globally

Input validation should happen before request data reaches your business logic.

A common starting configuration is:

typescript
app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
  }),
);

Check that:

  • ValidationPipe is configured globally.
  • Specific DTOs are used for input.
  • Required fields are explicitly validated.
  • Enums are validated.
  • Maximum lengths are defined.
  • Strings have appropriate validation rules.
  • IDs are validated.
  • Pagination values are limited.
  • Unexpected properties are rejected or removed.

The whitelist option can remove properties that do not have validation decorators, while forbidNonWhitelisted can reject requests containing unexpected properties.


5. Never rely on TypeScript as a security mechanism

This:

typescript
createUser(dto: CreateUserDto)

does not prevent an HTTP request from sending arbitrary JSON.

TypeScript provides compile-time assistance, but HTTP input arrives from outside the application and must be validated at runtime.

In other words:

text
TypeScript ≠ runtime input validation

Validation must happen at the boundary between your application and the outside world.


6. Configure CORS explicitly

CORS should not simply be treated as:

typescript
app.enableCors();

without considering the application’s architecture.

Explicitly define which origins are allowed to consume your API.

For example:

typescript
app.enableCors({
  origin: [
    'https://app.example.com',
  ],
  credentials: true,
});

Avoid allowing arbitrary origins in environments that use cookie-based authentication.

Also remember:

CORS does not replace authentication or authorization.

It controls how browsers enforce cross-origin access. It does not prevent another HTTP client from directly calling your API.


If authentication relies on cookies that browsers send automatically, evaluate whether the application requires protection against Cross-Site Request Forgery (CSRF).

Also review authentication cookies for attributes such as:

text
HttpOnly
Secure
SameSite

The correct strategy depends on how the frontend, backend, and authentication domains are structured.

An API using:

http
Authorization: Bearer <token>

has a different threat model from an application relying on authenticated sessions stored in cookies.


8. Rate-limit login and sensitive operations

Endpoints such as:

text
POST /auth/login
POST /auth/forgot-password
POST /auth/reset-password
POST /auth/verify-code
POST /auth/send-code

should not accept unlimited attempts.

NestJS provides @nestjs/throttler for implementing rate limiting and protecting against attacks such as brute force.

For example:

typescript
ThrottlerModule.forRoot({
  throttlers: [
    {
      ttl: 60_000,
      limit: 10,
    },
  ],
});

But do not necessarily use the same policy for every endpoint.

A request such as:

http
GET /products

may require a completely different limit from:

http
POST /auth/login

For distributed applications, also make sure the rate-limiting storage strategy is compatible with multiple application instances.


9. Never store passwords in plain text

Passwords should never be stored as:

text
password123

and they should not be stored using reversible encryption.

Use a proper password hashing algorithm.

NestJS documents approaches using libraries such as bcrypt and argon2.

Example:

typescript
const hash = await bcrypt.hash(password, 12);

And to verify:

typescript
const valid = await bcrypt.compare(password, hash);

Never implement your own password hashing algorithm.


10. Secrets should never be committed to Git

Search your repository for values such as:

text
JWT_SECRET=
DATABASE_PASSWORD=
AWS_SECRET_ACCESS_KEY=
API_KEY=
PRIVATE_KEY=

Secrets should not be stored in source code.

Use an appropriate secret-management strategy for each environment:

text
Development → local .env
Staging     → secret manager / environment
Production  → secret manager / environment

Also review:

text
.gitignore
Docker images
CI/CD logs
build artifacts
backups
logs

Removing a secret from the current source file does not necessarily fix a leak that already exists in Git history.


11. Validate environment variables

A common production problem is starting an application with an incomplete configuration:

text
DATABASE_URL=
JWT_SECRET=
REDIS_HOST=

and discovering the problem only when a specific endpoint is used.

Validate required environment variables during application startup.

The application should fail fast when a required configuration value is missing or invalid.

That is much safer than allowing the application to start in a partially configured state.


12. Use HTTPS

Never treat HTTPS as something only the frontend is responsible for.

All communication involving:

  • credentials;
  • tokens;
  • personal data;
  • financial information;
  • administrative data;

should use secure transport.

In production, also review:

text
HTTP → HTTPS
HSTS
TLS configuration
Secure cookies
Proxy configuration

If a reverse proxy is being used, verify that the application correctly handles the original scheme and client IP when those values affect authentication, logging, or rate limiting.


13. Configure security headers

HTTP applications can use security headers to enable browser-side protections.

For current NestJS versions, app.useSecurityHeaders() can be used to apply security headers through the underlying security-header implementation. Helmet is also documented as an alternative.

Example:

typescript
const app = await NestFactory.create(AppModule);

app.useSecurityHeaders();

For versions that do not provide this API, Helmet can be used directly:

typescript
import helmet from 'helmet';

app.use(helmet());

Always test the resulting configuration with Swagger, GraphQL IDEs, embedded resources, and other components that may be affected by Content Security Policy or other headers.


14. Do not expose sensitive information in error messages

Avoid responses such as:

json
{
  "error": "SQLSTATE[HY000]: Access denied for user..."
}

or:

json
{
  "error": "/var/www/app/src/modules/users/users.service.ts:42"
}

Production error responses should provide enough information for the client without exposing:

  • stack traces;
  • internal paths;
  • credentials;
  • SQL queries;
  • internal service names;
  • infrastructure details.

Detailed diagnostic information should remain available to internal observability systems rather than being returned to end users.


15. Never log secrets

Pay special attention to:

text
Authorization
Cookie
password
refreshToken
accessToken
apiKey
clientSecret

Avoid:

typescript
logger.debug(request.body);
logger.debug(request.headers);

unless you fully understand the contents being logged.

Logs are valuable for incident investigation, but they must be treated as potentially sensitive data.


16. Do not trust frontend validation alone

This:

typescript
if (password.length < 8) {
  // ...
}

in React, Vue, or Angular does not protect the API.

An attacker can completely bypass the user interface and send:

bash
curl

or use their own HTTP client.

Every relevant security rule must be enforced on the backend.


17. Protect against SQL and NoSQL injection

Never build queries by directly concatenating user-controlled values.

Avoid:

typescript
const query = `
  SELECT *
  FROM users
  WHERE email = '${email}'
`;

Prefer parameterized queries and the safe query mechanisms provided by your ORM or database driver.

The same concern applies to:

  • SQL;
  • MongoDB;
  • Redis;
  • search engines;
  • dynamic filters;
  • external commands.

Using an ORM can reduce certain classes of risk, but it does not automatically make every query safe.


18. Limit pagination and result sizes

An endpoint such as:

http
GET /users

should not allow:

http
GET /users?limit=999999999

Define explicit limits:

text
page >= 1
limit >= 1
limit <= 100

This reduces risks related to:

  • excessive memory consumption;
  • expensive queries;
  • huge responses;
  • automated abuse.

This is also related to Unrestricted Resource Consumption, identified as API4 in the OWASP API Security Top 10.


19. Be careful with file uploads

File uploads are an attack surface by themselves.

Do not trust only the extension:

text
image.jpg

Check, as appropriate:

  • maximum size;
  • MIME type;
  • allowed extensions;
  • actual file content when necessary;
  • number of uploaded files;
  • file name;
  • storage location;
  • antivirus or sandboxing requirements.

Never store uploaded files in a directory where the web server can execute them.


20. Protect download endpoints

Hiding a file link is not authorization.

Avoid assuming that:

http
GET /files/123

is safe simply because the ID exists.

Verify:

text
authenticated user
resource ownership
required permission
resource availability

A file download endpoint needs authorization just like any other API resource.


21. Protect external integrations against SSRF

Be especially careful with endpoints such as:

http
POST /fetch-url

receiving:

json
{
  "url": "https://example.com"
}

If the backend makes arbitrary HTTP requests to user-provided URLs, the application may become vulnerable to Server-Side Request Forgery (SSRF).

When possible:

  • use host allowlists;
  • block internal networks;
  • block localhost;
  • validate redirects;
  • enforce connection timeouts;
  • limit response sizes;
  • disable unnecessary protocols.

SSRF is included as API7 in the OWASP API Security Top 10.


22. Validate incoming webhooks

Integrations such as:

text
Stripe
GitHub
Mercado Pago
PagSeguro
internal systems
authentication providers

often send webhooks.

Do not rely only on the source IP.

Whenever the provider offers it, validate:

text
signature
timestamp
payload
idempotency

Also store event identifiers so the same event cannot be processed multiple times.


23. Implement idempotency where necessary

Operations such as:

text
create payment
issue document
create order
send charge
process webhook

may receive the same request more than once.

A secure application should not turn a retry into multiple financial or administrative operations.

When appropriate, use an idempotency key:

http
Idempotency-Key: 9c3d...

and ensure that the operation is processed only once.


24. Restrict administrative functions

Endpoints such as:

http
POST /admin/users
DELETE /admin/users/:id
GET /admin/audit
POST /admin/settings

deserve an additional authorization layer.

Do not scatter checks such as:

typescript
if (user.role === 'admin')

throughout the application.

Centralize authorization rules using Guards, policies, or another consistent mechanism.

NestJS provides examples for RBAC and other authorization strategies.


25. Do not expose unnecessary internal endpoints

Review the API for routes such as:

text
/debug
/test
/internal
/admin
/metrics
/swagger
/api-json

Not every endpoint created during development needs to be publicly available.

Swagger deserves special attention because it can expose both the API interface and the OpenAPI document.

In production, consider whether the documentation should be:

text
protected by authentication
available only internally
disabled

The important thing is to make the decision consciously.


26. Maintain an endpoint inventory

A large API may contain:

text
/users
/users/:id
/orders
/orders/:id
/admin
/internal
/webhooks
/debug

and each endpoint may have a different security policy.

Perform regular reviews of the exposed API surface.

For each endpoint, document:

text
Method
URL
Authentication
Authorization
Input data
Response data
Rate limit
Sensitivity

This helps identify forgotten, obsolete, or inconsistently protected endpoints.

The OWASP API Security Top 10 also highlights Improper Inventory Management as API9.


27. Return only the fields that are necessary

Imagine:

typescript
return user;

and the entity contains:

text
id
name
email
passwordHash
refreshToken
resetToken
internalNotes

Even if serialization happens automatically, sensitive information may accidentally be exposed.

Prefer response DTOs:

typescript
export class UserResponseDto {
  id: string;
  name: string;
  email: string;
}

The API response should be an explicit decision.


28. Keep dependencies updated and audited

A NestJS application is not made up only of the code you wrote.

It depends on components such as:

text
NestJS
Express/Fastify
ORM
JWT libraries
bcrypt/argon2
Swagger
Redis
RabbitMQ
helper libraries

Include dependency checks in CI/CD:

bash
npm audit

and keep dependencies updated in a controlled way.

Also monitor vulnerabilities in transitive dependencies.


29. Automate security testing

Before deployment, test at least:

text
401 → unauthenticated endpoint
403 → unauthorized user
404 → nonexistent resource
400 → invalid payload
429 → too many requests
413 → oversized payload

Also test scenarios such as:

text
user A accessing user B's resource
regular user accessing an admin endpoint
expired token
invalid token
refresh token reuse
manipulated IDs
unexpected fields in payloads
invalid uploads
malicious external URLs

The goal is not only to prove that the happy path works.

The goal is to prove that forbidden paths are actually blocked.


30. Review infrastructure together with the code

A secure NestJS application can still be exposed because of infrastructure mistakes.

Before deploying to production, review:

text
HTTPS
Firewall
Reverse proxy
Database
Redis
Queues
Docker
Volumes
Secrets
Backups
SSH
Logs
Monitoring
Alerts

Avoid exposing services such as:

text
PostgreSQL
MySQL
Redis
RabbitMQ
MongoDB

directly to the internet unless there is a specific reason.

Ideally, the public surface should remain small:

text
Internet
   ↓
Reverse Proxy / Load Balancer
   ↓
NestJS
   ↓
Internal Services

A main.ts starting point

A basic setup can centralize several of these protections:

typescript
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';

import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.setGlobalPrefix('api');

  app.enableCors({
    origin: ['https://app.example.com'],
    credentials: true,
  });

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );

  app.useSecurityHeaders();

  await app.listen(process.env.PORT ?? 3000);
}

bootstrap();

The exact configuration depends on the NestJS version, HTTP adapter, authentication strategy, and application architecture.

For versions where useSecurityHeaders() is not available, Helmet remains a documented alternative.


Final pre-deployment checklist

Use this as a final review:

text
[ ] HTTPS enforced
[ ] Authentication implemented
[ ] Authorization implemented
[ ] Guards protect private endpoints
[ ] DTOs defined
[ ] Global ValidationPipe configured
[ ] whitelist enabled
[ ] Mass assignment reviewed
[ ] CORS explicitly configured
[ ] CSRF evaluated
[ ] Rate limiting configured
[ ] Brute force protection implemented
[ ] Passwords hashed with bcrypt/Argon2
[ ] JWT expiration configured
[ ] Secrets kept out of Git
[ ] Environment variables validated
[ ] Error responses contain no sensitive information
[ ] Logs contain no tokens or passwords
[ ] SQL/NoSQL injection reviewed
[ ] Pagination limits enforced
[ ] Uploads restricted
[ ] Downloads authorized
[ ] SSRF reviewed
[ ] Webhook signatures validated
[ ] Critical operations are idempotent
[ ] Administrative endpoints protected
[ ] Swagger/API documentation protected
[ ] Unused/internal endpoints removed
[ ] Dependencies audited
[ ] Automated security tests implemented
[ ] Infrastructure reviewed

Security is not a middleware

One of the biggest mistakes when working with NestJS is believing that installing Helmet, enabling CORS, and configuring JWT means the application is secure.

These tools are important, but they represent only part of the problem.

API security exists across multiple layers:

text
                 Internet
                    │
                    ▼
              HTTPS / Proxy
                    │
                    ▼
             Rate Limiting
                    │
                    ▼
             Authentication
                    │
                    ▼
             Authorization
                    │
                    ▼
              Validation
                    │
                    ▼
             Business Rules
                    │
                    ▼
              Database

Each layer needs to assume that the previous one may be insufficient.

An authenticated request may still be unauthorized.

An authorized request may still contain malicious or invalid data.

Valid input can still abuse a resource.

And a properly protected application can still leak secrets through logs or infrastructure configuration.

That is why security should be treated as a continuous process rather than as a single middleware configuration.

The best time to run this checklist is not after an incident.

It is before the first production deployment.


References

  • NestJS — Authentication
  • NestJS — Authorization
  • NestJS — Validation
  • NestJS — Rate Limiting
  • NestJS — Security Headers / Helmet
  • NestJS — CORS
  • NestJS — CSRF Protection
  • NestJS — Encryption and Hashing
  • NestJS — File Upload
  • OWASP API Security Top 10
  • OWASP Secrets Management Cheat Sheet
  • OWASP Logging Cheat Sheet