DEV Community

Cover image for Database with Prisma ORM, Docker and Postgres - NestJs with Passport #02
Gabriel Menezes
Gabriel Menezes

Posted on • Edited on

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

In the last post start with a blank configuration, 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"] 
Enter fullscreen mode Exit fullscreen mode

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: 
Enter fullscreen mode Exit fullscreen mode

Create a .env file and add the PostgreSQL credentials

# .env # PostgreSQL POSTGRES_USER=nestAuth POSTGRES_PASSWORD=nestAuth POSTGRES_DB=nestAuth 
Enter fullscreen mode Exit fullscreen mode

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"); 
Enter fullscreen mode Exit fullscreen mode

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

docker-compose up 
Enter fullscreen mode Exit fullscreen mode

Docker running in terminal

And our app is Running 👏


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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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") } 
Enter fullscreen mode Exit fullscreen mode

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

DATABASE_URL="postgresql://nestAuth:nestAuth@postgres:5432/nestAuth" 
Enter fullscreen mode Exit fullscreen mode

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"] 
Enter fullscreen mode Exit fullscreen mode

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 } 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

and edit your .env file

DATABASE_URL="postgresql://nestAuth:nestAuth@localhsot:5432/nestAuth" 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

Run Prisma migration in terminal

Rollback .env file

DATABASE_URL="postgresql://nestAuth:nestAuth@nestauth:5432/nestAuth" 
Enter fullscreen mode Exit fullscreen mode

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


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(); }); } } 
Enter fullscreen mode Exit fullscreen mode

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 others structures. For now, we run

nest g service users 
Enter fullscreen mode Exit fullscreen mode

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

npx prisma generate 
Enter fullscreen mode Exit fullscreen mode

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); } } 
Enter fullscreen mode Exit fullscreen mode

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; } } 
Enter fullscreen mode Exit fullscreen mode

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

nest g controller users 
Enter fullscreen mode Exit fullscreen mode

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) }); } } 
Enter fullscreen mode Exit fullscreen mode

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 {} 
Enter fullscreen mode Exit fullscreen mode

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 {} 
Enter fullscreen mode Exit fullscreen mode

Let's Test

Now lets up our docker container

docker-compose up 
Enter fullscreen mode Exit fullscreen mode

Docker running

And that's it! The app is running 👏

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" }' 
Enter fullscreen mode Exit fullscreen mode

Create user route

curl --location --request GET 'http://0.0.0.0:3000/users/37' 
Enter fullscreen mode Exit fullscreen mode

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 logo mnzsss / nest-auth-explained

🔒 Authentication using JWT and Google with Nest.js Explained


References

Top comments (0)