API Advice
Choosing your Python REST API framework
Nolan Di Mare Sullivan
December 9, 2024
We’re fortunate to live in a time when a wide selection of Python API frameworks is available to us. But an abundance of choice can also be overwhelming. Do you go for the latest, trending option or stick with the tried-and-tested framework that offers security and control?
Whether you’re a startup founder who needs to deliver an MVP in a few weeks while taking scale and performance into consideration, or part of a large organization running hundreds of microservices needing reliable and robust technologies, choosing the right API framework is a critical decision. The key is recognizing that every framework choice involves trade-offs, which shift based on your project’s unique needs. Failing to account for this can lead to frustration down the road.
In this post, we discuss the factors to consider when choosing a REST API framework and explores popular options, highlighting each framework’s strengths and weaknesses. At the end of the article, we’ll suggest a pragmatic approach you can take to make an informed decision.
Python Frameworks Evaluated
Factors to consider when choosing a Python API framework
Iteration speed
For startups or fast-moving teams, the pressure to ship an MVP or new features quickly can outweigh concerns about the project’s long-term architecture. But this short-term focus can lead to technical debt, making it harder to scale or adapt the API later.
To strike the right balance between speed and maintainability, it helps to understand when speed is essential and when it’s worth investing time in a more robust foundation. The solution lies in using tools that offer the flexibility to write code quickly while setting aside some initial scalability or performance concerns, with the option to refactor and evolve your architecture as your project grows.
Start with a simple, script-like setup for exposing endpoints without committing to a solid architecture upfront. Once the business is stable, you can take advantage of the framework’s features to transition to a more complex and robust architecture.
Enterprise needs: Scale and security
Your MVP has succeeded, and your project now serves a significant user base. Or maybe you’re operating in an enterprise environment, building a service that must handle thousands or even millions of daily requests. While flexibility is still appealing at this stage, relying on tools that prioritize flexibility over structure is no longer wise. Instead, focus on well-structured frameworks designed to help with scalability, simplify complex processes, and abstract away the challenges introduced by your growing needs.
When choosing a framework for mature or large-scale projects, you need to consider:
- Request volume: The number of requests your application needs to handle.
- Authorization: How to manage user permissions securely and efficiently.
- Database optimization: Ensuring database queries are performant and scalable.
- Logging: Implementing proper logging for monitoring and debugging.
- Performance: Maintaining responsiveness under heavy traffic and load.
While lightweight frameworks can handle these challenges with careful implementation, your top priorities should shift to performance, robustness, and security.
When evaluating frameworks for these needs, consider these three critical factors:
- Framework maturity and adoption: A framework with wide industry adoption can be a sign of reliability. A strong community and long-standing development history often reflect a framework’s stability and available support.
- Security: A framework with many built-in features may introduce security vulnerabilities. Assess the framework’s history of handling security issues, its track record with security updates, and the quality of its documentation.
- Robustness: Evaluate the framework’s architecture for its ability to abstract complex tasks effectively, ensuring scalability and maintainability over time.
Async support
Asynchronous programming is known for its performance benefits, especially in non-blocking operations. For example, imagine an API that handles file uploads: The user doesn’t need the upload to finish immediately or receive a download link right away. They just want confirmation that the process has started and that they’ll be notified of its success or failure later. This is where async frameworks shine, allowing the API to respond without waiting for the file upload to complete.
Synchronous frameworks like Flask or Django can still handle asynchronous-like tasks using background job libraries like Celery paired with tools like Redis or RabbitMQ. While these frameworks have introduced partial async support in their architectures, they are not fully asynchronous yet. Background job solutions like Celery, Redis, and RabbitMQ are robust for task delegation, but they come with additional setup complexity and don’t achieve proper non-blocking behavior within the API.
Frameworks built with async programming in mind, like Tornado and FastAPI, provide a more intuitive coding experience for async tasks.
Popular Python API frameworks
Flask-RESTX: Familiar, lightweight, and flexible
Flask alone is sufficient to build a REST API. However, to add important REST API features like automatic Swagger documentation, serialization, and error handling, Flask-RESTX (opens in a new tab) offers tools that simplify additional parts of your workflow.
Here’s an example that creates an application to list payments:
from flask import Flaskfrom flask_restx import Api, Resource, fieldsapp = Flask(__name__)api = Api(app, doc="/docs") # Swagger UI documentation available at /docsns = api.namespace('payments', description="Payment operations")payment_model = api.model('Payment', {'id': fields.Integer(description="The unique ID of the payment", required=True),'amount': fields.Float(description="The amount of the payment", required=True),'currency': fields.String(description="The currency of the payment", required=True),'status': fields.String(description="The status of the payment", required=True),})# Sample datapayments = [{'id': 1, 'amount': 100.0, 'currency': 'USD', 'status': 'Completed'},{'id': 2, 'amount': 50.5, 'currency': 'EUR', 'status': 'Pending'},{'id': 3, 'amount': 200.75, 'currency': 'GBP', 'status': 'Failed'},]@ns.route('/')class PaymentList(Resource):@ns.marshal_list_with(payment_model)def get(self):return paymentsapi.add_namespace(ns)if __name__ == "__main__":app.run(debug=True)
This code snippet creates an application that runs on port 5000 and provides two endpoints:
/payments
, for listing payments./docs
, for automatically documenting the payments endpoint.
The Flask-RESTX marshaling feature is noteworthy for how it automatically maps the results – whether from a database, file, or API request – to a defined schema and sends a structured response to the client. This functionality ensures consistency and reduces boilerplate code for formatting responses.
The Flask ecosystem gives you the flexibility to create your application in the way that suits your needs. When the time comes to scale, Flask combined with Flask-RESTX (opens in a new tab) provides you with the features you need to handle larger, more complex projects effectively.
Sanic: For lightweight and production-ready real-time APIs
Sanic (not to be confused with Sonic the Hedgehog, though it’s just as speedy) is a lightweight, asynchronous Python web framework designed for high-performance and real-time applications. While these characteristics might suggest complexity, writing an application that serves both an HTTP endpoint and a WebSocket server is surprisingly straightforward.
from sanic import Sanicfrom sanic.response import jsonapp = Sanic("ConfigAPI")configs = {"app_name": "My App","version": "1.0.0","debug": True,"max_connections": 100,"allowed_hosts": ["localhost", "127.0.0.1"],}@app.get("/configs")async def get_configs(request):return json(configs)if __name__ == "__main__":app.run(host="127.0.0.1", port=8000, debug=True)
Sanic intuitively handles static files, making it a user-friendly alternative to popular frameworks like Django, which can require more complex configurations for similar tasks (opens in a new tab).
app = Sanic("ConfigAPI")app.static('/static', './static')
Another point in Sanic’s favor is its interesting approach to handling TLS, a process that can be complicated to understand and set up. With Sanic, you can start your server using your certificate files, or even better, let it automatically set up local TLS certificates, enabling secure access with little configuration.
sanic path.to.server:app \--dev \--auto-tls
FastAPI: Build modern and highly typed REST APIs
FastAPI’s excellent developer experience has made it one of the most popular Python frameworks. By combining async programming, type hints, and automatic OpenAPI document generation, FastAPI enables you to create highly documented APIs with minimal effort.
FastAPI’s design is also async-first, making it an excellent choice for real-time APIs, high-concurrency workloads, and systems needing rapid prototyping with built-in tools. FastAPI offers modern convenience and a healthy ecosystem of complementary tooling without compromising on performance.
The following code example demonstrates creating a REST API for listing and creating invoices.
from fastapi import FastAPI, HTTPExceptionfrom pydantic import BaseModelfrom typing import Listapp = FastAPI()class Invoice(BaseModel):id: intcustomer_uid: stramount: floatstatus: str# In-memory storage for invoicesinvoices = [Invoice(id=1, customer_uid="4r3dd", amount=250.50, status="Paid"),Invoice(id=2, customer_uid="f3f3f3f", amount=150.00, status="Pending"),]@app.get("/invoices", response_model=List[Invoice])async def list_invoices():return invoices
Django REST framework
If what you care about is security, reliability, and maturity, Django REST framework (DRF) (opens in a new tab) is what you want. Django is the most mature Python framework and rose to prominence thanks to its abstractions of the tedious but essential parts of backend development: authentication, authorization, logging, multiple database connections, caching, testing, and much more.
However, this abstraction comes with trade-offs. Django is not especially flexible or lightweight, and its enforced Model-View-Template (MVT) structure can feel verbose and rigid compared to more modern frameworks. However, if you embrace its design principles, Django can be one of the most stable and effective frameworks you’ve ever used.
When it comes to async support, DRF does not currently support async functionality. This limitation means you cannot create async API views or viewsets using DRF, as its core features – like serializers, authentication, permissions, and other utilities – are not designed to work asynchronously.
Third-party package ADRF (Async DRF) (opens in a new tab) adds async support, but it’s not officially supported and may not be stable for production. That undermines the core value of Django REST framework: stability.
To create an API with DRF, you need to define a model first.
from django.db import modelsclass Item(models.Model):name = models.CharField(max_length=255)description = models.TextField()price = models.DecimalField(max_digits=10, decimal_places=2)created_at = models.DateTimeField(auto_now_add=True)def __str__(self):return self.name
Then, you need to define a serializer that will convert the Python object retrieved from the Django ORM to a JSON object and vice versa.
from rest_framework import serializersfrom .models import Itemclass ItemSerializer(serializers.ModelSerializer):class Meta:model = Itemfields = ['id', 'name', 'description', 'price', 'created_at']
Next, you need to write a view (or in standard terms, controller) to handle the API logic, in this case, listing.
from rest_framework.generics import ListCreateAPIViewfrom .models import Itemfrom .serializers import ItemSerializerclass ItemListCreateView(ListCreateAPIView):queryset = Item.objects.all()serializer_class = ItemSerializer
Finally, you need to register the view in a urls.py
file.
from django.urls import pathfrom .views import ItemListCreateViewurlpatterns = [path('items/', ItemListCreateView.as_view(), name='item-list-create'),]
This example illustrates how verbose Django can be. But by following its well-documented architecture, you ensure your application is robust and scalable while following proven design principles.
Tornado: Pure async logic
Tornado is a lightweight framework built entirely around asynchronous programming, making it ideal for building APIs where non-blocking I/O is critical, like WebSocket-based applications or systems with high-concurrency needs. If you don’t have the immediate pressure of needing an extensive feature set or an existing ecosystem, Tornado can be an excellent choice for applications requiring pure async workflows.
from tornado.ioloop import IOLoopfrom tornado.web import Application, RequestHandlerimport json# In-memory storageorders = []# Handler to list all ordersclass OrderListHandler(RequestHandler):async def get(self):self.set_header("Content-Type", "application/json")self.write(json.dumps(orders))# Initialize the Tornado appdef make_app():return Application([(r"/orders", OrderListHandler), # Endpoint to list all orders])if __name__ == "__main__":app = make_app()app.listen(8888)print("Server is running on http://127.0.0.1:8888")IOLoop.current().start()
However, Tornado lacks some of the built-in tools and abstractions found in more modern frameworks like FastAPI, meaning you might spend more time building features available out of the box elsewhere.
Making pragmatic choices
The Python API frameworks we’ve discussed each have distinct strengths and trade-offs, but choosing the right framework for your project might still be a daunting task.
To help you select a framework, we’ve created a flowchart that simplifies the decision-making process and a table that maps use cases to recommended frameworks. To use these resources, start with the flowchart to narrow your options based on your project’s stage, requirements, and priorities. Then, consult the table to match your use case and requirements to recommended frameworks.
Use case | Requirements | Recommended frameworks |
---|---|---|
MVP with limited resources | Quick setup, simplicity, flexibility | Flask-RESTX, FastAPI |
Complex project | Scalability, structure, robust tools | Django + DRF |
Secure enterprise application | Strong security, maintainability, scalability | Django + DRF |
Fully async workload | High concurrency, non-blocking performance | FastAPI, Tornado |
Real-time application | WebSocket support, low latency | Tornado, Sanic |
Existing project | Gradual migration to async or scaling needs | Django (with ASGI), FastAPI |
Consider:
- What does your project need most — stability or speed?
- Are you starting fresh or scaling an existing application?
- Does the framework support your required features without adding unnecessary risk?
- How well does the framework align with your team’s expertise?
If your team has extensive experience with one framework, that might be your go-to for creating a REST API. If stability, reliability, and enterprise-grade features are your priorities, then Django REST framework (DRF) (opens in a new tab) probably makes sense. If your priorities are a modern developer experience, performance, or emerging async capabilities, then a cutting-edge framework like FastAPI is a great choice.