Building Secure APIs with NestJS and TypeScript

Introduction

API security is not optional—it's essential. In today’s interconnected world, vulnerabilities in your API can lead to data breaches, financial losses, and reputational damage. For backend developers like me using NestJS with TypeScript, implementing security features is not only straightforward but also elegant, thanks to the powerful tools the framework provides.

In this post, we’ll explore how to build secure APIs with NestJS and TypeScript, covering authentication, authorization, and strategies to prevent common vulnerabilities.


1. Authentication: Securing Access with JWT

Authentication is the first line of defense for your API. JSON Web Tokens (JWT) are a popular choice because they’re stateless and scalable. Here’s how to set up JWT-based authentication in a NestJS application:

Install Dependencies

Start by installing the necessary packages:

yarn add @nestjs/jwt @nestjs/passport passport passport-jwt
yarn add -D @types/passport-jwt

Create the JWT Strategy

A strategy defines how authentication should work. With the passport-jwt library, you can create a strategy to validate JWTs:

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(),
      secretOrKey: process.env.JWT_SECRET,
    });
  }

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

Protect Endpoints

With the strategy in place, you can guard your endpoints using NestJS’s AuthGuard:

import { Controller, Get, UseGuards } from "@nestjs/common";
import { JwtAuthGuard } from "./auth/jwt-auth.guard";

@Controller("profile")
export class ProfileController {
  @Get()
  @UseGuards(JwtAuthGuard)
  getProfile() {
    return { message: "This is a protected route" };
  }
}

Now, only authenticated users with a valid JWT can access the /profile endpoint.


2. Authorization: Role-Based Access Control (RBAC)

Once users are authenticated, you need to control what they can do. Role-Based Access Control (RBAC) is a straightforward way to achieve this.

Define Roles

Start by creating a simple Roles enum:

export enum Role {
  Admin = "admin",
  User = "user",
}

Create a Role Guard

NestJS makes it easy to create custom guards for authorization:

import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
import { Reflector } from "@nestjs/core";

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

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.get<string[]>(
      "roles",
      context.getHandler()
    );
    if (!requiredRoles) {
      return true;
    }

    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}

Apply Roles to Routes

Use decorators to specify roles for routes:

import { SetMetadata } from "@nestjs/common";

export const Roles = (...roles: Role[]) => SetMetadata("roles", roles);

Then, apply it in your controller:

import { Controller, Get, UseGuards } from "@nestjs/common";
import { RolesGuard } from "./auth/roles.guard";
import { Roles } from "./auth/roles.decorator";
import { Role } from "./auth/role.enum";

@Controller("admin")
@UseGuards(RolesGuard)
export class AdminController {
  @Get()
  @Roles(Role.Admin)
  getAdminPanel() {
    return { message: "Welcome, Admin!" };
  }
}

3. Input Validation: Preventing Injection Attacks

Input validation is crucial for preventing attacks like SQL injection and cross-site scripting (XSS). With NestJS and TypeScript, you can leverage class-validator and class-transformer:

Install Dependencies

yarn add class-validator class-transformer

Define DTOs for Validation

Use Data Transfer Objects (DTOs) to validate incoming requests:

import { IsString, IsEmail } from "class-validator";

export class CreateUserDto {
  @IsString()
  name: string;

  @IsEmail()
  email: string;
}

Apply Validation in Controllers

Validate inputs directly in your endpoints:

import { Body, Controller, Post } from "@nestjs/common";
import { CreateUserDto } from "./dto/create-user.dto";

@Controller("users")
export class UsersController {
  @Post()
  createUser(@Body() createUserDto: CreateUserDto) {
    return { message: "User created", data: createUserDto };
  }
}

Now, invalid inputs will be automatically rejected, ensuring only sanitized data reaches your application logic.


4. Other Security Best Practices

Use HTTPS

Always deploy your API over HTTPS to protect data in transit. Tools like Let’s Encrypt make it easy to set up SSL certificates.

Rate Limiting

Prevent brute force attacks by limiting the number of requests from a single client. Use packages like nestjs-rate-limiter.

IP Blacklisting

Deny requests from malicious IPs using middleware or a reverse proxy like NGINX.

Secure Sensitive Data

Encrypt sensitive data, such as passwords, before storing them. Libraries like bcrypt are great for hashing passwords.


Conclusion

Building secure APIs requires more than just technical know-how; it demands a security-first mindset. With NestJS and TypeScript, implementing robust security measures is straightforward, thanks to the framework’s modular design and TypeScript’s static typing.

Whether you’re just starting with NestJS or have experience building APIs, following these security best practices will help you create applications that users trust.

What security measures do you use in your APIs? Share your thoughts and tips in the comments below!

← Back to posts