Learn by Directing AI
Unit 4

API Routes

Step 1: Understand REST API routes

Before you build anything, understand what you're building. A REST API route handles a specific HTTP method at a specific path. GET /api/inventory returns inventory data. POST /api/orders creates an order. PUT /api/orders/:id updates an order's status.

The API is a contract between the frontend and the backend. Your frontend will promise Marco's staff certain data in a certain shape -- the inventory dashboard promises to show all products grouped by stage and farm. The API has to deliver on that promise. When the contract breaks -- a renamed field, a null where an array was expected, a missing relationship -- the frontend fails silently. The server logs show nothing wrong. Only testing the API directly against its promised contract catches this.

Every response has two parts: a status code and a body. The status code tells the consumer what happened. 200 means the request succeeded. 201 means a resource was created. 400 means the request was invalid. 404 means the resource doesn't exist. 409 means the request conflicts with existing data. 500 means the server broke. These codes aren't decorative -- they're the communication layer between services.

Open materials/api-contract.md. This is the contract your implementation has to honor: every endpoint, its method, the request/response shapes, and the status codes it returns.

Step 2: Build the inventory routes

Direct Claude to build the inventory API routes. Be specific about what each route does and what it returns:

Build the inventory API routes following materials/api-contract.md. Three routes: GET /api/inventory (returns all inventory with farm name, stage, batch info, and product counts), GET /api/inventory/:id (returns a single inventory item with full batch traceability), PUT /api/inventory/:id (updates the processing stage). Use the Prisma client to query the database. Return appropriate status codes: 200 for success, 404 when the item doesn't exist, 400 when the stage value is invalid.

Notice the constraint specification. You told Claude which status codes to use and when. Without that constraint, Claude defaults to returning 200 for everything -- successful lookups, failed lookups, invalid input, server errors. An API that returns 200 for every request is not a working API. It's hiding failures from every consumer.

Step 3: Review error handling

Check what Claude generated. Open the inventory route files and look at the error handling. AI commonly generates routes that catch errors and return 200 with an empty body or a generic message. That pattern swallows failures.

Ask yourself: if someone requests GET /api/inventory/999 and that ID doesn't exist, does the route return 404 with a clear error message? Or does it return 200 with null? The first tells the frontend "this doesn't exist -- show the user a meaningful message." The second tells the frontend "everything's fine" while rendering nothing.

If the routes swallow errors, direct Claude to fix them. Be specific:

The inventory routes should return 404 with { "error": "Inventory item not found" } when the ID doesn't exist. They should never return 200 for a failed lookup.

Step 4: Add input validation

Every route that accepts user input must validate before touching the database. The order creation route is the critical one -- it handles customer names, product selections, and quantities.

Direct Claude to add validation to the order creation endpoint:

Add input validation to POST /api/orders following the api-contract.md spec. Validate: customer_name must be non-empty and not whitespace-only, country must be provided, product IDs must reference existing products, quantities must be positive integers. Return 400 with a specific error message for each validation failure.

AI commonly omits server-side validation because it assumes the frontend validates first. That assumption is architecturally wrong. The API is a public contract -- any client can call it, not just your frontend. A curl command, a script, a future mobile app. Server-side validation is the last line of defense for Marco's data.

Step 5: Build the order routes with allocation

The order routes are the core of Marco's system. When a shop in Portland orders 72% Lacandon bars, those specific products get allocated to that order. No one else can allocate the same products.

Build the order API routes following materials/api-contract.md. POST /api/orders creates an order and allocates the specified products. Before confirming, check that each product's allocated_to_order_id is null -- if any product is already allocated, return 409 Conflict. GET /api/orders returns all orders with their allocated products and status. PUT /api/orders/:id updates the order status.

The 409 Conflict response is the double-allocation prevention Marco needs. It's a business rule enforced at the API level. The database schema allows only one order per product (the nullable foreign key), and the API route checks availability before writing.

Step 6: Test with curl

Test the API from the terminal. Start the development server if it's not running, then send requests directly.

Test a successful inventory query:

curl -s http://localhost:3000/api/inventory | jq

The response should be JSON with inventory items including farm names, stages, and product counts. The status code should be 200.

Test the double-allocation check. First, find a product that's already allocated (check the seed data). Then try to create an order for it:

curl -s -X POST http://localhost:3000/api/orders -H "Content-Type: application/json" -d '{"customer_name":"Test Shop","country":"US","products":[{"id":1,"quantity":1}]}' -w "\n%{http_code}"

If product 1 is already allocated, the response should be 409 Conflict with an error message explaining why.

Test the whitespace validation:

curl -s -X POST http://localhost:3000/api/orders -H "Content-Type: application/json" -d '{"customer_name":"   ","country":"US","products":[{"id":10,"quantity":1}]}' -w "\n%{http_code}"

The response should be 400 Bad Request. Not 200. Not 201. The API refuses to create an order with a blank customer name.

If any of these tests fail -- if the API returns 200 when it should return 409, or accepts whitespace-only names -- fix the routes before moving on. The frontend will rely on these status codes to show meaningful messages to Marco's staff.

✓ Check

Check: Send a POST to /api/orders allocating an already-allocated product. Send a POST with a whitespace-only customer name.