Fetching Product By ID
Products API: Fetching a Product by ID
Welcome back! 👋 In the last lesson, you implemented the products collection endpoint: GET /api/products for listing/searching with pagination, and POST /api/products for creating products with validation and conflict handling.
Now we’ll add the next building block: fetching one specific product. This is the endpoint a product details page depends on—when a user clicks a product card, the client needs a reliable way to load that single product record.
Previously…
Previously, you built the collection-level route in src/app/api/products/route.ts, keeping the route handler thin and delegating rules and database work to the service layer. That same structure continues here: the route handler focuses on HTTP concerns (like validating the path parameter), while the service layer focuses on business outcomes (like “product exists” vs “not found”).
The contract of GET /api/products/:id
When a client requests /api/products/:id, there are three meaningful outcomes:
-
The ID is not a valid UUID → return 400 The request is malformed, so we reject it immediately (and we don’t hit the database).
-
The ID is valid, but no product exists → return 404 The request is well-formed, but the resource doesn’t exist.
-
The ID is valid and the product exists → return 200 with the product The happy path.
This endpoint is simple on the surface, but it’s where you learn to be precise about the difference between invalid input and missing data.
Dynamic route handler: src/app/api/products/[id]/route.ts
This file exists under products/[id] because [id] is a dynamic route segment in Next.js. Next.js provides the id value through context.params, and in this project it’s typed as a Promise<{ id: string }>—so we await it inside the handler.
Reading the route param and validating the UUID:
This first portion of the handler pulls id from context.params and rejects malformed UUIDs with a 400.
const { id } = await context.params;is the official Next.js pattern this project uses for dynamic route params. It keeps param handling explicit and avoids URL string parsing.- We validate the param with
isUUID(id)fromsrc/lib/http/validation. This makes “bad IDs” a client error, not a missing resource. - Returning a
400early prevents wasted work and avoids confusing clients. If a client sends/api/products/not-a-uuid, that’s not “not found”—it’s “your request format is wrong.” - The route uses the shared
error(...)helper so even errors follow the same response envelope structure as successful responses.
