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:

  1. 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).

  2. The ID is valid, but no product exists → return 404 The request is well-formed, but the resource doesn’t exist.

  3. 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.

import { NextRequest } from 'next/server';
import { success, error } from '@/lib/http/response';
import { getProductService } from '@/lib/services/productsService';
import { isUUID } from '@/lib/http/validation';

export async function GET(
  _req: NextRequest,
  context: { params: Promise<{ id: string }> }
) {
  try {
    const { id } = await context.params;

    if (!isUUID(id)) {
      return error('VALIDATION_ERROR', 'Invalid id', undefined, 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) from src/lib/http/validation. This makes “bad IDs” a client error, not a missing resource.
  • Returning a 400 early 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.
Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal