Database with Prisma ORM, Docker and Postgres - NestJs with Passport #02
Gabriel Menezes
Posted on January 21, 2022
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"]
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
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
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
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 👏
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 others 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 interfacePrisma.UserWhereUniqueInput
forget user by unique columns -
createUser
: to create a new user using data from interfacePrisma.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 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 runcreateUser
service and return a data from them -
getUserProfile
: Get an id of the user sent by the route and run theuser
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
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"
}'
curl --location --request GET 'http://0.0.0.0:3000/users/37'
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
mnzsss / nest-auth-explained
🔒 Authentication using JWT and Google with Nest.js Explained
References
Posted on January 21, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.