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.
In this session: The journey ยท 1. The decorator is the router ยท 2. async & the loop ยท 3. dict โ JSON ยท Bonus: camelCase bridge ยท Try it ยท Quiz
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.
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.
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".
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.
@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:
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:
async def handlers run on the event loop (like JS).def handlers run in a threadpool (FastAPI moves them off the loop for you).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)
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.
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.)
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,
)
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.
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.
Answer before scrolling on. Each click tells you why.