Listing and Fetching Orders

Now that checkout can turn an open cart into a pending order, we need a way to actually see what was created. This lesson focuses on the two most common “read” operations in any backend: listing many resources (a collection) and fetching one resource (a single record).

You’ll implement and understand the two Orders endpoints our project exposes: GET /api/orders for paginated lists and GET /api/orders/:id for order details. Along the way, you’ll see how query params and route params are validated at the route layer, while the service layer handles defaults, caps, and “not found” behavior consistently.

Previously: From Cart to Order

In the previous lesson, we implemented checkout as a transactional workflow that snapshots cart items into order items, creates an order with status pending, and marks the cart as checked_out so it can’t be edited anymore. That gave us reliable order records in the database—now we’re building the read side so clients can browse and inspect those records.

Orders Retrieval: Collections vs. Single Resources

There are two ways clients typically access orders:

  • Listing orders (collection): show a page of results, usually newest first.
  • Fetching one order (resource): show details for a specific order, including its items.

In this project, those map to two Next.js route handlers:

  • src/app/api/orders/route.tsGET /api/orders
  • src/app/api/orders/[id]/route.tsGET /api/orders/:id

Both routes follow the same overall pattern: validate inputs early, call a service function, and return a consistent API envelope using success(...) and error(...).

Listing Orders with Pagination

The list endpoint must be safe and scalable. Even a small shop can quickly accumulate thousands of orders, so we never want to return “all orders” in one response. Instead, we paginate using page and pageSize query parameters.

Route: parse and validate query parameters

This code lives in src/app/api/orders/route.ts. Its job is to read query params from the URL, validate them, then delegate to the service layer.

  • req.nextUrl.searchParams is the Next.js-friendly way to access query parameters in route handlers. It gives us a URLSearchParams object, which is exactly what our validation helper expects.
  • parseOptionalIntParam(...) does the heavy lifting of converting string values into numbers and enforcing bounds. The min: 1 for page prevents invalid pages like or , which would make pagination math nonsensical.
Service Layer: List Orders with Defaults and Safety Caps

src/lib/services/ordersService.ts is where we enforce the “product-level” rules for listing: default paging, max page size, and consistent error handling if anything goes wrong.

Service: normalize pagination and call the repository

  • The service accepts { page?: number; pageSize?: number } so it can be used safely from multiple entry points (HTTP routes, tests, internal calls). That’s why it can’t assume the route validated everything perfectly—it defensively normalizes the values anyway.
  • page defaults to 1 when missing or invalid. This means the “first page” is always reachable even if a client forgets to send page, and it avoids surprising behavior like a 0 offset from page=0.
  • pageSize defaults to 20, which is a reasonable “page” for many UIs. More importantly, it applies Math.min(params.pageSize, 100) so page sizes are always capped at 100, even if a future route forgets to enforce the max.
  • The only job after normalization is calling the repository and wrapping the result in . This keeps business logic out of the repository while still ensuring repository calls are safe and predictable.
Fetching a Single Order by ID

Fetching one order is a classic resource route: it uses a dynamic segment ([id]) and returns either the order object or a NOT_FOUND.

Route: validate the UUID route param

This code lives in src/app/api/orders/[id]/route.ts. Its job is to extract id from the route params and validate that it’s a UUID before calling the service.

  • The route uses the modern Next.js params pattern: const { id } = await context.params;. In this project, params is typed as a Promise, so this is the correct, codebase-aligned way to access it.
  • isUUID(id) is an early guardrail that prevents unnecessary database traffic. If a client sends /api/orders/not-a-real-id, we immediately return a 400 rather than performing a query that cannot possibly succeed.
  • After validation, the route delegates to . This keeps the route thin and focused on HTTP concerns (params, status codes, response envelopes).
Service Layer: Fetch One Order and Handle Not Found

The service function getOrderService is the “single source of truth” for what should happen when an order is missing. The route just forwards that result.

  • getOrderById(id) returns either an Order or null, which is a clean repository contract: “missing data is not an exception.” The service is responsible for turning that into an API-friendly 404.
  • The NOT_FOUND error includes httpStatus: 404, which means the route doesn’t need to guess the correct status code. It simply forwards order.error.httpStatus.
  • Returning ok(order) keeps success results consistent with all other services in the codebase. This uniform ServiceResult shape is what makes routes simple and predictable.
  • The catch block again uses handleException(e), ensuring that unexpected failures become structured errors rather than bubbling up as unhandled exceptions. This helps keep your API responses consistent even under database or runtime issues.
Recap

In this unit, we added the read side of our Orders API:

  • GET /api/orders (src/app/api/orders/route.ts) lists orders with pagination, validating query params and delegating to listOrdersService.
  • listOrdersService (src/lib/services/ordersService.ts) enforces safe defaults and caps (page=1, pageSize=20, max 100) before calling the repository.
  • GET /api/orders/:id (src/app/api/orders/[id]/route.ts) fetches a single order, validating that id is a UUID before calling getOrderService.
  • getOrderService consistently returns a 404 NOT_FOUND when the order doesn’t exist, and wraps all other failures through shared exception handling.

With these endpoints in place, the system can now not only create orders during checkout, but also retrieve them for order history screens, admin dashboards, and customer order detail pages.

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