Routes and HTTP Methods in Fast APIs

In Stage 1 you built a basic FastAPI app with GET routes. Now we go deeper — all HTTP methods, path parameters, query parameters, and building a proper mini API.


HTTP Methods — Quick Refresher

Since you work with NestJS you know these well. Just mapping them to FastAPI:

Method

Purpose

NestJS

FastAPI

GET

Fetch data

@Get()

@app.get()

POST

Create data

@Post()

@app.post()

PUT

Update (full)

@Put()

@app.put()

PATCH

Update (partial)

@Patch()

@app.patch()

DELETE

Delete data

@Delete()

@app.delete()


Setup — In-Memory Data Store

Before touching a database, we'll use a Python list to store data. This keeps focus on routes — not database setup.

Create a fresh main.py:


    from fastapi import FastAPI

    app = FastAPI(title="Users API", version="1.0.0")

    # In-memory database — just a list for now
    users_db = [
        {"id": 1, "name": "Rahul", "email": "rahul@email.com", "age": 20},
        {"id": 2, "name": "Priya", "email": "priya@email.com", "age": 21},
        {"id": 3, "name": "Gagan", "email": "gagan@email.com", "age": 22},
    ]


GET Routes — Fetching Data

GET All Items


    @app.get("/users")
    def get_all_users():
        return users_db

Visit http://localhost:8000/users:

[
    {"id": 1, "name": "Rahul", "email": "rahul@email.com", "age": 20},
    {"id": 2, "name": "Priya", "email": "priya@email.com", "age": 21},
    {"id": 3, "name": "Gagan", "email": "gagan@email.com", "age": 22}
]

FastAPI automatically converts your Python list of dicts to a JSON array.


GET Single Item — Path Parameter


    @app.get("/users/{user_id}")
    def get_user(user_id: int):
        for user in users_db:
            if user["id"] == user_id:
                return user
        return {"error": "User not found"}

Visit http://localhost:8000/users/1:

{"id": 1, "name": "Rahul", "email": "rahul@email.com", "age": 20}

Visit http://localhost:8000/users/99:

{"error": "User not found"}

user_id: int — FastAPI automatically converts the URL string "1" to integer 1. If someone passes "abc" instead of a number — FastAPI rejects it automatically with a validation error. No extra code needed.


Proper Error Responses with HTTPException

Returning {"error": "..."} is not standard. Use HTTPException for proper HTTP status codes:


    from fastapi import FastAPI, HTTPException

    @app.get("/users/{user_id}")
    def get_user(user_id: int):
        for user in users_db:
            if user["id"] == user_id:
                return user
        raise HTTPException(status_code=404, detail="User not found")

Now visiting /users/99 returns:

{"detail": "User not found"}

With HTTP status code 404 — proper REST API behavior.

Common status codes you'll use:

Code

Meaning

200

OK — success

201

Created — new resource created

400

Bad Request — invalid input

401

Unauthorized — not logged in

403

Forbidden — logged in but no permission

404

Not Found

422

Unprocessable Entity — validation error

500

Internal Server Error



Query Parameters

Query parameters come after ? in the URL: /users?age=21&city=Delhi

In FastAPI — any function parameter that is NOT in the path is automatically treated as a query parameter:


    @app.get("/users")
    def get_all_users(skip: int = 0, limit: int = 10):
        return users_db[skip: skip + limit]

  • /users — returns first 10 users (defaults)
  • /users?limit=2 — returns first 2 users
  • /users?skip=1&limit=2 — skips first, returns next 2

The = 0 and = 10 are default values — query params are optional when they have defaults.


Optional Query Parameters


    @app.get("/users/search")
    def search_users(name: str | None = None, age: int | None = None):
        results = users_db

        if name:
            results = [u for u in results if name.lower() in u["name"].lower()]

        if age:
            results = [u for u in results if u["age"] == age]

        return results

  • /users/search — returns all users
  • /users/search?name=gagan — filter by name
  • /users/search?age=21 — filter by age
  • /users/search?name=priya&age=21 — filter by both

str | None = None means optional — if not provided, defaults to None.


IMPORTANT — Route Order Matters

# This causes a problem
@app.get("/users/{user_id}")    # registered first
def get_user(user_id: int):
    ...

@app.get("/users/search")       # FastAPI treats "search" as user_id — WRONG
def search_users():
    ...

Fix — always put specific routes BEFORE dynamic routes:

@app.get("/users/search")       # specific — register first
def search_users():
    ...

@app.get("/users/{user_id}")    # dynamic — register after
def get_user(user_id: int):
    ...

This is a common beginner mistake. Always remember — specific before dynamic.


POST Routes — Creating Data

POST receives data in the request body — not in the URL.

For now we'll accept data as query parameters (simple way). In Stage 3 we'll use proper Pydantic models for request body.


    @app.post("/users")
    def create_user(name: str, email: str, age: int):
        # Generate new ID
        new_id = max(u["id"] for u in users_db) + 1 if users_db else 1

        new_user = {
            "id": new_id,
            "name": name,
            "email": email,
            "age": age
        }

        users_db.append(new_user)
        return new_user

You can't test POST in a browser URL bar. Use the /docs page — it has a form for POST requests.

Or use Thunder Client in VS Code (same as Postman):

Response:

{"id": 4, "name": "Amit", "email": "amit@email.com", "age": 25}

Setting Response Status Code

By default POST returns 200. Convention is 201 for created resources:


    from fastapi import FastAPI, HTTPException, status

    @app.post("/users", status_code=status.HTTP_201_CREATED)
    def create_user(name: str, email: str, age: int):
        new_id = max(u["id"] for u in users_db) + 1 if users_db else 1
        new_user = {"id": new_id, "name": name, "email": email, "age": age}
        users_db.append(new_user)
        return new_user

Using status.HTTP_201_CREATED instead of raw 201 — more readable and less error prone.


PUT Routes — Updating Data

PUT replaces the entire resource with new data:


    @app.put("/users/{user_id}")
    def update_user(user_id: int, name: str, email: str, age: int):
        for index, user in enumerate(users_db):
            if user["id"] == user_id:
                users_db[index] = {
                    "id": user_id,
                    "name": name,
                    "email": email,
                    "age": age
                }
                return users_db[index]

        raise HTTPException(status_code=404, detail="User not found")

Test in /docs or Thunder Client:

  • Method: PUT
  • URL: http://localhost:8000/users/1?name=Rahul Kumar&email=rahulk@email.com&age=21

Response:

{"id": 1, "name": "Rahul Kumar", "email": "rahulk@email.com", "age": 21}

PATCH Routes — Partial Update

PATCH updates only the fields you provide — other fields stay unchanged:


    @app.patch("/users/{user_id}")
    def partial_update_user(
        user_id: int,
        name: str | None = None,
        email: str | None = None,
        age: int | None = None
    ):
        for user in users_db:
            if user["id"] == user_id:
                if name is not None:
                    user["name"] = name
                if email is not None:
                    user["email"] = email
                if age is not None:
                    user["age"] = age
                return user

        raise HTTPException(status_code=404, detail="User not found")

Now you can update just one field:

  • PATCH /users/1?name=Rahul Singh — only updates name, email and age stay same

DELETE Routes — Deleting Data


    @app.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
    def delete_user(user_id: int):
        for index, user in enumerate(users_db):
            if user["id"] == user_id:
                users_db.pop(index)
                return

        raise HTTPException(status_code=404, detail="User not found")

204 No Content — standard response for successful deletion. No body returned.


Complete main.py So Far

Here's everything together — a complete CRUD API:


    from fastapi import FastAPI, HTTPException, status

    app = FastAPI(title="Users API", version="1.0.0")

    users_db = [
        {"id": 1, "name": "Rahul", "email": "rahul@email.com", "age": 20},
        {"id": 2, "name": "Priya", "email": "priya@email.com", "age": 21},
        {"id": 3, "name": "Gagan", "email": "gagan@email.com", "age": 22},
    ]


    @app.get("/")
    def root():
        return {"message": "Users API is running"}


    @app.get("/users/search")
    def search_users(name: str | None = None, age: int | None = None):
        results = users_db
        if name:
            results = [u for u in results if name.lower() in u["name"].lower()]
        if age:
            results = [u for u in results if u["age"] == age]
        return results


    @app.get("/users")
    def get_all_users(skip: int = 0, limit: int = 10):
        return users_db[skip: skip + limit]


    @app.get("/users/{user_id}")
    def get_user(user_id: int):
        for user in users_db:
            if user["id"] == user_id:
                return user
        raise HTTPException(status_code=404, detail="User not found")


    @app.post("/users", status_code=status.HTTP_201_CREATED)
    def create_user(name: str, email: str, age: int):
        new_id = max(u["id"] for u in users_db) + 1 if users_db else 1
        new_user = {"id": new_id, "name": name, "email": email, "age": age}
        users_db.append(new_user)
        return new_user


    @app.put("/users/{user_id}")
    def update_user(user_id: int, name: str, email: str, age: int):
        for index, user in enumerate(users_db):
            if user["id"] == user_id:
                users_db[index] = {"id": user_id, "name": name, "email": email, "age": age}
                return users_db[index]
        raise HTTPException(status_code=404, detail="User not found")


    @app.patch("/users/{user_id}")
    def partial_update_user(
        user_id: int,
        name: str | None = None,
        email: str | None = None,
        age: int | None = None
    ):
        for user in users_db:
            if user["id"] == user_id:
                if name is not None:
                    user["name"] = name
                if email is not None:
                    user["email"] = email
                if age is not None:
                    user["age"] = age
                return user
        raise HTTPException(status_code=404, detail="User not found")


    @app.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
    def delete_user(user_id: int):
        for index, user in enumerate(users_db):
            if user["id"] == user_id:
                users_db.pop(index)
                return
        raise HTTPException(status_code=404, detail="User not found")

Run it and open /docs — you'll see a complete documented API with all 7 routes, ready to test.


Testing All Routes

Open http://localhost:8000/docs and test every route:

GET /users — should return all 3 users GET /users/1 — should return Rahul GET /users/99 — should return 404 GET /users/search?name=priya — should return Priya POST /users — create a new user (fill the form in docs) PUT /users/1 — update all fields of user 1 PATCH /users/2 — update only one field of user 2 DELETE /users/3 — delete Gagan (sorry Gagan)


Multiple Path Parameters

You can have multiple path parameters:


    @app.get("/users/{user_id}/orders/{order_id}")
    def get_user_order(user_id: int, order_id: int):
        return {
            "user_id": user_id,
            "order_id": order_id,
            "message": f"Order {order_id} for user {user_id}"
        }


Path Parameter with Enum — Restrict Valid Values

Sometimes you want to restrict what values are allowed:


    from enum import Enum

    class UserRole(str, Enum):
        admin = "admin"
        user = "user"
        moderator = "moderator"

    @app.get("/users/role/{role}")
    def get_users_by_role(role: UserRole):
        return {"role": role, "message": f"Getting all {role} users"}

Now only admin, user, or moderator are valid — anything else gives automatic 422 error. FastAPI even shows the valid options in /docs.


Summary — What You Learned

  • All 5 HTTP methods in FastAPI
  • Path parameters with automatic type conversion
  • Query parameters with defaults and optional values
  • HTTPException for proper error responses
  • Status codes using status module
  • Route ordering — specific before dynamic
  • Enum for restricting parameter values

Exercise 🏋️

Build a complete Books API with in-memory storage:

Data structure:

books_db = [
    {"id": 1, "title": "Python Crash Course", "author": "Eric Matthes", "price": 500, "in_stock": True},
    {"id": 2, "title": "Clean Code", "author": "Robert Martin", "price": 800, "in_stock": True},
    {"id": 3, "title": "The Pragmatic Programmer", "author": "David Thomas", "price": 700, "in_stock": False},
]

Build these routes:

  • GET /books — get all books with skip and limit query params
  • GET /books/{book_id} — get single book, 404 if not found
  • GET /books/search — search by author or in_stock status
  • POST /books — add new book (title, author, price, in_stock)
  • PUT /books/{book_id} — update all fields
  • PATCH /books/{book_id} — update only price or in_stock
  • DELETE /books/{book_id} — delete book, 404 if not found

Test everything in /docs before moving on.


No comments:

Post a Comment

Routes and HTTP Methods in Fast APIs

In Stage 1 you built a basic FastAPI app with GET routes. Now we go deeper — all HTTP methods, path parameters, query parameters, and buildi...