Updating and Deleting Users
User Update and Deletion API Exploration
Welcome back! Now that you can create users with POST, it’s time to explore what it looks like to modify and remove users from an API. Updating and deleting are two of the most common backend operations—and they come with some new considerations like “What if the user doesn’t exist?” and “What should we return when deletion succeeds?”
In this lesson, you’ll work with the dynamic user route app/routes/api.users.$id.tsx, which handles requests like /api/users/3. You’ll see how Remix uses loader() to fetch a single user by ID, and how action() can support multiple mutation methods—in this case, PUT (replace/update) and DELETE (remove). Finally, you’ll connect that behavior to the UI in app/routes/_index.tsx, focusing on handleReplace() and handleDelete().
The dynamic user route: /api/users/:id
This project uses a separate file for “single-user” operations: app/routes/api.users.$id.tsx.
-
The
$idin the filename is Remix’s dynamic segment syntax. -
A request to
/api/users/5will land in this file, and Remix will provideparams.idto your route handlers. -
This route supports:
- GET
/api/users/:idvialoader() - PUT
/api/users/:idviaaction() - DELETE
/api/users/:idviaaction()
- GET
Fetching a user by ID with the loader
This loader() is responsible for looking up one user by ID and returning either the user object or a 404 if it doesn’t exist. The code below lives in app/routes/api.users.$id.tsx.
params.idcomes from the URL segment in/api/users/:id. TheparseInt(..., 10)converts it into a number so it can be compared against numericuser.idvalues.users.find(...)searches the in-memory array for the first matching ID. This is a simple stand-in for what would be a database query in a real app.- Returning a
404with{ error: 'User not found' }is important: it tells clients the resource doesn’t exist (as opposed to a generic “bad request”). - When the user exists, the response is
json(user)—again, this route returns the raw object, not a wrapped{ user: ... }payload.
One action, multiple methods: PUT and DELETE in action()
In Remix, action() can handle any non-GET method, so this route branches based on request.method. The action() below lives in app/routes/api.users.$id.tsx.
- This route starts by parsing
params.idonce, and then reusesuserIdfor both PUT and DELETE paths. That’s a small but helpful pattern: parse shared inputs up front. - The method branching (coming next) is what allows a single route file to support multiple behaviors cleanly.
- Notice that there is no explicit validation around
params.idbeing missing or invalid—params.id!asserts it exists. In this project, the UI always supplies an ID, and invalid IDs will simply fail to match any user and return404.
Replacing a user with PUT
This chunk handles the PUT case. In the UI, this corresponds to “PUT replace user,” and on the server it replaces the stored user object for that ID.
findIndex(...)is used instead offind(...)because we need the array index to overwrite the existing entry. If the user isn’t present,findIndexreturns-1, and we respond with a404.await request.json()reads the replacement payload. In this project, the UI always sends a full user-shaped payload (name, email, isActive, and optionally role).users[idx] = { id: userId, ...updatedData }performs a full replacement while ensuring the ID is controlled by the URL, not the client body. Even if a client tries to send anidinupdatedData, placingid: userIdfirst ensures the server uses the URL ID.- The server returns the updated user object with a standard
200 OKresponse (the default when you don’t pass a status). This makes it easy for the UI to show what the record looks like after replacement.
Concept reminder: This implementation behaves like a replace (classic PUT semantics), not a partial update. If a field is omitted from the body, it will be missing from the stored record afterward. The UI avoids this by requiring name and email before sending the request.
Deleting a user with DELETE and returning 204
This chunk handles the DELETE case, removing the user from the array and returning a 204 No Content.
- Just like PUT, DELETE first checks whether the user exists. If not, it returns a
404, which helps clients distinguish “didn’t exist” from “deleted successfully.” users.splice(userIndex, 1)mutates the in-memory array by removing exactly one item at the found index. In a database-backed system, this would be a delete query.return new Response(null, { status: 204 })is a key detail: 204 means success with no response body. That’s why the UI’s request handler checksif (response.status !== 204)before trying to parse a body.- This is a common REST pattern: after deletion, the server doesn’t need to send the deleted object back unless you specifically want that behavior.
Method fallback: rejecting unsupported methods
Finally, if a request method is neither PUT nor DELETE, the route returns a 405.
- This makes the route’s contract explicit: it supports PUT and DELETE (and GET via loader), but not other methods like POST at this URL.
- Returning
405is especially useful during debugging because it immediately tells you “you hit the right route, but used the wrong method.”
How the UI triggers PUT and DELETE
The playground UI in app/routes/_index.tsx is designed to test these endpoints without Postman or curl. You don’t need to deeply study the entire component again—what matters here is how handleReplace() and handleDelete() build correct requests for /api/users/:id.
The UI relies on the shared helper handleRequest(method, path, body?) to actually send the HTTP call; handleReplace and handleDelete are mainly responsible for validation and choosing the correct endpoint.
PUT from the UI: handleReplace()
DELETE from the UI: handleDelete()
What to look for when you test in the playground
When you click PUT replace user:
- If the ID doesn’t exist, the server returns
404with{ error: "User not found" }. - If it exists, the response body will be the updated user object and
statuswill be200.
When you click DELETE user:
- If the ID doesn’t exist, you’ll see a
404. - If it succeeds, the response
statuswill be204and the UI will showbody: null(because there’s intentionally no content to parse).
Recap
In this lesson, you explored update and deletion through the dynamic user route app/routes/api.users.$id.tsx:
-
loader()handles GET /api/users/:id to fetch a single user (or return 404). -
action()branches by method:- PUT replaces a user record for the given ID and returns the updated user.
- DELETE removes the user and returns 204 No Content.
-
The playground UI in
app/routes/_index.tsxtriggers these behaviors using:handleReplace()to validate inputs and send PUT to/api/users/:idhandleDelete()to validate the ID and send DELETE to/api/users/:id
With GET + POST + PUT + DELETE, you now have the full basic CRUD workflow for users—implemented with simple, testable Remix routes and an in-browser playground.
