Introduction: Why CRUD Matters for Books and Users

Welcome back! In the previous lesson, you learned how to create new resources in your API using DTOs and validation. Now, we will complete the core features for managing both books and users by implementing all CRUD operations — Create, Read, Update, and Delete. These operations are the foundation of almost every web application, allowing you to add, view, change, and remove data as needed.

CRUD stands for:

  • Create – Add new data (via POST)
  • Read – Retrieve existing data (GET one or all)
  • Update – Modify data (PATCH or PUT)
  • Delete – Remove data (DELETE)

These actions mirror how real apps behave — whether you're building a task manager, social platform, or e-commerce dashboard. In NestJS, each of these actions maps to a method in a controller, which delegates logic to a service, ensuring separation of concerns.

In this lesson, you will see how these operations work together in the Reading Tracker API. By the end, you will understand how to manage both books and users through the API, setting you up for hands-on practice and real-world projects.

Quick Recap: Project Structure and Setup

Before we dive in, let’s quickly remind ourselves how the project is organized. You already have a NestJS app with modules for books and users, each with its own controller, service, and DTOs. The data is stored in a simple in-memory database service.

Here’s a quick summary of the setup:

// Example: Project structure and data access
src/
  books/
    books.controller.ts   // Handles HTTP requests for books
    books.service.ts      // Business logic for books
    dto/
      create-book.dto.ts
      update-book.dto.ts // will be implemented in this unit
  users/
    users.controller.ts   // Handles HTTP requests for users
    users.service.ts      // Business logic for users
    dto/
      create-user.dto.ts
      update-user.dto.ts // will be implemented in this unit
  database/
    database.service.ts   // Provides access to in-memory data

This structure helps keep your code organized and makes it easy to add new features.

Controllers: Handling Requests for Books and Users

Controllers are responsible for handling incoming HTTP requests and sending responses. Each controller method matches a specific API endpoint and HTTP method.

Let’s look at some examples from the BooksController and UsersController:

// src/books/books.controller.ts
import { Controller, Get, Post, Body, Patch, Param, Delete, ParseIntPipe } from '@nestjs/common';
import { BooksService } from './books.service';
import { CreateBookDto } from './dto/create-book.dto';
import { UpdateBookDto } from './dto/update-book.dto';

@Controller('books')
export class BooksController {
  constructor(private readonly booksService: BooksService) {}

  @Post()
  create(@Body() createBookDto: CreateBookDto) {
    const book = this.booksService.create(createBookDto);
    return { success: true, data: book };
  }

  @Get()
  findAll() {
    const books = this.booksService.findAll();
    return { success: true, data: books };
  }

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    const book = this.booksService.findOne(id);
    return { success: true, data: book };
  }

  @Patch(':id')
  update(@Param('id', ParseIntPipe) id: number, @Body() updateBookDto: UpdateBookDto) {
    const updated = this.booksService.update(id, updateBookDto);
    return { success: true, data: updated };
  }

  @Delete(':id')
  remove(@Param('id', ParseIntPipe) id: number) {
    const removed = this.booksService.remove(id);
    return { success: true, data: removed };
  }
}

Each method does the following:

  • @Post() handles creating a new book.
  • @Get() returns all books.
  • @Patch(':id') updates a book by its ID.
  • @Delete(':id') removes a book by its ID.

The @Patch(':id') route is used for partial updates. For example, you might only want to change the book's title without updating the author.

  • @Param('id', ParseIntPipe) extracts and converts the id from the URL.
  • @Body() maps the request body into a typed DTO and applies validation.
  • The controller passes this sanitized input to the service layer.

The same pattern is used in the UsersController for managing users.

Explanation:
When a request comes in (for example, a POST to /books), the controller method receives the request data, calls the appropriate service method, and returns the result. The @Param and @Body decorators help extract data from the request.

Services: The Logic Behind CRUD Operations

Services contain the business logic for your application. They interact with the data and handle all the actual work behind each operation.

Here’s a look at some key methods from the BooksService:

// src/books/books.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { DatabaseService } from '../database/database.service';
import { CreateBookDto } from './dto/create-book.dto';
import { UpdateBookDto } from './dto/update-book.dto';
import { v4 as uuidv4 } from 'uuid';

@Injectable()
export class BooksService {
  constructor(private readonly db: DatabaseService) {}

  /**
   * Create and store a new book.
   */
  create(createBookDto: CreateBookDto) {
    const newBook = {
      id: uuidv4(), 
      ...createBookDto,
    };
    this.db.getBooks().push(newBook);
    return newBook;
  }

  /**
   * Get all books.
   */
  findAll() {
    return this.db.getBooks();
  }

  /**
   * Get a single book by ID.
   * @throws NotFoundException if book is not found.
   */
  findOne(id: string) {
    const book = this.db.findBookById(id);
    if (!book) {
      throw new NotFoundException(`Book with ID ${id} not found.`);
    }
    return book;
  }

  /**
   * Update an existing book's information.
   */
  update(id: string, updateBookDto: UpdateBookDto) {
    const book = this.findOne(id);
    Object.assign(book, updateBookDto);
    return book;
  }

  /**
   * Remove a book by ID.
   */
  remove(id: string) {
    const books = this.db.getBooks();
    const index = books.findIndex((b) => b.id === id);
    if (index === -1) {
      throw new NotFoundException(`Book with ID ${id} not found.`);
    }
    const [removedBook] = books.splice(index, 1);
    return removedBook;
  }
}

Explanation:

  • create adds a new book to the list.
  • findAll returns all books.
  • update finds a book by ID and updates its properties. Object.assign(book, updateBookDto); merges the new data into the existing book object. If the user only sends { "title": "Updated Title" }, only the title is changed. The author field stays untouched. This mirrors the behavior of HTTP PATCH — partial update without replacing the full object.
  • remove deletes a book by ID or throws an error if not found.

The UsersService works the same way for user data.

Output Example:
If you send a POST request to /books with { "title": "1984", "author": "George Orwell" }, you might get back:

{
  "success": true,
  "data": {
    "id": "e3a32cd5-9e6b-4b2c-a36c-0a5f4c0f4c2b",
    "title": "1984",
    "author": "George Orwell"
  }
}
DTOs: Updating and Validating Data

DTOs (Data Transfer Objects) help you control and validate the data that comes into your API. You have already used them for creating new resources. For updates, you can use a special helper called PartialType to make all fields optional.

Here’s how the update DTO is defined:

// src/books/dto/update-book.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateBookDto } from './create-book.dto';

export class UpdateBookDto extends PartialType(CreateBookDto) {}

Explanation:

  • PartialType(CreateBookDto) creates a new DTO where all fields from CreateBookDto are optional.
  • This is useful for PATCH requests, where you might only want to update one or two fields.

Let’s say the original CreateBookDto required both title and author. But when you update a book, you might only want to change the author. With PartialType, all fields become optional and still inherit the original validation rules if present.

The same pattern is used for updating users.

Let’s walk through what happens when a user updates a book:

  • Frontend sends a PATCH request to /books/1 with a JSON body:
{
  "title": "New Title"
}
  • BooksController:
    • Extracts the id using @Param('id')
    • Applies validation to the request body using UpdateBookDto
    • Calls booksService.update(1, dto)
  • BooksService:
    • Calls findOne(1) to retrieve the book or throw NotFoundException
    • Uses Object.assign() to update the fields
    • Returns the updated book
  1. Client sees:
{
  "success": true,
  "data": {
    "id": "e3a32cd5-9e6b-4b2c-a36c-0a5f4c0f4c2b",
    "title": "New Title",
    "author": "George Orwell"
  }
}

This clean separation of responsibilities makes the API predictable and easy to scale.

Summary And What’s Next

In this lesson, you learned how to complete the core CRUD operations for both books and users in your Reading Tracker API. You saw how controllers handle requests, services perform the logic, and DTOs help with data validation and updates.

You have now reached the end of this course! Well done for making it through all the lessons. You are ready to put your knowledge into practice with the exercises that follow. Keep experimenting and building — these skills are the foundation for many real-world applications. Congratulations on your progress!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal