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:
Request and Response objects and leaves parsing to you, while FastAPI parses and validates the body from type hints.Depends, while Starlette has none.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'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.
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.
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.
Encore works with either when you want databases and buckets running locally and provisioned in your own cloud account.
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.
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.
Slightly, because FastAPI adds validation and serialization work on each request. The difference is small next to database and network time in most APIs.
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.