SkillShare Logo
STEP 5 OF 7 · FEATURES

Product CRUD

The heart of the app: Create, Read, Update, and Delete products, plus a stock dashboard. Every route sits behind the auth gate you just built.

01 — THE API

The Five Endpoints

A standard REST resource (the standard way to expose create/read/update/delete over HTTP). All of these require a valid session cookie (the requireAuth middleware / JwtAuthGuard from the last page).

REST reference
POST   /api/products        # create a product
GET    /api/products        # list all (the stock view)
GET    /api/products/:id    # read one
PUT    /api/products/:id    # update details / restock
DELETE /api/products/:id    # remove a product
02 — VALIDATION

Validate the Input

One Zod schema guards create and update so bad data never reaches the database.

server/src/schemas.ts
export const ProductSchema = z.object({
  name: z.string().min(1),
  sku: z.string().min(1),
  quantity: z.number().int().min(0),
  minStock: z.number().int().min(0),
});
// ProductSchema.partial() → for PUT, where any field is optional
03 — BACKEND

CRUD Handlers

TypeORM repository calls shown; Mongoose equivalents are in the comments. The checkLowStock() helper is built on the next page.

server/src/products.routes.ts
import { Router } from "express";
import { ProductSchema } from "./schemas";
import { checkLowStock } from "./low-stock";

const router = Router();
const repo = AppDataSource.getRepository(Product);

// CREATE
router.post("/", async (req, res) => {
  const parsed = ProductSchema.safeParse(req.body);
  if (!parsed.success) return res.status(400).json(parsed.error.flatten());
  const product = await repo.save(repo.create(parsed.data));
  // Mongoose: await ProductModel.create(parsed.data)
  await checkLowStock(product);
  res.status(201).json(product);
});

// READ all (stock view)
router.get("/", async (_req, res) => {
  res.json(await repo.find());           // ProductModel.find()
});

// READ one
router.get("/:id", async (req, res) => {
  const p = await repo.findOneBy({ id: req.params.id }); // findById
  if (!p) return res.status(404).json({ error: "Not found" });
  res.json(p);
});

// UPDATE / restock
router.put("/:id", async (req, res) => {
  const data = ProductSchema.partial().parse(req.body);
  await repo.update(req.params.id, data); // findByIdAndUpdate
  const product = await repo.findOneBy({ id: req.params.id });
  if (!product) return res.status(404).json({ error: "Not found" });
  await checkLowStock(product);   // ← triggers the alert email
  res.json(product);
});

// DELETE
router.delete("/:id", async (req, res) => {
  await repo.delete(req.params.id);   // findByIdAndDelete
  res.status(204).end();
});

export default router;
// mount it: app.use("/api/products", requireAuth, router);
04 — FRONTEND

The Stock Dashboard

Fetch the list, show it in a table, and add a form to create products. Remember credentials: "include" on every request so the cookie rides along.

web/app/products/page.tsx ("use client")
"use client";
import { useEffect, useState } from "react";

const API = "http://localhost:4000/api";
const get = (p: string) => fetch(API + p, { credentials: "include" }).then(r => r.json());

export default function Products() {
  const [items, setItems] = useState<any[]>([]);
  useEffect(() => { get("/products").then(setItems); }, []);

  return (
    <table className="w-full text-left">
      <thead><tr><th>Name</th><th>SKU</th><th>Qty</th><th>Min</th></tr></thead>
      <tbody>
        {items.map(p => (
          <tr key={p.id} className={p.quantity <= p.minStock ? "text-red-500" : ""}>
            <td>{p.name}</td><td>{p.sku}</td><td>{p.quantity}</td><td>{p.minStock}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
add a product
async function addProduct(body: object) {
  await fetch(API + "/products", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    credentials: "include",
    body: JSON.stringify(body), // { name, sku, quantity, minStock }
  });
}
  • Create returns 201 and the product appears in the list
  • Update changes the values; restocking reflects immediately
  • Delete removes the row (204 No Content)
  • Low-stock rows render in red when quantity ≤ minStock