Python for JS Devs · FastAPI
If you've built an Express server, you'll have a FastAPI one running in five minutes — with free interactive docs on top.
uv init api-demo && cd api-demo
uv python pin 3.13
uv add fastapi "uvicorn[standard]"
Create main.py:
const app = express();
app.get("/", (req, res) => {
res.json({ ok: true });
});
app.listen(8000);from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def root():
return {"ok": True} # dict → JSON automaticallyTwo things jump out: routes are decorators (@app.get("/")) on a function, and you return a value instead of calling res.json() — FastAPI serializes whatever you return.
listen)uv run uvicorn main:app --reload
# main:app → the `app` object inside main.py
# --reload → hot reload, like nodemon
Open http://127.0.0.1:8000. Then the payoff Express doesn't give you for free:
/docs for a live Swagger UI where you can call your endpoints, and /openapi.json for the generated OpenAPI schema. FastAPI builds these from your routes and type hints automatically — no annotations library, no decorators to maintain. This alone is a reason teams pick it.
Here's the FastAPI "aha": function parameters are the request contract, read from your type hints (Lesson 06 pays off now).
@app.get("/items/{item_id}")
def get_item(item_id: int, q: str | None = None):
# item_id comes from the PATH and is validated as int
# q comes from the QUERY string (?q=...) and is optional
return {"item_id": item_id, "q": q}
Because item_id: int, hitting /items/abc auto-returns a clean 422 validation error — you wrote zero validation code. A parameter named in the path ({item_id}) is a path param; any other parameter with a scalar type becomes a query param.
Get the app running with --reload, add the /items/{item_id} route, then open /docs and call it from the browser. Try /items/abc and watch FastAPI reject it with a 422. A running, self-documenting API is the win — everything else builds on this.
/docs (Swagger UI) and /openapi.json are generated automatically from your routes and hints.item_id: int not in the path is treated as:def vs async def for a route — which should I use here? (Big one — it's the whole of Lesson 17. Ask now if curious.)