Difficulty: Advanced
Building REST APIs with Flask starts with returning JSON responses. While Flask can serve HTML templates, modern web development often separates the frontend (React, Vue, mobile apps) from the backend (Flask API). The API communicates using JSON, a lightweight data format understood by virtually every programming language.
Flask provides two ways to return JSON. The simplest is returning a dict from a view function, which Flask automatically converts to JSON with the correct Content-Type header (application/json). The explicit way is using jsonify(), which provides more flexibility: it can handle top-level arrays, custom kwargs, and lets you chain with make_response for custom headers.
API responses should follow consistent conventions. Every response should include meaningful data, appropriate HTTP status codes, and a consistent structure. A common pattern is to always include a top-level key for the data and optional metadata like pagination info, error details, or request ID.
HTTP status codes communicate the result semantically. 200 OK for successful GETs. 201 Created for successful POSTs. 204 No Content for successful DELETEs. 400 Bad Request for invalid input. 401 Unauthorized for missing authentication. 403 Forbidden for insufficient permissions. 404 Not Found for missing resources. 422 Unprocessable Entity for validation failures. 500 Internal Server Error for server bugs.
Content negotiation is the process of serving different response formats based on the client's Accept header. While most APIs serve only JSON, some need to support XML, CSV, or other formats. Flask's request.accept_mimetypes lets you check what the client accepts.
Serialization is converting Python objects to JSON-safe representations. Python dicts, lists, strings, numbers, and booleans map directly to JSON. But datetime objects, Decimal, SQLAlchemy models, and custom classes need custom serialization. A common approach is to add a to_dict() method to your models that returns a JSON-safe dictionary.
For complex serialization needs, consider libraries like marshmallow (schema-based serialization/deserialization with validation), pydantic (data validation using type annotations), or Flask-RESTX (swagger-documented API with automatic serialization).
from flask import Flask, jsonify, request
app = Flask(__name__)
books = [
{'id': 1, 'title': 'Flask Web Development', 'author': 'Miguel Grinberg'},
{'id': 2, 'title': 'Two Scoops of Django', 'author': 'Daniel Feldroy'},
]
@app.get('/api/books')
def list_books():
return jsonify(books) # Returns JSON array
@app.get('/api/books/<int:book_id>')
def get_book(book_id):
book = next((b for b in books if b['id'] == book_id), None)
if not book:
return jsonify({'error': 'Book not found'}), 404
return jsonify(book)
@app.post('/api/books')
def create_book():
data = request.get_json()
if not data or 'title' not in data:
return jsonify({'error': 'Title is required'}), 400
book = {'id': len(books) + 1, 'title': data['title'], 'author': data.get('author', 'Unknown')}
books.append(book)
return jsonify(book), 201
GET returns data with 200. POST validates input, creates the resource, and returns it with 201. Missing resources return 404. Invalid input returns 400. jsonify handles the JSON conversion.
from flask import Flask, jsonify
from functools import wraps
app = Flask(__name__)
def api_response(data=None, message='Success', status=200, kwargs):
"""Create a consistent API response."""
response = {
'status': 'success' if status < 400 else 'error',
'message': message,
}
if data is not None:
response['data'] = data
response.update(kwargs)
return jsonify(response), status
@app.get('/api/users')
def list_users():
users = [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}]
return api_response(data=users, message='Users retrieved')
# {"status": "success", "message": "Users retrieved", "data": [...]}
@app.get('/api/users/<int:user_id>')
def get_user(user_id):
if user_id != 1:
return api_response(message='User not found', status=404)
return api_response(data={'id': 1, 'name': 'Alice'})
A helper function ensures every API response follows the same structure. This makes it easy for clients to parse responses consistently. Include status, message, and data fields.
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
db = SQLAlchemy()
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80))
email = db.Column(db.String(120))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
posts = db.relationship('Post', backref='author', lazy='dynamic')
def to_dict(self, include_posts=False):
data = {
'id': self.id,
'name': self.name,
'email': self.email,
'created_at': self.created_at.isoformat(),
}
if include_posts:
data['posts'] = [p.to_dict() for p in self.posts]
return data
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200))
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'title': self.title,
'author_id': self.user_id,
'created_at': self.created_at.isoformat(),
}
# Usage in views:
@app.get('/api/users/<int:user_id>')
def get_user(user_id):
user = User.query.get_or_404(user_id)
return jsonify(user.to_dict(include_posts=True))
Model to_dict() methods serialize SQLAlchemy objects to JSON-safe dictionaries. Handle datetime with .isoformat(). Control nesting depth with optional parameters to avoid infinite recursion.
from flask import Flask, request, jsonify
from functools import wraps
app = Flask(__name__)
def require_json(*required_fields):
"""Decorator to validate JSON request body."""
def decorator(f):
@wraps(f)
def wrapper(*args, kwargs):
data = request.get_json(silent=True)
if data is None:
return jsonify({'error': 'Request must be JSON'}), 400
missing = [field for field in required_fields if field not in data]
if missing:
return jsonify({
'error': 'Missing required fields',
'fields': missing
}), 400
return f(*args, kwargs)
return wrapper
return decorator
@app.post('/api/users')
@require_json('name', 'email')
def create_user():
data = request.get_json()
# data is guaranteed to have 'name' and 'email'
return jsonify({'created': data['name']}), 201
A decorator validates the JSON request body before the view runs. This keeps view functions clean and ensures required fields are present. The decorator returns 400 for invalid requests.
jsonify, JSON Serialization, Content Negotiation, API Design, Status Codes