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.
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-nameTest that the project boots:
npm run start:devAdd 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 postgresAdd 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 pgConfiguration 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=trueDefine 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:prodAdd 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();Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
