DTO·Service·중복 검사·HTTP 상태를 하나의 업무 API로 통합합니다.
Controller는 입력과 HTTP를 처리하고 Service는 중복과 생성 규칙을 담당합니다. DTO 검증과 업무 충돌을 서로 다른 오류로 반환합니다.
실제 저장소에서는 unique constraint를 최종 방어선으로 두고 race condition의 충돌을 409로 변환합니다.
인증 사용자와 조직 범위를 Service와 repository query에 전달해 tenant 경계를 지킵니다.
먼저 SELECT로 중복 확인한 것만 믿으면 동시 요청 사이에 같은 값이 입력될 수 있습니다.
로컬 프로젝트에서 확인해보세요DELETE endpoint와 존재하지 않는 id의 404 처리를 추가하세요.
import "reflect-metadata";
import {
Body,
ConflictException,
Controller,
Get,
Injectable,
Module,
Post,
ValidationPipe,
} from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import { IsString, Length } from "class-validator";
class CreateCourseDto {
@IsString() @Length(2, 50) title!: string;
}
@Injectable()
class CourseService {
private items = [{ id: 1, title: "NestJS" }];
findAll() {
return this.items;
}
create(title: string) {
if (this.items.some((x) => x.title === title))
throw new ConflictException("같은 제목이 있습니다.");
const item = { id: this.items.length + 1, title };
this.items.push(item);
return item;
}
}
@Controller("courses")
class CourseController {
constructor(private readonly service: CourseService) {}
@Get() all() {
return this.service.findAll();
}
@Post() create(@Body() dto: CreateCourseDto) {
return this.service.create(dto.title);
}
}
@Controller()
class HomeController {
@Get() home() {
return `<main><h1>강좌 API</h1><a href="/courses">목록 조회</a><p>POST /courses { "title": "새 강좌" }</p></main>`;
}
}
@Module({ controllers: [HomeController, CourseController], providers: [CourseService] })
class AppModule {}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));
await app.listen(4173, "0.0.0.0");
}
bootstrap();