Logo

dev-resources.site

for different kinds of informations.

Creating a Secure NestJS Backend with JWT Authentication and Prisma

Published at
6/13/2024
Categories
nestjs
prisma
jwt
typescript
Author
Tharindu Dulshan Fernando
Categories
4 categories in total
nestjs
open
prisma
open
jwt
open
typescript
open
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

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

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

Update your .env file with your database connection string.

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

Initialize Prisma in your project and configure the database connection:

npx prisma init

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
}

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

npx prisma migrate dev --name init

Generate the Prisma client:

npx prisma generate

Step 5: Set Up Authentication

Generate the Auth module, controller, and service:

nest generate module auth
nest generate controller auth
nest generate service auth

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 {}

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;
  }
}

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);
  }
}

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 };
  }
}

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') {}

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();
  }
}

Step 7: Create Books Module

Generate the Books module, controller, and service:

nest generate module books
nest generate controller books
nest generate service books

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 {}

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);
  }
}

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);
  }
}

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 {}

Running the Application

npm run start:dev

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

Featured ones: