Building a JSON REST API
Building a JSON REST API
Flask is excellent for building JSON APIs — lightweight, explicit, and easy for AI to generate reliably. This tutorial covers response envelopes, API key authentication, and clean route patterns.
Consistent Response Envelope
Every endpoint should return the same shape so clients never have to guess the structure:
from flask import jsonify
def api_response(data=None, error=None, status=200):
"""Wrap any response in a consistent envelope."""
return jsonify({
"data": data,
"error": error,
"status": status
}), status
Usage in routes:
# Success
return api_response(data={"id": 1, "title": "Hello"})
# Not found
return api_response(error="Tutorial not found", status=404)
# Validation error
return api_response(error="Title is required", status=400)
Clients always receive {"data": ..., "error": ..., "status": ...}. The error field is null on success, and data is null on errors.
API Key Authentication
Create the api_keys Table
CREATE TABLE IF NOT EXISTS api_keys (
id SERIAL PRIMARY KEY,
key VARCHAR(64) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL, -- label for this key, e.g. "Mobile App"
active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMP
);
CREATE INDEX idx_api_keys_key ON api_keys(key);
Generate a Key
import secrets
def generate_api_key():
return secrets.token_hex(32) # 64-character hex string
# Run once to create a key:
# python -c "from app import generate_api_key; print(generate_api_key())"
# Then INSERT it into the api_keys table manually
The Auth Decorator
import functools
from flask import request
def require_api_key(f):
@functools.wraps(f)
def decorated(*args, **kwargs):
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return api_response(error="Missing Authorization header", status=401)
key = auth_header[len("Bearer "):]
with get_db() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT id FROM api_keys WHERE key = %s AND active = TRUE",
(key,)
)
api_key = cur.fetchone()
if not api_key:
return api_response(error="Invalid or inactive API key", status=401)
# Update last_used_at (optional, useful for auditing)
with get_db() as conn:
with conn.cursor() as cur:
cur.execute(
"UPDATE api_keys SET last_used_at = NOW() WHERE id = %s",
(api_key["id"],)
)
conn.commit()
return f(*args, **kwargs)
return decorated
Versioned API Routes
Prefix all routes with /api/v1/ so you can introduce /api/v2/ later without breaking existing clients:
API_PREFIX = "/api/v1"
@app.route(f"{API_PREFIX}/tutorials")
@require_api_key
def api_tutorials():
page = request.args.get("page", 1, type=int)
per_page = min(request.args.get("per_page", 20, type=int), 100)
offset = (page - 1) * per_page
with get_db() as conn:
with conn.cursor() as cur:
cur.execute("""
SELECT id, title, slug, excerpt, category, difficulty, estimated_time
FROM tutorials
ORDER BY published_at DESC
LIMIT %s OFFSET %s
""", (per_page, offset))
tutorials = cur.fetchall()
cur.execute("SELECT COUNT(*) FROM tutorials")
total = cur.fetchone()["count"]
return api_response(data={
"tutorials": tutorials,
"page": page,
"per_page": per_page,
"total": total,
"total_pages": (total + per_page - 1) // per_page
})
@app.route(f"{API_PREFIX}/tutorials/<slug>")
@require_api_key
def api_tutorial_detail(slug):
with get_db() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT * FROM tutorials WHERE slug = %s",
(slug,)
)
tutorial = cur.fetchone()
if not tutorial:
return api_response(error="Tutorial not found", status=404)
return api_response(data=tutorial)
Proper HTTP Status Codes
| Code | Meaning | When to Use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST that creates a resource |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Missing or invalid input |
| 401 | Unauthorized | Missing or invalid API key |
| 403 | Forbidden | Valid key but not allowed this action |
| 404 | Not Found | Resource does not exist |
| 409 | Conflict | Duplicate (e.g. unique constraint) |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unexpected server failure |
Creating a Resource (POST)
@app.route(f"{API_PREFIX}/tutorials", methods=["POST"])
@require_api_key
def api_create_tutorial():
body = request.get_json(silent=True)
if not body:
return api_response(error="Request body must be JSON", status=400)
title = body.get("title", "").strip()
slug = body.get("slug", "").strip()
if not title:
return api_response(error="title is required", status=400)
if not slug:
return api_response(error="slug is required", status=400)
try:
with get_db() as conn:
with conn.cursor() as cur:
cur.execute("""
INSERT INTO tutorials (title, slug, excerpt, content, category, difficulty)
VALUES (%s, %s, %s, %s, %s, %s)
RETURNING id
""", (
title, slug,
body.get("excerpt", ""),
body.get("content", ""),
body.get("category", "backend"),
body.get("difficulty", "beginner")
))
new_id = cur.fetchone()["id"]
conn.commit()
except Exception:
return api_response(error="Slug already exists", status=409)
return api_response(data={"id": new_id}, status=201)
Testing With curl
# List tutorials
curl -H "Authorization: Bearer your-api-key-here" \
https://yourapp.com/api/v1/tutorials
# Get a specific tutorial
curl -H "Authorization: Bearer your-api-key-here" \
https://yourapp.com/api/v1/tutorials/flask-authentication
# Create a tutorial
curl -X POST \
-H "Authorization: Bearer your-api-key-here" \
-H "Content-Type: application/json" \
-d "{\"title\": \"New Tutorial\", \"slug\": \"new-tutorial\", \"category\": \"backend\", \"difficulty\": \"beginner\"}" \
https://yourapp.com/api/v1/tutorials
AI Prompt That Generated This
"Build a versioned JSON REST API in Flask under /api/v1/. Include: a consistent api_response() envelope helper, a require_api_key decorator that reads from Authorization: Bearer header and validates against an api_keys table in PostgreSQL using psycopg2, and GET/POST routes for a tutorials resource. Use proper HTTP status codes."
Next Steps
- Add Rate Limiting per API key to prevent abuse
- Write pytest tests for each API endpoint using the test client
Was this helpful?
Get More Flask Vibe Tutorials
Join 1,000+ developers getting weekly Flask tips and AI-friendly code patterns.
No spam. Unsubscribe anytime.