Creating a Secure NestJS Backend with JWT Authentication and Prisma
Tags: nestjs, prisma, jwt, typescript
Published: 2024-06-13 15:09:51

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