Skip to main content
FastAPI · lesson 1 of 19
FastAPI beginner Lesson 1 of 19

FastAPI 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

fastapi python uvicorn api beginner

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 def functions 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

  1. Add a GET /about route to shop.py that 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.

  1. Send a DELETE request 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.

  1. Give the shop a version: FastAPI(title="Tiny Shop", version="1.2.0"). Read the info block 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.

  1. Request /products/ with a trailing slash. Predict whether you get 200 or 404, then run it with curl -i and again with curl -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.

Frequently Asked Questions

What is FastAPI used for?
FastAPI is a Python framework for building HTTP APIs: programs that answer requests with JSON instead of web pages. You write ordinary Python functions, mark each one with a decorator such as @app.get('/items'), and FastAPI turns the return value into a JSON response. It also checks incoming data against your type hints and generates interactive documentation at /docs.
How is FastAPI different from Flask and Django?
Flask is a small framework where you choose your own validation and documentation tools. Django is a full web framework with an ORM, admin site and HTML templates built in. FastAPI sits closer to Flask in size but builds request validation (through Pydantic) and OpenAPI documentation in from the start, and it supports async def endpoints natively. It has no ORM or admin site of its own.
Why do I need Uvicorn to run FastAPI?
FastAPI is the application, but it doesn't listen on a network port by itself. Uvicorn is an ASGI server: it accepts HTTP connections, turns each request into a call to your FastAPI app, and writes the response back. You start it with uvicorn main:app, where main is the Python file and app is the FastAPI object inside it.
Where is the automatic documentation in FastAPI?
Every FastAPI app serves interactive Swagger UI documentation at /docs, a ReDoc page at /redoc, and the raw OpenAPI schema at /openapi.json. They are generated from your routes and type hints, so they update whenever the code changes. Open http://127.0.0.1:8000/docs in a browser while Uvicorn is running to try each endpoint.
What does uvicorn --reload do?
With --reload, Uvicorn watches your source files and restarts the server process whenever one changes, so you see edits without stopping and starting it yourself. The log shows a line such as 'StatReload detected changes in main.py. Reloading...'. Use it only while developing, since watching files costs CPU and restarts drop in-flight requests.