NestJs CRUD Operations Example
This article provides a comprehensive example of implementing CRUD operations using NestJS, a powerful Node.js framework.
1. Introduction
NestJS is a progressive Node.js framework for building efficient, reliable, scalable server-side applications. Built with TypeScript, it takes advantage of the strong typing and modern features of the language, making it an excellent choice for developers who want a structured, organized codebase.
Inspired by Angular’s design principles, NestJS leverages decorators, dependency injection, and modules to create a cohesive, easily maintainable architecture. This modular design allows developers to break their applications into smaller, reusable components, making it easier to manage complex projects.
One of the key strengths of NestJS is its ability to integrate seamlessly with a variety of libraries and tools. It supports ORMs like TypeORM, Sequelize, and Prisma for database interaction, and provides built-in support for WebSockets, GraphQL, and microservices. Whether you’re building a RESTful API, real-time application, or distributed system, NestJS provides the tools and flexibility you need.
Additionally, NestJS emphasizes developer productivity by offering a robust CLI (Command Line Interface) for scaffolding projects, generating modules, and automating repetitive tasks. It also promotes best practices through its built-in testing utilities and adherence to the SOLID principles of object-oriented programming.
With its growing community and extensive documentation, NestJS is a powerful and versatile framework that caters to both beginners and experienced developers. It empowers teams to build high-quality, maintainable applications
while reducing development time and effort.
2. Setting up PostgreSQL on Docker and adding data
To run a PostgreSQL on Docker, we will use the docker-compose file to configure and start a postgresql container.
version: '3.8'
services:
postgres:
image: 'postgres:15'
container_name: postgres_container
restart: always
ports:
- '5432:5432'
environment:
POSTGRES_USER: your_username
POSTGRES_PASSWORD: your_password
POSTGRES_DB: mydatabase
volumes:
- 'postgres_data:/var/lib/postgresql/data'
volumes:
postgres_data:
Save the docker-compose.yml file and run the following command in the same directory.
docker-compose up -d
After the Docker container is up and running, connect to the database and execute the SQL script below to create and populate the todos table in the mydatabase. This table will be used for CRUD operations with the NestJS application endpoints.
/*
Step 1: Create the 'todos' table
- `id`: Primary key, auto-incremented.
- `title`: Task title, required (max length 50 characters).
- `is_completed`: Indicates if the task is completed, defaults to false.
- `created_at` and `updated_at`: Track when the record is created and last updated.
*/
CREATE TABLE todos (
id SERIAL PRIMARY KEY,
title VARCHAR(50) NOT NULL,
is_completed BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
/*
Step 2: Define a function to automatically update the 'updated_at' column
- This function sets 'updated_at' to the current timestamp whenever the row is updated.
*/
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$ LANGUAGE plpgsql;
/*
Step 3: Create a trigger to invoke the function before any row in the 'todos' table is updated
- Ensures the 'updated_at' column is kept current.
*/
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON todos
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
/*
Step 4: Insert initial mock data into the 'todos' table
- Adds two example tasks for testing and development.
*/
INSERT INTO todos (title) VALUES ('First Task');
INSERT INTO todos (title) VALUES ('Second Task');
3. Creating the application
Set up the Next.js project and install the necessary dependencies.
npm install @nestjs/typeorm typeorm pg @nestjs/config class-validator class-transformer
3.1 Setting up the Database
After setting up the application skeleton, we’ll begin by creating an external database configuration. Start by creating a .env file.
# Do not expose your Neon credentials to the browser DATABASE_URL='your_database_endpoint'
3.2 Updating App module
Update the app module (app.module.ts) to integrate the ConfigModule and configure TypeORM.
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TodoModule } from './todo/todo.module';
@Module({
imports: [
ConfigModule.forRoot(),
TypeOrmModule.forRoot({
type: 'postgres',
url: process.env.DATABASE_URL,
synchronize: false, // Set to false in production
entities: [__dirname + '/**/*.entity{.ts,.js}'],
logging: false,
}),
TodoModule,
],
controllers: [],
providers: [],
})
export class AppModule {}
3.3 Create the ToDo Module and Entity
Create a module, controller, and service for the ToDo functionality.
# Generate a new module for ToDo functionality nest generate module todo # Generate a controller for the ToDo module, excluding the spec file nest generate controller todo --no-spec # Generate a service for the ToDo module, excluding the spec file nest generate service todo --no-spec
3.3.1 Define the Todo Entity
Create the ToDo entity in the src/todo/todo.entity.ts file.
// src/todo/entities/todo.entity.ts
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('todos')
export class Todo {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 50 })
title: string;
@Column({ default: false })
is_completed: boolean;
@CreateDateColumn()
created_at: Date;
@UpdateDateColumn()
updated_at: Date;
}
The code defines a “Todo” entity using TypeORM. The @Entity('todos') decorator specifies that the entity corresponds to the “todos” table in the database.
- The
idfield is the primary key and is automatically generated using the@PrimaryGeneratedColumn()decorator. - The
titlefield is a string with a maximum length of 50 characters, as specified by the@Column({ length: 50 })decorator. - The
is_completedfield is a boolean with a default value offalse, indicating whether the task is completed or not. - The
created_atfield is automatically set to the current timestamp when the record is created, thanks to the@CreateDateColumn()decorator. - Similarly, the
updated_atfield is automatically updated with the current timestamp whenever the record is modified, as defined by the@UpdateDateColumn()decorator.
3.3.2 Register the ToDo Entity
Add the entity to the TodoModule for registration.
// src/todo/todo.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Todo } from './entities/todo.entity';
import { TodoController } from './todo.controller';
import { TodoService } from './todo.service';
@Module({
imports: [TypeOrmModule.forFeature([Todo])],
providers: [TodoService],
controllers: [TodoController],
})
export class TodoModule {}
3.4 Implement CRUD operations
Please note that the DTO classes used in the service and controller layers have been omitted for brevity.
3.4.1 Create a Service class
Create a ToDo service in the src/todo/todo.service.ts file.
// src/todo/todo.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';
import { Todo } from './entities/todo.entity';
@Injectable()
export class TodoService {
constructor(
@InjectRepository(Todo) private readonly todoRepository: Repository<Todo>,
) {}
// Utility method to find a Todo by ID or throw an exception
private async getTodoById(id: number): Promise<Todo> {
const todo = await this.todoRepository.findOneBy({ id });
if (!todo) throw new NotFoundException(`Todo with ID ${id} not found`);
return todo;
}
// Create a new Todo
async create(createTodoDto: CreateTodoDto): Promise<Todo> {
console.log('Creating new Todo with data:', createTodoDto);
const newTodo = this.todoRepository.create(createTodoDto);
const savedTodo = await this.todoRepository.save(newTodo);
console.log('New Todo created:', savedTodo);
return savedTodo;
}
// Get all Todos
async findAll(): Promise<Todo[]> {
console.log('Fetching all Todos...');
const todos = await this.todoRepository.find();
console.log('Found Todos:', todos);
return todos;
}
// Get a Todo by ID
async findOne(id: number): Promise<Todo> {
console.log(`Fetching Todo with ID: ${id}`);
const todo = await this.getTodoById(id);
console.log('Found Todo:', todo);
return todo;
}
// Update a Todo
async update(id: number, updateTodoDto: UpdateTodoDto): Promise<Todo> {
console.log(`Updating Todo with ID: ${id} with data:`, updateTodoDto);
await this.getTodoById(id); // Ensures the Todo exists
await this.todoRepository.update(id, updateTodoDto);
const updatedTodo = await this.getTodoById(id);
console.log('Updated Todo:', updatedTodo);
return updatedTodo;
}
// Delete a Todo
async remove(id: number): Promise<void> {
console.log(`Deleting Todo with ID: ${id}`);
await this.getTodoById(id); // Ensures the Todo exists
await this.todoRepository.delete(id);
console.log(`Todo with ID: ${id} deleted.`);
}
}
The code defines a TodoService class in NestJS, marked with the @Injectable() decorator, which allows it to be injected into other parts of the application. This service interacts with the Todo entity through the todoRepository to perform CRUD operations.
- The constructor of the service uses the
@InjectRepository(Todo)decorator to inject thetodoRepository, which is an instance ofRepository<Todo>responsible for interacting with the database. - A utility method
getTodoById(id: number)is defined to fetch a Todo by its ID. If no Todo is found, aNotFoundExceptionis thrown, indicating the Todo with the provided ID does not exist. - The
create(createTodoDto: CreateTodoDto)method creates a new Todo using the provided data, saves it to the database, and returns the newly created Todo. The method logs the creation process for debugging purposes. - The
findAll()method retrieves all Todo items from the database and logs the fetched Todos before returning them. - The
findOne(id: number)method retrieves a Todo by its ID using thegetTodoByIdutility method and returns the found Todo. - The
update(id: number, updateTodoDto: UpdateTodoDto)method updates an existing Todo by its ID, ensuring it exists with thegetTodoByIdmethod before proceeding. It then updates the Todo and returns the updated Todo, logging the process. - The
remove(id: number)method deletes a Todo by its ID, first checking that the Todo exists with thegetTodoByIdmethod. After deletion, it logs the removal process and confirms the deletion.
3.4.2 Create a Controller class
Create a ToDo controller in the src/todo/todo.controller.ts file.
// src/todo/todo.controller.ts
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
} from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';
import { TodoService } from './todo.service';
import { Todo } from './entities/todo.entity';
@Controller('todos')
export class TodoController {
constructor(private readonly todoService: TodoService) {}
// Utility method to generate HATEOAS links
private generateLinks(todo: Todo): any[] {
return [
{ rel: 'self', href: `/todos/${todo.id}` },
{ rel: 'update', href: `/todos/${todo.id}` },
{ rel: 'delete', href: `/todos/${todo.id}` },
{ rel: 'all', href: '/todos' },
];
}
// Create a new Todo
@Post()
async create(@Body() createTodoDto: CreateTodoDto): Promise<any> {
const todo = await this.todoService.create(createTodoDto);
return {
todo,
links: this.generateLinks(todo),
};
}
// Get all Todos
@Get()
async findAll(): Promise<any> {
const todos = await this.todoService.findAll();
return todos.map((todo) => ({
todo,
links: this.generateLinks(todo),
}));
}
// Get a single Todo
@Get(':id')
async findOne(@Param('id') id: number): Promise<any> {
const todo = await this.todoService.findOne(id);
return {
todo,
links: this.generateLinks(todo),
};
}
// Update a Todo
@Put(':id')
async update(
@Param('id') id: number,
@Body() updateTodoDto: UpdateTodoDto,
): Promise<any> {
const updatedTodo = await this.todoService.update(id, updateTodoDto);
return {
todo: updatedTodo,
links: this.generateLinks(updatedTodo),
};
}
// Delete a Todo
@Delete(':id')
async remove(@Param('id') id: number): Promise<any> {
await this.todoService.remove(id);
return {
message: `Todo with id ${id} has been deleted.`,
links: [{ rel: 'all', href: '/todos' }],
};
}
}
The given code defines a TodoController class in NestJS that handles CRUD operations for the “todos” resource. The controller is responsible for managing Todo items using methods that correspond to HTTP requests (POST, GET, PUT, DELETE).
- The controller constructor injects the
TodoServiceto perform the actual CRUD operations. A utility method,generateLinks(todo: Todo), generates HATEOAS (Hypermedia as the engine of application state) links for a given Todo item. These links allow clients to navigate related actions, such as self-reference, update, delete, and view all Todos. - The
@Post()method handles creating a new Todo item. It calls thecreatemethod of theTodoServiceand returns the newly created Todo along with the associated HATEOAS links. - The
@Get()method retrieves all Todo items by invoking thefindAllmethod of theTodoService. Each Todo is returned with its respective HATEOAS links. - The
@Get(':id')method fetches a single Todo by its ID. It uses thefindOnemethod of theTodoServiceand returns the Todo with the associated HATEOAS links. - The
@Put(':id')method updates a Todo by its ID. It calls theupdatemethod of theTodoServiceto modify the Todo’s properties and then return the updated Todo along with HATEOAS links. - The
@Delete(':id')method deletes a Todo by its ID using theremovemethod of theTodoService. After deletion, it returns a message indicating that the Todo has been deleted along with a link to view all Todos.
4. Run Locally and Test
Before running the application, ensure that all the necessary dependencies are installed and the environment variables are properly configured. Once everything is set up, you can start the development server by running the following command:
npm run start:dev
Test the endpoints using Postman or Curl.
POST /todos to create a new todo.
{
"title": "{{#string}}",
"is_completed": false
}
GET /todos to retrieve all todos.
GET /todos/:id to retrieve a single todo by ID.
PUT /todos/:id to update a todo.
{
"title": "Finish NestJS app (Updated)",
"is_completed": true
}
DELETE /todos/:id to delete a todo.
5. Conclusion
In conclusion, we have successfully implemented a simple Todo application using NestJS, integrating CRUD operations with PostgreSQL. With tools like Postman and Curl, you can now easily test the endpoints and enhance your application further. NestJS provides a robust framework for building scalable applications, and with PostgreSQL as the backend, your app is ready for production. Happy coding!
6. Download the code
You can download the full source code of this example here: NestJS CRUD Operations Example

