{
  "slug": "Nestjs---PostgreSQL-database-with-docker-compose-and-Liquibase-faec0b09d944",
  "title": "Nestjs : PostgreSQL database with docker-compose and Liquibase",
  "subtitle": "Building a CRUD API with NestJS, PostgreSQL, and Liquibase",
  "excerpt": "Building a CRUD API with NestJS, PostgreSQL, and Liquibase",
  "date": "2024-06-27",
  "tags": [
    "Node.js",
    "Postgres",
    "Docker",
    "API"
  ],
  "readingTime": "6 min",
  "url": "https://medium.com/@mobinshaterian/nestjs-postgresql-database-with-docker-compose-and-liquibase-faec0b09d944",
  "hero": "https://cdn-images-1.medium.com/max/800/1*j2IUBxzA0q9_OE6O5uuKKQ.jpeg",
  "content": [
    {
      "type": "heading",
      "level": 2,
      "text": "Building a CRUD API with NestJS, PostgreSQL, and Liquibase"
    },
    {
      "type": "paragraph",
      "html": "This report outlines the steps involved in creating a CRUD (Create, Read, Update, Delete) API using NestJS, PostgreSQL, and Liquibase for database management."
    },
    {
      "type": "image",
      "src": "https://cdn-images-1.medium.com/max/800/1*j2IUBxzA0q9_OE6O5uuKKQ.jpeg",
      "alt": "Nestjs : PostgreSQL database with docker-compose and Liquibase",
      "caption": "",
      "width": 1024,
      "height": 1024
    },
    {
      "type": "heading",
      "level": 3,
      "text": "Technologies Used:"
    },
    {
      "type": "paragraph",
      "html": "<strong>NestJS</strong>: A Node.js framework for building efficient and scalable server-side applications.<br>&nbsp;<strong>PostgreSQL</strong>: An open-source object-relational database management system (ORDBMS).<br>&nbsp;<strong>Liquibase</strong>: An open-source database migration tool for managing database schema changes.<br>&nbsp;<strong>TypeORM</strong>: An object-relational mapper (ORM) that simplifies working with databases in TypeScript or JavaScript."
    },
    {
      "type": "heading",
      "level": 3,
      "text": "Setting Up the Environment"
    },
    {
      "type": "heading",
      "level": 3,
      "text": "Docker Compose:"
    },
    {
      "type": "paragraph",
      "html": "The provided docker-compose.yml file defines services for:"
    },
    {
      "type": "paragraph",
      "html": "<strong>postgres-tasks</strong>: Runs a PostgreSQL container with specific configurations.<br>&nbsp;<strong>adminer</strong>: Runs an admin interface for managing the PostgreSQL database.<br>&nbsp;<strong>liquibase</strong>: Runs a Liquibase container to handle database schema changes."
    },
    {
      "type": "heading",
      "level": 3,
      "text": "Liquibase Configuration:"
    },
    {
      "type": "paragraph",
      "html": "<strong>changelog.xml</strong>: This file defines how Liquibase manages database schema changes.<br><strong> migrations/*.sql</strong>: These files contain the actual SQL statements for creating and modifying database tables."
    },
    {
      "type": "heading",
      "level": 3,
      "text": "NestJS Application:"
    },
    {
      "type": "paragraph",
      "html": "The application code utilizes TypeORM to interact with the PostgreSQL database."
    },
    {
      "type": "paragraph",
      "html": "<strong>app.module.ts</strong>: Configures the connection to the PostgreSQL database using TypeORM.<br><strong> task.entity.ts:</strong> Defines the structure of the Task entity to be stored in the database.<br><strong> task.repository.ts: </strong>Provides methods for interacting with the Task entity in the database.<br><strong> tasks.controller.ts, tasks.service.ts</strong>: Implement the CRUD operations for managing tasks in the database."
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Connect the Next Js application to Postgres using typeorm."
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Source Code:"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Run PostgreSQL with Docker-compose:"
    },
    {
      "type": "paragraph",
      "html": "docker-compose"
    },
    {
      "type": "code",
      "lang": "yaml",
      "code": "version: \"3.9\"services:\n  postgres-tasks:\n    image: postgres:13.4-buster\n    environment:\n      POSTGRES_USER: user\n      POSTGRES_PASSWORD: pass\n      POSTGRES_DB: tasks\n    volumes:\n      - postgres-db:/var/lib/postgresql/data\n    restart: always\n    ports:       - 3001:5432\n    networks:\n      - tasks-network\n  admin:\n    image: adminer\n    restart: always\n    depends_on:       - postgres-tasks\n    ports:\n      - 10000:8080\n    networks:\n      - tasks-network\n  liquibase:\n    image: liquibase/liquibase\n    command: tail -f /liquibase/changelog.xml\n    volumes:\n      - ./sql/changelog.xml:/liquibase/changelog.xml\n      - ./sql/migrations:/liquibase/migrations\n    networks:\n      - tasks-networkvolumes:\n  postgres-db:\n    driver: localnetworks:\n  tasks-network:\n    external: true"
    },
    {
      "type": "heading",
      "level": 3,
      "text": "Liquibase"
    },
    {
      "type": "image",
      "src": "https://cdn-images-1.medium.com/max/800/1*1WSg3Qh3DXsZFaT1qc4M0Q.png",
      "alt": "Nestjs : PostgreSQL database with docker-compose and Liquibase",
      "caption": "",
      "width": 235,
      "height": 106
    },
    {
      "type": "heading",
      "level": 3,
      "text": ".SQL"
    },
    {
      "type": "code",
      "lang": "sql",
      "code": "-- liquibase formatted sql\n-- changeset mobin:1\nCREATE TABLE IF NOT EXISTS \"tasks\" (\n    \"id\" BIGSERIAL PRIMARY KEY,\n    \"title\" TEXT NOT NULL,\n    \"description\" TEXT NOT NULL,\n    \"created_at\" TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nCREATE INDEX ON \"tasks\" (\"id\");\n\n-- rollback DROP TABLE \"tasks\";"
    },
    {
      "type": "heading",
      "level": 3,
      "text": "Changelog.xml"
    },
    {
      "type": "code",
      "lang": "xml",
      "code": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog  xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\nxmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\nxsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog         http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd\">\n<includeAll  path=\"migrations/\" relativeToChangelogFile=\"true\"/>\n</databaseChangeLog>"
    },
    {
      "type": "heading",
      "level": 3,
      "text": "Command Run docker-compose"
    },
    {
      "type": "code",
      "lang": "bash",
      "code": "docker network create tasks-network\nsudo\ndocker-compose up"
    },
    {
      "type": "image",
      "src": "https://cdn-images-1.medium.com/max/800/1*dulODqQ7FZ45tCtzQ-I69Q.png",
      "alt": "Nestjs : PostgreSQL database with docker-compose and Liquibase",
      "caption": "",
      "width": 761,
      "height": 508
    },
    {
      "type": "heading",
      "level": 3,
      "text": "Liquibase migration"
    },
    {
      "type": "code",
      "lang": "bash",
      "code": "sudo\ndocker-compose -f docker-compose.yml exec liquibase /bin/sh liquibase update\n--url=jdbc:postgresql://postgres-tasks:5432/task?currentSchema=public --changelog-file=changelog.xml\n--username=user --password=pass"
    },
    {
      "type": "heading",
      "level": 3,
      "text": "Adminer"
    },
    {
      "type": "code",
      "lang": "text",
      "code": "http://127.0.0.1:10000/?pgsql=postgres-tasks&username=user&db=task&ns=public"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Object Relational Mapping (ORM) — TypeORM"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Install packages"
    },
    {
      "type": "code",
      "lang": "bash",
      "code": "yarn add typeorm @nestjs/typeorm pg"
    },
    {
      "type": "paragraph",
      "html": "app.module.ts"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "@Module({\n  imports: [TasksModule, TypeOrmModule.forRoot({\n    type: 'postgres', host: '127.0.0.1', port: 3001, username: 'user', password: 'pass', database:\n    'task', autoLoadEntities: true, synchronize: true,\n  }),],\n})\nexport class AppModule {\n}"
    },
    {
      "type": "paragraph",
      "html": "problem connection&nbsp;:"
    },
    {
      "type": "image",
      "src": "https://cdn-images-1.medium.com/max/800/1*eHOiLQ7r9T9pkRo1JrhJHw.png",
      "alt": "Nestjs : PostgreSQL database with docker-compose and Liquibase",
      "caption": "",
      "width": 945,
      "height": 260
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Create Entity:"
    },
    {
      "type": "paragraph",
      "html": "task.entity.ts"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "import {\n  Column, Entity, PrimaryGeneratedColumn\n}\nfrom 'typeorm';\nimport {\n  TaskStatus\n}\nfrom './tasks.model';\n@Entity()\nexport class Task {\n  @PrimaryGeneratedColumn('uuid') id: string;\n  @Column() title: string;\n  @Column() description: string;\n  @Column() status: TaskStatus;\n}"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Active Record vs Data Mapper"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "How to do custom repository using TypeORM (MongoDB) in NestJS?"
    },
    {
      "type": "paragraph",
      "html": "I have a question. With <code>@EntityRepository</code> decorator being marked as deprecated in <code>typeorm@^0.3.6</code>, what is now the recommended or TypeScript-friendly way to create a custom repository for an entity in NestJS? A custom repository before would look like this:"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Add Repository"
    },
    {
      "type": "paragraph",
      "html": "task.repository.ts"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "import {\n  DataSource, Repository\n}\nfrom 'typeorm';\nimport {\n  Injectable\n}\nfrom '@nestjs/common';\nimport {\n  Task\n}\nfrom './task.entity';\n@Injectable()\nexport class TaskRepository extends Repository<Task> {\n  constructor(private dataSource: DataSource) {\n    super(Task, dataSource.createEntityManager());\n  }\n}"
    },
    {
      "type": "paragraph",
      "html": "task.module.ts"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "import {\n  Module\n}\nfrom '@nestjs/common';\nimport {\n  TasksController\n}\nfrom './tasks.controller';\nimport {\n  TasksService\n}\nfrom './tasks.service';\nimport {\n  TypeOrmModule\n}\nfrom '@nestjs/typeorm';\nimport {\n  Task\n}\nfrom './task.entity';\n@Module({\n  imports: [TypeOrmModule.forFeature([Task])], controllers: [TasksController], providers:\n  [TasksService],\n})\nexport class TasksModule {\n}"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "What is a promise?"
    },
    {
      "type": "paragraph",
      "html": "A promise is essentially an improvement of callbacks that manage all asynchronous data activities. A JavaScript promise represents an activity that will either be completed or declined. If the promise is fulfilled, it is resolved; otherwise, it is rejected. Promises, unlike typical callbacks, may be chained."
    },
    {
      "type": "image",
      "src": "https://cdn-images-1.medium.com/max/800/1*1QRnTZ-55sNx-uJ_5llVhA.png",
      "alt": "Nestjs : PostgreSQL database with docker-compose and Liquibase",
      "caption": "",
      "width": 939,
      "height": 247
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Add Get Task by ID"
    },
    {
      "type": "paragraph",
      "html": "controller"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "import {\n  Controller, Get, Param\n}\nfrom '@nestjs/common';\nimport {\n  TasksService\n}\nfrom './tasks.service';\nimport {\n  Task\n}\nfrom './task.entity';\n@Controller('tasks')\nexport class TasksController {\n  constructor(private tasksService: TasksService) {\n  }\n  @Get('/:id') getTaskById(@Param('id') id: string): Promise<Task> {\n    return this.tasksService.getTaskById(id);\n  }\n}"
    },
    {
      "type": "paragraph",
      "html": "service"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "import {\n  Injectable, NotFoundException\n}\nfrom '@nestjs/common';\nimport {\n  TaskRepository\n}\nfrom './task.repository';\nimport {\n  InjectRepository\n}\nfrom '@nestjs/typeorm';\nimport {\n  Task\n}\nfrom './task.entity';\n@Injectable()\nexport class TasksService {\n  constructor(@InjectRepository(Task) private taskRepository: TaskRepository,) {\n  }\n  async getTaskById(id: string): Promise<Task> {\n    const found = await this.taskRepository.findOne({\n      where: {\n        id: id\n      }\n    });\n    if (!found) {\n      throw new NotFoundException(`Task with ID \"${id}\" not found`);\n    }\n    return found;\n  }\n}"
    },
    {
      "type": "paragraph",
      "html": "<a href=\"https://typeorm.io/repository-api\" target=\"_blank\" rel=\"noreferrer noopener\">https://typeorm.io/repository-api</a>"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Add Repository layer"
    },
    {
      "type": "paragraph",
      "html": "module"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "import {\n  Module\n}\nfrom '@nestjs/common';\nimport {\n  TasksController\n}\nfrom './tasks.controller';\nimport {\n  TasksService\n}\nfrom './tasks.service';\nimport {\n  TypeOrmModule\n}\nfrom '@nestjs/typeorm';\nimport {\n  Task\n}\nfrom './task.entity';\nimport {\n  TaskRepository\n}\nfrom './task.repository';\n@Module({\n  imports: [TypeOrmModule.forFeature([Task])], controllers: [TasksController], providers:\n  [TasksService, TaskRepository],\n})\nexport class TasksModule {\n}"
    },
    {
      "type": "paragraph",
      "html": "controller"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "import {\n  Body, Controller, Get, Param, Post\n}\nfrom '@nestjs/common';\nimport {\n  TasksService\n}\nfrom './tasks.service';\nimport {\n  Task\n}\nfrom './task.entity';\nimport {\n  CreateTaskDto\n}\nfrom './dto/create-task.dto';\n@Controller('tasks')\nexport class TasksController {\n  constructor(private tasksService: TasksService) {\n  }\n  @Get('/:id') getTaskById(@Param('id') id: string): Promise<Task> {\n    return this.tasksService.getTaskById(id);\n  }\n  @Post() createTask(@Body() createTaskDto: CreateTaskDto): Promise<Task> {\n    return this.tasksService.createTask(createTaskDto);\n  }\n}"
    },
    {
      "type": "paragraph",
      "html": "service"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "import {\n  Injectable\n}\nfrom '@nestjs/common';\nimport {\n  TaskRepository\n}\nfrom './task.repository';\nimport {\n  InjectRepository\n}\nfrom '@nestjs/typeorm';\nimport {\n  Task\n}\nfrom './task.entity';\nimport {\n  CreateTaskDto\n}\nfrom './dto/create-task.dto';\n@Injectable()\nexport class TasksService {\n  constructor(@InjectRepository(TaskRepository) private taskRepository: TaskRepository,) {\n  }\n  async getTaskById(id: string): Promise<Task> {\n    const task = await this.taskRepository.getTaskById(id);\n    return task;\n  }\n  async createTask(createTaskDto: CreateTaskDto): Promise<Task> {\n    const task = await this.taskRepository.createTask(createTaskDto);\n    return task;\n  }\n}"
    },
    {
      "type": "paragraph",
      "html": "repository"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "import {\n  DataSource, Repository\n}\nfrom 'typeorm';\nimport {\n  Injectable, NotFoundException\n}\nfrom '@nestjs/common';\nimport {\n  Task\n}\nfrom './task.entity';\nimport {\n  TaskStatus\n}\nfrom './tasks-status.enum';\nimport {\n  CreateTaskDto\n}\nfrom './dto/create-task.dto';\n@Injectable()\nexport class TaskRepository extends Repository<Task> {\n  constructor(private ds: DataSource) {\n    super(Task, ds.createEntityManager());\n  }\n  public async getTaskById(id: string): Promise<Task> {\n    const found = await this.findOne({\n      where: {\n        id: id\n      }\n    });\n    if (!found) {\n      throw new NotFoundException(`Task with ID \"${id}\" not found`);\n    }\n    return found;\n  }\n  public async createTask(createTaskDto: CreateTaskDto): Promise<Task> {\n    const {\n      title, description\n    }\n    = createTaskDto;\n    const task = this.create({\n      title, description, status: TaskStatus.OPEN,\n    });\n    await this.save(task);\n    return task;\n  }\n}"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "synchronize: true"
    },
    {
      "type": "paragraph",
      "html": "Be careful not to set “true” synchronize in production."
    },
    {
      "type": "paragraph",
      "html": "module"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "import {\n  Module\n}\nfrom '@nestjs/common';\nimport {\n  TasksModule\n}\nfrom './tasks/tasks.module';\nimport {\n  TypeOrmModule\n}\nfrom '@nestjs/typeorm';\n@Module({\n  imports: [TasksModule, TypeOrmModule.forRoot({\n    type: 'postgres', host: '127.0.0.1', port: 3001, username: 'user', password: 'pass', database:\n    'task', autoLoadEntities: true, // synchronize: true,\n  }),],\n})\nexport class AppModule {\n}"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Delete Task"
    },
    {
      "type": "paragraph",
      "html": "controller"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "@Delete('/:id') deleteTask(@Param('id') id: string): Promise<void> {\n  return this.tasksService.deleteTask(id);\n}"
    },
    {
      "type": "paragraph",
      "html": "service"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "async deleteTask(id: string): Promise<void> {    return await this.taskRepository.deleteTask(id);  }"
    },
    {
      "type": "paragraph",
      "html": "repository"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "public async deleteTask(id: string): Promise<void> {    const result = await this.delete(id);    if\n(result.affected === 0) {      throw new NotFoundException(`Task with ID \"${id}\" not found`);    }\n}"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Update Task"
    },
    {
      "type": "paragraph",
      "html": "controller"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "@Patch('/:id/status') updateTaskStatus(@Param('id') id: string, @Body() updateTaskStatusDto:\nUpdateTaskStatusDto,): Promise<Tasks> {\n  const {\n    status\n  }\n  = updateTaskStatusDto;\n  return this.tasksService.updateTaskStatus(id, status);\n}"
    },
    {
      "type": "paragraph",
      "html": "service"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "async updateTaskStatus(id: string, status: TaskStatus): Promise<Tasks> {    return await\nthis.taskRepository.updateTaskStatus(id, status);  }"
    },
    {
      "type": "paragraph",
      "html": "repository"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "public async updateTaskStatus(    id: string,    status: TaskStatus,  ): Promise<Tasks> {    const\ntask = await this.getTaskById(id);    task.status = status;    await this.save(task);    return\ntask;  }"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Search Task"
    },
    {
      "type": "paragraph",
      "html": "controller"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "@Get() getTasks(@Query() filterDto: GetTasksFilterDto): Promise<Tasks[]> {\n  return this.tasksService.getTasks(filterDto);\n}"
    },
    {
      "type": "paragraph",
      "html": "service"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "async getTasks(filterDto: GetTasksFilterDto): Promise<Tasks[]> {    return await\nthis.taskRepository.getTasks(filterDto);  }"
    },
    {
      "type": "paragraph",
      "html": "repository"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "async getTasks(filterDto: GetTasksFilterDto): Promise<Tasks[]> {    const { status, search } =\nfilterDto;    const query = this.createQueryBuilder('tasks');    if (status) {\nquery.andWhere('tasks.status = :status', { status });    }    if (search) {      query.andWhere(\n'LOWER(tasks.title) LIKE LOWER(:search) OR LOWER(tasks.description) LIKE LOWER(:search)',        {\nsearch: `%${search}%` },      );    }    const tasks = await query.getMany();    return tasks;  }"
    },
    {
      "type": "paragraph",
      "html": "<strong>Conclusion:</strong><br>This report demonstrates how to leverage NestJS, PostgreSQL, Liquibase, and TypeORM to build a robust and scalable CRUD API with database management capabilities. The provided code offers a starting point for developing more complex applications with NestJS."
    }
  ]
}
