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/products means: “Give me a list of products” (optionally filtered and paginated).
  • POST /api/products means: “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.

// app/routes/api.products.ts

import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node';
import { success, error, parseJson } from '@/lib/http/response';
import { listProducts, createProductService, validateCreateProduct } from '@/lib/services/productsService';
import { parseOptionalIntParam } from '@/lib/http/validation';
  • LoaderFunctionArgs and ActionFunctionArgs are Remix types describing what Remix passes into loader and action. The key thing you’ll use here is the request, which is the standard Web Request.
  • success() and error() come from src/lib/http/response.ts and 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() (from src/lib/http/validation.ts) handles pagination query params like page and pageSize with good messages and optional bounds.
  • The services (listProducts, validateCreateProduct, createProductService) live in src/lib/services/productsService.ts. The route’s job is to call them and translate outcomes into HTTP 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