Quick Answer

FastAPI is a modern Python framework for building web APIs. You describe each request and response with standard type hints and Pydantic models, and FastAPI validates incoming data, serializes responses to JSON, and generates interactive documentation for free. It runs on an ASGI server such as Uvicorn and lets route handlers be either def or async def.

Your first endpoint

A FastAPI application is an instance of the FastAPI class with route handlers attached by decorator. Save this as main.py:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"status": "ok"}

FastAPI does not bundle a web server, so install one and run it:

pip install "fastapi[standard]" uvicorn
uvicorn main:app --reload

Open http://127.0.0.1:8000 and you get {"status": "ok"}. The return value is a plain dict; FastAPI serializes it to JSON and sets the content type. Now open http://127.0.0.1:8000/docs. That interactive Swagger UI page is generated from your code - every route, parameter, and model you declare appears there automatically, and you can send test requests from the browser. A second format lives at /redoc. The --reload flag restarts the server whenever you save a file; leave it off in production.

Type hints do the validation

Annotate a path parameter and FastAPI parses and checks it before your function runs:

@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None):
    return {"item_id": item_id, "q": q}

item_id is declared int, so /items/42 hands you the integer 42, not the string "42". Any function argument that is not part of the path becomes a query parameter: q is optional and defaults to None.

Send a bad value and you get a precise error. GET /items/abc returns HTTP 422 Unprocessable Entity with a body that names the problem:

{
  "detail": [
    {
      "type": "int_parsing",
      "loc": ["path", "item_id"],
      "msg": "Input should be a valid integer, unable to parse string as an integer"
    }
  ]
}

You wrote no validation code. The type hint is the specification, and FastAPI both enforces it and documents it in /docs.

Request bodies with Pydantic models

For anything more complex than a few scalars, declare a Pydantic model. A parameter typed as a model is read from the JSON request body:

from pydantic import BaseModel, Field

class Course(BaseModel):
    title: str
    price: int = Field(gt=0)
    published: bool = True

@app.post("/courses", status_code=201)
def create_course(course: Course):
    return {"id": 1, **course.model_dump()}

POST /courses with {"title": "FastAPI", "price": 499} returns 201 and {"id": 1, "title": "FastAPI", "price": 499, "published": true} - the default filled in.

Break a rule and you get 422 before the handler runs. A price of -5 returns "Input should be greater than 0". Omit title entirely and the error loc is ["body", "title"] with type "missing". The model also shows up in /docs as an example schema, so anyone calling your API can see the exact shape you expect.

The async gotcha that blocks your server

FastAPI lets a handler be def or async def, and the difference matters more than beginners expect. An async def handler runs on the event loop. If you call a blocking function inside it - time.sleep, requests.get, a synchronous database driver, heavy CPU work - nothing else on that worker can make progress until it returns.

import time

@app.get("/blocking-async")
async def blocking_async():
    time.sleep(1)            # freezes the whole event loop
    return {"done": True}

@app.get("/blocking-sync")
def blocking_sync():
    time.sleep(1)            # FastAPI runs this in a threadpool
    return {"done": True}

Fire five requests at each route at once. The async def version handles them one after another and takes about five seconds in total. The plain def version finishes in about one second, because FastAPI runs synchronous handlers in a worker threadpool that never touches the loop.

The rule: use async def only when the body awaits async libraries the whole way down. If you are calling ordinary blocking code, a plain def handler is the safe - and often faster - choice.

Dependencies keep handlers small

The Depends mechanism lets a handler declare what it needs and have FastAPI supply it. A dependency is just a function:

from fastapi import Depends

def pagination(skip: int = 0, limit: int = 10):
    return {"skip": skip, "limit": limit}

@app.get("/courses")
def list_courses(page: dict = Depends(pagination)):
    return {"page": page, "total": 0}

GET /courses?limit=5 returns {"page": {"skip": 0, "limit": 5}, "total": 0}. The dependency's own parameters are merged into the route's query string and appear in the docs.

Dependencies compose and are cached within a single request, so a database session, the current authenticated user, or a settings object can each be a dependency that many routes share. Because a dependency is an ordinary function, a test can replace it through app.dependency_overrides and never touch a real database - override get_db with a function returning a fake and every route that depends on it uses the fake. This is how larger FastAPI codebases stay readable: handlers state intent, dependencies do the wiring.

Response models control what goes out

By default a handler's return value is serialized as-is. Set response_model and FastAPI filters the response through that model, dropping anything the model does not declare:

class UserIn(BaseModel):
    username: str
    password: str

class UserOut(BaseModel):
    username: str

@app.post("/register", response_model=UserOut)
def register(user: UserIn):
    return user            # full object in, UserOut shape out

Register with a username and password and the response is {"username": "asha"}. The password field never leaves the server even though the handler returned the whole object. This is the safe default for any model backed by a database row, where columns like password hashes, internal flags, or soft-delete timestamps should stay hidden.

One note if you are following older tutorials: this is Pydantic v2. Methods were renamed - .dict() is now .model_dump() and .json() is .model_dump_json(). The old names still run but emit a deprecation warning.

Frequently Asked Questions

Is FastAPI faster than Flask or Django? For I/O-bound APIs served over ASGI with async handlers, FastAPI usually has higher throughput because it does not tie up a worker per request. For blocking code the gap narrows. Raw speed is rarely the deciding factor - the automatic validation and docs are the bigger draw.
Do I have to use async def for every route? No. Use async def only when you await async libraries throughout the handler. For synchronous database drivers or libraries like requests, a plain def handler is correct - FastAPI runs it in a threadpool so it does not block the event loop.
Why does GET /users/me return a 422 error? Route order. If /users/{user_id} is declared before /users/me, the literal path 'me' is matched against the parameter route first. With an int type hint that fails validation; with a str hint it silently calls the wrong handler. Declare specific paths before parameterized ones.
What runs FastAPI in production? An ASGI server, usually Uvicorn: uvicorn main:app --host 0.0.0.0 --port 8000. Many teams run it under a process manager or with Gunicorn using Uvicorn worker classes. Drop the --reload flag outside development.
Can I use FastAPI with a database? Yes. FastAPI is unopinionated about storage. SQLAlchemy 2.0, SQLModel, Tortoise ORM, and raw drivers all work. Provide the session as a dependency so each request gets its own and it is closed afterwards.