DEV Community

Cover image for FastAPI is Overkill: Starlette and Pydantic Are All You Really Need
Leapcell
Leapcell

Posted on

3 1 1 1 1

FastAPI is Overkill: Starlette and Pydantic Are All You Really Need

Image description

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"}
Enter fullscreen mode Exit fullscreen mode

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"})
Enter fullscreen mode Exit fullscreen mode

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"})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Image description

🚀 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.

Image description

📖 Explore Our Documentation

🔹 Follow us on Twitter: @LeapcellHQ

Quadratic AI

Quadratic AI – The Spreadsheet with AI, Code, and Connections

  • AI-Powered Insights: Ask questions in plain English and get instant visualizations
  • Multi-Language Support: Seamlessly switch between Python, SQL, and JavaScript in one workspace
  • Zero Setup Required: Connect to databases or drag-and-drop files straight from your browser
  • Live Collaboration: Work together in real-time, no matter where your team is located
  • Beyond Formulas: Tackle complex analysis that traditional spreadsheets can't handle

Get started for free.

Watch The Demo 📊✨

Top comments (0)

Jetbrains Survey

Calling all developers!

Participate in the Developer Ecosystem Survey 2025 and get the chance to win a MacBook Pro, an iPhone 16, or other exciting prizes. Contribute to our research on the development landscape.

Take the survey

AWS Security LIVE!

Hosted by security experts, AWS Security LIVE! showcases AWS Partners tackling real-world security challenges. Join live and get your security questions answered.

Tune in to the full event

DEV is partnering to bring live events to the community. Join us or dismiss this billboard if you're not interested. ❤️