◐ ai-teacher

🐍 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 ?, like qty=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 FastAPI app.

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/vada

First-week FastAPI gotchas

Words to know

Path operationA method + path bound to a function with a decorator such as @app.get("/menu/{item}").
Path parameterA named slot in the URL template, e.g. {item}; always required, converted to the hinted type.
Query parameterA key=value pair after '?'; any simple function argument not in the path. A default makes it optional.
HTTPExceptionRaise it in a handler to send a chosen status code with {"detail": ...} as the body.
ASGIThe interface between async Python servers like uvicorn and frameworks like FastAPI.
uvicornThe ASGI server that listens on a port, speaks HTTP and calls your FastAPI app.
OpenAPIA machine-readable description of every route; FastAPI generates it at /openapi.json and renders it at /docs.

In two minutes

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}?

  1. Reads qty from the JSON body
  2. Treats qty as an optional query parameter, converts it to int, and answers 422 if it can't
  3. Treats qty as a path parameter
  4. 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?

  1. uvicorn rejects non-numeric query strings
  2. Python raises TypeError inside the function
  3. FastAPI validates inputs against the type hints before calling the function, and 'two' isn't an int
  4. 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?

  1. If its name appears in the path template it's a path parameter; otherwise a simple-typed argument is a query parameter
  2. By the argument's position
  3. By whether it has a type hint
  4. 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?

  1. 404 Not Found
  2. 422 Unprocessable Content
  3. 200 with total: null
  4. 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?

  1. 404 Not Found
  2. FastAPI answers 307 Temporary Redirect to /menu
  3. The handler runs with an empty path parameter
  4. 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?

  1. Path parameters identify which resource (/menu/chai) and are always required; query parameters tune the request (?qty=3) and can be optional
  2. Query parameters are more secure
  3. Path parameters can only be strings
  4. 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.

Learn it so it stays. Read it, test yourself, then recall it on two different days. The app schedules that for you, free.

Start this lesson →