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!