NestJS WebAPI with TypeORM and JWT

In this example we will guide u to build Nest js framework of node js with the power of TypeScript, TypeOrm and JWT.

Sopheaktra

Eang Sopheaktra

May 31 2024 08:05 pm

0 240

What is Nest JS?

NestJS is a Node. js framework for building scalable and efficient server-side applications, with built-in features and modules for dependency injection, middleware, routing, and more, as well as support for multiple databases and testing tools.

Features of Nest JS

  • TypeScript and JavaScript Support: Nest.js allows you to write your code in TypeScript (a statically typed superset of JavaScript) or plain JavaScript. TypeScript provides type safety and better tooling support.

  • Modularity: Nest.js encourages organizing your application into self-contained modules. Each module can encapsulate related functionality, making it easier to manage and maintain large codebases.

  • Dependency Injection: Nest.js leverages dependency injection, which allows you to inject dependencies (services, repositories, etc.) into your components. This promotes better testability and separation of concerns.

  • Decorators and Metadata: Decorators are a powerful feature in Nest.js. They allow you to add metadata to classes, methods, and properties. For example, you can use decorators to define routes, middleware, and guards.

  • Scalability: Nest.js provides an efficient and battle-tested component-based architecture, making it suitable for building scalable applications. It’s commonly used for microservices and APIs.

  • Rich Ecosystem: Nest.js has a growing ecosystem of packages and libraries that enhance its functionality. You can find tools for database integration, authentication, validation, and more.

  • Enterprise-Ready: Many leading companies and organizations worldwide trust Nest.js for their backend development. Its design patterns and features make it suitable for enterprise-grade applications.

Why need to use TypeORM in Nest JS?

TypeORM is a popular Object-Relational Mapping (ORM) library for Node.js and TypeScript. It’s commonly used with Nest.js for several reasons:

  • Database Abstraction: TypeORM abstracts the interaction with databases, allowing you to work with entities (models) in a more object-oriented manner. You define your data models as TypeScript classes, and TypeORM handles the database operations (queries, inserts, updates, etc.) for you.

  • Type Safety: Since Nest.js encourages the use of TypeScript, TypeORM aligns well with this approach. You can define your entity properties with specific types, ensuring type safety throughout your application.

  • Decorators and Metadata: TypeORM uses decorators to define entities, columns, relations, and other database-related metadata. These decorators allow you to express your data model directly in your code, making it more readable and maintainable.

  • Query Builder and Repository: TypeORM provides a powerful query builder that allows you to construct complex SQL queries using a fluent API. Additionally, it offers a repository pattern for managing database operations, making it easier to work with data.

  • Migrations: TypeORM supports database migrations, which are essential for maintaining database schema changes over time. Migrations allow you to evolve your database schema without losing data.

  • Multiple Database Support: TypeORM works with various databases, including PostgreSQL, MySQL, SQLite, and SQL Server. You can switch between databases seamlessly by changing the connection configuration.

  • Active Community: TypeORM has an active community, regular updates, and good documentation. If you encounter issues or need help, you’ll find resources and discussions online

Setup Nest JS

First if you don't have nestjs/cli on your global please run this command:

npm install -g @nestjs/cli

Then we can create our project with this command:

nest new NestJS-WebAPI-TypeOrmAndJWT

Choosing your template base on question from command line and installation dependency below:

  • Passport-JWT
npm i passport-jwt
  • Typeorm, pg and nestjs typeorm paginate
npm i typeorm pg nestjs-typeorm-paginate 
  • Validation dependency
npm i class-validator class-transformer 

Coding

Create environment .env and add contents to it like below:

POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DATABASE=dbname

ACCESS_TOKEN_SECRET=test
REFRESH_TOKEN_SECRET=test1

Please don't forget to change your database connection with your db.

After the env is already setup please create "user" directory for holding flow on user-management, authorization and authentication.

*Note: we will skip user-management on this tutorial if you need it can look at the github's repository.

Let's go: create model

  • User model
import { AutoMap } from '@automapper/classes';
import { Exclude } from 'class-transformer';
import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
  ManyToMany,
  JoinTable,
} from 'typeorm';
import { Role } from './role.model';
import { RoleDto } from '../dtos/responses/role/role.dto';

@Entity({name: "nestjs_users"})
export class User {
  @AutoMap()
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @AutoMap()
  @Column()
  firstName: string;

  @AutoMap()
  @Column()
  lastName: string;

  @AutoMap()
  @Column({ unique: true })
  email: string;

  @Column({ select: false })
  hash: string;

  @AutoMap()
  @Column({ default: true })
  isActive: boolean;

  @AutoMap()
  @CreateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
  createdAt: Date;

  @AutoMap()
  @UpdateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
  updatedAt: Date;

  @AutoMap(() => RoleDto)
  @ManyToMany(() => Role, (role) => role.users,{
    cascade: true,
  })
  @JoinTable()
  roles: Role[];
}

export class UserModel {
  id: string;
  firstName: string;
  lastName: string;
  email: string;
  isActive: boolean;
  createdAt: Date;
  updatedAt: Date;

  @Exclude()
  hash: string;

  constructor(partial: Partial<UserModel>) {
    Object.assign(this, partial);
  }
}
  • Role model
import { AutoMap } from '@automapper/classes';
import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
  ManyToMany,
  JoinTable,
} from 'typeorm';
import { Permission } from './permission.model';
import { User } from './user.model';
import { PermissionDto } from '../dtos/responses/permission/permission.dto';

@Entity({name: "nestjs_roles"})
export class Role {
  @AutoMap()
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @AutoMap()
  @Column()
  name: string;

  @AutoMap()
  @Column()
  description: string;
  
  @AutoMap()
  @Column({ default: true })
  isActive: boolean;

  @AutoMap()
  @CreateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
  createdAt: Date;

  @AutoMap()
  @UpdateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
  updatedAt: Date;

  @ManyToMany(() => User, (user) => user.roles)
  @JoinTable()
  users: User[];

  @AutoMap(() => PermissionDto)
  @ManyToMany(() => Permission, (permission) => permission.roles,{
    cascade: true,
  })
  @JoinTable()
  permissions: Permission[];
}
  • Permission model
import { AutoMap } from '@automapper/classes';
import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
  ManyToMany,
  JoinTable,
} from 'typeorm';
import { Role } from './role.model';

@Entity({name: "nestjs_permissions"})
export class Permission {
  @AutoMap()
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @AutoMap()
  @Column()
  name: string;

  @AutoMap()
  @Column()
  description: string;
  
  @AutoMap()
  @Column({ default: true })
  isActive: boolean;

  @AutoMap()
  @CreateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
  createdAt: Date;

  @AutoMap()
  @UpdateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
  updatedAt: Date;

  @ManyToMany(() => Role, (role) => role.permissions)
  @JoinTable()
  roles: Role[];
}
  • Refresh token model
import {
    Entity,
    PrimaryGeneratedColumn,
    CreateDateColumn,
    UpdateDateColumn,
    OneToOne,
    JoinColumn,
    Column,
  } from 'typeorm';
  import { User } from './user.model';
  
  @Entity({name: "nestjs_refresh_token"})
  export class RefreshToken {
    @PrimaryGeneratedColumn('uuid')
    id: string;
  
    @OneToOne((type) => User,{ onDelete: 'CASCADE' })
    @JoinColumn()
    user: User;
  
    @Column()
    token: string;
  
    @CreateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
    createdAt: Date;
  
    @UpdateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
    updatedAt: Date;
  }

 

Using nest/cli for generate module, controller and service

Generate module of user like below command:

nest generate module user

Generate controller of authentication like below command

nest generate controller user/controllers/authentication

Generate service of authentication like below command

nest generate service user/services/authentication/authentication

Yayyy you finished your generate with nestjs/cli for generate some code and add auto to your module.

Go Go to our definition on our generate above 

User Module

import { Module } from '@nestjs/common';
import { UserController } from './controllers/user/user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RefreshToken } from './models/refresh-token.model';
import { User } from './models/user.model';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UserService } from './services/user/user.service';
import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies';
import { UserProfile } from './mappers';
import { AuthenticationController } from './controllers/authentication/authentication.controller';
import { AuthenticationService } from './services/authentication/authentication.service';
import { Role } from './models/role.model';
import { Permission } from './models/permission.model';
import { RoleProfile } from './mappers/role/role.profile';
import { PermissionProfile } from './mappers/permission/permission.profile';
import { RoleService } from './services/role/role.service';
import { PermissionService } from './services/permission/permission.service';
import { RoleController } from './controllers/role/role.controller';
import { PermissionController } from './controllers/permission/permission.controller';

@Module({
  imports: [
    TypeOrmModule.forFeature([User, Role, Permission, RefreshToken]),
    JwtModule.register({}),
    PassportModule.register({
      session: false
    })
  ],
  providers: [UserService, AuthenticationService, PermissionProfile, RoleProfile, UserProfile, AccessTokenStrategy, RefreshTokenStrategy, RoleService, PermissionService],
  controllers: [UserController, AuthenticationController, RoleController, PermissionController]
})
export class UserModule {}

Authentication Controller

import { Body, ClassSerializerInterceptor, Controller, Delete, Get, HttpCode, HttpStatus, Patch, Post, Res, UseGuards, UseInterceptors } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Response } from '../../../global/payload/responses/Response';
import { AccessTokenSecurity, RefreshTokenSecurity } from '../../securities';
import { SerializeUser } from '../../decorators';
import { UserDto } from '../../dtos/responses';
import { RefreshtokenDto, RegisterDto, SigninDto } from '../../dtos/requests';
import { AuthenticationService } from '../../services/authentication/authentication.service';
import { UpdateProfileDto } from '../../dtos/requests/authentication/update-profile.dto';

@Controller('auth')
@ApiTags('auth')
export class AuthenticationController {
    constructor(private readonly authenticationService: AuthenticationService) {}
    
    @ApiBearerAuth()
    @UseGuards(AccessTokenSecurity)
    @Get('user-info')
    getUser(@SerializeUser() user: UserDto): Response<UserDto> {
      return Response.data(user);
    }
  
    @UseInterceptors(ClassSerializerInterceptor)
    @Post('register')
    async register(@Body() dto: RegisterDto): Promise<Response<UserDto>> {
      return Response.data(await this.authenticationService.register(dto));
    }
  
    @HttpCode(HttpStatus.OK)
    @Post('signin')
    async signin(@Body() dto: SigninDto): Promise<Response<{
      access_token: string;
      refresh_token: string;
    }>> {
      return Response.data(await this.authenticationService.signin(dto));
    }
  
    @ApiBearerAuth()
    @UseGuards(RefreshTokenSecurity)
    @HttpCode(HttpStatus.OK)
    @Post('refresh')
    async refreshToken(
      @SerializeUser() user: UserDto,
      @Body() dto: RefreshtokenDto,
    ): Promise<Response<{
      access_token: string;
    }>> {
      return Response.data(await this.authenticationService.refreshToken(user, dto));
    }
  
    @ApiBearerAuth()
    @UseGuards(AccessTokenSecurity)
    @Patch('update-profile')
    async editUser(
      @SerializeUser('id') userId: string,
      @Body() dto: UpdateProfileDto,
    ): Promise<Response<UserDto>> {
      return Response.data(await this.authenticationService.updateUser(userId, dto));
    }

    @ApiBearerAuth()
    @UseGuards(RefreshTokenSecurity)
    @HttpCode(HttpStatus.NO_CONTENT)
    @Delete('signout')
    async signout(@SerializeUser() user: UserDto): Promise<Response<string>> {
      await this.authenticationService.signout(user);
      return Response.data("Successfully deleted user.");
    }
}

Authentication Service

import { Mapper } from '@automapper/core';
import { InjectMapper } from '@automapper/nestjs';
import { ForbiddenException, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { QueryFailedError, Repository } from 'typeorm';
import * as argon from 'argon2';
import { RefreshToken, User } from '../../models';
import { RefreshtokenDto, RegisterDto, SigninDto } from '../../dtos/requests';
import { UserDto } from '../../dtos/responses';
import { JwtConfig, Payload, accessTokenConfig, refreshTokenConfig } from '../../../global/config';
import { UpdateProfileDto } from '../../dtos/requests/authentication/update-profile.dto';

@Injectable()
export class AuthenticationService {
    constructor(
        @InjectRepository(User)
        private readonly userRepo: Repository<User>,
        @InjectRepository(RefreshToken)
        private readonly refreshTokenRepo: Repository<RefreshToken>,
        @InjectMapper() 
        private readonly classMapper: Mapper,
        private readonly jwtService: JwtService,
      ) {}
    
      public async register(dto: RegisterDto): Promise<UserDto> {
        try {
          const user = this.classMapper.map(dto, RegisterDto, User);
          const hash = await argon.hash(dto.password);
          return this.classMapper.mapAsync(await this.userRepo.save({...user, hash: hash}), User, UserDto);
        } catch (error) {
          if (error instanceof QueryFailedError) {
            throw new ForbiddenException(`Email ${dto.email} is already in use`);
          }
    
          throw error;
        }
      }
    
      public async signin(dto: SigninDto): Promise<{
        access_token: string;
        refresh_token: string;
      }> {
        const user = await this.userRepo.findOne({
          where: {
            email: dto.email,
          },
          select: {
            id: true,
            email: true,
            hash: true,
          },
        });
    
        if (!user) throw new NotFoundException('Invalid username and password');
    
        const passwordMatch = await argon.verify(user.hash, dto.password);
        if (!passwordMatch) throw new UnauthorizedException('Invalid username and password');
    
        const payload: Payload = {
          sub: user.id,
          email: user.email,
        };
    
        const accessToken = this.generateJWT(payload, accessTokenConfig());
        const refreshToken = this.generateJWT(payload, refreshTokenConfig());
    
        const doc = await this.refreshTokenRepo.findOne({ where: { user } });
    
        if (doc) {
          await this.refreshTokenRepo.update({ user }, { token: refreshToken });
    
          return {
            access_token: accessToken,
            refresh_token: refreshToken,
          };
        }
    
        await this.refreshTokenRepo.save({
          token: refreshToken,
          user,
        });
    
        return {
          access_token: accessToken,
          refresh_token: refreshToken,
        };
      }
    
      public async refreshToken(
        user: UserDto,
        dto: RefreshtokenDto,
      ): Promise<{
        access_token: string;
      }> {
        const refreshToken = await this.refreshTokenRepo.findOne({
          where: { token: dto.refreshToken },
        });
    
        if (!refreshToken) throw new UnauthorizedException();
    
        const payload: Payload = {
          sub: user.id,
          email: user.email,
        };
    
        const accessToken = this.generateJWT(payload, accessTokenConfig());
    
        return {
          access_token: accessToken,
        };
      }
    
      public async updateUser(
        userId: string,
        dto: UpdateProfileDto,
      ): Promise<UserDto> {
        const userData = await this.userRepo.findOne({
          where: {
            id: userId,
          }
        })
        if(!userData) throw new NotFoundException('User not found!');
        if (dto.password) {
          const { password, ...rest } = dto;
          const hash = await argon.hash(password);
    
          return this.classMapper.mapAsync(await this.userRepo.save(
            {
              hash,
              id: userId,
              ...rest,
            },
          ), User, UserDto);
        }
    
        return this.classMapper.mapAsync(await this.userRepo.save(
          {
            id: userId,
            hash: userData.hash,
            ...dto,
          },
        ), User, UserDto);
      }
    
      public async signout(user: UserDto): Promise<void> {
        await this.refreshTokenRepo.delete({ user });
      }
    
      public generateJWT(payload: Payload, config: JwtConfig): string {
        return this.jwtService.sign(payload, {
          secret: config.secret,
          expiresIn: config.expiresIn,
        });
      }
}

First I created 3 decorators with directory "user/decorators"

  • Permissions decorator: using for check permission
import { SetMetadata, applyDecorators } from '@nestjs/common';

export const Permissions = (...args: string[]) => applyDecorators(
    SetMetadata('roles', args?.filter(s => s?.toLowerCase()?.includes('role_'))),
    SetMetadata('permissions', args?.filter(s => !s?.includes('role_'))))
  • SerializeUser decorator: using for serialize current user from request(access_token)
import { ExecutionContext, createParamDecorator } from '@nestjs/common';

export const SerializeUser = createParamDecorator(
  (data: string | undefined, ctx: ExecutionContext) => {
    const request: Express.Request = ctx.switchToHttp().getRequest();
    if (data) {
      return request.user[data];
    }
    return request.user;
  },
);
  • UniqueExistConstraint decorator: using for validate from dubplicate email on user from db.
import { Injectable } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import { registerDecorator, ValidationArguments, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
import { EntityManager } from 'typeorm';

@Injectable()
@ValidatorConstraint({ async: true })
export class UniqueExistConstraint implements ValidatorConstraintInterface {
  constructor(
    @InjectEntityManager()
    private readonly entityManager: EntityManager,
  ){}
  async validate(value: any, args: ValidationArguments) {
    const e: any= args.object.constructor;
    const entity = args.object[`class_entity_${args.property}`];
    const dataExist = await this.entityManager
        .getRepository(entity)
        .createQueryBuilder()
        .where({ [args.property]: value })
        .getExists();
    return !dataExist;  
  }
}

export function Unique(entity: Function, validationOptions?: ValidationOptions) {
  validationOptions = { ...{ message: '$value already exists.' }, ...validationOptions };
  return function (object: Object, propertyName: string) {
    object[`class_entity_${propertyName}`] = entity;
    registerDecorator({
      target: object.constructor,
      propertyName: propertyName,
      options: validationOptions,
      constraints: [],
      validator: UniqueExistConstraint,
    });
  };
}

And then one more step is security guard step with AuthGuard with directory "user/securities"

  • PermissionGuard: is using for check permission with our permission decorator(@permission)
import {
    CanActivate,
    ExecutionContext,
    ForbiddenException,
    Injectable,
    UnauthorizedException,
  } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from '../models';
import { Repository } from 'typeorm';
  
  @Injectable()
  export class PermissionGuard implements CanActivate {
    constructor(
      private reflector: Reflector,
      private jwtService: JwtService,
      private config: ConfigService,
      @InjectRepository(User)
      private readonly userRepo: Repository<User>
    ) {}
  
    async canActivate(context: ExecutionContext): Promise<boolean> {
      const request = context.switchToHttp().getRequest();
      const token = this.extractTokenFromHeader(request);
      if (!token) {
        throw new UnauthorizedException();
      }
      try {
        const payload = await this.jwtService.verifyAsync(
          token,
          {
            secret: this.config.get('ACCESS_TOKEN_SECRET'),
          }
        );
        const user = await this.userRepo.createQueryBuilder('user')
          .leftJoinAndSelect("user.roles", "roles", "roles.isActive = true")
          .leftJoinAndSelect('roles.permissions', 'permissions', 'permissions.isActive = true')
          .where('user.id = :userId', { userId: payload?.sub }).getOne();
        const roles = this.reflector.get<string[]>('roles', context.getHandler()) || [];
        const permissions = this.reflector.get<string[]>('permissions', context.getHandler()) || [];
        // 💡 We're assigning the payload to the request object here
        // so that we can access it in our route handlers
        const test = user?.roles?.filter(s => roles.includes(`role_${s.name}`) || s.permissions?.filter(i => permissions.includes(i.name))?.length > 0);
        if(test.length === 0) {
          throw new ForbiddenException();
        }
        request['user'] = {...payload, roles: roles, permissions: permissions};
      } catch(exception) {
        if(exception instanceof ForbiddenException) throw new ForbiddenException(exception);
        throw new UnauthorizedException();
      }
     
      return true;
    }
  
    private extractTokenFromHeader(request: Request): string | undefined {
      const [type, token] = request.headers.authorization?.split(' ') ?? [];
      return type === 'Bearer' ? token : undefined;
    }
  }
  • AccessTokenSecurity: using for required access_token on our request
import { AuthGuard } from '@nestjs/passport';

export class AccessTokenSecurity extends AuthGuard('access-jwt') {
  constructor() {
    super();
  }
}
  • RefreshTokenSecurity: using for required refresh_token on our request
import { AuthGuard } from '@nestjs/passport';

export class RefreshTokenSecurity extends AuthGuard('refresh-jwt') {
  constructor() {
    super();
  }
}

*Note: please try to using nestjs/cli instead of manaul create by yourself. For more details please check out this link: here

Also postman's collection is on my github repository.

Summary

Download the source code for the sample application with nestjs web api with awesome dependency: typeorm, pg, password-jwt, ...etc

Comments

Subscribe to our newsletter.

Get updates about new articles, contents, coding tips and tricks.

Weekly articles
Always new release articles every week or weekly released.
No spam
No marketing, share and spam you different notification.
© 2023-2025 Tra21, Inc. All rights reserved.