Python Backend ยท Session 1

How a request becomes a response

The one map you need before you change anything: how an HTTP request travels through this FastAPI service โ€” and the three Python ideas that make it work, told from a frontend brain.

๐ŸŽฏ Mission link: you want to ship changes to this service yourself. Every change you'll ever make โ€” new endpoint, new field, new rule โ€” lives somewhere on this request path. Learn the path first; everything else hangs off it.

In this session:   The journey ยท 1. The decorator is the router ยท 2. async & the loop ยท 3. dict โ†’ JSON ยท Bonus: camelCase bridge ยท Try it ยท Quiz

The journey of one request

When something POSTs an order to this service, here's the path. We'll zoom into the three highlighted ideas โ€” the rest comes in later sessions.

HTTP requestPOST /process_order โ†’ Gunicorn worker1 of 4 processes โ†’ MiddlewareTimingMiddleware โ†’ Routermatches the path โ‘  โ†’ Pydanticvalidates body โ†’ Your handlerasync def โ‘ก โ†’ JSON responseauto-serialized โ‘ข

We'll start at the smallest possible endpoint in the codebase so nothing distracts us โ€” the health check.

two_picking_positions_engine/app/app.py

@app.get("/ping")
async def ping():
    return {"status": "ok"}

Three lines, three ideas. Let's take them one at a time.


โ‘  The decorator is the router

That @app.get("/ping") line is a decorator. A decorator is just a function that takes the function written below it and does something with it โ€” here, "register this function as the handler for GET /ping".

๐ŸŒ‰ From your world: this is Express's app.get('/ping', handler) โ€” except instead of passing the handler as an argument, you stick the route above the function. Same outcome: a path is now wired to a function.

Decorators feel magic until you see one written out. Good news: this repo defines its own, so you can read exactly what "magic" means. In routes.py:

two_picking_positions_engine/app/routes/routes.py

def development_only(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        warnings.warn(f"The endpoint '{func.__name__}' is for development only.")
        return await func(*args, **kwargs)
    return wrapper

@router.post("/process_order_without_decision")
@development_only
async def process_order_without_decision(order: InputRequest, ...):
    ...

Read it as: development_only takes a function (func), builds a new function (wrapper) that prints a warning and then calls the original, and returns that wrapper in its place. Stacking @development_only under @router.post(...) means "register, but wrap with a warning first." It's just functions returning functions โ€” exactly like a higher-order component or a middleware wrapper in React/Express.

โš ๏ธ Read the body, not the name. @development_only sounds like it disables the endpoint in production. It doesn't โ€” it only emits a warning and still runs. When you ship changes here, a decorator's name is a hint; its body is the truth. This habit will save you.

Real Python: decorators primer ยท Python glossary: decorator ยท FastAPI: first steps


โ‘ก async def and the event loop (your superpower)

The handler is async def, yet it never awaits anything. So why async? And here's the part that actually bites people shipping to this service:

๐ŸŒ‰ From your world: Python's event loop is the same idea as JavaScript's. One thread, cooperative multitasking, await yields control so other work runs. You already think this way โ€” that's a head start most Python learners don't have.

In FastAPI there's a fork in the road:

โš ๏ธ The trap: if you do blocking work (a synchronous DB call, time.sleep, a heavy CPU loop) inside an async def handler, you freeze the loop โ€” and every other request waiting on that worker stalls. This is the #1 way a frontend dev accidentally tanks a Python service's latency.

This isn't hypothetical โ€” it's why this very repo writes its database report through a background task instead of inline. From app.py's process_order:

two_picking_positions_engine/app/app.py

# run after the response is sent โ€” opens a DB connection, must not block the request
background_tasks.add_task(
    report_to_db,
    order=order_copy,
    ...
)

The comment is the lesson. The DB write is slow and blocking, so it's pushed off the request path. When you add logic here, ask: "is this blocking, and am I on the loop?"

FastAPI: Concurrency and async / await (read this one โ€” it's written for exactly your situation) ยท MDN: async function (the JS version you already know)


โ‘ข Return a dict โ†’ get JSON for free

The handler returns a plain Python dict: {"status": "ok"}. It never touches a response object. FastAPI takes the return value, serializes it to JSON, and sets Content-Type: application/json automatically.

๐ŸŒ‰ From your world: in Express you call res.json(obj) by hand. In FastAPI the return value is the response body. Less ceremony โ€” but it means the shape you return is the API contract.

For real endpoints the contract is enforced by a return type annotation. Look at process_order:

two_picking_positions_engine/app/app.py

async def process_order(order: InputRequest, ...) -> OutputResponse:
    ...
    return response   # validated & shaped against OutputResponse before it leaves

That -> OutputResponse isn't decoration โ€” FastAPI uses it to validate and document the response. Change that model and you change the API. (That's Session 2.)

FastAPI: response model


Bonus bridge: the API speaks camelCase, Python speaks snake_case

You'll notice Python uses warehouse_id (snake_case) but the JSON coming from the frontend uses warehouseId (camelCase). How do they meet? Every model in this repo extends CamelModel:

two_picking_positions_engine/models/pydantic_utils.py

class CamelModel(BaseModel):
    model_config = ConfigDict(
        alias_generator=to_camel,   # snake_case field โ‡„ camelCase JSON
        populate_by_name=True,
        from_attributes=True,
    )
๐ŸŒ‰ From your world: this is the seam between JS conventions and Python conventions. The wire format stays camelCase (happy frontend); the Python code stays snake_case (idiomatic Python). Pydantic translates. Remember this when you add a field โ€” name it snake_case in Python, it'll appear camelCase on the wire.

Pydantic: aliases


Try it (hands-on, ~15 min)

๐Ÿงช Exercise A โ€” see the response. Start the stack and hit the health check:
docker compose up
# in another terminal:
curl -i localhost:8080/ping
Look at the response. Find the Content-Type header and the body. Nobody wrote that header by hand โ€” idea โ‘ข in action.
๐Ÿงช Exercise B โ€” write your first endpoint. In app.py, add a tiny endpoint that returns the list of supported countries (hint: COUNTRIES_WAREHOUSES is already defined near the top of the file). Decide: should it be async def or def? Write 5โ€“6 lines. Don't ask the AI to write it โ€” write it, run it, then ask the AI to review whether it's idiomatic. That review loop is where the learning happens.

Quiz โ€” immediate feedback

Answer before scrolling on. Each click tells you why.

Score: 0 / 5