Welcome back to the NestJS Expert Series! 🎉
In Part https://dev.to/devto_with_yog/nestjs-expert-series-part-2-database-integration-with-prisma-typeorm-ica, we integrated our NestJS app with a database using Prisma and TypeORM.
Now it’s time to secure our application with Authentication (who you are) and Authorization (what you can do).
In this article, we’ll build a JWT-based authentication system and use Guards to implement role-based access control.
⚡ Why JWT for Authentication?
JWT (JSON Web Token) is a compact, URL-safe token format widely used for stateless authentication.
✅ Stateless – No need to store session data on the server.
✅ Scalable – Works well in distributed systems.
✅ Cross-platform – Can be used by web, mobile, and microservices.
📦 Step 1: Install Required Packages
🛠️ Step 2: Auth Module Setup
Generate an auth module, service, and controller:
nest g module auth
nest g service auth
nest g controller auth
👤 Step 3: User Entity (with Prisma Example)
If you’re using Prisma, update your schema:
model User {
id Int @id @default(autoincrement())
email String @unique
password String
role String @default("user")
}
Run migration:
npx prisma migrate dev --name add_user_model
🔑 Step 4: Auth Service (Register & Login)
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(private jwtService: JwtService) {}
async hashPassword(password: string): Promise {
return bcrypt.hash(password, 10);
}
async validatePassword(password: string, hash: string): Promise {
return bcrypt.compare(password, hash);
}
async generateToken(user: { id: number; email: string; role: string }) {
return this.jwtService.sign({ sub: user.id, email: user.email, role: user.role });
}
}
📜 Step 5: Auth Controller
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('register')
async register(@Body() body: { email: string; password: string }) {
const hashed = await this.authService.hashPassword(body.password);
// Save user to DB with hashed password
return { message: 'User registered successfully' };
}
@Post('login')
async login(@Body() body: { email: string; password: string }) {
// Find user in DB and validate password
// If valid, generate token
return { access_token: await this.authService.generateToken({ id: 1, email: body.email, role: 'user' }) };
}
}
🛡️ Step 6: Guards for Authorization
NestJS Guards decide whether a request should proceed or not.
Create a roles.guard.ts:
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('roles', context.getHandler());
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.includes(user.role);
}
}
Use a custom decorator roles.decorator.ts:
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
Apply it in a controller:
import { Controller, Get, UseGuards } from '@nestjs/common';
import { Roles } from './roles.decorator';
import { RolesGuard } from './roles.guard';
@Controller('admin')
export class AdminController {
@Get()
@Roles('admin')
@UseGuards(RolesGuard)
findAdminData() {
return { secret: 'This is admin-only data' };
}
}
🚀 Conclusion
✅ We built JWT-based authentication.
✅ We secured routes with Guards and Role-based Authorization.
✅ We now have the foundation for a secure, multi-user NestJS application.
In the next part, we’ll tackle Validation & Error Handling with Pipes and Filters to make our APIs more robust.
💡 If you found this helpful, drop a ❤️ on Dev.to and follow me for Part 4 of the NestJS Expert Series.