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

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

  1. voslot Avatar

    It’s fascinating how easily accessible online gaming has become, but security is key! Seeing platforms like voslot app casino emphasize KYC & secure registration is reassuring-responsible gaming starts with verification. It’s a smart move for players too!

  2. 77win Avatar

    77win, heard good things. Giving it a shot tonight! Trying out some of the live casino games. Wish me luck, fellas! Let me know if you have any tips!. 77win

  3. jljl55 Avatar

    Dice games are fascinating – the math behind them is surprisingly complex! Seeing platforms like jljl55 play cater to local preferences with easy GCash deposits is smart. It’s all about accessibility & fun, right? Great to see Filipino gaming spirit embraced!

  4. ph888one Avatar

    Interesting read! Seeing trends in gaming, especially with localized platforms like ph888one slot download, is fascinating. Easy access & secure verification (like they mention) seem key for Filipino players. Hoping for responsible gaming practices too!

  5. phl789 Avatar

    That analysis was spot on! Seeing platforms like phl789 games really elevate the esports betting scene in the Philippines with quick deposits (GCash is key!). Exciting times for local gamers & bettors! 🔥

  6. 7m.cn ma cao Avatar

    Heads up, everyone! If you’re hunting for quick updates on Macau games, 7m.cn ma cao is where it’s at. Pretty solid source, in my opinion. Click here to jump in 7m.cn ma cao.

  7. tải b29bet Avatar

    Alright, so ‘tải b29bet’, downloading it, is pretty fast. No annoying waiting. Good start!tải b29bet

  8. 7m.cn.ma cao bóng đá Avatar

    For all my soccer fanatics out there, trying to find some 7m.cn.Ma Cao bóng đá updates? Hopefuly you’ll find what you are looking for here 7m.cn.ma cao bóng đá. Maybe we can all celebrate our winnings later.

  9. 678winapp Avatar

    678winapp? The app’s pretty sleek, runs smoothly on my phone. Selection’s a bit limited, but they seem to be adding new games often. Keep an eye on it: 678winapp

  10. gameking98 Avatar

    Gameking98… Simple and straightforward. Not the fanciest site, but it gets the job done. Good for casual play, I reckon. Give it a go: gameking98

  11. mx711club Avatar

    Alright alright, mx711club is pretty solid. Decent selection of games and easy to get around. Quick signup too! Might be my new go-to. Check it out here: mx711club

  12. 82betlogin Avatar

    82betlogin’s website is easy to access and use to get into the 82bet games. The registration process was quick and simple. Give it a go: 82betlogin.

  13. 8800betdownload Avatar

    The 8800betdownload process was pretty straightforward. Had the app up and running in no time. Definitely makes betting easier. Check it out for yourself: 8800betdownload.

  14. 5sbetvip Avatar

    Thinking of checking out 5sbetvip. Heard they have some nice VIP perks. Anyone have any experience with them? Let me know! In the meantime: 5sbetvip.

Leave a Reply

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