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 /posts | title, content | 201, новая статья с 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 работает, и ответ содержит статью.