Building a NestJS Authentication API with MongoDB and JWT: A Step-by-Step Guide
Authentication is a critical part of any application. In this guide, we’ll build a complete authentication system using NestJS, MongoDB, and JWT. We'll cover everything from setting up the project to implementing authentication routes and securing them with guards.
Prerequisites
- Node.js and npm installed
- Basic understanding of NestJS
- MongoDB installed locally or access to a MongoDB cloud instance
- A willingness to learn (and maybe some coffee ☕)
To install the NestJS CLI globally, run:
npm install -g @nestjs/cli
Verify the installation:
nest --version
Step 1: Initialize the Project
Create a new NestJS project:
nest new auth-app
cd auth-app
Install the required dependencies:
npm install @nestjs/mongoose mongoose @nestjs/jwt @nestjs/passport passport-jwt bcryptjs
npm install --save-dev @types/bcryptjs @types/passport-jwt
Setting Up MongoDB
Step 2: Configure MongoDB Connection
NestJS provides the @nestjs/mongoose package to integrate MongoDB. Let’s set up the connection.
Create a DatabaseModule
:
nest generate module database
Configure the connection in database.module.ts
:
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
MongooseModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
uri: configService.get<string>('DATABASE_URL'),
}),
inject: [ConfigService],
}),
],
})
export class DatabaseModule {}
Add the DATABASE_URL
to your .env
file:
DATABASE_URL=mongodb://localhost/nest-auth
Creating the User Module
Step 3: Define the User Schema
Generate the User module:
nest generate module users
nest generate service users
nest generate controller users
Create the User schema in user.schema.ts
:
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type UserDocument = User & Document;
@Schema({ timestamps: true })
export class User {
@Prop({ required: true, unique: true })
username: string;
@Prop({ required: true })
password: string;
@Prop({ required: true, unique: true })
email: string;
@Prop({ default: null })
refreshToken: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
Update the UsersModule
to include the schema:
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User, UserSchema } from './schemas/user.schema';
@Module({
imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])],
providers: [UsersService],
controllers: [UsersController],
exports: [UsersService],
})
export class UsersModule {}
Implementing Authentication
Step 4: Create the Auth Module
Generate the Auth module:
nest generate module auth
nest generate service auth
nest generate controller auth
Configure JWT and Passport in auth.module.ts
:
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from '../users/users.module';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET || 'fallback-secret',
signOptions: { expiresIn: '15m' },
}),
],
providers: [AuthService, JwtStrategy],
controllers: [AuthController],
})
export class AuthModule {}
Add the JWT_SECRET
to your .env
file:
JWT_SECRET=your_jwt_secret_key_here
Step 5: Implement the Auth Service
The AuthService
handles the business logic for authentication.
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcryptjs';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async validateUser(username: string, password: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
throw new UnauthorizedException('Invalid credentials');
}
const { password: _, ...result } = user.toObject();
return result;
}
async login(user: any) {
const payload = { username: user.username, sub: user._id };
return {
access_token: this.jwtService.sign(payload),
};
}
async register(username: string, email: string, password: string) {
const hashedPassword = await bcrypt.hash(password, 10);
return this.usersService.create(username, email, hashedPassword);
}
}
Step 6: Implement the Auth Controller
The AuthController defines the authentication routes.
import { Controller, Post, Body, UseGuards, Get, Request } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-auth.guard';
import { LoginDto, RegisterDto } from '../common/dto/auth.dto';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('login')
async login(@Body() loginDto: LoginDto) {
const user = await this.authService.validateUser(loginDto.username, loginDto.password);
return this.authService.login(user);
}
@Post('register')
async register(@Body() registerDto: RegisterDto) {
return this.authService.register(registerDto.username, registerDto.email, registerDto.password);
}
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
}
Step 7: Secure Routes with Guards
Create a JWT Guard in jwt-auth.guard.ts
:
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
Implement the JWT Strategy in 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 || 'fallback-secret',
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
Conclusion
In this guide, we built a complete authentication system using NestJS, MongoDB, and JWT. We covered everything from setting up the project to implementing secure routes with guards. This setup can serve as a foundation for more advanced features like role-based access control, email verification, and password reset functionality.
Don't Forget to Like and Follow❤️❤️
Here's the GitHub Repo for reference Nest-Auth-App
If You're coming from express.js
no worries(●'◡'●) I Gat you, Check out my article on Understanding a NestJS Authentication App for an Express.js Developer
Happy coding! 🎉
Top comments (0)