DEV Community

Cover image for TypeScript CRUD Rest API, using Nest.js, TypeORM, Postgres, Docker and Docker Compose
Francesco Ciulla
Francesco Ciulla

Posted on • Updated on

TypeScript CRUD Rest API, using Nest.js, TypeORM, Postgres, Docker and Docker Compose

Let's create a CRUD Rest API in Typescript, using:

  • NestJS (NodeJS framework)
  • TypeORM (ORM: Object Relational Mapper)
  • Postgres (relational database)
  • Docker (for containerization)
  • Docker Compose

If you prefer a video version:

All the code is available in the GitHub repository (link in the video description): https://www.youtube.com/live/gqFauCpPSlw


🏁 Intro

Here is a schema of the architecture of the application we are going to create:

crud, read, update, delete, to a NestJS app and Postgres service, connected with Docker compose. Postman and Tableplus to test it

We will create 5 endpoints for basic CRUD operations:

  • Create
  • Read all
  • Read one
  • Update
  • Delete

Here are the steps we are going through:

  1. Create a new NestJS application
  2. Create a new module for the users, with a controller, a service and an entity
  3. Dockerize the application
  4. Create docker-compose.yml to run the application and the database
  5. Test the application with Postman and Tableplus

We will go with a step-by-step guide, so you can follow along.


Requirements:

  • Node installed (I'm using v16)
  • Docker installed and running
  • (Optional): Postman and Tableplus to follow along, but any testing tool will work
  • NestJS CLI (command below)

💻 Create a new NestJS application

We will create our project using the NestJS CLI

if you don't have the NestJS CLI installed, you can install it with:

npm install -g @nestjs/cli
Enter fullscreen mode Exit fullscreen mode

This will install the NestJS CLI globally, so you can use it from anywhere.

Then you can create move to your workspace folder and create a new NestJS application with (you can replace nest-crud-app with what you want):

nest new nest-crud-app
Enter fullscreen mode Exit fullscreen mode

Just hit enter to go with the default options.
This will create a new project for you (it will take a while).

successfully created project nest-crud-app

Step into the directory:

cd nest-crud-app
Enter fullscreen mode Exit fullscreen mode

Now install the dependencies we need:

npm i pg typeorm @nestjs/typeorm @nestjs/config
Enter fullscreen mode Exit fullscreen mode
  • pg: Postgres driver for NodeJS
  • typeorm: ORM for NodeJS
  • @nestjs/typeorm: NestJS module for TypeORM
  • @nestjs/config: NestJS module for configuration

Once it's done, open the project in your favorite editor (I'm using VSCode).

code .
Enter fullscreen mode Exit fullscreen mode

Before we start coding, let's test if everything is working.

npm start
Enter fullscreen mode Exit fullscreen mode

And we should see something like that:

Hello World on the left, vs code witn npm start command run, NestJS scaffold project

Now you can stop the server with Ctrl + C.

🐈‍⬛ Create the NestJS application

Now we are going to work on the NestJS application.

Let's create a new module, a controller, a service and an entity.

nest g module users
nest g controller users
nest g service users
touch src/users/user.entity.ts
Enter fullscreen mode Exit fullscreen mode

This will create the following files (and 2 more test files we will not use)

  • src/users/users.module.ts
  • src/users/users.controller.ts
  • src/users/users.service.ts
  • src/users/user.entity.ts

Your folder structure should look like that:

folder structure of the NestJS app, the 4 files are ina  folder called users in src

Now let's work on these 4 files.

User Entity

Open the file "src/users/user.entity.ts" and populate it like that:

import { Entity, PrimaryGeneratedColumn, Column,  } from "typeorm";

@Entity()
export class User {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @Column()
    email: string;
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • We are using the decorator @Entity() to tell TypeORM that this is an entity

  • We are using the decorator @PrimaryGeneratedColumn() to tell TypeORM that this is the primary key of the table

  • We are using the decorator @Column() to tell TypeORM that this is a column of the table

We are creating a User entity with 3 columns: id, name and email.

User Service

Open the file "src/users/users.service.ts" and populate it like that:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {User} from './user.entity';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}

  async findAll(): Promise<User[]> {
    return this.userRepository.find();
  }

  async findOne(id: number): Promise<User> {
    return this.userRepository.findOne({ where: { id } });
  }

  async create(user: Partial<User>): Promise<User> {
    const newuser = this.userRepository.create(user);
    return this.userRepository.save(newuser);
  }

  async update(id: number, user: Partial<User>): Promise<User> {
    await this.userRepository.update(id, user);
    return this.userRepository.findOne({ where: { id } });
  }

  async delete(id: number): Promise<void> {
    await this.userRepository.delete(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • We are using the decorator @Injectable() to tell NestJS that this is a service

  • We are using the decorator @InjectRepository(User) to tell NestJS that we want to inject the repository of the User entity

  • We are using the decorator @Repository(User) to tell NestJS that we want to inject the repository of the User entity

  • We are creating a UserService with 5 methods: findAll, findOne, create, update and delete

User Controller

Open the file "src/users/users.controller.ts" and populate it like that:

import { Controller, Get, Post, Body, Put, Param, Delete, NotFoundException } from '@nestjs/common';
import { UsersService } from './users.service';
import { User } from './user.entity';

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

  //get all users
  @Get()
  async findAll(): Promise<User[]> {
    return this.usersService.findAll();
  }

  //get user by id
  @Get(':id')
  async findOne(@Param('id') id: number): Promise<User> {
    const user = await this.usersService.findOne(id);
    if (!user) {
      throw new NotFoundException('User does not exist!');
    } else {
      return user;
    }
  }

  //create user
  @Post()
  async create(@Body() user: User): Promise<User> {
    return this.usersService.create(user);
  }

  //update user
  @Put(':id')
  async update (@Param('id') id: number, @Body() user: User): Promise<any> {
    return this.usersService.update(id, user);
  }

  //delete user
  @Delete(':id')
  async delete(@Param('id') id: number): Promise<any> {
    //handle error if user does not exist
    const user = await this.usersService.findOne(id);
    if (!user) {
      throw new NotFoundException('User does not exist!');
    }
    return this.usersService.delete(id);
  }
}

Enter fullscreen mode Exit fullscreen mode

Explanation:

  • We are using the decorator @Controller('users') to tell NestJS that this is a controller, and that the route is "users"

  • We are defining the constructor of the class, and injecting the UserService

  • We are defining 5 methods: findAll, findOne, create, update and delete, decorated with the HTTP method we want to use, and we are using the UserService to call the corresponding method

User Module

Open the file "src/users/users.module.ts" and populate it like that:

import { Module } from '@nestjs/common';
import { UserController } from './users.controller';
import { UserService } from './users.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UserController],
  providers: [UserService]
})
export class UsersModule {}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • We are importing the TypeOrmModule and the User entity (UserController and UserService are already imported)

  • We are using the decorator @Module() to tell NestJS that this is a module

  • We add the TypeOrmModule.forFeature([User]) to the imports array, to tell NestJS that we want to use the User entity

Update the Main Module

Open the file "src/app.module.ts" and populate it like that:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';


@Module({
  imports: [
    ConfigModule.forRoot(),
    UsersModule,
    TypeOrmModule.forRoot({
      type: process.env.DB_TYPE as any,
      host: process.env.PG_HOST,
      port: parseInt(process.env.PG_PORT),
      username: process.env.PG_USER,
      password: process.env.PG_PASSWORD,
      database: process.env.PG_DB,
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: true,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • We are importing the ConfigModule, the UsersModule and the TypeOrmModule

  • We are importing the ConfigModule, UsersModule and TypeOrmModule in the imports array

  • For TypeOrmModule, we are using the method forRoot() to tell NestJS that we want to use the default connection, and we define some environment variables to connect to the database. We will set the in the docker-compose.yml file soon.

  • the synchronize option is set to true, so that the database schema is automatically updated when the application is started


🐳 Dockerize the application

Let's create 3 files to dockerize the application: a Dockerfile and a .dockerignore file.

touch Dockerfile .dockerignore docker-compose.yml
Enter fullscreen mode Exit fullscreen mode

.dockerignore

A .dockerignore file is used to tell Docker which files and directories to ignore when building the image.

If you are familiar with the .gitignore file, it works the same way.

Open the file ".dockerignore" and populate it like that:

node_modules
dist
.git
Enter fullscreen mode Exit fullscreen mode

This will tell Docker to ignore the node_modules, dist and .git directories when building the image.

Dockerfile

A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image.

Open the file "Dockerfile" and populate it like that:

FROM node:16

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

RUN npm run build

EXPOSE 3000

CMD ["npm", "run", "start:prod"]
Enter fullscreen mode Exit fullscreen mode

Explanation:

FROM node:16 is used to tell Docker which image to use as a base image.

WORKDIR is the directory where the commands will be executed. In our case, it's the /app directory.

COPY package*.json is used to copy the package.json and package-lock.json files to the /app directory.

RUN npm install is used to install the dependencies.

COPY . . is used to copy all the files from the current directory to the /app directory.

RUN npm run build is used to build the application.

EXPOSE is used to expose the port 3000 to the host.

CMD is used to execute a command when the container is started, in our case, it's "npm run start:prod".

docker-compose.yml file

We will use docker compose to run the application and the database.

Populate the file "docker-compose.yml" like that:

version: '3.9'
services:
  nestapp:
    container_name: nestapp
    image: francescoxx/nestapp:1.0.0
    build: .
    ports:
      - '3000:3000'
    environment:
      - DB_TYPE=postgres
      - PG_USER=postgres
      - PG_PASSWORD=postgres
      - PG_DB=postgres
      - PG_PORT=5432
      - PG_HOST=db
    depends_on:
      - db
  db:
    container_name: db
    image: postgres:12
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres
    ports:
      - '5432:5432'
    volumes:
      - pgdata:/var/lib/postgresql/data
volumes:
  pgdata: {}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • We are using the version 3.9 of the docker-compose.yml file format

  • We are defining 2 services: nestapp and db

  • The nestapp service is used to run the NestJS application

  • The db service is used to run the Postgres database

  • The nestapp service depends on the db service, so that the db service is started before the nestapp service

For the nestapp service:

container_name is used to set the name of the container

image is used to set the image to use, in our case, it's francescoxx/nestapp:1.0.0 change francescoxxx with your docker hub username

build is used to build the image from the Dockerfile. we are using the current directory as the build context.

ports is used to expose the port 3000 to the host

environment is used to set the environment variables: DB_TYPE, PG_USER, PG_PASSWORD, PG_DB, PG_PORT, PG_HOST. these variables will be used by the application to connect to the database

depends_on is used to tell docker-compose that the db service must be started before the nestapp service.

For the db service:

container_name is used to set the name of the container

image is used to set the image to use, in our case, it's postgres:12

environment is used to set the environment variables: POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB

ports is used to expose the port 5432 to the host

volumes is used to mount a volume to the container. In our case, we are mounting the pgdata volume to the /var/lib/postgresql/data directory.

We also define the pgdata volume at the end of the file.

Run the Postgres service

To run the Postgres service, we will use the docker-compose command.

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

This will run the db service in detached mode.

To check if the service is running, we can use the docker ps command:

docker ps -a
Enter fullscreen mode Exit fullscreen mode

We should see something like that:

$ docker ps -a<br>
CONTAINER ID   IMAGE         COMMAND                  CREATED          STATUS          PORTS                    NAMES<br>
e045f74a36ca   postgres:12   "docker-entrypoint.s…"   23 seconds ago   Up 22 seconds   0.0.0.0:5432->5432/tcp   db

But let's check it with TablePlus. Open the TablePlus application and connect to the database, by creating a new "Postgres" connection.

You can use the UI and set:

  • Host: localhost
  • Port: 5432
  • Username: postgres
  • Password: postgres
  • Database: postgres

Then hit the "Connect" button at the bottom-right.

TablePlus application

Now we are ready to build the Nest app image and run the application.

Build the Nest app image

To build the Nest app image, we will use the docker compose command.

docker compose build
Enter fullscreen mode Exit fullscreen mode

This will build the image from the Dockerfile.

To check if the image is built, we can use the docker images command:

$ docker images<br>
REPOSITORY            TAG       IMAGE ID       CREATED             SIZE<br>
francescoxx/nestapp   1.0.0     53267c897590   About an hour ago   1.16GB<br>
postgres              12        1db9fa309607   44 hours ago        373MB

Run the Nest app service

To run the Nest app service, we will use the docker-compose command.

docker compose up 
Enter fullscreen mode Exit fullscreen mode

Test the application

To Test the application, we can use the Postman or any other API client.

First of all let's test if the app is running. Open Postman and create a new GET request.

Postman Get request to localhost:3000

Get all users

To get all users, we can make a GET request to localhost:3000/users.

If we see an empty array it means that its working.

Postman Get request to localhost:3000/users

Create a user

To create a user, we can make a POST request to localhost:3000/users.

In the body, we can use the raw JSON format and set the following data:

{
  "name": "aaa",
  "email": "aaa@mail"
}
Enter fullscreen mode Exit fullscreen mode

Postman Post request to localhost:3000/users

You can create 2 more users with the following data:

{
  "name": "bbb",
  "email": "bbb@mail"
}
Enter fullscreen mode Exit fullscreen mode
{
  "name": "ccc",
  "email": "ccc@mail"
}
Enter fullscreen mode Exit fullscreen mode

Get all the three users

To get all the three users, we can make a GET request to localhost:3000/users.

Postman Get request to localhost:3000/users

Get a user by id

To get a single user, we can make a GET request to localhost:3000/users/2.

Postman Get request to localhost:3000/users/2

Update a user

To update a user, we can make a PUT request to localhost:3000/users/2.

Let's change the name from "bbb" to "Francesco" and the email from "bbb@mail" to "francesco@mail".

{
  "name":"Francesco",
  "email":"francesco@mail"
}
Enter fullscreen mode Exit fullscreen mode

Postman PUT request to localhost:3000/users/2

Delete a user

Finally, to delete a user, we can make a DELETE request to localhost:3000/users/3.

Postman DELETE request to localhost:3000/users/3

The answer comes directly from the database.

Final test with TablePlus

Let's check if the data is correctly stored in the database.

As a final test, let's go back to TablePlus and check if the data has been updated.

TablePlus application


🏁 Conclusion

We made it! We have built a CRUD rest API in TypeScript, using:

  • NestJS (NodeJS framework)
  • TypeORM (ORM: Object Relational Mapper)
  • Postgres (relational database)
  • Docker (for containerization)
  • Docker Compose

If you prefer a video version:

All the code is available in the GitHub repository (link in the video description): https://www.youtube.com/live/gqFauCpPSlw

That's all.

If you have any question, drop a comment below.

Francesco

Top comments (18)

Collapse
 
vladyslavshapoval profile image
Vladyslav Shapoval

Some crucial points are missing, like DTOs to validate incoming requests and transform responses.
And the general approach is far from a perfect one.
If you're a novice, then this article might be helpful, yet it doesn't follow best practices.
For instance, you shouldn't have any business logic inside controllers: you should get a user and check if it exists inside a service.
Next, better to have a data service to work with database. A service shouldn't know anything about database and ORM you use. All those things should be in a data service

Collapse
 
jakeroid profile image
Ivan Karabadzhak

Agree

However, for newbies this article should be okay

Collapse
 
francescoxx profile image
Francesco Ciulla

that's the point of the article

Collapse
 
francescoxx profile image
Francesco Ciulla

yes this is not intended to be a production ready code. thanks for the feedback

Collapse
 
langstra profile image
Wybren Kortstra

Nice article, thanks for your help. May I ask if you've considered using MikroORM instead of TypeORM. I have used both and found MikroORM to work a lot better, but I am interested in your opinion.

Collapse
 
vladyslavshapoval profile image
Vladyslav Shapoval

I haven't heard about MikroORM, yet personally I prefer Prisma. You needn't create classes for your tables. It generates classes/types automatically. It's interesting that all auto generated stuff is located in node_modules, so that your sources don't have them. I strongly recommend to try it out

Collapse
 
francescoxx profile image
Francesco Ciulla

never heard fo that

Collapse
 
langstra profile image
Wybren Kortstra

It is a typescript ORM, like TypeORM, but with far fewer issues, more active development, better support and overall just better in my opinion. However, I think with the current state of typescript orm's you should also have a look at Prisma. In general I believe that TypeORM should not be used anymore.

Thread Thread
 
phuoctrungppt profile image
Trung Phan

why don't use TypeORM? Please help me clarify this

Thread Thread
 
langstra profile image
Wybren Kortstra

In my experience MikroORM has far fewer bugs. The support by the maintainer is very good, the maintainer is active in development, responds quickly to problems. Actually just the same reasons as I already stated.

Collapse
 
samchon profile image
Jeongho Nam

What about adapting nestia?

With it, you can use pure TypeScript interface type as DTO, and also can generate advanced SDK (something like tRPC) and swagger than ever.

Collapse
 
francescoxx profile image
Francesco Ciulla

the goal of this artricle is to focus on the basics for beginners

Collapse
 
gseriche profile image
Gonzalo Seriche Vega

I'm a DevOps trying to understand and make life easy by creating some API. This tutorial helps me create a single API and understand the code for the container deployment in a local env.
Thanks for the guide.

Collapse
 
francescoxx profile image
Francesco Ciulla

you are welcome

Collapse
 
franciscogilt profile image
Francisco Gil

Francesco, I found this article very helpful and well structured. Thank you.

Collapse
 
francescoxx profile image
Francesco Ciulla

thank you Francisco

Collapse
 
shoban12 profile image
Mude Shoban Babu

thank you Francisco, very detailed explaination and code. thanks a lot.

Collapse
 
francescoxx profile image
Francesco Ciulla

you are welcome Shoban.