Paying and Cancelling Orders
Paying and Cancelling Orders
At this point, your backend can create orders (via checkout) and let clients browse them (list + fetch). Now we’ll complete the basic order lifecycle by adding two actions that transition an order from one status to another: pay and cancel.
In this lesson you’ll see how our codebase models state transitions safely: the API routes validate the order ID and call service functions, and the service layer enforces which transitions are allowed (pending → paid, and “cancel unless shipped/already cancelled”). You’ll also learn why we use dedicated action routes like /pay and /cancel instead of a generic “update order” endpoint.
Previously: Creating and Reading Orders
In the earlier lessons, we built checkout to convert an open cart into a pending order, snapshotting cart items into order_items and marking the cart as checked_out. Then we added read endpoints so clients can list orders with pagination and fetch a single order by its UUID.
Now that orders exist and are visible, we’re ready to let clients move them forward through the lifecycle.
Why “Action Routes” for State Transitions
Paying and cancelling aren’t just ordinary updates like “change a shipping address.” They’re business actions with strict rules and side effects (even in simplified form). That’s why this project uses action-based endpoints:
POST /api/orders/:id/payPOST /api/orders/:id/cancel
These endpoints say exactly what the client intends to do. They also make it easy to enforce rules like “only pending orders can be paid” without exposing a general “set status to anything” API.
Pay Endpoint: Route Handler
This code lives in src/app/api/orders/[id]/pay/route.ts. Its responsibility is to extract the id from the dynamic route, validate it, call the service, and return a consistent API response.
- The dynamic segment
[id]is accessed using the codebase’s modern pattern:const { id } = await context.params;. In this project,paramsis typed as aPromise, so this is the correct way to read it. isUUID(id)is a fast validation guard that stops malformed IDs early. This prevents wasted database work and ensures invalid inputs consistently return a 400 with a validation-style error code.- The route calls
payOrderService(id)and then simply forwards theServiceResult. This keeps the route “thin,” with business rules living in the service layer instead of being duplicated in HTTP handlers. - Both success and failure responses go through
success(...)anderror(...). That matters because it keeps every endpoint speaking the same “response language,” which makes clients and UI integration much easier.
