Implementing JWT Authentication in NestJS with Guards
Technology Blogs

Implementing JWT Authentication in NestJS with Guards

Arzoo Jain
Associate Software Engineer
Table of Content

If you’ve recently started working with NestJS, chances are you’ll bump into one major requirement:

“We need authentication!”

And not just any authentication — something secure, scalable, and clean to maintain.

“Authentication is one of the most essential parts of any modern-day backend application. It serves as a primary defense against unauthorized access, protecting sensitive data in applications like APIs or healthcare systems.”

That’s where JWT (JSON Web Token) comes in and where NestJS really shines with its modular structure and built-in Guards.

In this blog, we’ll learn exactly how to implement JWT authentication with Guards in NestJS, step by step, while understanding why each step is needed. By the end, we’ll know how to:

  • Issues token while logging in
  • Validate tokens using a strategy
  • Protect routes using guards
  • Access user details from the token

Let’s Jump In!

Why use JWT in a Web Application?

JWTs have become the go-to solution for stateless authentication because:

  • No session storage required at the backend
  • Works seamlessly with SPA apps (React, Angular, Vue)
  • Easy to carry user identity and roles inside the token
  • Secured with a signature, so tampering is detectable

Think of JWT as a secure event entry pass. Once you enter with a valid pass, the server doesn’t need to check the databases
repeatedly.

Setting Up a Fresh NestJS Project

First, install the Nest CLI globally with:

npm i -g @nestjs/cli

And then run:

nest new project-name (or nest n project-name)

Navigate and run:

cd project-name followed by npm run start:dev (or yarn start:dev).?

Now install the required authentication dependencies:

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

Here’s what’s being installed:

  • passport → A popular auth framework
  • nestjs/passport → Passport adapter for NestJS
  • passport-jwt → Allows Passport to validate JWT tokens
  • @nestjs/jwt → Helps us generate and decode JWT
  • bcrypt → For password hashing

Step 1: Database Setup (PostgreSQL + TypeORM)

Install DB packages:

npm install pg typeorm @nestjs/typeorm

Database Configuration (AppModule)

Inside app.module.ts we configure TypeORM to talk to Postgres,
This connects our app to Postgres and lets us store users securely.

import { Module } from '@nestjs/common';

import { TypeOrmModule } from '@nestjs/typeorm';

import { AuthModule } from './auth/auth.module';

import { User } from './users/user.entity';

import { UsersModule } from './users/users.module';

@Module({

 imports: [

   TypeOrmModule.forRoot({

     type: 'postgres',

     host: 'localhost',

     port: 5432,

     username: 'postgres',

     password: 'admin',   // change yours

     database: 'nestauth',

     entities: [User],

     synchronize: true,   // only for dev!

   }),

   UsersModule,

   AuthModule,

 ],

})

export class AppModule {}

Step 2: Creating User Entity

We need a table to store users — unique emails, hashed passwords, etc.

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity()

export class User {

 @PrimaryGeneratedColumn()

 id: number;

 @Column({ unique: true })

 email: string;

 @Column()

 password: string;

}

Step 3: Users Module, Service, and Controller

Generate:

nest g module users

nest g service users

nest g controller users

Step 4: User Service (Signup + Find User)

This is where we handle:

  • Checking existing users
  • Hashing password using bcrypt
  • Saving users
  • Finding users (for validation later)
// users/users.service.ts

import { Injectable, ConflictException } from '@nestjs/common';

import { InjectRepository } from '@nestjs/typeorm';

import { Repository } from 'typeorm';

import * as bcrypt from 'bcrypt';

import { User } from './user.entity';

@Injectable()

export class UsersService {

 constructor(

   @InjectRepository(User)

   private userRepo: Repository<User>,

 ) {}

 // Create user (Signup)

 async create(email: string, password: string) {

   const existing = await this.userRepo.findOne({ where: { email } });

   if (existing) throw new ConflictException('Email already registered');

   const hashed = await bcrypt.hash(password, 10);

   const user = this.userRepo.create({ email, password: hashed });

   return this.userRepo.save(user);

 }

 // Find user by email (for login)

 async findByEmail(email: string) {

   return this.userRepo.findOne({ where: { email } });

 }

}

Step 5: User Signup Controller

// users/users.controller.ts

import { Controller, Post, Body } from '@nestjs/common';

import { UsersService } from './users.service';

@Controller('users')

export class UsersController {

 constructor(private userService: UsersService) {}

 @Post('signup')

 async signup(@Body() body: any) {

   return this.userService.create(body.email, body.password);

 }

}

At this point, users can register and will get stored in Postgres with hashed passwords

Related read: Building Powerful REST APIs with Node.js, Prisma ORM, and PostgreSQL

Next, let’s move to authentication.

Start Building Secure NestJS Backends with JWT

Step 6: Auth Module & AuthService (Integrating with DB)

AuthService handles:

  • Validating credentials
  • Checking password matches
  • Returning a signed JWT

If credentials are wrong, we throw an Unauthorized exception.

Generate starter files:

nest g module auth

nest g service auth

nest g controller auth

Inside auth.service.ts

// auth.service.ts

import { Injectable, UnauthorizedException } from '@nestjs/common';

import { JwtService } from '@nestjs/jwt';

import * as bcrypt from 'bcrypt';

import { UsersService } from '../users/users.service';

@Injectable()

export class AuthService {

 constructor(

   private usersService: UsersService,

   private jwt: JwtService,

 ) {}

 // Login validation

 async validateUser(email: string, password: string) {

   const user = await this.usersService.findByEmail(email);

   if (!user) throw new UnauthorizedException('Invalid credentials');

   const match = await bcrypt.compare(password, user.password);

   if (!match) throw new UnauthorizedException('Invalid credentials');

   return { id: user.id, email: user.email };

 }

 // Generate token

 async login(user: any) {

   const payload = { sub: user.id, email: user.email };

   return { access_token: this.jwt.sign(payload) };

 }

}

Step 7: JWT Strategy (Token Validator)

This strategy:

  • Reads the token from the Authorization: Bearer <token> header
  • Verifies its signature
  • Attaches user data to the request

This is done under the hood — Nest makes it simple!

// auth/jwt.strategy.ts

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 || 'secret123',

   });

 }

 validate(payload: any) {

   return { id: payload.sub, email: payload.email };

 }

}

Step 8: JWT Guard (Route Protector)

We create JwtAuthGuard to protect specific operations like viewing profile pages, dashboards, etc.

If the token is valid → request allowed
Else → request blocked

// auth/jwt-auth.guard.ts

import { Injectable } from '@nestjs/common';

import { AuthGuard } from '@nestjs/passport';

@Injectable()

export class JwtAuthGuard extends AuthGuard('jwt') {}

Step 9: Auth Module (Register JWT + Strategy)

// auth/auth.module.ts

import { Module } from '@nestjs/common';

import { JwtModule } from '@nestjs/jwt';

import { PassportModule } from '@nestjs/passport';

import { AuthService } from './auth.service';

import { JwtStrategy } from './jwt.strategy';

import { UsersModule } from '../users/users.module';

import { AuthController } from './auth.controller';

@Module({

 imports: [

   UsersModule,

   PassportModule,

   JwtModule.register({

     secret: process.env.JWT_SECRET || 'secret123',

     signOptions: { expiresIn: '1h' },

   }),

 ],

 controllers: [AuthController],

 providers: [AuthService, JwtStrategy],

 exports: [AuthService],

})

export class AuthModule {}

Step 10: Auth Controller

// auth/auth.controller.ts

import { Controller, Post, Body, Get, UseGuards, Req } from '@nestjs/common';

import { AuthService } from './auth.service';

import { JwtAuthGuard } from './jwt-auth.guard';

@Controller('auth')

export class AuthController {

 constructor(private authService: AuthService) {}

 // Login (returns JWT)

 @Post('login')

 async login(@Body() body: any) {

   const user = await this.authService.validateUser(

     body.email,

     body.password,

   );

   return this.authService.login(user);

 }

 // Protected profile route

 @UseGuards(JwtAuthGuard)

 @Get('profile')

 getProfile(@Req() req) {

   return req.user;

 }

}

Testing (via Postman)

POST /users/signup

{

 "email": "arzoo@gmail.com",

 "password": "123456"

}

Response:

{
   "email": "arzoo@gmail.com",

   "password": "$2b$10$psCclpvH.L...",

   "id": 1
}

POST /auth/login

{

 "email": "arzoo@gmail.com",

 "password": "123456"

}

Response:

{
   "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd..."
}

Now, we can use this access_token in the routes that are protected.

GET /auth/profile

Headers –  Authorization  Bearer <token>

Response:

{
   "id": 1,

   "email": "arzoo@gmail.com"
}

Authentication successfully implemented!

coma

Conclusion

Learning authentication in NestJS feels intimidating at first strategies guards modules tokens headers there’s a lot.
But the moment JWT kicks in and you access a protected route everything starts making sense.

We store users securely validate their credentials generate a signed token use guards to protect routes let authenticated users access restricted resources.

If you take away one thing from this blog, it should be – “Authentication is not just a security layer it’s a contrast of trust between your application and its users.”

Arzoo Jain

Arzoo Jain

Associate Software Engineer

Arzoo is a skilled Associate Software Engineer with strong full-stack development expertise. She’s detail-oriented and genuinely passionate about building smooth, intuitive user experiences, turning ideas into well-crafted, working solutions. Comfortable working across both front-end and back-end systems, Arzoo brings a thoughtful, hands-on approach to her work and is always looking for ways to innovate and improve.

Share This Blog

Read More Similar Blogs

Let’s Transform
Healthcare,
Together.

Partner with us to design, build, and scale digital solutions that drive better outcomes.

Location

5900 Balcones Dr, Ste 100-7286, Austin, TX 78731, United States

Contact form