Under the Hood: FastAPI Is Just Starlette + Pydantic
Daniel Hayes
Full-Stack Engineer ยท Leapcell

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
๐ Build with Your Favorite Language
Develop effortlessly in JavaScript, Python, Go, or Rust.
๐ Deploy Unlimited Projects for Free
Only pay for what you useโno requests, no charges.
โก Pay-as-You-Go, No Hidden Costs
No idle fees, just seamless scalability.
๐ Explore Our Documentation
๐น Follow us on Twitter: @LeapcellHQ