Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

CRUD for posts end to end

CRUD is the four data actions: Create, Read, Update and Delete. Everything they need has been covered already: DTOs, validation, pipes, exceptions and the repository. Here those parts come together into a finished posts API.

The service

The whole src/posts/posts.service.ts file:

// src/posts/posts.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { Post } from './entities/post.entity';
 
const PAGE_SIZE = 10;
 
@Injectable()
export class PostsService {
  constructor(
    @InjectRepository(Post)
    private readonly postsRepository: Repository<Post>,
  ) {}
 
  findAll(page: number): Promise<Post[]> {
    return this.postsRepository.find({
      order: { createdAt: 'DESC' },
      skip: (page - 1) * PAGE_SIZE,
      take: PAGE_SIZE,
    });
  }
 
  async findOne(id: number): Promise<Post> {
    const post = await this.postsRepository.findOne({
      where: { id },
      relations: { comments: true },
    });
    if (!post) {
      throw new NotFoundException(`Post ${id} not found`);
    }
    return post;
  }
 
  create(dto: CreatePostDto): Promise<Post> {
    return this.postsRepository.save(this.postsRepository.create(dto));
  }
 
  async update(id: number, dto: UpdatePostDto): Promise<Post> {
    const post = await this.findOne(id);
    this.postsRepository.merge(post, dto);
    return this.postsRepository.save(post);
  }
 
  async remove(id: number): Promise<void> {
    const post = await this.findOne(id);
    await this.postsRepository.remove(post);
  }
}

skip and take skip and take the right number of rows — that is how pagination works. update() and remove() call findOne() first, so both answer 404 for a missing post. merge() copies only the submitted fields into the object.

The controller

// src/posts/posts.controller.ts
import {
  Body, Controller, DefaultValuePipe, Delete, Get, HttpCode,
  Param, ParseIntPipe, Patch, Post, Query,
} from '@nestjs/common';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { PostsService } from './posts.service';
 
@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}
 
  @Get()
  findAll(@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number) {
    return this.postsService.findAll(page);
  }
 
  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.postsService.findOne(id);
  }
 
  @Post()
  create(@Body() dto: CreatePostDto) {
    return this.postsService.create(dto);
  }
 
  @Patch(':id')
  update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdatePostDto) {
    return this.postsService.update(id, dto);
  }
 
  @Delete(':id')
  @HttpCode(204)
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.postsService.remove(id);
  }
}

The controller only takes data from the request and passes it to the service. All database work happens in the service.

Checking

RequestBodyResponse
GET /posts?page=2—200, the second ten posts
GET /posts/1—200, the post with comments
POST /poststitle, content201, the new post with an id
PATCH /posts/1any of the fields200, the updated post
DELETE /posts/1—204, an empty response
GET /posts/999—404
curl -X PATCH http://localhost:3000/posts/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"Updated title"}'

The server returns the post with the new title, and content stays the same.

Common mistake

People update with repository.update(id, dto) and return its result. But update() does not check that the row exists and returns a technical UpdateResult object rather than the post. The client gets 200 even for a non-existent id. Load the post with findOne(), change it and save it with save() — then the 404 works and the response contains the post.

Thanks for reading!

Donate to the site — we work hard for you.