Oct 23, 20264 min read

FastAPI vs Starlette

FastAPI is built on Starlette. How the two compare on validation, API docs, dependency injection and overhead, with the same endpoint written in each.

FastAPI and Starlette are not competitors in the usual sense. The FastAPI class is a subclass of Starlette, so every FastAPI app is a Starlette app with more on top, so the question is whether that extra layer helps your service or gets in its way.

The main differences:

  • Starlette gives you Request and Response objects and leaves parsing to you, while FastAPI parses and validates the body from type hints.
  • FastAPI generates an OpenAPI schema and interactive docs, while Starlette has schema helpers but no automatic docs.
  • FastAPI adds a dependency injection system through Depends, while Starlette has none.
  • Starlette has fewer moving parts, which suits proxies and custom frameworks.

FastAPI vs Starlette at a glance

CapabilityEncoreFastAPIStarlette
RoleInfrastructure SDK added to either oneAPI framework built on StarletteASGI toolkit
Request parsingFrom the frameworkPydantic models from type hintsawait request.json()
ValidationFrom the frameworkAutomaticManual
API docsFrom the frameworkOpenAPI and Swagger UI built inNot generated
Dependency injectionFrom the frameworkDependsNone
Middleware and WebSocketsFrom the frameworkInherited from StarletteBuilt in

Starlette: the toolkit

The Starlette project calls it a lightweight ASGI framework or toolkit, and lists WebSockets, background tasks, startup and shutdown events, a test client and middleware such as CORS and GZip. Handlers are async functions that receive a Request, built on the async and await syntax that PEP 492 added to Python:

import os from contextlib import asynccontextmanager import asyncpg from starlette.applications import Starlette from starlette.requests import Request from starlette.responses import JSONResponse from starlette.routing import Route async def create_user(request: Request) -> JSONResponse: body = await request.json() await request.app.state.pool.execute( "INSERT INTO users (email) VALUES ($1)", body["email"] ) return JSONResponse({"email": body["email"]}, status_code=201) @asynccontextmanager async def lifespan(app: Starlette): app.state.pool = await asyncpg.create_pool(os.environ["DATABASE_URL"]) yield await app.state.pool.close() app = Starlette(routes=[Route("/users", create_user, methods=["POST"])], lifespan=lifespan)

A missing email raises a KeyError and returns a 500. Starlette leaves it to you to check the body and return a 400.

FastAPI: Starlette plus types

FastAPI's page on alternatives and inspirations explains why it was built on Starlette and what it adds. The same endpoint returns a 422 with field errors when email is missing, and the route appears in /docs:

import os from contextlib import asynccontextmanager import asyncpg from fastapi import FastAPI, Request from pydantic import BaseModel class Signup(BaseModel): email: str @asynccontextmanager async def lifespan(app: FastAPI): app.state.pool = await asyncpg.create_pool(os.environ["DATABASE_URL"]) yield await app.state.pool.close() app = FastAPI(lifespan=lifespan) @app.post("/users", status_code=201) async def create_user(req: Signup, request: Request) -> dict: await request.app.state.pool.execute( "INSERT INTO users (email) VALUES ($1)", req.email ) return {"email": req.email}

Starlette middleware, routes and responses work inside FastAPI, so you can drop down to Starlette for a streaming endpoint without leaving the app.

Performance

Both run on ASGI servers such as Uvicorn. Starlette does less per request, so it benchmarks a little faster. FastAPI's validation and serialization cost is small next to a database round trip, so the gap rarely matters for a typical API.

Adding infrastructure to either one with Encore

Encore combines the framework you chose with infrastructure declared in Python. Its infrastructure SDK is the encore-dev package, and Starlette or FastAPI keeps routing and request handling. Postgres databases, Pub/Sub topics, object storage buckets, cron jobs and secrets are module-level declarations that Encore discovers and provisions.

In Starlette, the lifespan function starts Encore and the handler uses the declared database instead of a pool on app.state:

from contextlib import asynccontextmanager import encore from encore.storage import postgres from starlette.applications import Starlette from starlette.requests import Request from starlette.responses import JSONResponse from starlette.routing import Route db = postgres.Database("users", migrations="./migrations") async def create_user(request: Request) -> JSONResponse: body = await request.json() await db.exec("INSERT INTO users (email) VALUES ($1)", body["email"]) return JSONResponse({"email": body["email"]}, status_code=201) @asynccontextmanager async def lifespan(app: Starlette): await encore.start() yield app = Starlette(routes=[Route("/users", create_user, methods=["POST"])], lifespan=lifespan)

A FastAPI app uses the same declaration and the same lifespan call, because a FastAPI lifespan is a Starlette lifespan.

encore run starts Postgres locally and applies the migrations, and the CLI reference covers encore test for tests that run against Encore-managed resources.

A deploy provisions the declarations in your own AWS or GCP account.

When an agent adds a bucket to a webhook handler, the declaration and the code that uses it are one change that runs locally before review. Cloud access sits with Encore's IAM role, not the agent, and the AI integration guide covers how to share these conventions with an agent.

Python support is in beta, and you can request access.

Try Encore →

How to choose

  • FastAPI for JSON APIs where validation and generated docs save work.
  • Starlette for proxies and small services where you want the plain request and response objects.

Encore works with either when you want databases and buckets running locally and provisioned in your own cloud account.

Frequently asked questions

What is the difference between FastAPI and Starlette?

Starlette is a lightweight ASGI toolkit that handles routing, requests, responses, middleware and WebSockets. FastAPI subclasses Starlette and adds validation from type hints and generated OpenAPI docs on top.

Should I use FastAPI or Starlette?

Use FastAPI for JSON APIs where validation and docs save work. Use Starlette directly for proxies and custom frameworks where you want the request and response objects without the extra layer.

Is Starlette faster than FastAPI?

Slightly, because FastAPI adds validation and serialization work on each request. The difference is small next to database and network time in most APIs.

How do I add a Postgres database to a Starlette app?

Install the encore-dev package, declare postgres.Database at module scope and call await encore.start() in the lifespan function. Handlers then call db.exec and db.query directly. encore run starts Postgres locally, and a deploy provisions the database in your own AWS or GCP account.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders