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"
}
}
Leave a Reply