FastAPI · lesson 1 of 19
Beginner
6Intermediate
10- 7 FastAPI Pydantic Validation: Field Constraints and Nested Models
- 8 FastAPI Validators and Serialization with Pydantic
- 9 FastAPI CRUD with SQLModel and SQLite
- 10 FastAPI Database Migrations with Alembic and SQLModel
- 11 FastAPI Authentication: Password Hashing, OAuth2 and JWT
- 12 FastAPI Roles, CORS and Settings from Environment Variables
- 13 FastAPI Dependency Injection: Depends, yield and Overrides
- 14 Testing FastAPI with pytest, TestClient and httpx
- 15 FastAPI with Async PostgreSQL: Pools, Transactions, and Real Errors
- 16 Sync vs Async Endpoints in FastAPI, Measured
Advanced
3Also part of FastAPI
6FastAPI Introduction: Your First API with Uvicorn
Install FastAPI, write a first app, run it with Uvicorn and call it with curl: real JSON responses, 404 and 405 errors, /docs, and --reload.
By SkillByExample · Published
What FastAPI Is
An API is a program that other programs talk to over HTTP. A client sends a request such as “give me the product list”, and the API sends back data, usually as JSON. FastAPI is a Python framework for writing APIs: each URL maps to an ordinary Python function, and whatever the function returns becomes the JSON response.
How it compares with the two other big Python web frameworks:
- Flask is a minimal framework. It routes URLs to functions and leaves validation, documentation and async support for you to add from extensions.
- Django is a full-stack framework with an ORM, an admin site, HTML templates and authentication built in. It suits websites with a database at their centre. Django REST Framework adds APIs on top.
- FastAPI is about Flask’s size, but it uses your Python type hints to validate incoming data (through the Pydantic library) and to generate API documentation automatically. It also runs
async deffunctions natively. It has no ORM, admin site or templates of its own.
This track starts from zero. You need Python 3.9 or newer and a terminal; curl is used to send requests and ships with macOS, Linux and Windows 10+. Every output below came from Python 3.12.10, FastAPI 0.143.0, Pydantic 2.14.1, Starlette 1.7.0 and Uvicorn 0.54.0.
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install fastapi uvicorn
pip install "fastapi[standard]" also installs Uvicorn plus a fastapi dev command; this track calls Uvicorn directly so you can see what each piece does.
The First App
Save this as main.py:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def home():
return {"message": "Hello, FastAPI"}
Start it with Uvicorn. main:app means “the object named app inside main.py”:
$ uvicorn main:app
INFO: Started server process [22416]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
The server keeps running and holds the terminal. Open a second terminal and send it a request. curl -i prints the response headers as well as the body:
$ curl -i http://127.0.0.1:8000/
HTTP/1.1 200 OK
date: Tue, 13 Oct 2026 06:49:59 GMT
server: uvicorn
content-length: 28
content-type: application/json
{"message":"Hello, FastAPI"}
The first line is the status line: 200 OK means the request succeeded. Next come the headers, metadata about the response. content-type: application/json tells the client the body is JSON, and content-length is the body size in bytes. After a blank line comes the body. You returned a Python dict, and FastAPI converted it to JSON. Note that the spaces after the colons are gone: the body is compact JSON, not Python’s repr.
FastAPI is the application, and Uvicorn is the server. Uvicorn listens on port 8000, reads each HTTP request, calls your app, and writes the response back. That split is why you run uvicorn main:app and not python main.py.
Any JSON-Compatible Value Works
A route doesn’t have to return a dict. Save this as shop.py:
from fastapi import FastAPI
app = FastAPI(title="Tiny Shop")
PRODUCTS = ["kettle", "toaster", "mug"]
@app.get("/products")
def list_products():
return PRODUCTS
@app.get("/products/count")
def count_products():
return len(PRODUCTS)
@app.get("/status")
def status():
return "ok"
Stop the first server with Ctrl+C, start this one with uvicorn shop:app, and request two of the routes:
$ curl -i http://127.0.0.1:8000/products
HTTP/1.1 200 OK
date: Tue, 13 Oct 2026 06:50:26 GMT
server: uvicorn
content-length: 26
content-type: application/json
["kettle","toaster","mug"]
$ curl -i http://127.0.0.1:8000/products/count
HTTP/1.1 200 OK
date: Tue, 13 Oct 2026 06:50:26 GMT
server: uvicorn
content-length: 1
content-type: application/json
3
The list becomes a JSON array and the integer becomes the bare JSON number 3. The string is the one to watch:
$ curl -i http://127.0.0.1:8000/status
HTTP/1.1 200 OK
date: Tue, 13 Oct 2026 06:50:26 GMT
server: uvicorn
content-length: 4
content-type: application/json
"ok"
The body is "ok" with the quotes, four bytes, because a JSON string is quoted. FastAPI JSON-encodes every return value, and the content type is still application/json. To send plain text, return a PlainTextResponse from fastapi.responses instead.
The decorator’s path decides which function runs. Two routes, /products and /products/count, live side by side because FastAPI matches the whole path, not a prefix.
When No Route Matches
Go back to uvicorn main:app. Ask for a path that doesn’t exist, then use a method the route doesn’t accept:
$ curl -i http://127.0.0.1:8000/missing
HTTP/1.1 404 Not Found
date: Tue, 13 Oct 2026 06:49:59 GMT
server: uvicorn
content-length: 22
content-type: application/json
{"detail":"Not Found"}
$ curl -i -X POST http://127.0.0.1:8000/
HTTP/1.1 405 Method Not Allowed
date: Tue, 13 Oct 2026 06:49:59 GMT
server: uvicorn
allow: GET
content-length: 31
content-type: application/json
{"detail":"Method Not Allowed"}
Status codes in the 400s mean the client asked for something wrong. 404 Not Found means no route has that path. 405 Method Not Allowed is different: the path / exists, but only for GET, and curl -X POST sent a POST. The allow: GET header lists the methods that would have worked. Both errors use the same JSON shape, {"detail": ...}, which is FastAPI’s convention for every error it produces.
Back in the server’s terminal, Uvicorn logs one line per request with the method, path and status:
INFO: 127.0.0.1:63304 - "GET / HTTP/1.1" 200 OK
INFO: 127.0.0.1:63305 - "GET /missing HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:63306 - "POST / HTTP/1.1" 405 Method Not Allowed
The Documentation You Didn’t Write
Every FastAPI app also serves interactive documentation at /docs. Open http://127.0.0.1:8000/docs in a browser to see each route and try it out. From the terminal, curl -I sends a HEAD request, which returns only the headers:
$ curl -I http://127.0.0.1:8000/docs
HTTP/1.1 200 OK
date: Tue, 13 Oct 2026 06:49:59 GMT
server: uvicorn
content-length: 1006
content-type: text/html; charset=utf-8
/docs is a 1,006-byte HTML page (Swagger UI). It loads its JavaScript from a CDN and then reads your API’s description from /openapi.json:
$ curl -s http://127.0.0.1:8000/openapi.json
{"openapi":"3.1.0","info":{"title":"FastAPI","version":"0.1.0"},"paths":{"/":{"get":{"summary":"Home","operationId":"home__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}}}
That JSON is an OpenAPI schema, a standard format for describing APIs. FastAPI built it from your code: the path /, the method get, and a summary Home taken from the function name home. The title is FastAPI because main.py passed none. In shop.py, FastAPI(title="Tiny Shop") changes it. Tools beyond Swagger UI read this file too, including client generators and API gateways. There is also a second documentation style at /redoc.
Reloading on Save
Restarting the server after every edit gets old fast. --reload makes Uvicorn watch your files. This run started the server, sent a request, changed "Hello, FastAPI" to "Hello again" in main.py, and sent the request again (it was captured on Linux in a python:3.12-slim container with the same package versions):
$ uvicorn main:app --reload
INFO: Will watch for changes in these directories: ['/app']
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [365] using StatReload
INFO: Started server process [375]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: 127.0.0.1:49068 - "GET / HTTP/1.1" 200 OK
WARNING: StatReload detected changes in 'main.py'. Reloading...
INFO: Shutting down
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
INFO: Finished server process [375]
INFO: Started server process [406]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: 127.0.0.1:49080 - "GET / HTTP/1.1" 200 OK
$ curl -i http://127.0.0.1:8000/
HTTP/1.1 200 OK
date: Tue, 13 Oct 2026 06:52:16 GMT
server: uvicorn
content-length: 25
content-type: application/json
{"message":"Hello again"}
Two processes appear. The reloader (365) only watches files. It starts the server process (375) that runs your app, and when main.py changes it stops 375 and starts a fresh 406. That’s why the server process ID changes and the reloader’s doesn’t. StatReload checks file modification times; installing uvicorn[standard] swaps in the faster WatchFiles. Use --reload for development only.
Practice
- Add a
GET /aboutroute toshop.pythat returns the shop’s name, how many products it has, and"open": true. Predict the exact JSON body before you run it.
Solution
@app.get("/about")
def about():
return {"shop": "Tiny Shop", "products": len(PRODUCTS), "open": True}
$ curl -i http://127.0.0.1:8000/about
HTTP/1.1 200 OK
date: Tue, 13 Oct 2026 06:52:27 GMT
server: uvicorn
content-length: 45
content-type: application/json
{"shop":"Tiny Shop","products":3,"open":true}
Python’s True becomes JSON’s lowercase true. None would become null.
- Send a
DELETErequest to/products. Which status code comes back, and which header tells you what would have worked?
Solution
$ curl -i -X DELETE http://127.0.0.1:8000/products
HTTP/1.1 405 Method Not Allowed
date: Tue, 13 Oct 2026 06:52:27 GMT
server: uvicorn
allow: GET
content-length: 31
content-type: application/json
{"detail":"Method Not Allowed"}
The path exists, so it’s 405, not 404. allow: GET lists the only method registered for /products.
- Give the shop a version:
FastAPI(title="Tiny Shop", version="1.2.0"). Read theinfoblock back out of/openapi.json.
Solution
$ curl -s http://127.0.0.1:8000/openapi.json | python -c "import json,sys; print(json.load(sys.stdin)['info'])"
{'title': 'Tiny Shop', 'version': '1.2.0'}
The version is your API’s version, shown at the top of /docs. It’s unrelated to FastAPI’s own version number.
- Request
/products/with a trailing slash. Predict whether you get 200 or 404, then run it withcurl -iand again withcurl -iL.
Solution
$ curl -i http://127.0.0.1:8000/products/
HTTP/1.1 307 Temporary Redirect
date: Tue, 13 Oct 2026 06:52:27 GMT
server: uvicorn
content-length: 0
location: http://127.0.0.1:8000/products
$ curl -iL http://127.0.0.1:8000/products/
HTTP/1.1 307 Temporary Redirect
date: Tue, 13 Oct 2026 06:52:32 GMT
server: uvicorn
content-length: 0
location: http://127.0.0.1:8000/products
HTTP/1.1 200 OK
date: Tue, 13 Oct 2026 06:52:32 GMT
server: uvicorn
content-length: 26
content-type: application/json
["kettle","toaster","mug"]
Neither. FastAPI answers 307 Temporary Redirect: no route matches /products/, but /products does, so the location header points the client there. 3xx codes mean “look elsewhere”. Plain curl stops at the redirect, and -L follows it, which takes two requests. Browsers follow redirects automatically.