FastAPI is Overkill: Starlette and Pydantic Are All You Really Need
Leapcell: The Best of Serverless Web Hosting Starlette and Pydantic: Building Powerful APIs Without FastAPI In the field of Python web development, FastAPI has received much attention due to its concise and user-friendly features. However, in reality, it is just a high-level encapsulation of Starlette and Pydantic. The official Starlette website showcases its rich features, and Pydantic is renowned for its powerful data validation capabilities. By directly using these two libraries, developers can flexibly build high-performance APIs without relying on the encapsulation of FastAPI. Next, we will elaborate in detail by combining the core functions and features of both. 1. Core Functions of Starlette and Examples 1.1 Asynchronous Request Handling Starlette is based on the ASGI standard and can efficiently handle asynchronous tasks. Comparing it with the way of writing in FastAPI can better reflect its underlying logic: FastAPI Example from fastapi import FastAPI import asyncio # Create a FastAPI application instance app = FastAPI() # Use a decorator to define a GET request route. The function is an asynchronous function and can handle time-consuming operations without blocking other requests @app.get("/async_items/") async def async_read_items(): await asyncio.sleep(1) # Simulate an I/O operation and pause for 1 second return {"message": "FastAPI asynchronous processing example"} Starlette Example from starlette.applications import Starlette from starlette.responses import JSONResponse import asyncio # Create a Starlette application instance app = Starlette() # Directly define the route on the application instance, specifying the path and request method. The handling function is an asynchronous function @app.route("/async_items/", methods=["GET"]) async def async_read_items(request): await asyncio.sleep(1) # Simulate an I/O operation and pause for 1 second return JSONResponse({"message": "Starlette asynchronous processing example"}) As can be seen, FastAPI simplifies the route definition through decorators, while Starlette is closer to the native ASGI operations. It directly defines routes on the application instance, providing more flexibility. 1.2 Use of Middleware Starlette supports a rich variety of middleware. For example, adding a simple logging middleware, which needs to be implemented through specific dependency injection in FastAPI: Starlette Middleware Example from starlette.applications import Starlette from starlette.responses import JSONResponse from starlette.middleware.base import BaseHTTPMiddleware import logging # Configure the logger logger = logging.getLogger(__name__) # Custom logging middleware, inheriting from BaseHTTPMiddleware class LoggingMiddleware(BaseHTTPMiddleware): async def dispatch(self, request, call_next): # Log the request information, including the request method and URL logger.info(f"Request: {request.method} {request.url}") # Continue to process the request and get the response response = await call_next(request) # Log the response status code logger.info(f"Response: {response.status_code}") return response # Create a Starlette application instance and pass in the middleware instance app = Starlette(middleware=[LoggingMiddleware(app)]) # Define the route handling function @app.route("/middleware_example/", methods=["GET"]) async def middleware_example(request): return JSONResponse({"message": "The middleware is in effect"}) To achieve similar functionality in FastAPI, it is necessary to rely on custom dependency functions and global dependency configurations. In comparison, the way of using middleware in Starlette is more intuitive and closer to the underlying ASGI specification. 1.3 WebSocket Support Starlette natively supports WebSocket. Here is a simple example of a WebSocket chat: from starlette.applications import Starlette from starlette.websockets import WebSocket, WebSocketDisconnect import json # Create a Starlette application instance app = Starlette() # Store the WebSocket objects of connected clients connected_clients = [] # Define the WebSocket route handling function @app.websocket_route("/ws") async def websocket_endpoint(websocket: WebSocket): await websocket.accept() # Accept the WebSocket connection connected_clients.append(websocket) # Add the connected client to the list try: while True: # Receive the text data sent by the client data = await websocket.receive_text() message = json.loads(data) # Parse the received JSON string into a Python object for client in connected_clients: if client != websocket: # Forward the message to other clients except the sender await client.send_text(json.dumps(message)) except WebSocketDisconnect: conne

Leapcell: The Best of Serverless Web Hosting
Starlette and Pydantic: Building Powerful APIs Without FastAPI
In the field of Python web development, FastAPI has received much attention due to its concise and user-friendly features. However, in reality, it is just a high-level encapsulation of Starlette and Pydantic. The official Starlette website showcases its rich features, and Pydantic is renowned for its powerful data validation capabilities. By directly using these two libraries, developers can flexibly build high-performance APIs without relying on the encapsulation of FastAPI. Next, we will elaborate in detail by combining the core functions and features of both.
1. Core Functions of Starlette and Examples
1.1 Asynchronous Request Handling
Starlette is based on the ASGI standard and can efficiently handle asynchronous tasks. Comparing it with the way of writing in FastAPI can better reflect its underlying logic:
FastAPI Example
from fastapi import FastAPI
import asyncio
# Create a FastAPI application instance
app = FastAPI()
# Use a decorator to define a GET request route. The function is an asynchronous function and can handle time-consuming operations without blocking other requests
@app.get("/async_items/")
async def async_read_items():
await asyncio.sleep(1) # Simulate an I/O operation and pause for 1 second
return {"message": "FastAPI asynchronous processing example"}
Starlette Example
from starlette.applications import Starlette
from starlette.responses import JSONResponse
import asyncio
# Create a Starlette application instance
app = Starlette()
# Directly define the route on the application instance, specifying the path and request method. The handling function is an asynchronous function
@app.route("/async_items/", methods=["GET"])
async def async_read_items(request):
await asyncio.sleep(1) # Simulate an I/O operation and pause for 1 second
return JSONResponse({"message": "Starlette asynchronous processing example"})
As can be seen, FastAPI simplifies the route definition through decorators, while Starlette is closer to the native ASGI operations. It directly defines routes on the application instance, providing more flexibility.
1.2 Use of Middleware
Starlette supports a rich variety of middleware. For example, adding a simple logging middleware, which needs to be implemented through specific dependency injection in FastAPI:
Starlette Middleware Example
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
import logging
# Configure the logger
logger = logging.getLogger(__name__)
# Custom logging middleware, inheriting from BaseHTTPMiddleware
class LoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
# Log the request information, including the request method and URL
logger.info(f"Request: {request.method} {request.url}")
# Continue to process the request and get the response
response = await call_next(request)
# Log the response status code
logger.info(f"Response: {response.status_code}")
return response
# Create a Starlette application instance and pass in the middleware instance
app = Starlette(middleware=[LoggingMiddleware(app)])
# Define the route handling function
@app.route("/middleware_example/", methods=["GET"])
async def middleware_example(request):
return JSONResponse({"message": "The middleware is in effect"})
To achieve similar functionality in FastAPI, it is necessary to rely on custom dependency functions and global dependency configurations. In comparison, the way of using middleware in Starlette is more intuitive and closer to the underlying ASGI specification.
1.3 WebSocket Support
Starlette natively supports WebSocket. Here is a simple example of a WebSocket chat:
from starlette.applications import Starlette
from starlette.websockets import WebSocket, WebSocketDisconnect
import json
# Create a Starlette application instance
app = Starlette()
# Store the WebSocket objects of connected clients
connected_clients = []
# Define the WebSocket route handling function
@app.websocket_route("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept() # Accept the WebSocket connection
connected_clients.append(websocket) # Add the connected client to the list
try:
while True:
# Receive the text data sent by the client
data = await websocket.receive_text()
message = json.loads(data) # Parse the received JSON string into a Python object
for client in connected_clients:
if client != websocket:
# Forward the message to other clients except the sender
await client.send_text(json.dumps(message))
except WebSocketDisconnect:
connected_clients.remove(websocket) # Remove the client from the list when the connection is disconnected
Although FastAPI also supports WebSocket, the implementation details are similar to those of Starlette. Starlette directly exposes the WebSocket processing interface, which is more convenient for developers to make in-depth customizations.
2. Application of Pydantic in Starlette
2.1 Data Validation and Serialization
Using Pydantic for data validation in Starlette, compared with FastAPI:
FastAPI Example
from fastapi import FastAPI
from pydantic import BaseModel
# Create a FastAPI application instance
app = FastAPI()
# Use Pydantic to define a data model for validating and serializing data
class Item(BaseModel):
name: str
price: float
# Define the route handling function. FastAPI will automatically validate the incoming data and serialize the response
@app.post("/fastapi_items/")
async def create_fastapi_item(item: Item):
return item
Starlette Example
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.requests import Request
from pydantic import BaseModel
# Create a Starlette application instance
app = Starlette()
# Use Pydantic to define a data model for validating and serializing data
class Item(BaseModel):
name: str
price: float
# Define the route handling function and manually handle the request data and validation logic
@app.route("/starlette_items/", methods=["POST"])
async def create_starlette_item(request: Request):
data = await request.json() # Get the JSON data from the request
try:
item = Item(**data) # Use Pydantic to validate the data. If it is not valid, an exception will be thrown
except ValueError as e:
return JSONResponse({"error": str(e)}, status_code=400) # Return an error response if the validation fails
return JSONResponse(item.dict()) # Return the serialized response if the validation passes
FastAPI will automatically handle data validation and error return, while in Starlette, developers need to manually catch exceptions and handle them. However, this approach also gives developers more control.
2.2 Complex Data Models and Nested Validation
When dealing with complex data models, the advantages of Pydantic become more obvious:
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.requests import Request
from pydantic import BaseModel
# Create a Starlette application instance
app = Starlette()
# Define the address data model
class Address(BaseModel):
street: str
city: str
zip_code: str
# Define the user data model, which contains a nested address model
class User(BaseModel):
username: str
email: str
address: Address
# Define the route handling function to handle the validation and storage of user data
@app.route("/users/", methods=["POST"])
async def create_user(request: Request):
data = await request.json() # Get the JSON data from the request
try:
user = User(**data) # Use Pydantic to validate the nested data. If it is not valid, an exception will be thrown
except ValueError as e:
return JSONResponse({"error": str(e)}, status_code=400) # Return an error response if the validation fails
return JSONResponse(user.dict()) # Return the serialized response if the validation passes
Whether it is Starlette or FastAPI, Pydantic can efficiently handle the validation of nested data structures to ensure the integrity and accuracy of the data.
3. Deep Integration of Starlette and Pydantic
By combining the routing and middleware of Starlette with the data validation of Pydantic, we can build a fully functional API:
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.requests import Request
from starlette.exceptions import HTTPException
from starlette.middleware.cors import CORSMiddleware
from pydantic import BaseModel
# Create a Starlette application instance
app = Starlette()
# Add CORS middleware to allow requests from all origins (in a production environment, specific domain names should be restricted)
app.add_middleware(CORSMiddleware, allow_origins=["*"])
# Use Pydantic to define the product data model
class Product(BaseModel):
name: str
price: float
quantity: int
# List to store product data
products = []
# Define the route handling function for creating products
@app.route("/products/", methods=["POST"])
async def create_product(request: Request):
data = await request.json() # Get the JSON data from the request
try:
product = Product(**data) # Use Pydantic to validate the data. If it is not valid, an exception will be thrown
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e)) # Return an HTTP exception if the validation fails
products.append(product.dict()) # Add the product data to the list if the validation passes
return JSONResponse(product.dict()) # Return the created product data
# Define the route handling function for getting all products
@app.route("/products/", methods=["GET"])
async def get_products(request):
return JSONResponse(products) # Return all product data
This example demonstrates the complete process of Starlette handling routing, cross-origin issues (through middleware), and Pydantic performing data validation and serialization. Compared with FastAPI, although it lacks functions such as automatically generating documentation, developers can flexibly choose third-party libraries for expansion according to actual needs, such as using drf-spectacular
or apispec
to generate API documentation.
Conclusion
The combination of Starlette and Pydantic can build high-performance and feature-rich APIs without relying on the encapsulation of FastAPI. Starlette provides a flexible ASGI application foundation, supporting core functions such as asynchronous processing, middleware, and WebSocket; Pydantic focuses on data validation and serialization. Although FastAPI simplifies the development process, directly using Starlette and Pydantic allows developers to have a deeper understanding of the underlying principles, make highly customized adjustments according to project requirements, and show stronger adaptability in complex scenarios.
Leapcell: The Best of Serverless Web Hosting
Finally, I would like to recommend a platform that is most suitable for deploying Python services: Leapcell