Building Products API
Lesson: Building Products API
Welcome! 👋 In this lesson, we’ll turn the products backend into a real, usable Products API that the app (and your Playground UI) can rely on.
You’ll see how Remix loaders and actions stay intentionally thin: they handle HTTP concerns (query params, JSON parsing, status codes), then delegate real work to the service layer. We’ll focus on two core behaviors that almost every backend needs early: listing products (with optional search + pagination) and creating a product (with validation and conflict handling). By the end, you’ll be able to trace a request from route → service → repository → database and understand why each layer exists.
The Products Collection Endpoint
In REST terms, /api/products is a collection resource:
GET /api/productsmeans: “Give me a list of products” (optionally filtered and paginated).POST /api/productsmeans: “Create a new product inside this collection.”
In Remix, these two operations live in the same route module:
loader()handles read requests (GET).action()handles mutations (POST, PUT, DELETE, etc.).
In this codebase, routes talk to services, not directly to SQL. That separation is what makes the API scalable: when you add product-by-id endpoints later, you’ll reuse the same services and repository patterns.
Route Overview: app/routes/api.products.ts
The file app/routes/api.products.ts is the entry point for /api/products. It exports both a loader and an action, and it relies on shared helpers for response envelopes and safe JSON parsing.
Route dependencies:
This chunk shows what the route depends on and hints at the architecture: the route imports services, not repositories.
LoaderFunctionArgsandActionFunctionArgsare Remix types describing what Remix passes intoloaderandaction. The key thing you’ll use here is therequest, which is the standard WebRequest.success()anderror()come fromsrc/lib/http/response.tsand guarantee that every API response uses the same envelope ({ data, meta }or{ error, meta }).parseJson()safely parses request bodies for actions. Instead of throwing on invalid JSON, it returns an “ok / not ok” result so the route can respond consistently.parseOptionalIntParam()(fromsrc/lib/http/validation.ts) handles pagination query params likepageandpageSizewith good messages and optional bounds.- The services (
listProducts,validateCreateProduct,createProductService) live insrc/lib/services/productsService.ts. The route’s job is to call them and translate outcomes into HTTP responses.
