Updating and Archiving Products
Lesson: Updating and Archiving Products
Welcome back! 👋 You can already list products, create products, and fetch a single product by ID. Now we’ll complete the “product lifecycle” by letting a product change over time:
- PATCH
/api/products/:id→ apply a partial update (only the fields you send change) - DELETE
/api/products/:id→ “delete” by archiving (soft delete), so the product is no longer active
This lesson is where your backend starts behaving like a real system: you’ll validate URL input (the id), validate body input (the patch payload), and keep responsibilities clean by delegating updates/archiving to the service layer, which then delegates SQL work to the repository.
Previously…
In the previous lesson, you implemented GET /api/products/:id in app/routes/api.products.$id.ts. You validated params.id with isUUID, then delegated to getProductService, and returned consistent envelopes with:
- 400 when the
idis malformed (bad UUID) - 404 when the product doesn’t exist
- 200 when a product is found
We’ll keep that exact same “be precise about outcomes” mindset here—except now we’re handling writes, so we also need safe JSON parsing and PATCH-specific validation.
Two write operations on a single product
When working with /api/products/:id, we support two common write actions:
- PATCH updates some fields without requiring the whole product object. This is how you implement “edit name” or “adjust inventory” without resending everything.
- DELETE archives the product rather than removing it. This avoids losing history and prevents downstream references (like orders) from pointing to missing rows.
Both operations must clearly separate:
- Invalid input → 400
- Missing resource → 404
- Success → 200 with the updated/archived product
Route file: app/routes/api.products.$id.ts
This route is the “single product endpoint.” Remix wires it to /api/products/:id because $id is a dynamic segment. The action handles all write methods, so we branch on request.method.
This first small block shows what the route depends on. The big idea is: routes deal with HTTP mechanics and call services—routes do not talk directly to SQL.
success(...)anderror(...)enforce a consistent response envelope, which makes client code simpler because every API response looks predictable.parseJson(request)is a safe JSON parsing helper that avoids “invalid JSON” causing unhandled exceptions; instead, it gives you a structured result you can branch on.validateUpdateProductlives in the service module because validation rules are domain logic you want to reuse and test, not something you want duplicated in multiple routes.isUUIDis used at the route boundary because the URL is untrusted input; validating early prevents meaningless database queries and keeps error messages clear.
