API Security Best Practices with Practical NestJS Examples

Posted 7 months ago
Updated 7 months ago
10 minutes reading
Viewed 0 times
NestJSAPI SecurityBackendAuthenticationAuthorization

APIs are the backbone of modern applications, but they are also prime targets for malicious attacks. Securing your API is critical to protect your data, users, and infrastructure. In this post, we'll explore API security best practices and show you how to implement them effectively in a NestJS application.

1. Enforce HTTPS

Using HTTPS ensures that data transmitted between the client and the server is encrypted, protecting it from interception and tampering.

Implementation in NestJS:

To enforce HTTPS in your NestJS app, you can configure an SSL certificate:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as fs from 'fs';

async function bootstrap() {
  const httpsOptions = {
    key: fs.readFileSync('./secrets/key.pem'),
    cert: fs.readFileSync('./secrets/cert.pem'),
  };
  const app = await NestFactory.create(AppModule, { httpsOptions });
  await app.listen(3000);
}
bootstrap();

2. Use Strong Authentication

Authentication is the process of verifying the identity of users or systems accessing your API.

Example: JWT Authentication

NestJS provides built-in support for JSON Web Tokens (JWT). To implement JWT authentication.

First, install the required packages:

npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt
npm install -D @types/passport-jwt @types/bcrypt

Create an authentication module:

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './strategies/jwt.strategy';

@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_SECRET, // Use a strong secret in production
      signOptions: { expiresIn: '1h' },
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy],
})
export class AuthModule {}

Implement the JWT strategy:

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: process.env.JWT_SECRET,
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

3. Implement Authorization

Authorization ensures users can only access resources they are allowed to. For example, admins might have more privileges than regular users.

Role-Based Access Control (RBAC) in NestJS:

You can implement RBAC using guards.

  1. Create a roles guard:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (!requiredRoles) {
      return true;
    }

    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}
  1. Apply roles to endpoints using metadata:
import { SetMetadata } from '@nestjs/common';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
  1. Use the guard in your controller
import { Controller, Get, UseGuards } from '@nestjs/common';
import { Roles } from './roles.decorator';
import { RolesGuard } from './roles.guard';

@Controller('users')
@UseGuards(RolesGuard)
export class UsersController {
  @Get('admin')
  @Roles('admin')
  getAdminData() {
    return 'Admin data';
  }
}

4. Limit Request Rates

Rate limiting protects your API from abuse by restricting the number of requests a client can make.

Implementation in NestJS:

Use the @nestjs/throttler package for rate limiting.

  1. Install the package:
npm install @nestjs/throttler
  1. Configure the module:
import { Module } from '@nestjs/common';
import { ThrottlerModule } from '@nestjs/throttler';

@Module({
  imports: [
    ThrottlerModule.forRoot({
      ttl: 60, // Time to live (in seconds)
      limit: 10, // Maximum number of requests per user within the TTL
    }),
  ],
})
export class AppModule {}
  1. Apply the throttler guard:
import { Controller, Get, UseGuards } from '@nestjs/common';
import { ThrottlerGuard } from '@nestjs/throttler';

@Controller('data')
@UseGuards(ThrottlerGuard)
export class DataController {
  @Get()
  getData() {
    return 'Rate-limited data';
  }
}

5. CORS (Cross-Origin Resource Sharing)

Configure CORS to allow requests only from trusted origins.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

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

  app.enableCors({
    origin: ['https://yourdomain.com'],
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    credentials: true, // If you need to send cookies
  });

  await app.listen(3000);
}
bootstrap();

6. Security Headers

Set security headers to protect against common web vulnerabilities. Helmet helps with this.

  • HSTS (HTTP Strict Transport Security): Enforces HTTPS connections.
  • X-Frame-Options: Prevents clickjacking attacks.
  • X-Content-Type-Options: Prevents MIME-sniffing attacks.
  • Content-Security-Policy (CSP): Defines a policy for allowed resources.

7. Error Handling

Avoid exposing sensitive information in error messages.

import { Catch, ExceptionFilter, ArgumentsHost, HttpException } from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const status = exception.getStatus();
    const exceptionResponse = exception.getResponse();

    response.status(status).json({
      statusCode: status,
      message:
        typeof exceptionResponse === 'string'
          ? exceptionResponse
          : (exceptionResponse as any).message || 'Internal server error', // Don't expose detailed error information in production
    });
  }
}

//In your main.ts
app.useGlobalFilters(new HttpExceptionFilter());

Conclusion

Securing your API is an ongoing process that requires vigilance and the application of best practices. By following the strategies outlined in this article, you can significantly enhance the security of your NestJS applications.


Subscribe to the newsletter

Get mail from me about web development, tech, and early access to new articles.