Skip to content

NestJS CRUD API

This lesson builds a complete CRUD (Create, Read, Update, Delete) feature end-to-end, wiring a controller to a service, using DTOs for input, and returning appropriate status codes for each operation.

The Complete CRUD Controller

A CRUD resource needs five operations: list, read one, create, update, and delete. In NestJS, the controller declares the routes and delegates each operation to the service, keeping the controller focused purely on request/response shape.

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Get()
  findAll() {
    return this.catsService.findAll();
  }

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.catsService.findOne(id);
  }

  @Post()
  create(@Body() dto: CreateCatDto) {
    return this.catsService.create(dto);
  }

  @Patch(':id')
  update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateCatDto) {
    return this.catsService.update(id, dto);
  }

  @Delete(':id')
  @HttpCode(204)
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.catsService.remove(id);
  }
}

Every method here follows the same shape: extract input with decorators, delegate to the service, return the result. The service owns all actual persistence logic.

The Matching Service

@Injectable()
export class CatsService {
  private cats: Cat[] = [];
  private nextId = 1;

  findAll() { return this.cats; }
  findOne(id: number) { /* find or throw NotFoundException */ }
  create(dto: CreateCatDto) { /* push new cat, return it */ }
  update(id: number, dto: UpdateCatDto) { /* find, merge, return */ }
  remove(id: number) { /* find, splice */ }
}
  • The service owns the data store (here, an in-memory array; in a real app, a database via a repository).
  • findOne should throw NotFoundException when the id doesn't exist, rather than returning undefined.
  • create typically returns the newly created resource, including its generated id.
  • remove commonly returns 204 No Content with an empty body.

CRUD Endpoint Cheatsheet

The five standard operations and their expected outcomes.

Operation Route Success Status Typical Failure
List GET /cats 200
Read one GET /cats/:id 200 404 if missing
Create POST /cats 201 400 on invalid body
Update PATCH /cats/:id 200 404 if missing
Delete DELETE /cats/:id 204 404 if missing

Handling Missing Resources Consistently

Every read, update, and delete operation on a single resource needs the same "not found" handling. Centralizing this in a small private helper inside the service keeps the behavior consistent and avoids repeating the same check five times.

private findOrThrow(id: number): Cat {
  const cat = this.cats.find((c) => c.id === id);
  if (!cat) {
    throw new NotFoundException(`Cat #${id} not found`);
  }
  return cat;
}

Moving From In-Memory to a Real Database

The in-memory array in this lesson is only for illustrating the CRUD flow clearly. In a real application, the same service methods would call into a repository backed by TypeORM, Prisma, or Mongoose, without changing the controller at all, since the controller only depends on the service's public method signatures.

Common Mistakes

  • Returning undefined or null for a missing resource instead of throwing NotFoundException.
  • Reusing the same DTO for create and update without a PartialType() wrapper.
  • Forgetting @HttpCode(204) on delete, leaving Nest's default 200 status instead.
  • Putting the in-memory array directly inside the controller instead of the service.

Key Takeaways

  • A complete CRUD feature needs list, read-one, create, update, and delete operations.
  • The controller stays thin, delegating persistence and business rules to the service.
  • Consistent "not found" handling avoids repeating the same check in every method.
  • The same controller works unchanged whether the service uses an array or a real database.

Pro Tip

Write the findOrThrow-style helper for "not found" handling on day one of any CRUD service, even before adding a real database, retrofitting consistent error handling across five already-written methods is far more tedious than building it in from the start.