Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

CRUD для статей от начала до конца

CRUD — четыре действия с данными: создание (Create), чтение (Read), изменение (Update) и удаление (Delete). Всё нужное для них уже пройдено: DTO, валидация, пайпы, исключения и репозиторий. Здесь эти части собраны в готовый API статей.

Сервис

Файл src/posts/posts.service.ts целиком:

// 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(`Статья ${id} не найдена`);
    }
    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 и take пропускают и берут нужное число строк — так получается постраничный вывод. update() и remove() сначала вызывают findOne(), поэтому для несуществующей статьи оба ответят 404. merge() копирует в объект только присланные поля.

Контроллер

// 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);
  }
}

Контроллер только достаёт данные из запроса и передаёт их сервису. Вся работа с базой — в сервисе.

Проверка

ЗапросТелоОтвет
GET /posts?page=2—200, вторые десять статей
GET /posts/1—200, статья с комментариями
POST /poststitle, content201, новая статья с id
PATCH /posts/1любые из полей200, изменённая статья
DELETE /posts/1—204, пустой ответ
GET /posts/999—404
curl -X PATCH http://localhost:3000/posts/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"Обновлённый заголовок"}'

Сервер вернёт статью с новым заголовком, а content останется прежним.

Частая ошибка

Для изменения используют repository.update(id, dto) и возвращают его результат. Но update() не проверяет, существует ли строка, и возвращает служебный объект UpdateResult, а не статью. Клиент получит 200 даже для несуществующего id. Загружайте статью через findOne(), меняйте и сохраняйте save() — тогда и 404 работает, и ответ содержит статью.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.