NestJS + PostgreSQL: Create Your First REST API

CRUD example with TypeORM

Building a REST API with NestJS, PostgreSQL, and TypeORM is a powerful, scalable choice for production apps. In this guide, you’ll create a complete CRUD API (Create, Read, Update, Delete) using a simple Todo resource.

We’ll cover project setup, database configuration, an entity + DTOs, a service, a controller, and how to test with cURL.


Prerequisites

  • Node.js 18+
  • PostgreSQL 13+ running locally (with a database created)
  • npm or pnpm
  • Nest CLI:
npm i -g @nestjs/cli

1) Create the Project

nest new nest-pg-crud
# Choose npm or pnpm
cd nest-pg-crud

Install required packages:

npm i @nestjs/typeorm typeorm pg class-validator class-transformer dotenv

2) Environment Variables

Create a .env file in the project root:

# .env
PORT=3000
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASS=postgres
DB_NAME=nest_crud

Ensure the database nest_crud exists:

CREATE DATABASE nest_crud;

3) TypeORM + PostgreSQL Setup

Open src/app.module.ts and configure TypeORM (v0.3+ style):

// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TodoModule } from './todo/todo.module';
import { Todo } from './todo/todo.entity';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    TypeOrmModule.forRootAsync({
      useFactory: () => ({
        type: 'postgres',
        host: process.env.DB_HOST,
        port: Number(process.env.DB_PORT),
        username: process.env.DB_USER,
        password: process.env.DB_PASS,
        database: process.env.DB_NAME,
        entities: [Todo],
        synchronize: true, // turn off in production; use migrations instead
        logging: false,
      }),
    }),
    TodoModule,
  ],
})
export class AppModule {}

4) Create the Todo Module

Generate files:

nest g module todo
nest g service todo
nest g controller todo

5) Define the Entity

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

@Entity()
export class Todo {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ length: 120 })
  title: string;

  @Column({ type: 'text', nullable: true })
  description?: string;

  @Column({ default: false })
  completed: boolean;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

Register the entity in the module:

// src/todo/todo.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Todo } from './todo.entity';
import { TodoService } from './todo.service';
import { TodoController } from './todo.controller';

@Module({
  imports: [TypeOrmModule.forFeature([Todo])],
  controllers: [TodoController],
  providers: [TodoService],
})
export class TodoModule {}

6) Create DTOs (Validation)

// src/todo/dto/create-todo.dto.ts
import { IsBoolean, IsOptional, IsString, MaxLength } from 'class-validator';

export class CreateTodoDto {
  @IsString()
  @MaxLength(120)
  title: string;

  @IsOptional()
  @IsString()
  description?: string;

  @IsOptional()
  @IsBoolean()
  completed?: boolean;
}
// src/todo/dto/update-todo.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateTodoDto } from './create-todo.dto';

export class UpdateTodoDto extends PartialType(CreateTodoDto) {}

Enable global validation pipe in main.ts:

// src/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, forbidNonWhitelisted: true, transform: true }));
  await app.listen(process.env.PORT || 3000);
}
bootstrap();

Install @nestjs/mapped-types (for PartialType) if not present:

npm i @nestjs/mapped-types

7) Service (Business Logic)

// src/todo/todo.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindOptionsWhere, ILike } from 'typeorm';
import { Todo } from './todo.entity';
import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';

@Injectable()
export class TodoService {
  constructor(
    @InjectRepository(Todo)
    private readonly repo: Repository<Todo>,
  ) {}

  async create(dto: CreateTodoDto): Promise<Todo> {
    const todo = this.repo.create(dto);
    return this.repo.save(todo);
  }

  async findAll(q?: string, page = 1, limit = 10): Promise<{ data: Todo[]; total: number; page: number; limit: number }> {
    const where: FindOptionsWhere<Todo>[] = q
      ? [{ title: ILike(`%${q}%`) }, { description: ILike(`%${q}%`) }]
      : [{}];

    const [data, total] = await this.repo.findAndCount({
      where,
      order: { createdAt: 'DESC' },
      skip: (page - 1) * limit,
      take: limit,
    });

    return { data, total, page, limit };
  }

  async findOne(id: string): Promise<Todo> {
    const todo = await this.repo.findOne({ where: { id } });
    if (!todo) throw new NotFoundException('Todo not found');
    return todo;
  }

  async update(id: string, dto: UpdateTodoDto): Promise<Todo> {
    const todo = await this.findOne(id);
    Object.assign(todo, dto);
    return this.repo.save(todo);
  }

  async remove(id: string): Promise<void> {
    const todo = await this.findOne(id);
    await this.repo.remove(todo);
  }
}

8) Controller (Routes)

// src/todo/todo.controller.ts
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
import { TodoService } from './todo.service';
import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';

@Controller('todos')
export class TodoController {
  constructor(private readonly service: TodoService) {}

  @Post()
  create(@Body() dto: CreateTodoDto) {
    return this.service.create(dto);
  }

  @Get()
  findAll(
    @Query('q') q?: string,
    @Query('page') page = '1',
    @Query('limit') limit = '10',
  ) {
    return this.service.findAll(q, Number(page), Number(limit));
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.service.findOne(id);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() dto: UpdateTodoDto) {
    return this.service.update(id, dto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.service.remove(id);
  }
}

9) Start the Server

npm run start:dev
# API runs at http://localhost:3000

10) Test the Endpoints (cURL)

Create

curl -X POST http://localhost:3000/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"Learn NestJS","description":"Build a CRUD API","completed":false}'

List (with pagination + search)

curl "http://localhost:3000/todos?page=1&limit=5&q=nest"

Get by ID

curl http://localhost:3000/todos/<UUID>

Update

curl -X PATCH http://localhost:3000/todos/<UUID> \
  -H "Content-Type: application/json" \
  -d '{"completed": true}'

Delete

curl -X DELETE http://localhost:3000/todos/<UUID>

Production Tips

  • Turn off synchronize and use migrations for schema changes.
  • Add a global exception filter and logging middleware.
  • Secure env vars with a proper config and secrets management.
  • Dockerize Postgres + API for consistent environments.

Wrap-Up

You now have a fully working NestJS + PostgreSQL REST API with TypeORM, DTO validation, pagination, and search. This structure scales well for real projects—just add modules for new domains and keep business logic inside services.


Optional: Package Scripts

// package.json (partial)
{
  "scripts": {
    "start": "nest start",
    "start:dev": "nest start --watch",
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\"",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix"
  }
}

Comments

8 responses to “NestJS + PostgreSQL: Create Your First REST API”

  1. g888win Avatar

    I’ve been seeing a lot about G888win and its popularity is well deserved. The interface is easy to use and I’ve had a great time with their available games, check it out! g888win

  2. hiperbbet Avatar

    Hiperbbet, heard whispers, decided to try. Not bad, not bad at all! Odds seem fair and the interface is clean. Seems legit. You know, maybe give hiperbbet a whirl when you’re feeling lucky!

  3. gamece88 Avatar

    Gamece88, heard they got some new stuff in. Hopped on and found a couple of hidden gems. Worth a look if you’re bored of your usual haunts. Check out gamece88 – you might find your next favorite game.

  4. slotvip ph Avatar

    Great breakdown! The async TypeORM configuration is exactly what production apps need. Reliability matters—much like how slotvip ph legit services maintain trust through solid infrastructure. This approach will definitely help others build scalable APIs.

  5. luck91 Avatar

    Hey dudes, check out luck91! I gave it a look and felt lucky. Give luck91 a try luck91!

  6. richphl Avatar

    Saw some ads for richphl so I checked it out. Not bad, I got to say! Look for richphl richphl!

  7. uuddbetcom Avatar

    Uuddbetcom? Seems alright. I didn’t have any issues, so go ahead and check Uuddbetcom uuddbetcom!

  8. jljl11 Avatar

    Excellent walkthrough! TypeORM’s entity decorators with NestJS modules make for such clean architecture. In high-traffic platforms like the jljl11 app, this PostgreSQL setup pattern really shines for handling concurrent user sessions efficiently.

Leave a Reply

Your email address will not be published. Required fields are marked *