DEV Community

Cover image for Creating a Secure NestJS Backend with JWT Authentication and Prisma
Tharindu Dulshan Fernando
Tharindu Dulshan Fernando

Posted on • Updated on

Creating a Secure NestJS Backend with JWT Authentication and Prisma

In this tutorial, we will create a secure backend application using NestJS, Prisma, and JWT-based authentication. Our application will include CRUD operations for managing books, with endpoints protected by JWT authentication.

Prerequisites

Before we start, ensure you have the following installed on your machine:

  • Node.js and npm(Better to have a Lts version Installed)
  • Nest CLI: Install globally using npm install -g @nestjs/cli
  • PostgreSQL (or any other Prisma-supported database) running and accessible

Step 1: Create a New NestJS Project

First, create a new NestJS project using the Nest CLI:

nest new book-store
cd book-store
Enter fullscreen mode Exit fullscreen mode

Step 2: Install Dependencies

Next, install the necessary dependencies for JWT authentication and Prisma:

npm install @nestjs/jwt @nestjs/passport passport passport-jwt @prisma/client prisma

Enter fullscreen mode Exit fullscreen mode

Step 3: Initialize Prisma

If you are using the docker image of Postgresql add the below lines in the docker-compose.yml.

version: '3.8'
services:
  postgres:
    container_name: postgres_container
    image: postgres:13
    ports:
      - 5434:5432
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: 123
      POSTGRES_DB: book-store
    volumes:
      - postgres_data:/var/lib/postgresql/data
Enter fullscreen mode Exit fullscreen mode

Update your .env file with your database connection string.

DATABASE_URL="postgresql://postgres:123@localhost:5434/book-store?schema=public"

Enter fullscreen mode Exit fullscreen mode

Initialize Prisma in your project and configure the database connection:

npx prisma init

Enter fullscreen mode Exit fullscreen mode

Step 4: Configure Prisma Schema

Edit prisma/schema.prisma to include the User and Book models:

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id       Int     @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  email    String  @unique

  firstName String?
  lastName  String?

  password String
}

model Book {
  id       Int    @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  title       String
  description String?
  link        String
  userId   Int
}
Enter fullscreen mode Exit fullscreen mode

Run the Prisma migration to apply the schema to the database:

npx prisma migrate dev --name init

Enter fullscreen mode Exit fullscreen mode

Generate the Prisma client:

npx prisma generate

Enter fullscreen mode Exit fullscreen mode

Step 5: Set Up Authentication

Generate the Auth module, controller, and service:

nest generate module auth
nest generate controller auth
nest generate service auth
Enter fullscreen mode Exit fullscreen mode

Configure the Auth module:

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './jwt.strategy';
import { PrismaService } from '../prisma.service';

@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_SECRET || 'secretKey',
      signOptions: { expiresIn: '60m' },
    }),
  ],
  providers: [AuthService, JwtStrategy, PrismaService],
  controllers: [AuthController],
})
export class AuthModule {}
Enter fullscreen mode Exit fullscreen mode

Configure the auth.service.ts

Implement the AuthService with registration and login functionality:

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma.service';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(
    private jwtService: JwtService,
    private prisma: PrismaService
  ) {}

  async validateUser(email: string, pass: string): Promise<any> {
    const user = await this.prisma.user.findUnique({ where: { email } });
    if (user && await bcrypt.compare(pass, user.password)) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  async login(user: any) {
    const payload = { email: user.email, sub: user.id };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }

  async register(email: string, pass: string) {
    const salt = await bcrypt.genSalt();
    const hashedPassword = await bcrypt.hash(pass, salt);

    const user = await this.prisma.user.create({
      data: {
        email,
        password: hashedPassword,
      },
    });

    const { password, ...result } = user;
    return result;
  }
}
Enter fullscreen mode Exit fullscreen mode

Configure the auth.controller.ts

Create endpoints for login and registration in AuthController:

import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post('login')
  async login(@Body() req) {
    return this.authService.login(req);
  }

  @Post('register')
  async register(@Body() req) {
    return this.authService.register(req.email, req.password);
  }
}
Enter fullscreen mode Exit fullscreen mode

Configure the 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 || 'secretKey',
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, email: payload.email };
  }
}
Enter fullscreen mode Exit fullscreen mode

Create the JWT authentication guard(jwt-auth.guard.ts):

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

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
Enter fullscreen mode Exit fullscreen mode

Step 6: Set Up Prisma Service

Create a Prisma service(prisma.service.ts) to handle database interactions:

import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 7: Create Books Module

Generate the Books module, controller, and service:

nest generate module books
nest generate controller books
nest generate service books
Enter fullscreen mode Exit fullscreen mode

Configure the Books module(books.module.ts):

import { Module } from '@nestjs/common';
import { BooksService } from './books.service';
import { BooksController } from './books.controller';
import { PrismaService } from '../prisma.service';

@Module({
  providers: [BooksService, PrismaService],
  controllers: [BooksController]
})
export class BooksModule {}
Enter fullscreen mode Exit fullscreen mode

Implement the BooksService(books.service.ts):

import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma.service';
import { Book } from '@prisma/client';

@Injectable()
export class BooksService {
  constructor(private prisma: PrismaService) {}

  async create(data: Omit<Book, 'id'>): Promise<Book> {
    return this.prisma.book.create({ data });
  }

  async findAll(userId: number): Promise<Book[]> {
    return this.prisma.book.findMany({ where: { userId } });
  }

  async findOne(id: number, userId: number): Promise<Book> {
    return this.prisma.book.findFirst({ where: { id, userId } });
  }

  async update(id: number, data: Partial<Book>, userId: number): Promise<Book> {
    return this.prisma.book.updateMany({
      where: { id, userId },
      data,
    }).then((result) => result.count ? this.prisma.book.findUnique({ where: { id } }) : null);
  }

  async remove(id: number, userId: number): Promise<Book> {
    return this.prisma.book.deleteMany({
      where: { id, userId },
    }).then((result) => result.count ? this.prisma.book.findUnique({ where: { id } }) : null);
  }
}
Enter fullscreen mode Exit fullscreen mode

Secure the BooksController with JWT authentication:

import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Request } from '@nestjs/common';
import { BooksService } from './books.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';

@Controller('books')
@UseGuards(JwtAuthGuard)
export class BooksController {
  constructor(private readonly booksService: BooksService) {}

  @Post()
  create(@Body() createBookDto, @Request() req) {
    return this.booksService.create({ ...createBookDto, userId: req.user.userId });
  }

  @Get()
  findAll(@Request() req) {
    return this.booksService.findAll(req.user.userId);
  }

  @Get(':id')
  findOne(@Param('id') id: string, @Request() req) {
    return this.booksService.findOne(+id, req.user.userId);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() updateBookDto, @Request() req) {
    return this.booksService.update(+id, updateBookDto, req.user.userId);
  }

  @Delete(':id')
  remove(@Param('id') id: string, @Request() req) {
    return this.booksService.remove(+id, req.user.userId);
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 8: Integrate Everything

Ensure all modules are correctly imported in the main app module:

import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { BooksModule } from './books/books.module';

@Module({
  imports: [AuthModule, BooksModule],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Running the Application

npm run start:dev

Enter fullscreen mode Exit fullscreen mode

Conclusion

In this tutorial, we created a NestJS application with Prisma for database interaction and JWT for securing the API endpoints. We covered setting up the Prisma schema, creating modules for authentication and books, and securing the endpoints using JWT guards. You now have a secure NestJS backend with JWT-based authentication and CRUD operations for books.

References

https://docs.nestjs.com/v5/

https://www.prisma.io/docs

https://jwt.io/introduction

Github : https://github.com/tharindu1998/book-store

Top comments (0)