Database with Prisma ORM, Docker, and Postgres - NestJs with Passport #02

Database with Prisma ORM, Docker, and Postgres - NestJs with Passport #02

In the last post start with a blank configuration, and understand how to NestJs works with routes, controllers, and services. Saw how easy is to set up Fastify to optimize our app.

Now, will set up the database and ORM for interacting and storing our data. We use PostgreSQL for the database using docker to create a default container for the app, will use Prisma for ORM because is the best Orm of the moment for interacting with the database.


Docker Container

Now that we have our app up, let's containerize it.

Start by creating the following files in the project's root directory:

  • Dockerfile - This file will be responsible for importing the Docker images, dividing them into development and production environments, copying all of our files, and installing dependencies.

  • docker-compose.yml - This file will be responsible for defining our containers, required images for the app other services, storage volumes, environment variables, etc.

Open the Dockerfile and add

# Dockerfile

FROM node:alpine As development

WORKDIR /usr/src/app

COPY package*.json ./

RUN yarn add glob rimraf

RUN yarn --only=development

COPY . .

RUN yarn build

FROM node:alpine as production

ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}

WORKDIR /usr/src/app

COPY package*.json ./

RUN yarn add glob rimraf

RUN yarn --only=production

COPY . .

COPY --from=development /usr/src/app/dist ./dist

CMD ["node", "dist/main"]

Open the docker-compose.yml file and add the following code

# docker-compose.yml

version: "3.7"

services:
  main:
    container_name: main
    build:
      context: .
      target: development
    volumes:
      - .:/usr/src/app
      - /usr/src/app/node_modules
    ports:
      - 3000:3000
    command: yarn start:dev
    env_file:
      - .env
    networks:
      - api
    depends_on:
      - postgres
  postgres:
    image: postgres:13
    container_name: postgres
    networks:
      - api
    env_file:
      - .env
    ports:
      - 5432:5432
    volumes:
      - pgdata:/var/lib/postgresql/data
networks:
  api:
volumes:
  pgdata:

Create a .env file and add the PostgreSQL credentials

# .env

# PostgreSQL
POSTGRES_USER=nestAuth
POSTGRES_PASSWORD=nestAuth
POSTGRES_DB=nestAuth

By default, Fastify listens only on the localhost 127.0.0.1 interface. For we access our app from other hosts we need to add 0.0.0.0 in the main.ts

// src/main.ts

await app.listen(3000, "0.0.0.0");

Awesome, we have our dockerized, and let's go test then. Run in the terminal for development

docker-compose up

Docker running in terminal

And our app is Running :clap:


Prisma ORM

Prisma is an open-source ORM, it is used as an alternative to writing plain SQL, or using another database access tool such as SQL query builders (like knex.js) or ORMs (like TypeORM and Sequelize).

Start installing Prisma CLI as a development

yarn add Prisma -D

As a best practice invoke the CLI locally by prefixing it with npx, to create your initial Prisma setup using the init command

npx prisma init

This command creates a new Prisma directory with the following contents

  • schema.prisma: Specifies your database connection and contains the database schema
  • .env: A dotenv file, typically used to store your database credentials in a group of environment variables

By default your database connection it's set to postgresql

// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

How our connection type is correct, we will set the DATABASE_URL in .env

DATABASE_URL="postgresql://nestAuth:nestAuth@postgres:5432/nestAuth"

Remember to add in .env in .gitignore and create a .env.example before creating the repository in Github

Generating the Prisma Client requires the schema.prisma file. COPY prisma ./prisma/ copies the whole Prisma directory in case you also need the migrations.

# Dockerfile

FROM node:alpine As development

WORKDIR /usr/src/app

COPY package*.json ./

# Here Prisma folder to the container
COPY prisma ./prisma/

RUN yarn add glob rimraf

RUN yarn --only=development

COPY . .

RUN yarn build

FROM node:alpine as production

ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}

WORKDIR /usr/src/app

COPY package*.json ./

# Here Prisma folder to the container
COPY prisma ./prisma/

RUN yarn add glob rimraf

RUN yarn --only=production

COPY . .

COPY --from=development /usr/src/app/dist ./dist

CMD ["node", "dist/main"]

First Model

Now to test the connection we will be creating a User model, inside schema.prisma insert

// prisma/schema.prisma

model User {
  id        Int            @id @default(autoincrement())
  email     String         @unique
  name      String
  password  String
  createdAt DateTime       @default(now())
  updatedAt DateTime       @updatedAt
}

With your model in place, you can generate your SQL migration files and run them against the database. Here I use migrate dev for a run in development mode and set init name for the migration

Before this you need up your docker

docker-compose up

and edit your .env file

DATABASE_URL="postgresql://nestAuth:nestAuth@localhsot:5432/nestAuth"

Always you run prisma migrate you need change the database host to localhost after you rollback to name of your database container.

npx prisma migrate dev --name init

Run Prisma migration in terminal

Rollback .env file

DATABASE_URL="postgresql://nestAuth:nestAuth@nestauth:5432/nestAuth"

Good News, is our configuration is working, now our database is in sync with our app :clap:


Setup Prisma

We will want to abstract away the Prisma Client API for database queries within a service. so let's create a new PrismaService that takes care of instantiating andPrismaClient to connecting to your database.

Create a prisma.service.ts inside src folder

// src/prisma.service.ts

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

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

  async enableShutdownHooks(app: INestApplication) {
    this.$on("beforeExit", async () => {
      await app.close();
    });
  }
}

First Service

Now we can write a user service to make database calls. So NestJs CLI has a command nest g to generate services, controllers, strategies, and other structures. For now, we run

nest g service users

Before start create a service we need to generate types of Prisma Model, we can generate with this

npx prisma generate

Inside the src folder the command creates a users folder with users.service.ts and the file test users.service.spec.ts. To test our database connection let's create a two services

  • user: to get a user using the Prisma interface Prisma.UserWhereUniqueInput forget user by unique columns
  • createUser: to create a new user using data from interface Prisma.userCreateInput that get auto the fields when the model needs to create a new register

And inside the createUser we need to encrypt the user password, so let's create a provider for this, in the src folder create a providers folder and create a password.ts file

// src/providers/password.ts

import { Injectable } from "@nestjs/common";
import * as bcrypt from "bcrypt";

const SALT_OR_ROUNDS = 10;

@Injectable()
export class PasswordProvider {
  async hashPassword(password: string): Promise<string> {
    return bcrypt.hashSync(password, SALT_OR_ROUNDS);
  }

  async comparePassword(password: string, hash: string): Promise<boolean> {
    return bcrypt.compareSync(password, hash);
  }
}

The class has two methods, hashPassword and comparePassword to encrypt and compare the password using brcypt. Inside the UsersService class we need to add in the constructor the provider PasswordProvider for use in the methods.

// src/users/users.service.ts

import { HttpException, HttpStatus, Injectable } from "@nestjs/common";
import { PrismaService } from "../prisma.service";
import { User, Prisma } from "@prisma/client";
import { PasswordProvider } from "src/providers/password";

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

  async user(
    userWhereUniqueInput: Prisma.UserWhereUniqueInput
  ): Promise<User | null> {
    const user = await this.prisma.user.findUnique({
      where: userWhereUniqueInput,
    });

    delete user.password;

    return user;
  }

  async createUser(data: Prisma.UserCreateInput): Promise<User> {
    const userExists = await this.prisma.user.findUnique({
      where: { email: data.email },
    });

    if (userExists) {
      throw new HttpException("User already exists", HttpStatus.CONFLICT);
    }

    const passwordHashed = await this.passwordProvider.hashPassword(
      data.password
    );

    const user = await this.prisma.user.create({
      data: {
        ...data,
        password: passwordHashed,
      },
    });

    delete user.password;

    return user;
  }
}

With the service created let's create a controller for route use that

nest g controller users

So this command create users.controller.ts and our test file inside src/users, so let's create two functions in the controller

  • signUpUser: For run createUser service and return a data from them
  • getUserProfile: Get an id of the user sent by the route and run the user service to find them
// src/users/users.controller.ts

import { Body, Controller, Get, Param, Post } from "@nestjs/common";
import { User } from "@prisma/client";
import { UsersService } from "./users.service";

// Set prefix route for this group. Ex.: for get profile /users/8126321
@Controller("users")
export class UsersController {
  constructor(private readonly usersService: UsersService) {}
  // Create user -> POST /users
  @Post()
  async signupUser(
    @Body() userData: { name: string; email: string; password: string }
  ): Promise<User> {
    return this.usersService.createUser(userData);
  }

  // Get user Profile -> GET /users/:id
  @Get("/:id")
  async profile(@Param("id") id: number): Promise<User> {
    return this.usersService.user({ id: Number(id) });
  }
}

Inside the users.module.ts file we need to add the providers, exports, and controllers array.

// src/users/users.module.ts

import { Module } from "@nestjs/common";
import { PrismaService } from "src/prisma.service";
import { PasswordProvider } from "src/providers/password";
import { UsersController } from "./users.controller";
import { UsersService } from "./users.service";

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

And pass the UsersModule to the AppModule for use.

//src/app.module.ts

import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { PrismaService } from "./prisma.service";
import { UsersModule } from "./users/users.module";
import { UsersService } from "./users/users.service";
import { PasswordProvider } from "./providers/password";

@Module({
  imports: [UsersModule],
  controllers: [AppController],
  providers: [PrismaService, UsersService, PasswordProvider],
})
export class AppModule {}

Let's Test

Now lets up our docker container

docker-compose up

Docker running

And that's it! The app is running :clap:

So in Postman let's try using the createUser and getProfile routes

curl --location --request POST 'http://0.0.0.0:3000/users' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "test@e3x.com",
    "name": "Gabriel Menezes",
    "password": "123123"
}'

Create user route

curl --location --request GET 'http://0.0.0.0:3000/users/37'

Get user route


Until Next Time

That is all we are going to cover in this article, we dockerize the app, set up Prisma, and create two routes. In the next piece in the series, we'll create and define our Auth providers for authenticating in our app.

Thank you for reading!


Follow repository to consulting code

github.com/mnzsss/nest-auth-explained


References