Retrieving Order Data

Welcome back! 👋 Now that checkout can safely create orders, the next step is making those orders easy to retrieve in a clean, client-friendly way. In a real system, “checkout” is only half the story—you also need “My Orders” lists, order detail pages, and backend admin views that can load an order and its line items reliably.

In this lesson you’ll implement the read side of the Orders API end-to-end: the repository performs the SQL reads and maps database rows into domain objects, the service layer applies pagination defaults and returns consistent ServiceResults, and the Remix routes validate input and translate service results into standard success(...) / error(...) envelopes.

Previously…

In the previous lesson, you implemented checkout at POST /api/carts/:id/checkout, including a transactional repository workflow that snapshots cart items into order_items, computes totals, decrements inventory, and seals the cart as checked_out. That means orders now exist in the database—so in this lesson, we’ll focus on the “read path”: listing orders and fetching a single order with its items.

What “Order Retrieval” Means in This Codebase

There are two main read endpoints:

  • GET /api/orders → returns a paginated list of orders (newest first)
  • GET /api/orders/:id → returns a single order, including items

The most important design goal is consistency: routes should never handle raw SQL rows, never guess status codes, and never implement business rules. Instead:

  • repositories do SQL + mapping
  • services do defaulting + result shaping
  • routes do validation + HTTP response envelope
Repository Mapping: Turning Raw Rows Into Domain Objects

Before we even talk about pagination or fetching by ID, the repository establishes a pattern: DB rows are not returned directly. Everything gets mapped into domain-shaped objects using mapOrderRow and mapItemRow.

This code lives in src/lib/repositories/ordersRepo.ts:

  • OrderRow and OrderItemRow describe the database shape, which is important because repositories work at the SQL boundary and must be explicit about column names and types.
  • mapOrderRow normalizes the order’s status string into a safe domain value. If the database contains an unexpected status, the code defaults to "pending" to avoid leaking invalid states to the rest of the system.
Repository: Listing Orders With Pagination

The list endpoint is backed by listOrders(page, pageSize) in src/lib/repositories/ordersRepo.ts. The key requirements for learners are:

  • sort by newest first: ORDER BY created_at DESC
  • paginate with LIMIT + OFFSET

Here is the repository implementation:

  • offset = (page - 1) * pageSize is the standard pagination formula: page 1 starts at offset 0, page 2 starts at pageSize, and so on. This makes paging predictable and easy for clients to reason about.
  • ORDER BY created_at DESC ensures the newest orders appear first, which matches typical “My Orders” UX and avoids confusing clients by reshuffling order results.
  • LIMIT $1 OFFSET $2 keeps pagination efficient and consistent. Even if the dataset grows, the query always returns at most pageSize records.
  • Returning rows.map(mapOrderRow) is the “no raw rows” rule in action: everything leaving the repository is already shaped as a domain .
Service: Defaulting and Clamping Pagination Inputs

The service layer exists so routes don’t have to implement “default page” or “limit page size” logic repeatedly.

In this codebase, listOrdersService({ page, pageSize }) applies:

  • default page = 1 if missing or invalid
  • default pageSize = 20 if missing or invalid
  • clamp pageSize to a max of 100

Here is the service implementation in src/lib/services/ordersService.ts:

  • The service treats missing or invalid page as “page 1,” which prevents accidental client mistakes from causing errors and makes the API friendlier to consume.
  • pageSize defaults to 20, which is a sensible tradeoff between payload size and usefulness. It avoids returning too few orders while also avoiding massive responses.
  • Math.min(params.pageSize, 100) clamps pageSize to at most 100. This protects the database from huge scans and protects the server from returning very large JSON responses.
Route: GET /api/orders With Query Param Validation

The list route is implemented in app/routes/api.orders.ts. This route’s job is to validate query params and translate the service result into an HTTP response.

Two query params are supported:

  • page (min 1)
  • pageSize (min 1, max 100)

Validation uses parseOptionalIntParam, and invalid values must return 400 VALIDATION_ERROR.

Here is the loader implementation:

  • new URL(request.url).searchParams is the standard way to access query params in a Remix loader, and it keeps query parsing purely request-driven (no global state).
  • parseOptionalIntParam(searchParams, "page", { min: 1 }) returns a structured result. If parsing fails (missing, non-integer, below min, etc.), becomes false and the route returns a clean .
Repository: Fetching One Order and Attaching Its Items

Order details require more than one SQL query in this codebase:

  1. load the order row from orders
  2. load all order_items for that order
  3. attach items onto the returned Order

The key requirement is: return null when the order doesn’t exist, so the service can map that to a 404.

Here is the repository implementation in src/lib/repositories/ordersRepo.ts:

  • The first query loads the order row itself. Returning null when missing is intentional: repositories shouldn’t invent HTTP behavior—they just report “found or not found.”
  • mapOrderRow(rows[0]) ensures the returned object is domain-shaped immediately. This avoids leaking a raw row object up the stack.
  • The second query loads order_items for the order and sorts them by created_at ASC. This produces stable ordering, which is helpful for consistent UI rendering and predictable API responses.
Service: getOrderService and 404 Mapping

The service layer’s job for getOrderService(id) is to translate null into a clean 404 error, and otherwise return ok(order).

Here is the implementation in src/lib/services/ordersService.ts:

  • getOrderById(id) returning null is treated as a normal “not found” outcome, and the service maps it to { code: "NOT_FOUND", httpStatus: 404 }.
  • On success, the service returns ok(order) and does not modify the returned domain object. That keeps the service contract simple and avoids duplication of mapping logic already handled by the repository.
  • Any unexpected failure is handled by handleException(e), which keeps error mapping consistent across all order-related service functions.
Route: GET /api/orders/:id With UUID Validation

The single order endpoint is implemented in app/routes/api.orders.$id.ts. Its responsibilities mirror the patterns you’ve used elsewhere:

  • validate params.id with isUUID
  • invalid UUID → 400 VALIDATION_ERROR
  • valid UUID → call getOrderService(id)
  • forward httpStatus for service failures

Here is the loader:

  • The UUID validation is an important contract difference: malformed identifiers are 400, not 404. A 404 implies the ID was valid but didn’t match a resource; 400 means the request itself is invalid.
  • This route forwards service failures as-is, including the service-provided . That keeps routes consistent and prevents accidental mismatches (like returning a 500 for a 404 case).
Recap

You now have the “read side” of the Orders API wired cleanly and consistently:

  • src/lib/repositories/ordersRepo.ts

    • listOrders(page, pageSize) uses ORDER BY created_at DESC and LIMIT/OFFSET, then maps rows into domain orders.
    • getOrderById(id) returns null if missing, otherwise loads order items and attaches them as order.items.
  • src/lib/services/ordersService.ts

    • listOrdersService({ page, pageSize }) applies defaults, clamps pageSize to 100, and returns ok(data) on success.
    • getOrderService(id) maps null to a 404 NOT_FOUND and returns ok(order) when it exists.
  • Routes

    • GET /api/orders validates and with and returns for invalid values.
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