Building Products API

Products API: Listing, Search, Pagination, and Create

Welcome! 👋 In this lesson, we’ll turn the “products” backend into a real, usable API endpoint that a frontend (or your Playground UI) can rely on.

You’ll see how Next.js Route Handlers act as thin HTTP “controllers” that parse requests and delegate real work to the service layer. We’ll focus specifically on two behaviors: listing products (with optional search and pagination) and creating a product (with validation and conflict handling). By the end, you’ll be able to trace a request from the route handler → service → repository/database (and back) and understand why each layer has a clear job.

The Products collection endpoint

In REST terms, /api/products represents a collection resource:

  • A GET request asks: “Give me a list of products” (optionally filtered and paginated).
  • A POST request says: “Create a new product in this collection.”

In this codebase, the route handler is responsible for the HTTP mechanics (reading query params, parsing JSON, returning status codes), while business rules and defaults live in the service layer. That division keeps your API easier to extend later (for example, when you add /api/products/:id endpoints).

Route handler overview: src/app/api/products/route.ts

This file is the entry point for /api/products. It exports functions named after HTTP methods (GET, POST), which Next.js automatically wires up to incoming requests. This first chunk shows what the route handler depends on. You’ll notice it doesn’t import repositories or SQL—routes talk to services, not directly to the database.

import { NextRequest } from "next/server";

import { error, success } from "@/lib/http/response";
import {
  createProductService,
  listProducts,
  validateCreateProduct,
} from "@/lib/services/productsService";

// IMPORTANT: this file must exist in your repo at src/lib/db/errors.ts
import { isPostgresError, pgErrorToApiError } from "@/lib/db/errors";
  • NextRequest is the Next.js request type used in Route Handlers. It gives you the request URL, headers, and body helpers like req.json().
  • success() and error() come from src/lib/http/response and ensure every response follows the same envelope shape (so clients don’t need special-case parsing).
  • listProducts, validateCreateProduct, and createProductService live in src/lib/services/productsService.ts. The route’s job is to call these and translate the result into HTTP responses.
  • isPostgresError and pgErrorToApiError allow the route to treat database-specific errors as structured API errors (instead of returning a generic 500 every time something DB-related goes wrong).
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