🐍 Backend with Python · Chapter 3
FastAPI: your first endpoint
Thirteen lines of Python give you routing, type conversion, input checking, JSON and live docs. The skill is knowing what those thirteen lines do for you, and what they quietly don't.
Study this lesson free in the app →
The basics
FastAPI is a Python framework that turns ordinary functions into HTTP endpoints. You write a function with type hints; FastAPI reads the hints and does the parsing, converting, checking and JSON for you.
The problem it solves: without a framework every handler re-does the same plumbing (parse the URL, turn "3" into 3, reject garbage, build JSON, document it), and every copy gets one detail wrong.
The mental model: the type hints are the spec. def get_price(item: str, qty: int = 1) tells FastAPI: item comes from the path, qty is an optional query parameter that must be a whole number. If the request doesn't fit, your function never runs and the client gets 422.
Words you must know 4
- Path operation
- a decorator like
@app.get("/menu/{item}"): one method + one path → one function. - Path parameter
- a named slot in the URL,
{item}. Always required. - Query parameter
- anything after
?, likeqty=3. A function argument not in the path becomes one; give it a default to make it optional. - ASGI server
- the program (
uvicorn) that speaks HTTP on a port and hands requests to your FastAPIapp.
Explained
Ask the server below for /menu/chai?qty=two and you get back:
HTTP/1.1 422 Unprocessable Content
{"detail":[{"type":"int_parsing","loc":["query","qty"],
"msg":"Input should be a valid integer, unable to parse string as an integer","input":"two"}]}You didn't write that check. You wrote qty: int. FastAPI read the hint and refused the request before your function ran.
Now ask for /menu/vada and you get 500 Internal Server Error. You didn't write that check either, and FastAPI can't guess it. Both halves of that are this chapter.
The printed order slip
A busy Udupi restaurant gives the waiters a printed slip with boxes: Table no. (a number), Item (from the menu), Plates (a number, blank means 1).
The cashier at the pass doesn't read handwriting essays. She glances at the boxes: table box has a number, plates box has a number. A slip that says Plates: "lots" goes straight back to the waiter, and the cook never sees it. That's the 422.
But the slip can't know whether "vada" is on today's menu. Only the cook knows that. If nobody checks and the cook just stares at an empty tray, the kitchen jams. That's the 500.
The printed boxes are your type hints. The rules that need knowledge of today's menu are your code.
What FastAPI reads from your function
For each parameter of a path-operation function, FastAPI decides where it comes from:
| You write | FastAPI treats it as | | --- | --- | | name appears in the path, {item} | path parameter, required | | simple type (int, str, bool…) not in the path | query parameter; required unless it has a default | | int \| None = None | optional query parameter, None when missing | | a Pydantic model | JSON body (next chapter) |
Then, per request: match the route → pull raw strings from the URL → convert them to your types with Pydantic → if anything fails, answer 422 with every error listed → otherwise call your function → turn the return value into JSON (dict, list, models all work) → send it with 200, or whatever status_code= you set on the decorator.
Two more things come free: /docs (an interactive Swagger UI) and /openapi.json (a machine-readable spec of every route), both built from the same type hints. Your docs can't drift from your code because they're generated from it.
main.py, running for real
from fastapi import FastAPI
app = FastAPI()
MENU = {"chai": 15, "samosa": 20, "vada pav": 25}
@app.get("/menu")
def get_menu():
return MENU
@app.get("/menu/{item}")
def get_price(item: str, qty: int = 1):
return {"item": item, "qty": qty, "total": MENU[item] * qty}pip install "fastapi[standard]" # fastapi + uvicorn + extras
uvicorn main:app --reload # main.py, the object called app
# (or: fastapi dev main.py)What the running server answered (curl -i):
GET /menu/chai?qty=3 → 200 {"item":"chai","qty":3,"total":45}
GET /menu/chai?qty=two → 422 {"detail":[{"type":"int_parsing","loc":["query","qty"],…}]}
GET /menu/vada → 500 Internal Server Error (text/plain; KeyError: 'vada' in the server log)Open http://127.0.0.1:8000/docs and both routes are listed with their parameters, with a Try it out button.
What happens between the socket and your function
Everything above the accent step is about shape: is this a number, is this field present. Nothing in the framework knows your business rules; those live in step 4.
Two requests through main.py
The conversion happens because of the type hint, not because of anything in the function body. Delete : int (keep = 1) and qty arrives as the string '3': 15 * '3' repeats the text, and the response is {"total":"333333333333333"} with status 200.
Predict: the vada nobody sells
Same main.py. A client asks for an item that isn't on the menu:
GET /menu/vadaFirst-week FastAPI gotchas
- Route order.
/users/{user_id}declared before/users/meswallowsmeand answers 422. Put fixed paths first. - Trailing slash.
@app.get("/menu")+ a request to/menu/→307redirect to/menu. Some clients don't follow redirects on POST; pick one style and use it everywhere. - No automatic HEAD.
HEAD /menuon a GET-only route returns405withAllow: GET. Uptime checkers that use HEAD will report you as broken; add@app.api_route(..., methods=["GET", "HEAD"])if you need it. async defwith blocking code. A plaindefhandler runs in a worker thread, so a slow call only ties up that thread. Anasync defthat callstime.sleeporrequests.getfreezes the whole event loop for everyone. Until be-009, usedef.- Forgetting
status_code=201on creation endpoints: the default is always 200. - Letting exceptions be your 404s. Raise
HTTPExceptionwith the code you mean.
Words to know
| Path operation | A method + path bound to a function with a decorator such as @app.get("/menu/{item}"). |
|---|---|
| Path parameter | A named slot in the URL template, e.g. {item}; always required, converted to the hinted type. |
| Query parameter | A key=value pair after '?'; any simple function argument not in the path. A default makes it optional. |
| HTTPException | Raise it in a handler to send a chosen status code with {"detail": ...} as the body. |
| ASGI | The interface between async Python servers like uvicorn and frameworks like FastAPI. |
| uvicorn | The ASGI server that listens on a port, speaks HTTP and calls your FastAPI app. |
| OpenAPI | A machine-readable description of every route; FastAPI generates it at /openapi.json and renders it at /docs. |
In two minutes
- FastAPI turns a typed Python function into an endpoint: the decorator gives method and path, the hints give the inputs.
- Names in the path template are path parameters; other simple arguments are query parameters, optional if they have a default.
- Inputs arrive as strings; FastAPI converts them to your types and answers 422 with every error if it can't, without calling your function.
- Return a dict, list or model and it's sent as JSON with 200, or the status_code you set on the decorator.
- Business rules are yours: raise HTTPException(404, ...) explicitly, or an uncaught KeyError becomes a 500.
- Fixed routes must come before parameterised ones; '/menu/' redirects to '/menu' with 307; HEAD isn't automatic.
- Plain def handlers run in a threadpool; async def must never call blocking code.
- /docs and /openapi.json are generated from the same hints, so the docs match the code.
Test yourself
1. What does FastAPI do with the argument qty: int = 1 in def get_price(item: str, qty: int = 1) on route /menu/{item}?
- Reads qty from the JSON body
- Treats qty as an optional query parameter, converts it to int, and answers 422 if it can't
- Treats qty as a path parameter
- Passes qty through as a string
Show the answer
Treats qty as an optional query parameter, converts it to int, and answers 422 if it can't — qty isn't in the path template and is a simple type, so it's a query parameter; the default makes it optional. A body would need a Pydantic model.
2. Why does GET /menu/chai?qty=two return 422 without your function ever running?
- uvicorn rejects non-numeric query strings
- Python raises TypeError inside the function
- FastAPI validates inputs against the type hints before calling the function, and 'two' isn't an int
- The route doesn't match
Show the answer
FastAPI validates inputs against the type hints before calling the function, and 'two' isn't an int — Validation is a gate in front of your function. The route does match: it's the parameter that fails, reported as int_parsing at loc ['query', 'qty'].
3. How does FastAPI decide whether a function argument is a path parameter or a query parameter?
- If its name appears in the path template it's a path parameter; otherwise a simple-typed argument is a query parameter
- By the argument's position
- By whether it has a type hint
- Path parameters must be ints
Show the answer
If its name appears in the path template it's a path parameter; otherwise a simple-typed argument is a query parameter — It's purely the name match against {…} in the path. Position and type don't decide it; a Pydantic-model argument would be read from the body instead.
4. Trap: in the chapter's main.py, what does GET /menu/vada return?
- 404 Not Found
- 422 Unprocessable Content
- 200 with total: null
- 500 Internal Server Error
Show the answer
500 Internal Server Error — 'vada' is a valid str so validation passes; MENU['vada'] raises KeyError and nothing catches it, so FastAPI answers 500. The intuitive 404 only happens if you raise HTTPException(404) yourself.
5. What happens when a client requests /menu/ but only @app.get("/menu") exists?
- 404 Not Found
- FastAPI answers 307 Temporary Redirect to /menu
- The handler runs with an empty path parameter
- 405 Method Not Allowed
Show the answer
FastAPI answers 307 Temporary Redirect to /menu — FastAPI's router redirects the slash variant with 307 (Location: …/menu). Clients that follow redirects get 200 in the end; ones that don't see the 307.
6. What is the difference between a path parameter and a query parameter?
- Path parameters identify which resource (/menu/chai) and are always required; query parameters tune the request (?qty=3) and can be optional
- Query parameters are more secure
- Path parameters can only be strings
- There is none in FastAPI
Show the answer
Path parameters identify which resource (/menu/chai) and are always required; query parameters tune the request (?qty=3) and can be optional — A missing path segment means a different URL (or no route at all), so path params can't be optional. Neither is more secure: both show up in logs and browser history.
There are 9 questions, 3 puzzles and 5 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.
Start this lesson →