How to Build a Scalable Nest.js API with Docker, PostgreSQL, and TypeORM

This step‑by‑step guide explains how to set up a Nest.js backend project, configure Dockerized PostgreSQL, manage database schemas with TypeORM, automate migrations, and expose a clean API using DTOs and Swagger documentation, all while keeping the development workflow streamlined and reproducible.

Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
How to Build a Scalable Nest.js API with Docker, PostgreSQL, and TypeORM

When a Node.js server project grows, organizing data and the database becomes difficult, so a solid project setup is essential. This guide shows how to set up a typical Nest.js project using a simple Node.js API, PostgreSQL for storage, and a suite of tools that make development easier.

Project and Tools

Nest.js provides a powerful CLI to generate project templates. Create a new project with:

npm i -g @nestjs/cli
nest new project-name

Test that the project boots:

npm run start:dev

Add a Persistent Data Layer

Use TypeORM to describe entities in code and sync them to the database. Docker is used to run a local PostgreSQL instance, avoiding coupling the project to a host‑installed server.

#!/bin/bash
set -e
SERVER="my_database_server"
PW="mysecretpassword"
DB="my_database"

echo "Stopping and removing old docker [$SERVER] and starting a fresh instance"
(docker kill $SERVER || :) && (docker rm $SERVER || :)) && \
  docker run --name $SERVER -e POSTGRES_PASSWORD=$PW -e PGPASSWORD=$PW -p 5432:5432 -d postgres

echo "Waiting for PostgreSQL to start"
sleep 3

echo "CREATE DATABASE $DB ENCODING 'UTF-8';" | docker exec -i $SERVER psql -U postgres

echo "\l" | docker exec -i $SERVER psql -U postgres

Add the script to package.json:

"start:dev:db": "./src/scripts/start-db.sh"

Nest.js Connects to the Database

Install the NestJS TypeORM integration:

npm install --save @nestjs/typeorm typeorm pg

Configuration Management

Use a ConfigService that reads environment variables (via dotenv) and provides a read‑only configuration object for TypeORM.

npm install --save dotenv
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=mysecretpassword
POSTGRES_DATABASE=my_database
PORT=3000
MODE=DEV
RUN_MIGRATIONS=true

Define the service:

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

@Module({
  imports: [TypeOrmModule.forRoot(configService.getTypeOrmConfig())],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
// src/config/config.service.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
require('dotenv').config();

class ConfigService {
  constructor(private env: { [k: string]: string | undefined }) {}
  private getValue(key: string, throwOnMissing = true): string {
    const value = this.env[key];
    if (!value && throwOnMissing) {
      throw new Error(`config error - missing env.${key}`);
    }
    return value;
  }
  public ensureValues(keys: string[]) { keys.forEach(k => this.getValue(k, true)); return this; }
  public getPort() { return this.getValue('PORT', true); }
  public isProduction() { return this.getValue('MODE', false) !== 'DEV'; }
  public getTypeOrmConfig(): TypeOrmModuleOptions {
    return {
      type: 'postgres',
      host: this.getValue('POSTGRES_HOST'),
      port: parseInt(this.getValue('POSTGRES_PORT')),
      username: this.getValue('POSTGRES_USER'),
      password: this.getValue('POSTGRES_PASSWORD'),
      database: this.getValue('POSTGRES_DATABASE'),
      entities: ['**/*.entity{.ts,.js}'],
      migrationsTableName: 'migration',
      migrations: ['src/migration/*.ts'],
      cli: { migrationsDir: 'src/migration' },
      ssl: this.isProduction(),
    };
  }
}

const configService = new ConfigService(process.env)
  .ensureValues([
    'POSTGRES_HOST',
    'POSTGRES_PORT',
    'POSTGRES_USER',
    'POSTGRES_PASSWORD',
    'POSTGRES_DATABASE',
  ]);

export { configService };

Development Restart

npm i --save-dev nodemon ts-node
{
  "watch": ["src"],
  "ext": "ts",
  "ignore": ["src/**/*.spec.ts"],
  "exec": "node --inspect=127.0.0.1:9223 -r ts-node/register -- src/main.ts",
  "env": {}
}
"start:dev": "nodemon --config nodemon.json"

Define and Load Entity Models

// base.entity.ts
import { PrimaryGeneratedColumn, Column, UpdateDateColumn, CreateDateColumn } from 'typeorm';

export abstract class BaseEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'boolean', default: true })
  isActive: boolean;

  @Column({ type: 'boolean', default: false })
  isArchived: boolean;

  @CreateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
  createDateTime: Date;

  @Column({ type: 'varchar', length: 300 })
  createdBy: string;

  @UpdateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
  lastChangedDateTime: Date;

  @Column({ type: 'varchar', length: 300, nullable: true })
  internalComment: string | null;
}
// item.entity.ts
import { Entity, Column } from 'typeorm';
import { BaseEntity } from './base.entity';

@Entity({ name: 'item' })
export class Item extends BaseEntity {
  @Column({ type: 'varchar', length: 300 })
  name: string;

  @Column({ type: 'varchar', length: 300 })
  description: string;
}

TypeORM CLI Setup

import fs = require('fs');
fs.writeFileSync('ormconfig.json', JSON.stringify(configService.getTypeOrmConfig(), null, 2));
"pretypeorm": "(rm ormconfig.json || :) && ts-node -r tsconfig-paths/register src/scripts/write-type-orm-config.ts",
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
"typeorm:migration:generate": "npm run typeorm -- migration:generate -n",
"typeorm:migration:run": "npm run typeorm -- migration:run"

Create and Run Migrations

npm run typeorm:migration:generate -- my_init
npm run typeorm:migration:run
#!/bin/bash
set -e
set -x
if [ "$RUN_MIGRATIONS" ]; then
  echo "Running migrations"
  npm run typeorm:migration:run
fi
echo "Starting server"
npm run start:prod

Add Business Logic

nest -- generate controller item
nest -- generate service item
// item.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Item } from '../model/item.entity';
import { Repository } from 'typeorm';

@Injectable()
export class ItemService {
  constructor(@InjectRepository(Item) private readonly repo: Repository<Item>) {}
  async getAll() { return await this.repo.find(); }
}
// item.controller.ts
import { Controller, Get } from '@nestjs/common';
import { ItemService } from './item.service';

@Controller('item')
export class ItemController {
  constructor(private serv: ItemService) {}
  @Get()
  async getAll() { return await this.serv.getAll(); }
}
// item.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ItemService } from './item.service';
import { ItemController } from './item.controller';
import { Item } from '../model/item.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Item])],
  providers: [ItemService],
  controllers: [ItemController],
})
export class ItemModule {}

DTO and Response Layer

// item.dto.ts
import { ApiModelProperty } from '@nestjs/swagger';
import { IsString, IsUUID } from 'class-validator';
import { Item } from '../model/item.entity';
import { User } from '../user.decorator';

export class ItemDTO implements Readonly<ItemDTO> {
  @ApiModelProperty({ required: true })
  @IsUUID()
  id: string;

  @ApiModelProperty({ required: true })
  @IsString()
  name: string;

  @ApiModelProperty({ required: true })
  @IsString()
  description: string;

  static from(dto: Partial<ItemDTO>) {
    const it = new ItemDTO();
    it.id = dto.id;
    it.name = dto.name;
    it.description = dto.description;
    return it;
  }

  static fromEntity(entity: Item) {
    return this.from({ id: entity.id, name: entity.name, description: entity.description });
  }

  toEntity(user: User = null) {
    const it = new Item();
    it.id = this.id;
    it.name = this.name;
    it.description = this.description;
    it.createDateTime = new Date();
    it.createdBy = user ? user.id : null;
    it.lastChangedBy = user ? user.id : null;
    return it;
  }
}

OpenAPI (Swagger) Setup

npm install --save @nestjs/swagger swagger-ui-express
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { configService } from './config/config.service';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  if (!configService.isProduction()) {
    const document = SwaggerModule.createDocument(app, new DocumentBuilder()
      .setTitle('Item API')
      .setDescription('My Item API')
      .build());
    SwaggerModule.setup('docs', app, document);
  }
  await app.listen(3000);
}
bootstrap();
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

DockerNode.jsPostgreSQLNestJSTypeORM
Tencent IMWeb Frontend Team
Written by

Tencent IMWeb Frontend Team

IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.