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
id
field is the primary key and is automatically generated using the@PrimaryGeneratedColumn()
decorator. - The
title
field is a string with a maximum length of 50 characters, as specified by the@Column({ length: 50 })
decorator. - The
is_completed
field is a boolean with a default value offalse
, indicating whether the task is completed or not. - The
created_at
field is automatically set to the current timestamp when the record is created, thanks to the@CreateDateColumn()
decorator. - Similarly, the
updated_at
field 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, aNotFoundException
is 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 thegetTodoById
utility method and returns the found Todo. - The
update(id: number, updateTodoDto: UpdateTodoDto)
method updates an existing Todo by its ID, ensuring it exists with thegetTodoById
method 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 thegetTodoById
method. 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
TodoService
to 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 thecreate
method of theTodoService
and returns the newly created Todo along with the associated HATEOAS links. - The
@Get()
method retrieves all Todo items by invoking thefindAll
method of theTodoService
. Each Todo is returned with its respective HATEOAS links. - The
@Get(':id')
method fetches a single Todo by its ID. It uses thefindOne
method of theTodoService
and returns the Todo with the associated HATEOAS links. - The
@Put(':id')
method updates a Todo by its ID. It calls theupdate
method of theTodoService
to 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 theremove
method 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