HTTP Methods and CRUD
Four things you do to data, and the verbs that mean them
In this lesson
Explain it like I’m 5
The four operations and their verbs
Each CRUD operation has a matching method:
- Create →
POST, add something new. - Read →
GET, fetch without changing anything. - Update →
PUTorPATCH,PUTreplaces the whole record,PATCHchanges some fields. - Delete →
DELETE, remove it.
The important property of GET is that it changes nothing. Browsers, caches, and search crawlers all assume this and will happily repeat a GET. Anything that modifies data must not be one.
Naming routes so they read predictably
The convention is that the URL names the thing and the method says what to do with it. Nouns in the path, verbs in the method. That is why the same path appears more than once in a well-designed API.
GET /api/books list every book
POST /api/books create a new book
GET /api/books/7 read book 7
PUT /api/books/7 replace book 7
PATCH /api/books/7 change some fields of book 7
DELETE /api/books/7 delete book 7
Fill in the table for a book tracker: give each CRUD action its method and path. Use /api/books for the collection and /api/books/1 for a single book.
def endpoint(action):
table = {
"create": ("POST", "/api/books"),
# TODO: add read, update, and delete
}
return table[action]
for action in ["create", "read", "update", "delete"]:
method, path = endpoint(action)
print(f"{action}: {method} {path}")
Reading the whole collection is GET /api/books. Update and delete act on one book, so they use /api/books/1.
def endpoint(action):
table = {
"create": ("POST", "/api/books"),
"read": ("GET", "/api/books"),
"update": ("PUT", "/api/books/1"),
"delete": ("DELETE", "/api/books/1"),
}
return table[action]
for action in ["create", "read", "update", "delete"]:
method, path = endpoint(action)
print(f"{action}: {method} {path}")
create: POST /api/books
read: GET /api/books
update: PUT /api/books/1
delete: DELETE /api/books/1
Answer with the right status code
The status code is part of your answer, not decoration. A handful covers nearly everything:
200 OK, the read or update worked.201 Created, aPOSTmade something new.204 No Content, it worked and there is nothing to send back, typical afterDELETE.400 Bad Request, the caller sent something invalid.404 Not Found, no such record.
Returning 200 with {"error": "not found"} in the body is a common and unhelpful habit: it tells every automated caller that the request succeeded.
@app.route("/api/books/<int:book_id>", methods=["DELETE"])
def delete_book(book_id):
book = find_book(book_id)
if book is None:
return jsonify({"error": "book not found"}), 404
BOOKS.remove(book)
return "", 204
Common mistake: Using GET for something that changes data
A link is the easiest thing to build, and GET is what links do.
Use POST, PUT, PATCH, or DELETE for anything that modifies. GET must be safe to repeat, because things you don’t control will repeat it.
Common mistake: Putting the verb in the URL
/api/createBook reads clearly to a human writing it.
Let the method be the verb and the path be the noun: POST /api/books. It keeps the API predictable and stops the route list doubling in size.
Common mistake: Returning 200 for every response
The endpoint ran without crashing, so it feels like a success.
Match the code to the outcome: 201 after creating, 204 after deleting, 400 for bad input, 404 when it doesn’t exist. Callers branch on the code, not on your error text.
Which method should add a new record?
POST creates. GET must never change anything.
What is the difference between PUT and PATCH?
PUT sends a complete replacement; PATCH sends only what changes.
A DELETE succeeds and there is nothing to return. Which status fits best?
204 says explicitly that it worked and there is no body, which is exactly the situation.
Mini exercise (medium)
Write route_for(action, item_id=None) for a book API. It should return a (method, path) tuple following the convention: collection actions use /api/books, single-item actions use /api/books/<id>, and the method comes from the CRUD operation.
Now you. Edit the starter code below, then Run it, everything happens in the browser.
def route_for(action, item_id=None):
return ("GET", "/api/books") # TODO: use the action and item_id
print(route_for("create"))
print(route_for("read", 7))
print(route_for("delete", 7))
A dict mapping each action to its method is cleaner than a chain of ifs. Then decide the path from whether item_id was given.
def route_for(action, item_id=None):
methods = {"create": "POST", "read": "GET", "update": "PUT", "delete": "DELETE"}
method = methods[action]
path = "/api/books" if item_id is None else f"/api/books/{item_id}"
return (method, path)
print(route_for("create"))
print(route_for("read", 7))
print(route_for("delete", 7))
('POST', '/api/books')
('GET', '/api/books/7')
('DELETE', '/api/books/7')
assert route_for("update", 3) == ("PUT", "/api/books/3"), "update on one item uses PUT and the id path"
assert route_for("read") == ("GET", "/api/books"), "no id means the collection"
print("✓ Looks good!")