Right, let’s talk about the four verbs that make the web go ‘round. Forget the RESTful dogma for a second; at its heart, a web API is just you, the client, asking a server to do one of four core things: get me some data, create this new data, update this existing data, or delete this data. FastAPI, being the sensible framework it is, maps these actions directly to Python functions using decorators so clear your grandma could guess what they do (if your grandma is a senior backend engineer).

We define these operations as “endpoints” or “routes.” The combination of an HTTP method (like GET) and a URL path (like /items/42) is unique. You can have a GET and a POST at the same path, and they’re completely different animals. FastAPI handles routing them correctly based on the incoming request’s method.

The GET: Just Fetching, No Side Effects

The @app.get() decorator is for when you’re just asking for information. A proper GET request should be idempotent and should not change the state of your application. Think of it as a read-only operation. It’s the equivalent of asking for a menu; you’re not ordering food yet.

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    price: float

# A totally realistic, not-at-all-fake database
fake_items_db = [{"name": "Foo", "price": 15.99}, {"name": "Bar", "price": 42.00}]

@app.get("/items/")
async def read_items(skip: int = 0, limit: int = 10):
    """Fetch a list of items, with pagination because we're not monsters."""
    return fake_items_db[skip : skip + limit]

@app.get("/items/{item_id}")
async def read_item(item_id: int):
    """Fetch one specific item by its ID. Hope it exists!"""
    # This is a classic pitfall: what if item_id is 999?
    for item in fake_items_db:
        if item.get("id") == item_id:  # Spoiler: our fake items don't have an 'id' key. This will fail.
            return item
    # We should handle the not-found case. We'll get to that.

See the issue? If a user requests /items/999, our function just runs to the end and returns None, which FastAPI happily converts into a 200 OK with a null body. That’s a lie! A 200 means “everything is great and here’s your data.” This is why we need proper error handling, which we’ll cover with HTTPException.

The POST: Bringing Something New Into the World

Use @app.post() when you’re submitting data to create a new resource. It’s not idempotent; hitting submit twice might create two identical orders, resulting in two pizzas at your door (which, honestly, isn’t always the worst outcome).

The magic here is that you declare a parameter with a Pydantic model type. FastAPI will automatically:

  1. Read the body of the request as JSON.
  2. Validate it against your model. Is price a number? Is name a string?
  3. Convert it into a proper Python object for you. It’s like having a very meticulous bouncer for your data.
@app.post("/items/")
async def create_item(item: Item):
    """Create a new item. Validate the input, you maniac."""
    # At this point, `item` is already a validated instance of the `Item` class.
    new_item = {"name": item.name, "price": item.price}
    fake_items_db.append(new_item)
    return {"message": "Item created successfully", "item": new_item}

The beauty is the validation is free. Try POSTing {"name": "My Item", "price": "not_a_number"} and watch FastAPI immediately send back a beautifully detailed 422 Unprocessable Entity error, pointing out exactly what you did wrong. This saves you from writing acres of tedious if statements.

The PUT: Full Updates for the Committed

A @app.put() operation is meant for full updates. The idea is “take the data I’m giving you and replace the entire resource at this URL with it.” It’s idempotent: sending the same request ten times has the same effect as sending it once.

You’ll typically use a path parameter to specify which resource to update and a Pydantic model for the new data.

@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    """Replace the item entirely. Missing fields? They're gone."""
    # Again, this is naive. Where's our error handling?!
    for idx, existing_item in enumerate(fake_items_db):
        if existing_item.get("id") == item_id:
            # The brutal PUT: replace the whole thing.
            fake_items_db[idx] = {"name": item.name, "price": item.price}
            return {"message": "Item replaced", "item": fake_items_db[idx]}
    # If we get here, the item wasn't found.
    return {"error": "Item not found"}  # Still a 200! We'll fix this.

Notice the philosophical stance of PUT: the client is expected to provide the complete representation of the resource. If the original item had a description field but the client’s PUT request doesn’t include it, the description is considered intentionally removed. For partial updates, you’d use PATCH, but PUT is more common and often simpler.

The DELETE: Oblivion, with a 200 OK

The @app.delete() decorator does exactly what it says on the tin. It’s meant to remove the resource identified by the path. It should be idempotent. If you delete something, it’s gone. If you try to delete it again, well, it’s still gone—that’s the same end state, so it’s still idempotent. Returning a 200 OK or a 204 No Content on success is standard practice.

@app.delete("/items/{item_id}")
async def delete_item(item_id: int):
    """Delete an item. Say goodbye."""
    for idx, existing_item in enumerate(fake_items_db):
        if existing_item.get("id") == item_id:
            # Poof!
            deleted_item = fake_items_db.pop(idx)
            return {"message": "Item deleted", "deleted_item": deleted_item}
    # If the item doesn't exist, what's the correct HTTP status?
    # Is deleting something that doesn't exist an error? Or a success?
    return {"message": "Item not found, so... considered deleted?"}  # This is philosophically messy.

This highlights a fun ambiguity. Is deleting a non-existent resource a success (a 204) because the desired end state—the item not existing—is already true? Or is it a client error (404 Not Found) because they tried to act on something that wasn’t there? The latter is more common and usually safer, as it might indicate a bug in the client’s logic.