Product CRUD API

The Full Controller

An in-memory version of the ShopEase product API. In the medium level the array is replaced by a database.

src/products/products.controller.ts

import { Body, Controller, Delete, Get, HttpCode, Param, ParseIntPipe, Patch, Post, Query } from '@nestjs/common';
import { ProductsService } from './products.service';
import { CreateProductDto } from './dto/create-product.dto';
import { UpdateProductDto } from './dto/update-product.dto';

@Controller('products')
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}

  @Get()
  findAll(@Query('category') category?: string) {
    return this.productsService.findAll(category);
  }

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

  @Post()
  create(@Body() dto: CreateProductDto) {
    return this.productsService.create(dto);
  }

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

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

The Service

src/products/products.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateProductDto } from './dto/create-product.dto';
import { UpdateProductDto } from './dto/update-product.dto';

export interface Product { id: number; name: string; description?: string; price: number; stock: number; category: string; }

@Injectable()
export class ProductsService {
  private products: Product[] = [
    { id: 1, name: 'boAt Airdopes 141', price: 1299, stock: 120, category: 'audio' },
    { id: 2, name: 'Redmi Note 13', price: 17999, stock: 35, category: 'mobiles' },
  ];
  private nextId = 3;

  findAll(category?: string) {
    return category ? this.products.filter((p) => p.category === category) : this.products;
  }

  findOne(id: number) {
    const product = this.products.find((p) => p.id === id);
    if (!product) throw new NotFoundException(`Product ${id} not found`);
    return product;
  }

  create(dto: CreateProductDto) {
    const product = { id: this.nextId++, ...dto };
    this.products.push(product);
    return product;
  }

  update(id: number, dto: UpdateProductDto) {
    const product = this.findOne(id);
    Object.assign(product, dto);
    return product;
  }

  remove(id: number) {
    this.findOne(id);
    this.products = this.products.filter((p) => p.id !== id);
  }
}

Testing Every Endpoint

Example

curl http://localhost:3000/products
curl -X POST http://localhost:3000/products -H "Content-Type: application/json" \
  -d '{"name":"OnePlus Nord Buds 2","price":2299,"stock":60,"category":"audio"}'
curl -X PATCH http://localhost:3000/products/3 -H "Content-Type: application/json" -d '{"stock":55}'
curl "http://localhost:3000/products?category=audio"
curl -i -X DELETE http://localhost:3000/products/1
curl -i http://localhost:3000/products/1

Example output

[{"id":1,"name":"boAt Airdopes 141","price":1299,"stock":120,"category":"audio"},{"id":2,"name":"Redmi Note 13","price":17999,"stock":35,"category":"mobiles"}]
{"id":3,"name":"OnePlus Nord Buds 2","price":2299,"stock":60,"category":"audio"}
{"id":3,"name":"OnePlus Nord Buds 2","price":2299,"stock":55,"category":"audio"}
[{"id":1,"name":"boAt Airdopes 141","price":1299,"stock":120,"category":"audio"},{"id":3,"name":"OnePlus Nord Buds 2","price":2299,"stock":55,"category":"audio"}]
HTTP/1.1 204 No Content
HTTP/1.1 404 Not Found
{"message":"Product 1 not found","error":"Not Found","statusCode":404}

Exercise

Exercise: Add GET /products/:id/stock that returns { id, stock, inStock } where inStock is true when stock is greater than 0.

Show answer
@Get(':id/stock')
stock(@Param('id', ParseIntPipe) id: number) {
  const { stock } = this.productsService.findOne(id);
  return { id, stock, inStock: stock > 0 };
}