Building APIs with FastAPI

· 5 min read · Updated March 14, 2026 · intermediate
fastapi api web python pydantic

FastAPI is a modern Python web framework that has become a go-to choice for building APIs. It combines the best of two worlds: the speed and developer experience of working with async Python, and the reliability of automatic data validation. If you have used Flask or Django but wanted better type safety and automatic documentation, FastAPI is the answer.

Why FastAPI?

FastAPI stands out for several reasons. First, it is built on top of Starlette for the web parts and Pydantic for the data parts, both of which are well-tested libraries. Second, it automatically generates OpenAPI documentation, so your API comes with interactive docs out of the box. Third, it supports async and await natively, making it one of the fastest Python frameworks available.

The framework is particularly popular because it requires very little boilerplate code. A basic API endpoint takes just a few lines. Yet it still gives you the power of type hints, which FastAPI uses to validate request and response data automatically.

Your First FastAPI Endpoint

First, install FastAPI and an ASGI server:

pip install fastapi uvicorn

Now create a simple API:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello, World!"}

Run the server with Uvicorn:

uvicorn main:app --reload

Visit http://127.0.0.1:8000 and you will see the JSON response. Visit http://127.0.0.1:8000/docs to see the auto-generated Swagger UI documentation.

Path Parameters

FastAPI makes it easy to capture dynamic values from the URL path:

from fastapi import FastAPI

app = FastAPI()

@app.get("/items/{item_id}")
def read_item(item_id: int):
    return {"item_id": item_id, "name": f"Item {item_id}"}

The type annotation item_id: int does two things: it converts the string from the URL to an integer, and it returns a clear error if the value cannot be converted. Try visiting /items/42 and then /items/abc to see the difference.

Query Parameters

Query parameters (the key-value pairs after the ? in a URL) are just as straightforward:

from fastapi import FastAPI

app = FastAPI()

@app.get("/search")
def search(q: str = "", limit: int = 10):
    return {"query": q, "limit": limit, "results": [f"Result {i}" for i in range(limit)]}

Both parameters have default values, making them optional. The API documentation will show them as optional parameters.

Request Bodies with Pydantic

For POST requests where the client sends data, FastAPI uses Pydantic models to define the expected structure:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None

@app.post("/items/")
def create_item(item: Item):
    if item.tax:
        total = item.price + item.tax
    else:
        total = item.price
    return {"item": item, "total_price": total}

The Item class defines the expected fields and their types. FastAPI validates incoming JSON against this model. If the client sends invalid data, they get a detailed error response without you writing any validation code.

The | None = None syntax is Python 3.10+ union syntax. Use Optional[str] = None if you are on an earlier version.

Response Models

You can also control what the API returns using response models:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class UserOut(BaseModel):
    username: str
    email: str

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

@app.post("/users/", response_model=UserOut)
def create_user(user: UserIn):
    # In a real app, you would hash the password here
    return user

Even though the input includes a password, the response model ensures it is never sent back to the client.

Async Endpoints

FastAPI supports async endpoints natively. When your handler is async, FastAPI runs it in an async event loop:

from fastapi import FastAPI
import asyncio

app = FastAPI()

@app.get("/slow")
async def slow_endpoint():
    await asyncio.sleep(2)
    return {"message": "Done waiting!"}

This is particularly useful when your endpoint calls external APIs or databases, as you can handle many concurrent requests without blocking.

Error Handling

FastAPI provides several ways to handle errors. The most common is HTTPException:

from fastapi import FastAPI, HTTPException

app = FastAPI()

items = {"1": "Item One", "2": "Item Two"}

@app.get("/items/{item_id}")
def get_item(item_id: str):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item": items[item_id]}

You can also create custom exception handlers for business logic errors that need to return specific HTTP status codes.

Dependency Injection

FastAPI has a powerful dependency injection system. It is useful for sharing logic across endpoints, such as database connections or authentication:

from fastapi import FastAPI, Depends

app = FastAPI()

def get_db():
    # This would open a real database connection
    return {"db": "fake connection"}

@app.get("/items/")
def list_items(db = Depends(get_db)):
    return {"items": ["Item 1", "Item 2"], "db": db}

@app.post("/items/")
def create_item(item: dict, db = Depends(get_db)):
    return {"item": item, "db": db}

Dependencies can also modify request processing, for example by checking authentication headers and raising an error if they are missing.

What’s Next?

FastAPI has many more features to explore. You can add CORS middleware for cross-origin requests, set up WebSocket endpoints for real-time communication, and integrate with OAuth2 for authentication. The framework integrates well with other tools in the Python ecosystem, including SQLAlchemy for databases and async libraries like asyncpg and aiomysql.

Start with the basics shown here, then layer in these advanced features as your API grows. The automatic documentation at /docs makes it easy to test your endpoints as you build them.

See Also