Agent skill

starlette

Build async web applications and APIs with Starlette 1.0, the lightweight ASGI framework for Python. Use this skill whenever a user wants to create an async Python web app, REST API, WebSocket server, or ASGI application using Starlette. Triggers include mentions of 'Starlette', 'ASGI', async Python web frameworks, or requests to build lightweight async APIs, WebSocket services, streaming responses, or middleware pipelines. Also use when the user is working with FastAPI internals (which is built on Starlette), needs ASGI middleware patterns, or wants a minimal async web server without a full framework. Covers routing, requests/responses, WebSockets, middleware, templates, static files, authentication, lifespan, background tasks, config, testing, schemas, and more.

Stars 479
Forks 38

Install this agent skill to your Project

npx add-skill https://github.com/simonw/research/tree/main/starlette-1-skill

SKILL.md

Starlette 1.0

Starlette is a lightweight ASGI framework/toolkit for building high-performance async web services in Python. Version 1.0 was released on March 22, 2026 — the first stable release after nearly eight years of development. It requires Python 3.10+ and depends only on anyio. It is the foundation for FastAPI and the Python MCP SDK.

Installation

bash
pip install starlette
pip install uvicorn          # ASGI server
pip install starlette[full]  # all optional deps: httpx, jinja2, python-multipart, itsdangerous, pyyaml

1 · Application

The Starlette class ties together routing, middleware, exception handling, and lifespan.

python
from contextlib import asynccontextmanager
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import JSONResponse
from starlette.routing import Route

@asynccontextmanager
async def lifespan(app):
    print("Starting up")
    yield
    print("Shutting down")

async def homepage(request):
    return JSONResponse({"hello": "world"})

app = Starlette(
    debug=True,
    routes=[Route("/", homepage)],
    middleware=[Middleware(SessionMiddleware, secret_key="secret")],
    exception_handlers={},
    lifespan=lifespan,
)

Run with: uvicorn main:app --reload

Starlette constructor parameters (1.0)

Parameter Description
debug Enable debug tracebacks on errors
routes List of Route, WebSocketRoute, Mount, Host
middleware List of Middleware(cls, **kwargs)
exception_handlers Dict mapping status codes or exception types to handlers
lifespan Async context manager for startup/shutdown

1.0 Breaking Change: on_startup, on_shutdown, @app.route(), @app.websocket_route(), @app.middleware(), @app.exception_handler(), on_event(), and add_event_handler() have all been removed. Use lifespan, routes, middleware, and exception_handlers parameters instead.

Storing and accessing state

python
# On the app
app.state.ADMIN_EMAIL = "admin@example.org"

# In an endpoint, access via request.app
async def dashboard(request):
    email = request.app.state.ADMIN_EMAIL
    return JSONResponse({"admin": email})

Using Starlette as a pure ASGI toolkit

You can use any Starlette component standalone, without the Starlette class:

python
from starlette.responses import PlainTextResponse

async def app(scope, receive, send):
    assert scope["type"] == "http"
    response = PlainTextResponse("Hello, world!")
    await response(scope, receive, send)

2 · Routing

Basic routes

python
from starlette.routing import Route, Mount, WebSocketRoute
from starlette.responses import PlainTextResponse

async def homepage(request):
    return PlainTextResponse("Home")

async def user_detail(request):
    username = request.path_params["username"]
    return PlainTextResponse(f"Hello, {username}")

routes = [
    Route("/", homepage),
    Route("/users/{username}", user_detail, methods=["GET"]),
]

Path parameter convertors

Built-in convertors: str (default), int, float, uuid, path.

python
Route("/items/{item_id:int}", get_item)
Route("/files/{file_path:path}", get_file)
Route("/values/{val:float}", get_value)
Route("/objects/{obj_id:uuid}", get_object)

Custom convertors

python
from datetime import datetime
from starlette.convertors import Convertor, register_url_convertor

class DateTimeConvertor(Convertor):
    regex = r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}"

    def convert(self, value: str) -> datetime:
        return datetime.strptime(value, "%Y-%m-%dT%H:%M:%S")

    def to_string(self, value: datetime) -> str:
        return value.strftime("%Y-%m-%dT%H:%M:%S")

register_url_convertor("datetime", DateTimeConvertor())
# Usage: Route("/history/{date:datetime}", history)

Submounting routes

python
from starlette.routing import Mount
from starlette.staticfiles import StaticFiles

routes = [
    Route("/", homepage),
    Mount("/users", routes=[
        Route("/", users_list, methods=["GET", "POST"]),
        Route("/{username}", user_detail),
    ]),
    Mount("/static", app=StaticFiles(directory="static"), name="static"),
]

Reverse URL lookups

python
# In an endpoint — produces a full URL
url = request.url_for("user_detail", username="alice")

# With named mounts
routes = [
    Mount("/users", name="users", routes=[
        Route("/{username}", user_detail, name="user_detail"),
    ])
]
url = request.url_for("users:user_detail", username="alice")

# Without a request (returns path only)
path = app.url_path_for("user_detail", username="alice")

Host-based routing

python
from starlette.routing import Host, Router

api = Router(routes=[...])
routes = [
    Host("api.example.org", app=api, name="api"),
    Host("{subdomain}.example.org", name="sub", app=Router(routes=[...])),
]

WebSocket routing

python
async def ws_endpoint(websocket):
    await websocket.accept()
    await websocket.send_text("Hello, WebSocket!")
    await websocket.close()

routes = [
    WebSocketRoute("/ws", ws_endpoint),
    WebSocketRoute("/ws/{name}", ws_user),
]

Route priority

More specific routes must come before general ones:

python
routes = [
    Route("/users/me", current_user),       # Specific first
    Route("/users/{username}", user_detail), # General second
]

Using Router directly

python
from starlette.routing import Router

app = Router(routes=[
    Route("/", homepage),
    Mount("/api", routes=[...]),
])

3 · Requests

python
from starlette.requests import Request

async def endpoint(request: Request):
    # Method
    method = request.method  # "GET", "POST", etc.

    # URL components
    url = request.url           # string-like, e.g. "http://localhost/path?q=1"
    path = request.url.path     # "/path"
    scheme = request.url.scheme # "http"
    port = request.url.port     # 8000

    # Headers (immutable, case-insensitive multi-dict)
    ct = request.headers["content-type"]

    # Query parameters (immutable multi-dict)
    search = request.query_params["search"]
    # or request.query_params.getlist("tags")

    # Path parameters
    user_id = request.path_params["user_id"]

    # Client address
    if request.client:
        host = request.client.host
        port = request.client.port

    # Cookies
    token = request.cookies.get("session_token")

    # Body (various interfaces)
    body_bytes = await request.body()
    body_json = await request.json()

    # Form data (requires python-multipart)
    async with request.form() as form:
        username = form["username"]

    # Streaming body
    body = b""
    async for chunk in request.stream():
        body += chunk

    # Check client disconnection
    if await request.is_disconnected():
        return

    # Access app instance
    app = request.app

    # Arbitrary state
    request.state.start_time = 1234567890

File uploads

python
async def upload(request):
    async with request.form(max_files=1000, max_fields=1000, max_part_size=1024*1024) as form:
        upload_file = form["file"]  # UploadFile instance
        filename = upload_file.filename       # "photo.jpg"
        content_type = upload_file.content_type  # "image/jpeg"
        size = upload_file.size               # bytes
        contents = await upload_file.read()
        await upload_file.seek(0)
        await upload_file.close()

4 · Responses

Basic response types

python
from starlette.responses import (
    Response,
    PlainTextResponse,
    HTMLResponse,
    JSONResponse,
    RedirectResponse,
    StreamingResponse,
    FileResponse,
)

# Plain response
Response("Hello", media_type="text/plain", status_code=200, headers={"X-Custom": "value"})

# Text
PlainTextResponse("Hello, world!")

# HTML
HTMLResponse("<h1>Hello</h1>")

# JSON
JSONResponse({"key": "value"})

# Redirect (default 307)
RedirectResponse(url="/new-location", status_code=301)

Custom JSON serialization (e.g. orjson)

python
import orjson
from starlette.responses import JSONResponse
from typing import Any

class OrjsonResponse(JSONResponse):
    def render(self, content: Any) -> bytes:
        return orjson.dumps(content)

Streaming response

python
import asyncio
from starlette.responses import StreamingResponse

async def generate():
    for i in range(10):
        yield f"data: {i}\n\n"
        await asyncio.sleep(0.5)

async def stream(request):
    return StreamingResponse(generate(), media_type="text/event-stream")

File response (with HTTP range request support)

python
from starlette.responses import FileResponse

async def download(request):
    return FileResponse(
        path="reports/annual.pdf",
        filename="annual-report.pdf",
        media_type="application/pdf",
        content_disposition_type="attachment",  # or "inline"
    )

FileResponse automatically includes Content-Length, Last-Modified, ETag, and Accept-Ranges: bytes headers.

Setting and deleting cookies

python
from starlette.responses import JSONResponse

async def login(request):
    response = JSONResponse({"ok": True})
    response.set_cookie(
        key="session",
        value="abc123",
        max_age=3600,
        httponly=True,
        secure=True,
        samesite="lax",
    )
    return response

async def logout(request):
    response = JSONResponse({"ok": True})
    response.delete_cookie(key="session")
    return response

5 · WebSockets

python
from starlette.websockets import WebSocket

async def ws_app(scope, receive, send):
    websocket = WebSocket(scope=scope, receive=receive, send=send)
    await websocket.accept()

    # Send data
    await websocket.send_text("hello")
    await websocket.send_bytes(b"\x00\x01")
    await websocket.send_json({"key": "value"})

    # Receive data
    text = await websocket.receive_text()
    data = await websocket.receive_bytes()
    obj = await websocket.receive_json()

    # Iterate over messages (exits on disconnect)
    async for message in websocket.iter_text():
        await websocket.send_text(f"Echo: {message}")

    # Close
    await websocket.close(code=1000, reason="Done")

WebSocket denial response

If you reject before accepting, a 403 is sent automatically. For custom denial:

python
from starlette.responses import JSONResponse

async def ws_endpoint(websocket):
    if not authorized:
        response = JSONResponse({"error": "Unauthorized"}, status_code=401)
        await websocket.send_denial_response(response)
        return
    await websocket.accept()

Or use HTTPException within a Starlette app (before accept()):

python
from starlette.exceptions import HTTPException

async def ws_endpoint(websocket):
    if not is_valid(websocket):
        raise HTTPException(status_code=401, detail="Unauthorized")
    await websocket.accept()
    ...

6 · Class-Based Endpoints

HTTPEndpoint

python
from starlette.endpoints import HTTPEndpoint
from starlette.responses import JSONResponse
from starlette.routing import Route

class UserEndpoint(HTTPEndpoint):
    async def get(self, request):
        return JSONResponse({"users": []})

    async def post(self, request):
        data = await request.json()
        return JSONResponse({"created": data}, status_code=201)

routes = [Route("/users", UserEndpoint)]

Responds with 405 for unimplemented methods automatically.

WebSocketEndpoint

python
from starlette.endpoints import WebSocketEndpoint
from starlette.routing import WebSocketRoute

class EchoEndpoint(WebSocketEndpoint):
    encoding = "text"  # "json", "bytes", or "text"

    async def on_connect(self, websocket):
        await websocket.accept()

    async def on_receive(self, websocket, data):
        await websocket.send_text(f"Echo: {data}")

    async def on_disconnect(self, websocket, close_code):
        pass

routes = [WebSocketRoute("/ws", EchoEndpoint)]

7 · Middleware

Applying middleware

python
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from starlette.middleware.gzip import GZipMiddleware
from starlette.middleware.sessions import SessionMiddleware

middleware = [
    Middleware(TrustedHostMiddleware, allowed_hosts=["example.com", "*.example.com"]),
    Middleware(HTTPSRedirectMiddleware),
    Middleware(CORSMiddleware,
        allow_origins=["https://example.com"],
        allow_methods=["*"],
        allow_headers=["*"],
        allow_credentials=True,
        allow_private_network=False,
        expose_headers=[],
        max_age=600,
    ),
    Middleware(GZipMiddleware, minimum_size=500, compresslevel=9),
    Middleware(SessionMiddleware,
        secret_key="super-secret",
        session_cookie="session",
        max_age=14 * 24 * 3600,
        same_site="lax",
        https_only=True,
    ),
]

app = Starlette(routes=routes, middleware=middleware)

Execution order: ServerErrorMiddleware → user middleware (top-to-bottom) → ExceptionMiddleware → Router → Endpoint.

CORSMiddleware

python
Middleware(CORSMiddleware,
    allow_origins=["https://example.com"],
    allow_origin_regex=r"https://[a-zA-Z0-9-]+\.example\.com",
    allow_methods=["GET", "POST"],
    allow_headers=["Authorization"],
    allow_credentials=True,
    allow_private_network=True,
    expose_headers=["X-Request-Id"],
    max_age=600,
)

For CORS on error responses too, wrap the whole app:

python
app = Starlette(routes=routes)
app = CORSMiddleware(app=app, allow_origins=["*"])

SessionMiddleware

python
async def set_session(request):
    request.session["username"] = "alice"
    return JSONResponse({"ok": True})

async def get_session(request):
    username = request.session.get("username", "anonymous")
    return JSONResponse({"user": username})

BaseHTTPMiddleware (custom middleware)

python
from starlette.middleware.base import BaseHTTPMiddleware

class TimingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        import time
        start = time.time()
        response = await call_next(request)
        duration = time.time() - start
        response.headers["X-Process-Time"] = str(duration)
        return response

middleware = [Middleware(TimingMiddleware)]

Note: BaseHTTPMiddleware has a limitation — contextvars.ContextVar changes from endpoints don't propagate back through the middleware. Use pure ASGI middleware for full control.

Pure ASGI middleware

python
from starlette.types import ASGIApp, Scope, Receive, Send, Message
from starlette.datastructures import MutableHeaders
from starlette.requests import Request

class SecurityHeadersMiddleware:
    def __init__(self, app: ASGIApp) -> None:
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return

        async def send_with_headers(message: Message) -> None:
            if message["type"] == "http.response.start":
                headers = MutableHeaders(scope=message)
                headers.append("X-Content-Type-Options", "nosniff")
                headers.append("X-Frame-Options", "DENY")
            await send(message)

        await self.app(scope, receive, send_with_headers)

Middleware on specific routes

python
from starlette.routing import Mount, Route
from starlette.middleware import Middleware
from starlette.middleware.gzip import GZipMiddleware

routes = [
    Mount("/api", routes=[
        Route("/data", data_endpoint),
    ], middleware=[Middleware(GZipMiddleware)]),
    Route("/health", health_endpoint, middleware=[Middleware(GZipMiddleware)]),
]

8 · Lifespan (Startup & Shutdown)

python
from contextlib import asynccontextmanager
from collections.abc import AsyncIterator
from typing import TypedDict
import httpx

from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import PlainTextResponse
from starlette.routing import Route

class State(TypedDict):
    http_client: httpx.AsyncClient

@asynccontextmanager
async def lifespan(app: Starlette) -> AsyncIterator[State]:
    async with httpx.AsyncClient() as client:
        yield {"http_client": client}
    # Cleanup happens after yield

async def homepage(request: Request[State]) -> PlainTextResponse:
    # Attribute-style access
    client = request.state.http_client
    # Dictionary-style access (type-safe, new in 0.52+)
    client = request.state["http_client"]
    resp = await client.get("https://httpbin.org/get")
    return PlainTextResponse(resp.text)

app = Starlette(lifespan=lifespan, routes=[Route("/", homepage)])

Dictionary-style access also works on WebSocket[State].


9 · Background Tasks

python
from starlette.background import BackgroundTask, BackgroundTasks
from starlette.responses import JSONResponse

async def send_email(to: str, subject: str):
    ...  # send the email

async def log_signup(username: str):
    ...  # log to analytics

# Single task
async def signup(request):
    data = await request.json()
    task = BackgroundTask(send_email, to=data["email"], subject="Welcome!")
    return JSONResponse({"status": "ok"}, background=task)

# Multiple tasks (run sequentially)
async def signup_v2(request):
    data = await request.json()
    tasks = BackgroundTasks()
    tasks.add_task(send_email, to=data["email"], subject="Welcome!")
    tasks.add_task(log_signup, username=data["username"])
    return JSONResponse({"status": "ok"}, background=tasks)

Tasks run after the response is sent. They execute in order; if one raises, subsequent tasks are skipped.


10 · Templates (Jinja2)

Requires: pip install jinja2

python
from starlette.templating import Jinja2Templates
from starlette.staticfiles import StaticFiles
from starlette.routing import Route, Mount

templates = Jinja2Templates(directory="templates")

async def homepage(request):
    return templates.TemplateResponse(request, "index.html", context={"title": "Home"})

routes = [
    Route("/", homepage),
    Mount("/static", StaticFiles(directory="static"), name="static"),
]

In templates, url_for is available automatically:

html
<link href="{{ url_for('static', path='/css/style.css') }}" rel="stylesheet" />

Custom Jinja2 environment

python
import jinja2
from starlette.templating import Jinja2Templates

env = jinja2.Environment(loader=jinja2.FileSystemLoader("templates"), autoescape=True)
templates = Jinja2Templates(env=env)

1.0 Breaking Change: The **env_options parameter and TemplateResponse(name, context) signature have been removed. Use env= parameter and TemplateResponse(request, name, ...) respectively.

Custom filters

python
templates = Jinja2Templates(directory="templates")
templates.env.filters["markdown"] = my_markdown_filter

Context processors

python
def app_context(request):
    return {"app_name": "MyApp", "user": request.state.user}

templates = Jinja2Templates(directory="templates", context_processors=[app_context])

Testing template responses

python
def test_homepage():
    client = TestClient(app)
    response = client.get("/")
    assert response.template.name == "index.html"
    assert "title" in response.context

11 · Static Files

python
from starlette.staticfiles import StaticFiles
from starlette.routing import Mount

routes = [
    Mount("/static", app=StaticFiles(directory="static"), name="static"),
]

Parameters: directory, packages, html (serves index.html for dirs), check_dir, follow_symlink.

HTML mode (SPA-like)

python
Mount("/", app=StaticFiles(directory="build", html=True), name="spa")

Serves index.html for directories and 404.html for missing files.

Package static files

python
Mount("/static", app=StaticFiles(packages=["bootstrap4"]), name="static")
# Or with custom subdirectory:
Mount("/static", app=StaticFiles(packages=[("mypackage", "assets")]), name="static")

12 · Exception Handling

python
from starlette.applications import Starlette
from starlette.exceptions import HTTPException, WebSocketException
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.websockets import WebSocket

async def not_found(request: Request, exc: HTTPException):
    return JSONResponse({"error": exc.detail}, status_code=404)

async def server_error(request: Request, exc: Exception):
    return JSONResponse({"error": "Internal server error"}, status_code=500)

async def http_exception_handler(request: Request, exc: HTTPException):
    return JSONResponse(
        {"detail": exc.detail},
        status_code=exc.status_code,
        headers=exc.headers,
    )

exception_handlers = {
    404: not_found,
    500: server_error,
    HTTPException: http_exception_handler,
}

app = Starlette(routes=routes, exception_handlers=exception_handlers)

Raising exceptions in endpoints

python
from starlette.exceptions import HTTPException

async def get_item(request):
    item_id = request.path_params["item_id"]
    item = db.get(item_id)
    if not item:
        raise HTTPException(status_code=404, detail="Item not found")
    return JSONResponse(item)

WebSocket exceptions

python
from starlette.exceptions import WebSocketException

async def ws_endpoint(websocket):
    await websocket.accept()
    raise WebSocketException(code=1008, reason="Policy violation")

13 · Authentication

python
import base64, binascii
from starlette.authentication import (
    AuthCredentials, AuthenticationBackend, AuthenticationError,
    SimpleUser, requires,
)
from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.responses import PlainTextResponse, JSONResponse
from starlette.routing import Route

class BasicAuthBackend(AuthenticationBackend):
    async def authenticate(self, conn):
        if "Authorization" not in conn.headers:
            return None
        auth = conn.headers["Authorization"]
        try:
            scheme, credentials = auth.split()
            if scheme.lower() != "basic":
                return None
            decoded = base64.b64decode(credentials).decode("ascii")
        except (ValueError, UnicodeDecodeError, binascii.Error):
            raise AuthenticationError("Invalid credentials")
        username, _, password = decoded.partition(":")
        # Verify credentials here...
        return AuthCredentials(["authenticated"]), SimpleUser(username)

async def homepage(request):
    if request.user.is_authenticated:
        return PlainTextResponse(f"Hello, {request.user.display_name}")
    return PlainTextResponse("Hello, anonymous")

@requires("authenticated")
async def dashboard(request):
    return PlainTextResponse("Dashboard")

@requires(["authenticated", "admin"], status_code=404)
async def admin_panel(request):
    return PlainTextResponse("Admin")

@requires("authenticated", redirect="homepage")
async def settings(request):
    return PlainTextResponse("Settings")

def on_auth_error(request, exc):
    return JSONResponse({"error": str(exc)}, status_code=401)

routes = [
    Route("/", homepage, name="homepage"),
    Route("/dashboard", dashboard),
    Route("/admin", admin_panel),
    Route("/settings", settings),
]

middleware = [
    Middleware(AuthenticationMiddleware, backend=BasicAuthBackend(), on_error=on_auth_error),
]

app = Starlette(routes=routes, middleware=middleware)

14 · Configuration

python
from starlette.config import Config
from starlette.datastructures import CommaSeparatedStrings, Secret

config = Config(".env")

DEBUG = config("DEBUG", cast=bool, default=False)
SECRET_KEY = config("SECRET_KEY", cast=Secret)
DATABASE_URL = config("DATABASE_URL")
ALLOWED_HOSTS = config("ALLOWED_HOSTS", cast=CommaSeparatedStrings)

# Prefixed env vars
config = Config(env_prefix="APP_")
DEBUG = config("DEBUG")  # reads APP_DEBUG

# Custom encoding
config = Config(".env", encoding="latin-1")

.env file:

DEBUG=True
SECRET_KEY=my-secret-key
DATABASE_URL=postgresql://user:pass@localhost/db
ALLOWED_HOSTS=127.0.0.1,localhost

Secret values

python
>>> SECRET_KEY
Secret('**********')
>>> str(SECRET_KEY)
'my-secret-key'

Safe testing with environ

python
# tests/conftest.py
from starlette.config import environ
environ["DEBUG"] = "TRUE"
environ["DATABASE_URL"] = "sqlite:///test.db"

15 · Test Client

Requires: pip install httpx

python
from starlette.testclient import TestClient

# Basic usage
client = TestClient(app)
response = client.get("/")
assert response.status_code == 200

# With lifespan support (must use context manager)
with TestClient(app) as client:
    response = client.get("/")

# Custom headers
client.headers = {"Authorization": "Bearer token123"}
response = client.get("/protected")

# POST with JSON
response = client.post("/users", json={"name": "Alice"})

# File upload
with open("photo.jpg", "rb") as f:
    response = client.post("/upload", files={"file": f})

# Disable server exception raising (for testing 500 pages)
client = TestClient(app, raise_server_exceptions=False)

# Custom client address
client = TestClient(app, client=("localhost", 8000))

# Using Trio backend
with TestClient(app, backend="trio") as client:
    response = client.get("/")

Testing WebSockets

python
def test_websocket():
    client = TestClient(app)
    with client.websocket_connect("/ws") as ws:
        ws.send_text("hello")
        data = ws.receive_text()
        assert data == "Echo: hello"
        ws.send_json({"key": "value"})
        result = ws.receive_json()
        ws.close()

Async testing with httpx

python
from httpx import AsyncClient, ASGITransport
import pytest

@pytest.mark.anyio
async def test_app():
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://testserver") as client:
        response = await client.get("/")
        assert response.status_code == 200

16 · Schema Generation (OpenAPI)

Requires: pip install pyyaml

python
from starlette.schemas import SchemaGenerator

schemas = SchemaGenerator(
    {"openapi": "3.0.0", "info": {"title": "My API", "version": "1.0"}}
)

def list_users(request):
    """
    responses:
      200:
        description: A list of users.
        examples:
          [{"username": "alice"}, {"username": "bob"}]
    """
    ...

def openapi_schema(request):
    return schemas.OpenAPIResponse(request=request)

routes = [
    Route("/users", list_users, methods=["GET"]),
    Route("/schema", openapi_schema, include_in_schema=False),
]

Generate schema programmatically:

python
schema = schemas.get_schema(routes=app.routes)

17 · Data Structures

Starlette provides several useful data structures:

python
from starlette.datastructures import (
    URL,
    Headers,
    MutableHeaders,
    QueryParams,
    State,
    UploadFile,
    FormData,
    Address,
    Secret,
    CommaSeparatedStrings,
)

# URL manipulation
url = URL("https://example.com/path?q=1")
url.scheme   # "https"
url.hostname # "example.com"
url.path     # "/path"
new_url = url.replace(port=8080, path="/new")

# Immutable headers
headers = Headers({"content-type": "text/html"})
headers["content-type"]  # "text/html"

# Mutable headers (for middleware)
mutable = MutableHeaders(headers=response.headers)
mutable["X-Custom"] = "value"

# Query params (immutable multi-dict)
params = QueryParams("page=1&tag=python&tag=async")
params["page"]          # "1"
params.getlist("tag")   # ["python", "async"]

# State
state = State()
state.counter = 0
state.counter  # 0

18 · Thread Pool

Starlette automatically runs synchronous code in a thread pool:

python
# Sync endpoint — automatically runs in thread pool
def sync_homepage(request):
    import time
    time.sleep(1)  # won't block the event loop
    return PlainTextResponse("Done")

Route("/", sync_homepage)

Adjust the thread pool size:

python
import anyio.to_thread

limiter = anyio.to_thread.current_default_thread_limiter()
limiter.total_tokens = 100  # default is 40

Applies to: sync endpoints, FileResponse, UploadFile, sync BackgroundTask functions.


19 · HTTP Status Codes

python
from starlette.status import (
    HTTP_200_OK,
    HTTP_201_CREATED,
    HTTP_204_NO_CONTENT,
    HTTP_301_MOVED_PERMANENTLY,
    HTTP_400_BAD_REQUEST,
    HTTP_401_UNAUTHORIZED,
    HTTP_403_FORBIDDEN,
    HTTP_404_NOT_FOUND,
    HTTP_500_INTERNAL_SERVER_ERROR,
    WS_1000_NORMAL_CLOSURE,
    WS_1008_POLICY_VIOLATION,
)

20 · Complete Application Example

python
from contextlib import asynccontextmanager
from collections.abc import AsyncIterator
from typing import TypedDict

import httpx
from starlette.applications import Starlette
from starlette.authentication import (
    AuthCredentials, AuthenticationBackend, SimpleUser, requires,
)
from starlette.background import BackgroundTask
from starlette.config import Config
from starlette.datastructures import Secret
from starlette.exceptions import HTTPException
from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse, PlainTextResponse
from starlette.routing import Route, Mount, WebSocketRoute
from starlette.staticfiles import StaticFiles
from starlette.templating import Jinja2Templates
from starlette.websockets import WebSocket

# --- Config ---
config = Config(".env")
DEBUG = config("DEBUG", cast=bool, default=False)
SECRET = config("SECRET_KEY", cast=Secret)

# --- Lifespan with typed state ---
class AppState(TypedDict):
    http_client: httpx.AsyncClient

@asynccontextmanager
async def lifespan(app: Starlette) -> AsyncIterator[AppState]:
    async with httpx.AsyncClient() as client:
        yield {"http_client": client}

# --- Auth ---
class TokenAuthBackend(AuthenticationBackend):
    async def authenticate(self, conn):
        token = conn.headers.get("Authorization", "")
        if token == "Bearer valid-token":
            return AuthCredentials(["authenticated"]), SimpleUser("admin")
        return None

# --- Templates ---
templates = Jinja2Templates(directory="templates")

# --- Endpoints ---
async def homepage(request: Request[AppState]):
    client = request.state["http_client"]
    return templates.TemplateResponse(request, "index.html")

@requires("authenticated")
async def api_data(request: Request[AppState]):
    client = request.state["http_client"]
    resp = await client.get("https://httpbin.org/json")
    return JSONResponse(resp.json())

async def create_item(request):
    data = await request.json()
    task = BackgroundTask(process_item, item=data)
    return JSONResponse({"status": "accepted"}, status_code=202, background=task)

async def process_item(item: dict):
    ...  # heavy processing

async def ws_echo(websocket: WebSocket):
    await websocket.accept()
    async for message in websocket.iter_text():
        await websocket.send_text(f"Echo: {message}")

async def not_found(request, exc):
    return JSONResponse({"error": "Not found"}, status_code=404)

# --- App assembly ---
routes = [
    Route("/", homepage, name="homepage"),
    Route("/api/data", api_data, methods=["GET"]),
    Route("/api/items", create_item, methods=["POST"]),
    WebSocketRoute("/ws", ws_echo),
    Mount("/static", StaticFiles(directory="static"), name="static"),
]

middleware = [
    Middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"]),
    Middleware(SessionMiddleware, secret_key=str(SECRET)),
    Middleware(AuthenticationMiddleware, backend=TokenAuthBackend()),
]

app = Starlette(
    debug=DEBUG,
    routes=routes,
    middleware=middleware,
    exception_handlers={404: not_found},
    lifespan=lifespan,
)

Quick Reference: What Was Removed in 1.0

Removed API Replacement
on_startup / on_shutdown params lifespan async context manager
@app.on_event("startup") lifespan
@app.route() / @app.websocket_route() Route / WebSocketRoute in routes=
@app.middleware() Middleware() in middleware=
@app.exception_handler() exception_handlers= dict
Jinja2Templates(**env_options) Jinja2Templates(env=jinja2.Environment(...))
TemplateResponse(name, context) TemplateResponse(request, name, ...)
FileResponse(method=...) Removed (was deprecated)

Didn't find tool you were looking for?

Be as detailed as possible for better results