DEV Community

Cover image for Initiating a NestJs app with PostgreSQL using Docker
Damilare Agba
Damilare Agba

Posted on

Initiating a NestJs app with PostgreSQL using Docker

Introduction

I sometimes find myself trying to remember exactly how to initiate a project after a very long time away. At this point, I've probably spent the last couple months working on a project off an entirely different stack, so much that I haven't initiated a new project in a while.

I decided to create this go-to article for initiating a new project using nestjs with typescript. I also thought it'll be a handy tool for other developers to leverage when getting their backend projects started. I'll simply give a step by step process to get a project started, create it's basic structure, connect to a database using docker and implement a authentication service.

Creating a NestJs App

First, I'll be going through creating a nestjs application, connecting to a postgresql database that will be spun up using docker, creating a User model and configuring an ORM - in this case prisma - to connect with our database. I'll also try to implement a simple authentication system to add some functionality to the app.

To get started with nestJs "a framework for building efficient, scalable Node.js server-side applications", I'll go to my terminal and install nest cli globally using the command below. This provides a way to create a new nest project which bundles with it the core files required to get the project started.

% npm i -g @nestjs/cli 
Enter fullscreen mode Exit fullscreen mode

I'll start the new project with this command -nest new <project-name>- which creates a directory where all our files will reside. It also creates a basic structure for the whole application.

% nest new nest-init
... # select a preferred package manager
% code nest-init # or open in your favorite IDE
Enter fullscreen mode Exit fullscreen mode

Now, the working directory looks something like

|-- node_modules/
|-- src
|    |-- main.ts
|    |-- app.module.ts
|-- test/
|-- package.json
|-- README.md
|-- tsconfig.json

Enter fullscreen mode Exit fullscreen mode

NOTE: The src directory initially contained more files but I removed them because I won't be needing them.

The Database

Starting the database

The next thing to do is to set up the database for the app to connect to. I'll do this using docker-compose which is a tool for "running multi-container applications on docker". I start by creating a docker-compose.yml file which will contain the code for running a postgreSQL instance. The file should contain something similar to the code below

version: '3.8'
services:
  postgres:
    image: postgres:13-alpine
    ports:
      - 5432:5432
    env_file:
      - .env
    volumes:
      - postgres:/var/lib/postgresql/data
    networks:
      - nest-init

volumes:
   postgres:
      name: nest-init-docker-db

networks:
   nest-init:
Enter fullscreen mode Exit fullscreen mode

To make use of this, a .env file is required which contains the credentials to be used when spinning up the postgreSQL instance. It should contain the following constants

POSTGRES_DB=<db_name>
POSTGRES_USER=<username>
POSTGRES_PASSWORD=<password>
Enter fullscreen mode Exit fullscreen mode

I then proceed to run the command below which spins up the database with docker; the -d flag allows this to run in detached mode which in-turn makes the terminal session still available for use.

% docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

Connecting to the database

Now that the database is running, I'll proceed to install and initiate prisma which creates a prisma directory and schema file. It also creates a .env file or in this case, adds a required line; DATABASE_URL to the file.

The database url in this case will look something like postgresql://dami:root@localhost:5432/nest-init?schema=public.

To install prisma

% npm install prisma --save-dev
...
% npx prisma init
Enter fullscreen mode Exit fullscreen mode

Now I have a schema.prisma file that contains the database connection variable. It will also contain all the yet to be created models that will be used in the application. It also defines the client to be used which in this case is prisma client. To make use of it though, it needs to be installed by running the command below

% npm install @prisma/client

Enter fullscreen mode Exit fullscreen mode

I also need to install nest config to easily access our configuration variables in the .env file.

% npm i --save @nestjs/config
Enter fullscreen mode Exit fullscreen mode

The ConfigModule needs to be added to the app.module.ts file to be able to make use of the config service with the aid of dependency injection. Now my file looks like this

import { Module } from '@nestjs/common';
import { PrismaModule } from './prisma/prisma.module';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true
    }),
    PrismaModule
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

I'll also create a simple User model to be used for the authentication system.

model User {
  id         Int      @id @default(autoincrement())
  email      String   @unique
  name       String
  password   String

  @@map("users")
}
Enter fullscreen mode Exit fullscreen mode

After creating the model, a migration needs to be made so the model will be mapped to the database. This is done by running a simple prisma command

% npx prisma migrate dev
Enter fullscreen mode Exit fullscreen mode

This creates a SQL migrations file and runs the migrations on the database.

Creating required services

Now, I can start creating the required services. Nest CLI provides handy tools for creation of modules, services and controllers. It also automatically bundles related files in a directory and adds the individual service module to the app.module.ts file. At the end or a project, the src folder should look something like;

src
 |-- auth
 |     |-- auth.controller.ts
 |     |-- auth.module.ts
 |     |-- auth.service.ts
 |-- prisma
 |     |-- prisma.module.ts
 |     |-- prisma.service.ts
 |-- app.module.ts
 |-- main.ts
Enter fullscreen mode Exit fullscreen mode

Prisma Service

I'll start by creating the prisma service while would be used to access the database from any part of the application.I'll generate to files, the prisma.module.ts and prisma.service.ts files using the command below.

% nest g module prisma
% nest g service prisma --no-spec
Enter fullscreen mode Exit fullscreen mode

The --no-spec flag ensures that prisma doesn't generate test files alongside the required files. Next, I'll add the following code to the prisma.service.ts file

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient{
    constructor(config: ConfigService) {
        super({
            datasources: {
                db: {
                    url: config.get('DATABASE_URL')
                }
            }
        })
    }

}

Enter fullscreen mode Exit fullscreen mode

and the prisma.module.ts file looks like

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

@Global()
@Module({
    providers: [PrismaService],
    exports: [PrismaService]
})
export class PrismaModule {}

Enter fullscreen mode Exit fullscreen mode

With this, I can also inject the PrismaService wherever the application needs to access the database.

Auth Service

Going forward, I'll be implementing a simple register endpoint in the authentication system to to test the apps functionality. To do this, the auth service, module and controller files need to be generated using the commands below

% nest g module auth
...
% nest g service auth --no-spec
...
% nest g controller auth --no-spec
Enter fullscreen mode Exit fullscreen mode

The generated module file already contains imports and bundles the service and controller files. I'll go ahead to add the route to the controller file and the register logic in the service file.

I'll create a simple DTO to serve as a schema for the expected payload and use class-validator to validate the incoming data. Note that this needs to be installed running

% npm i class-validator
% npm i class-transformer
Enter fullscreen mode Exit fullscreen mode

The validation pipe also needs to be added to the main.ts file to allow validation in the whole application.

main.ts

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true
  }))
  await app.listen(3000);
}
bootstrap();

Enter fullscreen mode Exit fullscreen mode

Now I'll write the auth controller

auth.controller.ts

import { Body, Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { IsEmail, IsNotEmpty, IsString } from "class-validator"

class RegisterDTO {
    @IsNotEmpty()
    @IsEmail()
    email: string

    @IsNotEmpty()
    @IsString()
    name: string

    @IsNotEmpty()
    @IsString()
    password: string

    @IsNotEmpty()
    @IsString()
    repeatPassword: string
}

@Controller('auth')
export class AuthController {
    constructor(private authService: AuthService) {}

    @Post('register')
    register(@Body() dto: RegisterDTO) {
        return this.authService.register(dto)
    }
}

Enter fullscreen mode Exit fullscreen mode

And in the auth.service.ts file

import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime';
import { PrismaService } from 'src/prisma/prisma.service';
import { RegisterDTO } from './auth.controller';
import * as CryptoJS from 'crypto-js';

@Injectable()
export class AuthService {
    constructor(private prisma: PrismaService, private config: ConfigService) {}

    async register(dto: RegisterDTO) {
        try {
            if (dto.password !== dto.repeatPassword) throw new BadRequestException("Passwords must be equal!")
            const key = CryptoJS.enc.Utf8.parse(this.config.get('SECRET_PASS'));
            const passwordHash = CryptoJS.AES.encrypt(dto.password, key, {iv: key}).toString()
            const user = await this.prisma.user.create({
                data: {
                    name: dto.name,
                    email: dto.email,
                    password: passwordHash
                }
            })
            const {password, ...result} = user
            return {
                statusCode: 201,
                message: "User created successfully.",
                data: result
            }
        } catch (error) {
            console.log(error)
            if (error instanceof PrismaClientKnownRequestError) {
                if (error.code === 'P2002') {
                    throw new ForbiddenException("Email or name has already been used!")
                }
            } throw error;
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Now I can start the server by running npm run start:dev and make a request to the register endpoint.

The complete code can be found here on Github.
Cheers!

Top comments (1)

Collapse
 
flacorlopes profile image
Flávio Lopes

Very useful. Thank you