I remember when I first tried to build a REST API. I had no idea where to start. The internet threw terms at me like “endpoint,” “middleware,” “serialization,” and “routing.” I felt lost. Then I found that Python makes this whole thing simple. The language gives you many libraries, each designed for a different kind of job. Some are tiny and fast. Others are big and full of features. All of them help you connect one piece of software to another over the web.
In this article I will walk you through six Python libraries that I have used myself to build REST APIs. I will show you real code and explain each step as if we were sitting at the same desk. I will keep the words plain and the examples short. By the end you will know which library fits your next project.
Flask – The Minimalist That Grows With You
Flask is a micro-framework. That means it comes with only the essential parts. No database layer. No user authentication. No form validation. Just a way to connect a URL to a Python function and return some data.
Why would anyone choose such a bare tool? Because you get total control. You decide what extras to install. You never carry unused code. This makes Flask very easy to learn. I built my first API with Flask in about twenty minutes.
Let me show you how it looks.
from flask import Flask, jsonify, request
app = Flask(__name__)
# A simple list of items to act as our database
items = [
{"id": 1, "name": "Apple", "price": 0.99},
{"id": 2, "name": "Banana", "price": 0.59}
]
@app.route('/api/items', methods=['GET'])
def get_items():
return jsonify(items)
@app.route('/api/items/<int:item_id>', methods=['GET'])
def get_item(item_id):
item = next((i for i in items if i['id'] == item_id), None)
if item is None:
return jsonify({"error": "Item not found"}), 404
return jsonify(item)
if __name__ == '__main__':
app.run(debug=True)
That is all you need. The @app.route decorator tells Flask what URL path triggers the function. The jsonify function turns your Python dictionary or list into a proper JSON response. I use methods=['GET'] to say this endpoint only handles read requests.
Flask works great for small projects, prototypes, or any situation where you want to keep your code close to pure Python. The extensions library adds things like SQLAlchemy for databases, Flask-Smorest for automatic documentation, or Flask-JWT for authentication. You pick only what you need.
Personally, I use Flask when I need to expose a few endpoints quickly and I do not want to spend time configuring a heavy framework. It is also perfect for embedding an API inside an existing application.
Django REST Framework – The Full Toolbox
Sometimes you need more than a micro-framework. You need a complete system that handles users, permissions, pagination, and complex data relationships. That is where Django REST Framework (DRF) comes in.
DRF sits on top of Django, which is a full-featured web framework. Django already gives you an object-relational mapper (ORM), an admin panel, and a user system. DRF adds the REST API layer on top.
I use DRF for projects that have many related database tables. For example, an e-commerce site with products, categories, customers, and orders. The serializers in DRF make it easy to convert these models into JSON without writing repetitive code.
Here is a simple example:
# models.py
from django.db import models
class Item(models.Model):
name = models.CharField(max_length=100)
price = models.DecimalField(max_digits=8, decimal_places=2)
# serializers.py
from rest_framework import serializers
from .models import Item
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Item
fields = ['id', 'name', 'price']
# views.py
from rest_framework import generics
from .models import Item
from .serializers import ItemSerializer
class ItemList(generics.ListCreateAPIView):
queryset = Item.objects.all()
serializer_class = ItemSerializer
class ItemDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Item.objects.all()
serializer_class = ItemSerializer
# urls.py
from django.urls import path
from .views import ItemList, ItemDetail
urlpatterns = [
path('api/items/', ItemList.as_view()),
path('api/items/<int:pk>/', ItemDetail.as_view()),
]
Notice how little actual code I wrote. The ModelSerializer looks at your database model and figures out the fields automatically. The generic views (ListCreateAPIView, RetrieveUpdateDestroyAPIView) handle GET, POST, PUT, DELETE with almost no effort.
DRF also gives you a browsable API. When you open the endpoint in a browser, you see a nice interface where you can test the endpoints directly. This is a huge help during development and debugging.
The tradeoff is that you must learn Django first. Django itself has a lot of conventions. If you are building a large, data-heavy application with a team, the learning investment pays off quickly. For a tiny one-off script, it might feel like overkill.
FastAPI – Speed and Automatic Documentation
FastAPI is my go‑to library when I want performance and modern Python features. It uses Python type hints to validate incoming data and generate OpenAPI documentation automatically.
I remember the first time I tried FastAPI. I wrote a function that said item_id: int and the framework automatically rejected a request if someone sent a string. No extra code needed. That level of validation saves hours of debugging.
FastAPI also supports asynchronous programming. That means it can handle many requests at the same time without waiting for one to finish before starting another. This is especially useful for APIs that call external services or wait for database queries.
Here is a classic example:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
# Define a data model using Pydantic
class Item(BaseModel):
name: str
price: float
# In-memory storage
items_db = {}
@app.post("/items/")
def create_item(item: Item):
item_id = len(items_db) + 1
items_db[item_id] = item
return {"id": item_id, **item.dict()}
@app.get("/items/{item_id}")
def read_item(item_id: int):
if item_id not in items_db:
raise HTTPException(status_code=404, detail="Item not found")
return {"id": item_id, **items_db[item_id].dict()}
The Item class inherits from BaseModel. This is a Pydantic model. It checks that name is a string and price is a float. If you send a request with price: "abc", FastAPI returns a 422 error with a clear message about what went wrong.
FastAPI also produces two documentation pages automatically: Swagger UI and ReDoc. You do not need to write any special comments. Just start the server and visit /docs. I use this all the time to share my API with frontend developers. They can see every endpoint, try it out, and copy the curl commands.
Performance-wise, FastAPI is very fast. It runs on Starlette (an ASGI framework) and uses asynchronous I/O. In benchmarks, it often matches Node.js or Go for simple JSON APIs. For most web applications, that is more than enough.
The main drawback is that FastAPI is relatively new. The ecosystem of extensions is smaller than Flask or DRF. But it is growing fast. I would choose FastAPI for any new project where I control the full stack and want modern Python features.
Falcon – Lean and Mean for High Throughput
Sometimes you need an API that handles thousands of requests per second on a small server. That is the niche Falcon fills. It strips away everything that is not directly related to processing requests and sending responses.
Falcon uses a class-based design. Each resource is a class with methods for each HTTP verb. The library does not have an ORM, templating, or admin panels. It focuses on letting you write performance‑sensitive code with minimal overhead.
Here is a simple Falcon application:
import falcon
class ItemResource:
def on_get(self, req, resp, item_id):
# Simple response
resp.media = {"id": item_id, "name": "Sample Item"}
resp.status = falcon.HTTP_200
def on_post(self, req, resp):
# Read JSON from request body
data = req.media
resp.media = {"received": data}
resp.status = falcon.HTTP_201
app = falcon.App()
app.add_route('/items/{item_id}', ItemResource())
app.add_route('/items', ItemResource())
Notice that I did not use any decorators. The route is added in a separate step with add_route. The on_get and on_post methods map directly to HTTP verbs. The resp.media attribute automatically serializes dictionaries to JSON.
Falcon also has a request‑response lifecycle called “hooks.” You can write functions that run before or after a method. This is useful for authentication, logging, or input validation. But the design stays lean.
I have used Falcon for a real‑time data pipeline that needed to ingest sensor data from hundreds of devices. The API ran on a Raspberry Pi and never missed a beat. If you are building an API that must run on constrained hardware or handle extreme traffic, Falcon is worth a look.
The downside is that you have to write more boilerplate for things like validation and serialization. Falcon does not have the automatic documentation that FastAPI provides. You have to add those features yourself.
Bottle – Tiny Enough to Fit in One File
Bottle is my secret weapon for small tasks. It is a single Python file with no dependencies outside the standard library. You can copy it, send it to a colleague, and they can run it without installing anything.
Bottle supports routing, templates, a built‑in HTTP server, and even a simple debugger. It is perfect for embedding an API inside an IoT device, a small script, or a teaching example.
Here is a REST API with Bottle:
from bottle import Bottle, run, request, response
import json
app = Bottle()
# In-memory storage
items = {}
@app.post('/items')
def create_item():
data = request.json
item_id = len(items) + 1
items[item_id] = data
response.status = 201
return {"id": item_id, **data}
@app.get('/items/<item_id>')
def get_item(item_id):
item_id = int(item_id)
if item_id not in items:
response.status = 404
return {"error": "Not found"}
return items[item_id]
run(app, host='localhost', port=8080)
The syntax is very similar to Flask. The main difference is that Bottle does not come with a jsonify function. You can return a dictionary directly, and Bottle converts it to JSON. The route parameters are captured with angle brackets like <item_id>.
Bottle also has a built‑in template engine called SimpleTemplate if you ever need to return HTML. But for REST APIs, you mostly stick with JSON.
I use Bottle when I want to add a quick API to an existing Python script. For example, I had a desktop application that needed to expose some status data. I added five lines of Bottle code and a new endpoint, and it worked. No large framework installation, no complex project structure.
The limitation is that Bottle is single‑threaded by default. For production, you would need to run it behind a proper WSGI server like Gunicorn or uWSGI. Also, the ecosystem is tiny compared to Flask. But for simple needs, it is perfect.
Hug – APIs from Plain Python Functions
Hug takes a different approach. Instead of decorating routes with URL paths, you decorate regular Python functions and let Hug figure out the URL based on the function name. The goal is to keep your code looking like normal Python.
Hug also supports type annotations for automatic validation, just like FastAPI. You can write a function, add type hints, and Hug generates the REST endpoint and documentation.
Here is an example:
import hug
@hug.get('/items/{item_id}')
def get_item(item_id: int):
return {"id": item_id, "name": "Sample"}
@hug.post('/items')
def create_item(body: hug.types.text):
# body is the raw request body as a string
return {"received": body}
Hug uses the function’s parameter type hints to validate input. If you specify item_id: int, it will reject non‑integer values automatically. The body parameter with hug.types.text captures the raw request body.
One feature I really like is that Hug can also work as a command‑line interface or an HTTP API. The same function can be called from the terminal or from a web request. This is handy for testing.
Hug also generates documentation, though not as polished as FastAPI’s Swagger. It provides a simple API documentation page at /documentation.
I used Hug for a small internal service that processed CSV files. The functions were easy to test because they were just plain Python functions. I did not need to spin up a server during unit tests.
The main disadvantage is that Hug is not as actively maintained as FastAPI or Flask. Its community is smaller. For a new project, I would lean toward FastAPI. But Hug is a neat demonstration of how close to pure Python an API framework can stay.
Choosing the Right Library for Your Project
You now have six tools in your belt. Each one shines in a different situation.
- Flask is for when you want a simple start and the freedom to add components as you go.
- Django REST Framework is for large, data‑heavy applications where you need an admin interface, authentication, and relational models.
- FastAPI is for modern projects where performance, automatic validation, and interactive documentation matter.
- Falcon is for high‑traffic APIs that run on limited hardware.
- Bottle is for tiny APIs embedded inside other scripts or devices.
- Hug is for developers who want to keep their code as normal Python functions with minimal framework overhead.
I have used all six in real projects. I started with Flask, then moved to DRF for a client’s e‑commerce system, then fell in love with FastAPI for my own side projects. I keep Bottle in my pocket for quick hacks.
The important thing is to pick the library that matches the size and complexity of your task. Do not use a sledgehammer to crack a nut. A micro‑framework will serve you better for a small API. A full framework will save you time when the project grows.
Now go open your editor. Write a few lines of code. Create a REST API. It is easier than you think.