Build Complete REST API Feature with Nest JS (Using Prisma and Postgresql) from Scratch - Beginner-friendly - PART 1
Tags: nestjs, typescript, api, backend
Published: 2023-10-25 23:52:48

## Scope of discussion:

1. What we want to build
2. Nest JS installation + new project initialization
3. Nest JS overview
4. Prisma configuration
5. Validation configuration
6. Create user endpoints (register, login, update, delete)
7. Password encryption

---

I have been looking for a great, nice, superb Node JS framework to build an API. After doing some exploration I found Nest JS which is super great. It's a Node JS framework with many built-in features written in Typescript. Check Nest JS documentation for more details [Nest JS Doc](https://docs.nestjs.com/).

In this article, I'm going to show how to create a REST API with these features:
- CRUD (Create Read Update Delete) with Prisma,
- Validation,
- Authentication,
- Route protection,
- Handle multiple .env files,
- Unit test,
- e2e test,
- Open API for documentation,
etc.

> Full code of the entire series can be accessed here: https://github.com/alfism1/nestjs-api

Okay, now let's get started.

First thing first, let's create a new Nest JS project called **superb-api**.

```sh
$ npm i -g @nestjs/cli
$ nest new superb-api
```
It will prompt a question to choose package manager (npm, yarn, or pnpm). Choose whichever you want.

Once the project initialization is completed, go to the project folder, open the code editor,  and run it.

```sh
$ cd superb-api
$ npm run start
```
## First App Run
By default, the Nest JS use 3000 as the default port.

![Hello world](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tsb8u3hipaw792g9vjil.png)

If you want to change the port number, just open up `src/main.ts` and update the port number `await app.listen(ANY_AVAILABLE_PORT_NUMBER);`.

### Nest JS Overview
Now, let's do a quick overview of the project folder structure.
![Folder structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/80lx1hv7dgh4gg92r83n.png)

Most of our codes will be written in the `src` folder. Let's take a look at the `src` folder. There are 5 files:
- `app.controller.ts`: A basic controller with a single route.
- `app.controller.spec.ts`: The unit tests for the controller.
- `app.module.ts`: The root module of the application.
- `app.service.ts`: A basic service with a single method.
- `main.ts`: The entry file of the application which uses the core function NestFactory to create a Nest application instance.

#### Controller
Controllers are responsible for handling incoming **requests** and returning **responses**. As per the Nest JS rules, controller will be written in a class with `@Controller()` decorator. We can also add a string as the prefix inside the controller decorator `@Controller('some-prefix')` then the URL will be `domain.com/some-prefix`. Inside the controller class, there will be functions with HTTP methods decorator `@GET()`, `@POST()`, `@PUT()`, `@PATCH()`, `@DELETE()`. Similar to the controller, HTTP methods decorator can also receive a string, for example `@GET('test')`, and then the endpoint URL will be `domain.com/some-prefix/test`.

```typescript
import { Controller, Get } from '@nestjs/common';

// Import the AppService class, which is defined in the app.service.ts file.
import { AppService } from './app.service';

// Controller decorator, which is used to define a basic route.
@Controller()
export class AppController {

  // Get decorator, which is a method decorator that defines a basic GET route.
  @Get()
  getHello(): string {
    // controller process goes here...
  }
}
```
For more details about the controller, check [controller documentation](https://docs.nestjs.com/controllers).

#### Service/Provider
Services or Providers are responsible for handling the main process. We're gonna write all the endpoint logic here. As per the Nest JS rules, provider will be written in a class with `@Injectable()` decorator.

```typescript
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}
```
In the code above, we have `getHello` method which is responsible for returning a string 'Hello World!'. Then we can inject this provider into the controller

```typescript
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  // inject the AppService into the AppController
  constructor(private readonly appService: AppService) {}

  @Get('test')
  getHello(): string {
    // call the AppService's getHello method
    return this.appService.getHello();
  }
}
```
For more details about the service or provider, check [provider documentation](https://docs.nestjs.com/providers).

#### Module
In NestJS, a module is a class annotated with the `@Module()` decorator. It is used to organize code into cohesive blocks of functionality. Modules have a few key responsibilities, such as defining providers (services, factories, repositories, etc.), controllers, and other related stuff. They also allow for defining imports and exports, which are used to manage dependencies between different parts of the application.

```typescript
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```
For more details about the service or provider, check [module documentation](https://docs.nestjs.com/modules).

### Let's build our superb API
#### Prisma installation
We're gonna use Prisma as the database ORM. Prisma is a great library for working with databases. It supports PostgreSQL, MySQL, SQL Server, SQLite, MongoDB and CockroachDB. 

First, let's install Prisma package in our project
```sh
$ npm install prisma --save-dev
```
Once prisma is installed, run the command below
```sh
$ npx prisma init
```
It will create `schema.prisma` file inside `prisma` folder
```prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
```
and `.env` file in the root folder
```env
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
```

> If you haven't installed PostgreSQL, just [download](https://www.postgresql.org/download/) it.

Make sure to set the correct `DATABASE_URL` in `.env` file.

Next, open up the `schema.prisma` and let's define the model schema
```prisma
model User {
  id       Int     @id @default(autoincrement())
  email    String  @unique
  password String
  name     String?
  posts    Post[]
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean? @default(false)
  author    User?    @relation(fields: [authorId], references: [id])
  authorId  Int?
}
```
Once the schema is created, sync the schema to the database by executing
```sh
npx prisma migrate dev --name init
```
and check the database. It will create these tables:

![new tables](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xu6ha1ags1s2legy5ntw.png)

Install prisma client
```sh
npm install @prisma/client
```

> Note that during installation, Prisma automatically invokes the `prisma generate` command for you. In the future, you need to run this command after every change to your Prisma models to update your generated Prisma Client.

> The `prisma generate` command reads your Prisma schema and updates the generated Prisma Client library inside `node_modules/@prisma/client`

Next, let's create a new file called `prisma.service.ts` inside `src/core/services/` directory:
```typescript
// src/core/services/prisma.service.ts

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

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

For the last configuration of Prisma, let's register `PrismaService` as a global module so it can be used on any other modules. Create a new file called `core.module.ts` inside `src/core/`
```typescript
// src/core/core.module.ts

import { Global, Module } from '@nestjs/common';
import { PrismaService } from './services/prisma.service';

@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class CoreModule {}
```
then update our `app.module.ts` by adding `CoreModule`:
```typescript
// src/app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './modules/users/users.module';
import { CoreModule } from './core/core.module';

@Module({
  imports: [UsersModule, CoreModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

```

Great! Now we're ready to interact with the database :)

---
#### Validation config
Nest JS can do request body and query validation very well (more details check [here](https://docs.nestjs.com/techniques/validation)).
First, install the packages:
```sh
$ npm i --save class-validator class-transformer
$ npm install @nestjs/mapped-types
```

and update our `main.ts`
```typescript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Global pipe that will be applied to all routes
  // This will validate the request body against the DTO
  app.useGlobalPipes(new ValidationPipe());

  await app.listen(3000);
}
bootstrap();
```

We added `app.useGlobalPipes(new ValidationPipe());` in `main.ts` to make it globally applied.

---

#### Create users endpoints
Let's move on to the endpoint creation.
In the `src` folder, create a new folder called `modules/users`.

Inside `modules/users`, we'll create endpoints that handle any logic related to user model such as; create user, update user, and delete user.

Before diving into the code, let's install some packages:

**bcrypt**
We'll encrypt the user's password during the user creation.
```sh
$ npm i bcrypt
$ npm install --save @types/bcrypt
```


Now let's create a new folder `src/modules/users/dtos` and create these files:
- create-user.dto.ts
Validation for user creation/registration
```typescript
import { IsEmail, IsNotEmpty } from 'class-validator';

export class CreateUserDto {
  @IsNotEmpty()
  @IsEmail()
  email: string;

  @IsNotEmpty()
  password: string;

  @IsNotEmpty()
  name: string;
}
```
- update-user.dto.ts
Validation for user update
```typescript
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

export class UpdateUsertDto extends PartialType(CreateUserDto) {}
```
- login-user.dto.ts
Validation for login
```typescript
import { IsEmail, IsNotEmpty } from 'class-validator';

export class LoginUserDto {
  @IsNotEmpty()
  @IsEmail()
  email: string;

  @IsNotEmpty()
  password: string;
}
```

Let's start with creating a new controller boilerplate called `users.controller.ts`
```typescript
import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  ParseIntPipe,
  Patch,
  Post,
} from '@nestjs/common';
import { CreateUserDto } from './dtos/create-user.dto';
import { UpdateUsertDto } from './dtos/update-user.dto';
import { LoginUserDto } from './dtos/login-user.dto';

@Controller('users')
export class UsersController {
  @Post('register')
  registerUser(@Body() createUserDto: CreateUserDto): string {
    console.log(createUserDto);
    return 'Post User!';
  }

  @Post('login')
  loginUser(@Body() loginUserDto: LoginUserDto): string {
    console.log(loginUserDto);
    return 'Login User!';
  }

  @Get('me')
  me(): string {
    return 'Get my Profile!';
  }

  @Patch(':id')
  updateUser(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateUserDto: UpdateUsertDto,
  ): string {
    console.log(updateUserDto);
    return `Update User ${id}!`;
  }

  @Delete(':id')
  deleteUser(@Param('id', ParseIntPipe) id: number): string {
    return `Delete User ${id}!`;
  }
}
```

`users.controller.ts` has five endpoints:
- `POST /users/register` for user registration process,
- `POST /users/login` for user login process,
- `GET /users/me` to Get the user's profile,
- `PATCH /users/:id` for user updates,
- `DELETE /users/:id` for user delete.

Each endpoint above already implemented validation as well according to the DTO, for example:
- Registration input validation (DTO `create-user.dto`)
![Registration input validation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i7uxl9ylsc1si72m6m2r.png)

- Login input validation (DTO `login-user.dto`)
![Login input validation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lefr2oo2t3799rqqvrps.png)

- Update user wrong `:id` (should be numeric)
![Update user wrong id](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jng2jyuiu61xpcvmoix7.png)

Now let's jump to the user service. In the service, we'll create functions to interact with the database via Prisma and they'll represent each endpoint mentioned above.

Here are the `users.service` boilerplate:
```typescript
import { ConflictException, HttpException, Injectable } from '@nestjs/common';
import { User } from '@prisma/client';
import { PrismaService } from 'src/core/services/prisma.service';
import { CreateUserDto } from './dtos/create-user.dto';
import { hash } from 'bcrypt';

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

  // async registerUser

  // async loginUser

  // async updateUser

  // async deleteUser
}
```

Let's start with the **register** function first:
```typescript
async registerUser(createUserDto: CreateUserDto): Promise<User> {
  try {
    // create new user using prisma client
    const newUser = await this.prisma.user.create({
      data: {
        email: createUserDto.email,
        password: await hash(createUserDto.password, 10), // hash user's password
        name: createUserDto.name,
      },
    });

    // remove password from response
    delete newUser.password;

    return newUser;
  } catch (error) {
    // check if email already registered and throw error
    if (error.code === 'P2002') {
      throw new ConflictException('Email already registered');
    }

    // throw error if any
    throw new HttpException(error, 500);
  }
}
```

`registerUser` will be responsible for handling register logic. We can see that now we interact with database using prisma `this.prisma.user.create`. We also encrypt the password to make our API secure `hash(createUserDto.password, 10)` and remove it from the response API `delete newUser.password`.

We also handle the error using `throw new ConflictException` if there is a duplicate email or any other error `throw new HttpException`.


Now let's move on to **login** function:
Login response will return an object with a single property called `access_token` (`interface LoginResponse`) which contains some user data (`interface UserPaylod`) generated by using `jwt`, so we need to install `@nestjs/jwt` first:
```sh
$ npm install --save @nestjs/jwt
```

Once `@nestjs/jwt` is installed, update UsersService's `constructor` by adding `private jwtService: JwtService,`
```typescript
constructor(
  private prisma: PrismaService,
  private jwtService: JwtService,
) {}
```
Last but not least, update our `app.module.ts` by adding jwt module as a global:
```typescript
// src/app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './modules/users/users.module';
import { CoreModule } from './core/core.module';
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [
    UsersModule,
    CoreModule,
    // add jwt module
    JwtModule.register({
      global: true,
      secret: 'super_secret_key',
      signOptions: { expiresIn: '12h' },
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```
> Please take note that the `secret` and `expiresIn` should be stored in a safe place like `.env` file. We'll cover this in the next section.

Once `jwt` configuration is completed, let's create a new interface file called `src/modules/users/interfaces/users-login.interface.ts`:
```typescript
export interface UserPayload {
  sub: number;
  name: string;
  email: string;
}

export interface LoginResponse {
  access_token: string;
}
```

Here is the `loginUser` code:
```typescript
async loginUser(loginUserDto: LoginUserDto): Promise<LoginResponse> {
    try {
      // find user by email
    const user = await this.prisma.user.findUnique({
      where: { email: loginUserDto.email },
    });

    // check if user exists
    if (!user) {
      throw new NotFoundException('User not found');
    }

    // check if password is correct by comparing it with the hashed password in the database
    if (!(await compare(loginUserDto.password, user.password))) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const payload: UserPayload = {
      // create payload for JWT
      sub: user.id, // sub is short for subject. It is the user id
      email: user.email,
      name: user.name,
    };

    return {
      // return access token
      access_token: await this.jwtService.signAsync(payload),
    };
  } catch (error) {
    // throw error if any
    throw new HttpException(error, 500);
  }
}
```

So far we already created two functions, `registerUser` and `loginUser`. Now let's continue on `updateUser` and `deleteUser`.

Here is the `updateUser` function:
```typescript
async updateUser(id: number, updateUserDto: UpdateUsertDto): Promise<User> {
  try {
    // find user by id. If not found, throw error
    await this.prisma.user.findUniqueOrThrow({
      where: { id },
    });

    // update user using prisma client
    const updatedUser = await this.prisma.user.update({
      where: { id },
      data: {
        ...updateUserDto,
        // if password is provided, hash it
        ...(updateUserDto.password && {
          password: await hash(updateUserDto.password, 10),
        }),
      },
    });

    // remove password from response
    delete updatedUser.password;

    return updatedUser;
  } catch (error) {
    // check if user not found and throw error
    if (error.code === 'P2025') {
      throw new NotFoundException(`User with id ${id} not found`);
    }

    // check if email already registered and throw error
    if (error.code === 'P2002') {
      throw new ConflictException('Email already registered');
    }

    // throw error if any
    throw new HttpException(error, 500);
  }
}
```

and last function `deleteUser`:
```typescript
async deleteUser(id: number): Promise<string> {
  try {
    // find user by id. If not found, throw error
    const user = await this.prisma.user.findUniqueOrThrow({
      where: { id },
    });

    // delete user using prisma client
    await this.prisma.user.delete({
      where: { id },
    });

    return `User with id ${user.id} deleted`;
  } catch (error) {
    // check if user not found and throw error
    if (error.code === 'P2025') {
      throw new NotFoundException(`User with id ${id} not found`);
    }

    // throw error if any
    throw new HttpException(error, 500);
  }
}
```


Now the `UsersService` is completed and here is the final full code:
```typescript
// src/modules/users/users.service.ts

import {
  ConflictException,
  HttpException,
  Injectable,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import { User } from '@prisma/client';
import { PrismaService } from 'src/core/services/prisma.service';
import { CreateUserDto } from './dtos/create-user.dto';
import { compare, hash } from 'bcrypt';
import { LoginUserDto } from './dtos/login-user.dto';
import { JwtService } from '@nestjs/jwt';
import { LoginResponse, UserPayload } from './interfaces/users-login.interface';
import { UpdateUsertDto } from './dtos/update-user.dto';

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

  async registerUser(createUserDto: CreateUserDto): Promise<User> {
    try {
      // create new user using prisma client
      const newUser = await this.prisma.user.create({
        data: {
          email: createUserDto.email,
          password: await hash(createUserDto.password, 10), // hash user's password
          name: createUserDto.name,
        },
      });

      // remove password from response
      delete newUser.password;

      return newUser;
    } catch (error) {
      // check if email already registered and throw error
      if (error.code === 'P2002') {
        throw new ConflictException('Email already registered');
      }

      // throw error if any
      throw new HttpException(error, 500);
    }
  }

  async loginUser(loginUserDto: LoginUserDto): Promise<LoginResponse> {
    try {
      // find user by email
      const user = await this.prisma.user.findUnique({
        where: { email: loginUserDto.email },
      });

      // check if user exists
      if (!user) {
        throw new NotFoundException('User not found');
      }

      // check if password is correct by comparing it with the hashed password in the database
      if (!(await compare(loginUserDto.password, user.password))) {
        throw new UnauthorizedException('Invalid credentials');
      }

      const payload: UserPayload = {
        // create payload for JWT
        sub: user.id, // sub is short for subject. It is the user id
        email: user.email,
        name: user.name,
      };

      return {
        // return access token
        access_token: await this.jwtService.signAsync(payload),
      };
    } catch (error) {
      // throw error if any
      throw new HttpException(error, 500);
    }
  }

  async updateUser(id: number, updateUserDto: UpdateUsertDto): Promise<User> {
    try {
      // find user by id. If not found, throw error
      await this.prisma.user.findUniqueOrThrow({
        where: { id },
      });

      // update user using prisma client
      const updatedUser = await this.prisma.user.update({
        where: { id },
        data: {
          ...updateUserDto,
          // if password is provided, hash it
          ...(updateUserDto.password && {
            password: await hash(updateUserDto.password, 10),
          }),
        },
      });

      // remove password from response
      delete updatedUser.password;

      return updatedUser;
    } catch (error) {
      // check if user not found and throw error
      if (error.code === 'P2025') {
        throw new NotFoundException(`User with id ${id} not found`);
      }

      // check if email already registered and throw error
      if (error.code === 'P2002') {
        throw new ConflictException('Email already registered');
      }

      // throw error if any
      throw new HttpException(error, 500);
    }
  }

  async deleteUser(id: number): Promise<string> {
    try {
      // find user by id. If not found, throw error
      const user = await this.prisma.user.findUniqueOrThrow({
        where: { id },
      });

      // delete user using prisma client
      await this.prisma.user.delete({
        where: { id },
      });

      return `User with id ${user.id} deleted`;
    } catch (error) {
      // check if user not found and throw error
      if (error.code === 'P2025') {
        throw new NotFoundException(`User with id ${id} not found`);
      }

      // throw error if any
      throw new HttpException(error, 500);
    }
  }
}
```

The next step is we need to update our `UsersController` to interact with `usersService` and here is the full code:
```typescript
// src/modules/users/users.controller.ts

import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  ParseIntPipe,
  Patch,
  Post,
} from '@nestjs/common';
import { CreateUserDto } from './dtos/create-user.dto';
import { UpdateUsertDto } from './dtos/update-user.dto';
import { LoginUserDto } from './dtos/login-user.dto';
import { UsersService } from './users.service';
import { User } from '@prisma/client';
import { LoginResponse } from './interfaces/users-login.interface';

@Controller('users')
export class UsersController {
  // inject users service
  constructor(private readonly usersService: UsersService) {}

  @Post('register')
  async registerUser(@Body() createUserDto: CreateUserDto): Promise<User> {
    // call users service method to register new user
    return this.usersService.registerUser(createUserDto);
  }

  @Post('login')
  loginUser(@Body() loginUserDto: LoginUserDto): Promise<LoginResponse> {
    // call users service method to login user
    return this.usersService.loginUser(loginUserDto);
  }

  @Get('me')
  me(): string {
    return 'Get my Profile!';
  }

  @Patch(':id')
  async updateUser(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateUserDto: UpdateUsertDto,
  ): Promise<User> {
    // call users service method to update user
    return this.usersService.updateUser(+id, updateUserDto);
  }

  @Delete(':id')
  async deleteUser(@Param('id', ParseIntPipe) id: number): Promise<string> {
    // call users service method to delete user
    return this.usersService.deleteUser(+id);
  }
}
```
> We haven't updated the `me` endpoint yet. Will do it later

We're almost done. We need to register `UsersController` and `UsersService` to `UsersModule`:
```typescript
// src/modules/users/users.module.ts

import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  imports: [],
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

```

Finally, we've done with `users` register, login, update, and delete. Now we can test these endpoints:
- `POST /users/register`,
![User register](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bbom01g3zvlmn5gzpwse.png)
- `POST /users/login`,
![User login](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g6i2boegh01g80whu8g9.png)
- `PATCH /users/:id`,
![Update user](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8tuy296ff95n4bfuxz8f.png)
- `DELETE /users/:id`
![Delete user](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/prx2g2ejarh6il7k1bep.png)

We've been doing great so far :)
The full code of part 1 can be accessed here:
https://github.com/alfism1/nestjs-api/tree/part-one.

Moving on to part 2:
https://dev.to/alfism1/build-complete-rest-api-feature-with-nest-js-using-prisma-and-postgresql-from-scratch-beginner-friendly-part-2-1g25