Resources and URLs
22 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)An API is a menu of things a client can ask for. A good REST API names those things with clear URLs and lets the HTTP method say what to do with them. Then a new developer can guess the next URL without reading the docs.
Think in resources
Resource is a thing an API exposes, such as a customer or an order, that has its own URL. REST is a style of API design where URLs name resources and HTTP methods act on them.
A resource is a thing your system knows about, such as a customer, an order or a product. Each one gets its own address. A group of them is a collection, and one member is an item.
customers = { 1: {"name": "Asha", "city": "Pune"}, 2: {"name": "Ben", "city": "Leeds"},}print(len(customers))print(customers[2]["name"])Example 1 · Resources as a tiny router
customers = {1: {"name": "Asha"}, 2: {"name": "Ben"}}
def read(path): parts = [p for p in path.split("/") if p] if parts == ["customers"]: return list(customers) if len(parts) == 2 and parts[0] == "customers": return customers.get(int(parts[1])) return None
print(read("/customers"))print(read("/customers/2"))print(read("/customers/9"))[1, 2]
{'name': 'Ben'}
NoneWhat this code does
- The path says which resource; the code only has to find it.
- A missing item gives
None, which a real API turns into a 404.
The whole dict is the collection at /customers. One entry is the item at /customers/2.
URLs name things
Use plural nouns, lower case, and hyphens between words. Put ids in the path. Do not put verbs in the URL, because the method already is the verb.
good = "/customers/7/orders"bad = "/getCustomerOrders?id=7"print(good.split("/")[1:])print(bad.startswith("/get"))Example 2 · Spotting verbs in URLs
VERBS = ("get", "set", "create", "delete", "update")
def has_verb(path): return any(part.lower().startswith(VERBS) for part in path.split("/") if part)
for path in ("/customers/7", "/getCustomer/7", "/createOrder", "/orders/7/items"): print(path, "->", "verb in URL" if has_verb(path) else "clean")/customers/7 -> clean /getCustomer/7 -> verb in URL /createOrder -> verb in URL /orders/7/items -> clean
What this code does
- A short check like this can run in a code review script.
- Clean URLs let the method carry the verb.
The bad path hides a verb (get) in the name. With a good path, GET already means "read".
Methods act on the URL
The same URL does different work with different methods. POST on a collection creates. GET reads. PUT replaces an item. PATCH changes part of it. DELETE removes it.
ROUTES = { ("GET", "/customers"): "list", ("POST", "/customers"): "create", ("GET", "/customers/7"): "read one", ("DELETE", "/customers/7"): "remove",}print(ROUTES[("POST", "/customers")])Example 3 · A full CRUD resource with FastAPI
from fastapi import FastAPI, HTTPException, Responsefrom fastapi.testclient import TestClientfrom pydantic import BaseModel
app = FastAPI()customers = {}counter = {"next": 1}
class CustomerIn(BaseModel): name: str city: str
def find(cid): if cid not in customers: raise HTTPException(status_code=404, detail="customer not found") return customers[cid]
@app.post("/customers", status_code=201)def create(data: CustomerIn, response: Response): cid = counter["next"] counter["next"] += 1 customers[cid] = {"id": cid, **data.model_dump()} response.headers["Location"] = f"/customers/{cid}" return customers[cid]
@app.get("/customers")def list_all(): return list(customers.values())
@app.get("/customers/{cid}")def read(cid: int): return find(cid)
@app.put("/customers/{cid}")def replace(cid: int, data: CustomerIn): find(cid) customers[cid] = {"id": cid, **data.model_dump()} return customers[cid]
@app.delete("/customers/{cid}", status_code=204)def remove(cid: int): find(cid) del customers[cid]
client = TestClient(app)r = client.post("/customers", json={"name": "Asha", "city": "Pune"})print(r.status_code, r.headers["location"])print(client.get("/customers/1").json())print(client.put("/customers/1", json={"name": "Asha", "city": "Delhi"}).json())print(client.delete("/customers/1").status_code)print(client.get("/customers/1").status_code)print(client.get("/customers").json())201 /customers/1
{'id': 1, 'name': 'Asha', 'city': 'Pune'}
{'id': 1, 'name': 'Asha', 'city': 'Delhi'}
204
404
[]What this code does
- One path,
/customers, and one with an id cover all five actions. - After the delete, the same GET gives 404 and the list is empty.
Four lines of routes cover a whole resource. This pattern is called CRUD: create, read, update, delete.
Nesting and the Location header
Location header is a response header that holds the URL of a newly created resource or the target of a redirect.
Nest a resource under its parent when it belongs to it, such as the orders of one customer. Keep nesting to two levels. After a create, answer 201 and put the new address in the Location header.
customer_id, order_id = 7, 31location = f"/customers/{customer_id}/orders/{order_id}"print(201, location)Example 4 · A nested collection
from fastapi import FastAPI, HTTPExceptionfrom fastapi.testclient import TestClient
app = FastAPI()orders = {1: ["pen", "book"], 2: []}
@app.get("/customers/{cid}/orders")def customer_orders(cid: int): if cid not in orders: raise HTTPException(status_code=404, detail="customer not found") return {"customer": cid, "orders": orders[cid]}
client = TestClient(app)print(client.get("/customers/1/orders").json())print(client.get("/customers/2/orders").json())r = client.get("/customers/3/orders")print(r.status_code, r.json()){'customer': 1, 'orders': ['pen', 'book']}
{'customer': 2, 'orders': []}
404 {'detail': 'customer not found'}What this code does
- The nested path shows that orders belong to a customer.
- A customer with no orders returns an empty list (200), not a 404. A customer that does not exist returns 404.
The client can follow Location to read what it just made.
Next step
Check your understanding
12 quick questions