JavaScript

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 of false, 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 the todoRepository, which is an instance of Repository<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, a NotFoundException 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 the getTodoById 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 the getTodoById 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 the getTodoById 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 the create method of the TodoService and returns the newly created Todo along with the associated HATEOAS links.
  • The @Get() method retrieves all Todo items by invoking the findAll method of the TodoService. Each Todo is returned with its respective HATEOAS links.
  • The @Get(':id') method fetches a single Todo by its ID. It uses the findOne method of the TodoService and returns the Todo with the associated HATEOAS links.
  • The @Put(':id') method updates a Todo by its ID. It calls the update method of the TodoService 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 the remove method of the TodoService. 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

Download
You can download the full source code of this example here: NestJS CRUD Operations Example

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button