GET, POST, PUT, PATCH, DELETE
HTTP methods are not interchangeable
In the last lesson, we built routes that use GET, POST, and DELETE. But we left something out: updating a book. If a user wants to change a book’s title or fix a typo in the genre, how do they do that?
That’s where PUT and PATCH come in. And understanding the difference between them is one of the most important things you’ll learn in this course, because getting it wrong leads to subtle bugs that are hard to track down.
Let’s start by going through every HTTP method and what it actually means.
The five methods
GET retrieves a resource. It does not change anything on the server. It’s safe to call a million times. Browsers can cache the response. Clients can retry it freely. If you’re reading data, you use GET.
POST creates a new resource. It has side effects. Every time you call it, something new gets created. It’s not safe to retry blindly, because retrying might create duplicates. If you’re creating something, you use POST.
PUT replaces a resource entirely. You send the complete object, and whatever was there before gets overwritten. If you call it twice with the same data, the result is the same. We say it’s “idempotent,” which we’ll cover in the next lesson.
PATCH partially updates a resource. You send only the fields you want to change. Everything else stays as it was.
DELETE removes a resource. Like PUT, it’s idempotent. Deleting something that’s already gone is not an error.
The PUT vs PATCH confusion
This is the most commonly confused distinction in REST API design, so let’s be really clear about it.
PUT replaces everything. You send the complete resource. Any field you don’t include gets wiped out.
Let’s implement it. First, the schema. Notice that every field is required. PUT demands the complete resource:
const UpdateBookBody = z.object({
title: z.string().min(1).max(500),
isbn: z.string().optional(),
genre: z.string().min(1),
publishedAt: z.string().optional(),
authorId: z.string().min(1),
}); Now the route:
// PUT /books/book-1 replaces the entire book
route.put("/books/:id", {
request: { params: z.object({ id: z.string() }), body: UpdateBookBody },
resolve: (c) => {
if (!c.input.ok) return Response.json({ error: c.input.issues }, { status: 400 });
const { id } = c.input.params;
const { title, isbn, genre, publishedAt, authorId } = c.input.body;
const existing = books.find((b) => b.id === id);
if (!existing) return Response.json({ error: "Not found" }, { status: 404 });
existing.title = title;
existing.isbn = isbn;
existing.genre = genre;
existing.publishedAt = publishedAt;
existing.authorId = authorId;
return Response.json(existing);
},
}); What happens if you send PUT /books/book-1 with just { "title": "New Title" } and leave out genre? The genre becomes undefined. You replaced the entire resource with your payload, and your payload didn’t include a genre. That’s how PUT works: it’s a full replacement.
PATCH updates only what you send. Everything else stays the same:
// PATCH /books/book-1 updates specific fields
const PatchBookBody = z.object({
title: z.string().min(1).max(500).optional(),
isbn: z.string().optional(),
genre: z.string().min(1).optional(),
publishedAt: z.string().optional(),
authorId: z.string().min(1).optional(),
});
route.patch("/books/:id", {
request: { params: z.object({ id: z.string() }), body: PatchBookBody },
resolve: (c) => {
if (!c.input.ok) return Response.json({ error: c.input.issues }, { status: 400 });
const { id } = c.input.params;
const existing = books.find((b) => b.id === id);
if (!existing) return Response.json({ error: "Not found" }, { status: 404 });
const updates = c.input.body;
if (updates.title !== undefined) existing.title = updates.title;
if (updates.isbn !== undefined) existing.isbn = updates.isbn;
if (updates.genre !== undefined) existing.genre = updates.genre;
if (updates.publishedAt !== undefined) existing.publishedAt = updates.publishedAt;
if (updates.authorId !== undefined) existing.authorId = updates.authorId;
return Response.json(existing);
},
}); Look at the key difference. In the PATCH handler, each field is only updated if the client actually sent it. We check updates.fieldName !== undefined before assigning. That means: if the client sent a new value, use it. If they didn’t, keep the old one.
So if you send PATCH /books/book-1 with just { "title": "New Title" }, only the title changes. The genre, ISBN, and everything else remain exactly as they were. That’s why every field in PatchBookBody is marked as optional().
When to use which
Use PUT when the client has the complete resource and wants to replace it. This is common in admin interfaces and integrations where the client manages the full object.
Use PATCH when the client wants to update one or two fields. This is common in user-facing forms where someone edits their name but not their email.
In practice, most APIs use PATCH more than PUT. If you’re only going to implement one, make it PATCH.
POST is not a catch-all
A common mistake is to use POST for everything. “I need to update a book? POST. I need to delete? POST. I need to read? Also POST.”
Don’t do this. POST creates resources. It does not retrieve them (use GET), update them (use PUT or PATCH), or delete them (use DELETE).
There is one exception. Sometimes you need to perform an action that doesn’t fit neatly into CRUD. For example, “ship an order” or “publish a book.” These are actions, not resource operations. POST is appropriate here: POST /orders/123/ship. We’ll see more of this later in the course.
What’s next
We now have all five HTTP methods in play. But there’s an important property some of these methods have that affects how clients can safely retry requests. What happens if a network error occurs and the client sends the same request twice? That depends on whether the method is idempotent. We’ll cover that next.
Exercises
Exercise 1: Implement both PUT and PATCH for books. Test the difference: PUT with a partial body should clear missing fields. PATCH with a partial body should preserve them.
Exercise 2: Implement PATCH for authors. Allow updating name and bio independently.
Exercise 3: What method would you use for “publish a book” (setting publishedAt to today)? Answer: PATCH, because you’re updating one field on an existing resource.
What is the difference between PUT and PATCH?