Crud Api w/ Server Side Pagination with NestJS & Angular - Crud Endpoints

Published on

Prerquisites

The following dependencies are required for this project.

  1. NodeJS
  2. Angular CLI
  3. NestJS CLI

 

Setup

This post builds upon our previous post here. You can also clone this Github repository if you prefer to skip the first part.

Creating Our CRUD Endpoints

We are going to quickly create endpoints to create, read, update and delete users from our SQLite database. We will also create a “find all” endpoint which will return all users in the database. This find all endpoint will be refactored in our next post to provide pagination functionality.

I’m going to keep this post brief and minimal in terms of explanation.

Creating Our DTOs & Exception Classes

First we’re going to create a couple DTO classes to model our HTTP request bodies for our create and update enpdoints. We’re also going to create a custom exception to throw when a user is not found.

mkdir -p src/users/dtos
mkdir -p src/users/exceptions

touch src/users/dtos/create-user.dto.ts \
  src/users/dtos/update-user.dto.ts \
  src/users/exceptions/user-not-found.exception.ts

Our CreateUserDto will look something like this…

import { IsEmail, IsNotEmpty } from 'class-validator';

export class CreateUserDto {
  @IsNotEmpty()
  public firstName: string;

  @IsNotEmpty()
  public lastName: string;
  
  @IsNotEmpty()
  @IsEmail()
  public email: string;
}

Our UpdateUserDto

import { IsEmail, IsNotEmpty } from 'class-validator';

export class CreateUserDto {
  @IsNotEmpty()
  public id: number;
  
  @IsNotEmpty()
  public firstName: string;

  @IsNotEmpty()
  public lastName: string;
  
  @IsNotEmpty()
  @IsEmail()
  public email: string;
}

The decorated DTO classes will allow us to validate the HTTP request’s body before it hits our controller. There are a plethora of decorators for validation but we’ll keep things minimal for this example.

Now we can create our custom UserNotFoundException class.

import { NotFoundException } from '@nestjs/common';

export class UserNotFoundException extends NotFoundException {
  constructor() {
    super(`User with supplied id was not found!`);
  }
}

 

Creating Our Conroller Enpdoints

We can now create a controller endpoints to perform all crud operations. One nice thing about NestJS is the decorator support for our controllers. We can use these decorators in our controller to map our routing, bind query params, bind HTTP body values, bind path params, amonst others.

Our decorated controller will look something like this…

import { Body, Controller, Get, Put, Delete, Logger, Post, Param } from '@nestjs/common';
import { UsersService } from './users.service';
import { User } from './user.entity';
import { CreateUserDto } from './dtos/create-user.dto';
import { UpdateUserDto } from './dtos/update-user.dto';

@Controller('users')
export class UsersController {
  constructor(
    private readonly _logger: Logger,
    private readonly _usersService: UsersService
  ) {
    this._logger.setContext(this.constructor.name);
  }

  @Post()
  public async createUser(@Body() createUserDto: CreateUserDto): Promise<User> {
    try {
      return this._usersService.createUser(createUserDto);
    } catch (error) {
      this._logger.error(error);
    }
  }

  @Get()
  public async getAllUsers(): Promise<User[]> {
    try {
      return this._usersService.getAllUsers();
    } catch (error) {
      this._logger.error(error);
    }
  }

  @Get(':id')
  public async getUserById(@Param('id') userId: number): Promise<User> {
    try {
      return this._usersService.getUserById(userId);
    } catch (error) {
      this._logger.error(error);
    }
  }

  @Put(':id')
  public async updateUserById(
      @Param('id') userId: number, @Body() updateUserDto: UpdateUserDto): Promise<User> {
    try {
      return this._usersService.updateUserById(userId, updateUserDto);
    } catch (error) {
      this._logger.error(error);
    }
  }

  @Delete(':id')
  public async deleteUserById(@Param('id') userId: number): Promise<User> {
    try {
      return this._usersService.deleteUserById(userId);
    } catch (error) {
      this._logger.error(error);
    }
  }
}

In our controller’s constructor we inject a Logger and our UsersService. Our UsersService will contain all logic that deals with accessing data from the database, essentially acting as our data access layer.

The controller is pretty minimal. Each method wraps a call to our service with a try/catch and logs any errors that are thrown.

Our service will look something like this…

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateUserDto } from './dtos/create-user.dto';
import { UpdateUserDto } from './dtos/update-user.dto';
import { UserNotFoundException } from './exceptions/user-not-found.exception';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly _usersRepository: Repository<User>
  ) {}

  public async createUser(createUserDto: CreateUserDto): Promise<User> {
    const user: User = this._usersRepository.create({
      ...createUserDto
    });
    return this._usersRepository.save(user);
  }

  public async getAllUsers(): Promise<User[]> {
    return this._usersRepository.find();
  }

  public async getUserById(userId: number): Promise<User> {
    const user: User = await this._usersRepository.findOne(userId);
    if (!user) {
      throw new UserNotFoundException();
    }
    return user;
  }

  public async updateUserById(userId: number, updateUserDto: UpdateUserDto): Promise<User> {
    const user: User = await this._usersRepository.findOne(userId);
    if (!user) {
      throw new UserNotFoundException();
    }
    user.firstName = updateUserDto.firstName;
    user.lastName = updateUserDto.lastName;
    user.email = updateUserDto.email;
    return this._usersRepository.save(user);
  }

  public async deleteUserById(userId: number): Promise<User> {
    const user: User = await this._usersRepository.findOne(userId);
    if (!user) {
      throw new UserNotFoundException();
    }
    this._usersRepository.delete(user);
    return user;
  }
}

 

This will give us the following endpoints

Path Method Description
/users POST Create a new user.
/users GET Get all users (Will be refactor with pagination).
/users/:id GET Get a user by id.
/users/:id PUT Update a user by id.
/users/:id DELETE Delete a user by id.

 

Now if we navigate to our server folder in our project, we can run an npm start to start our server. This will serve our API on http://localhost:3000/. If we open up a brower, we can navigate to http://localhost:3000/users and we will get a list of all 30 mock users we seeded our database with in our previous post.

Next we will look at refactoring our find all users endpoint to return a page of users.

 

Final Github Repository

 

comments powered by Disqus