
Python
Beyond basic REST, professional APIs require careful architectural design. Understanding HATEOAS, content negotiation, proper versioning strategies, and URI design patterns separates production APIs from simple endpoints.
Understanding API maturity levels helps design better services:
# Level 0: RPC-style (single endpoint)
POST /api?action=getUser&id=1
POST /api?action=createUser&name=Alice
# Level 1: Resources (multiple endpoints)
GET /users/1
POST /users
PUT /users/1
# Level 2: HTTP verbs (RESTful)
GET /users/1 # Safe, idempotent
POST /users # Not idempotent
PUT /users/1 # Idempotent
DELETE /users/1 # Idempotent
# Level 3: HATEOAS (Hypermedia)
{
"id": 1,
"name": "Alice",
"_links": {
"self": {"href": "/users/1"},
"all": {"href": "/users"},
"posts": {"href": "/users/1/posts"}
}
}HATEOAS makes APIs self-documenting by including links to related resources:
from flask import Flask, jsonify, url_for
app = Flask(__name__)
@app.route('/users/<int:user_id>')
def get_user(user_id):
"""User resource with HATEOAS links"""
user = {'id': user_id, 'name': 'Alice', 'email': 'alice@example.com'}
return jsonify({
'data': user,
'_links': {
'self': {
'href': url_for('get_user', user_id=user_id, _external=True),
'method': 'GET'
},
'update': {
'href': url_for('update_user', user_id=user_id, _external=True),
'method': 'PUT'
},
'delete': {
'href': url_for('delete_user', user_id=user_id, _external=True),
'method': 'DELETE'
},
'all_users': {
'href': url_for('list_users', _external=True),
'method': 'GET'
},
'user_posts': {
'href': url_for('get_user_posts', user_id=user_id, _external=True),
'method': 'GET'
}
}
})
# Client doesn't hardcode URLs - discovers them from links
response = requests.get('https://api.example.com/users/1')
data = response.json()
# Follow links without knowing URL structure
update_link = next(l for l in data['_links'] if l['rel'] == 'update')
requests.put(update_link['href'], json=updated_data)Support multiple formats based on Accept headers:
from flask import Flask, jsonify, request
import json
import xml.etree.ElementTree as ET
app = Flask(__name__)
def serialize_user(user, format='json'):
"""Serialize user in requested format"""
if format == 'json':
return jsonify(user)
elif format == 'xml':
root = ET.Element('user')
for key, value in user.items():
child = ET.SubElement(root, key)
child.text = str(value)
return ET.tostring(root, encoding='unicode')
elif format == 'csv':
headers = ','.join(user.keys())
values = ','.join(str(v) for v in user.values())
return f"{headers}\n{values}"
else:
return jsonify({'error': 'Unsupported format'}), 415
@app.route('/users/<int:user_id>')
def get_user(user_id):
"""Return user in requested format"""
user = {'id': user_id, 'name': 'Alice', 'email': 'alice@example.com'}
# Get format from Accept header
accept = request.headers.get('Accept', 'application/json')
if 'application/xml' in accept:
return serialize_user(user, 'xml'), 200, {'Content-Type': 'application/xml'}
elif 'text/csv' in accept:
return serialize_user(user, 'csv'), 200, {'Content-Type': 'text/csv'}
else: # Default to JSON
return serialize_user(user, 'json')
# Client requests specific format
headers = {'Accept': 'application/xml'}
response = requests.get('https://api.example.com/users/1', headers=headers)
# Receives XML response# Version in URL
GET /api/v1/users
GET /api/v2/users
# Pros: Clear, easy to maintain multiple versions
# Cons: Can lead to code duplication
@app.route('/api/v1/users/<int:user_id>')
def get_user_v1(user_id):
return jsonify({
'id': user_id,
'name': 'Alice',
'email': 'alice@example.com'
})
@app.route('/api/v2/users/<int:user_id>')
def get_user_v2(user_id):
return jsonify({
'id': user_id,
'name': 'Alice',
'email': 'alice@example.com',
'created_at': '2024-01-01T12:00:00Z', # New field
'updated_at': '2024-01-15T10:30:00Z'
})# Version in Accept header
GET /api/users
Accept: application/vnd.api+json;version=2
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
version = request.headers.get('Accept', '')
if 'version=2' in version:
return jsonify({
'id': user_id,
'name': 'Alice',
'email': 'alice@example.com',
'timestamps': {
'created': '2024-01-01T12:00:00Z',
'updated': '2024-01-15T10:30:00Z'
}
})
# Default to v1
return jsonify({
'id': user_id,
'name': 'Alice',
'email': 'alice@example.com'
})from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/api/v1/users/<int:user_id>')
def get_user_v1(user_id):
"""Deprecated endpoint - will be removed"""
user_data = {'id': user_id, 'name': 'Alice'}
response = jsonify(user_data)
# Add deprecation headers
response.headers['Deprecated'] = 'true'
response.headers['Sunset'] = 'Sun, 01 Jun 2025 23:59:59 GMT'
response.headers['Link'] = '</api/v2/users/1>; rel="successor-version"'
return response
# Client can see deprecation warning
response = requests.get('https://api.example.com/api/v1/users/1')
if response.headers.get('Deprecated'):
print(f"Warning: This endpoint is deprecated. Migrate to v2 by {response.headers.get('Sunset')}")from flask import Flask, jsonify, request
app = Flask(__name__)
@app.route('/users')
def list_users():
"""Implement proper pagination"""
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
per_page = min(per_page, 100) # Max 100 per page
# Simulate fetching from database
all_users = [{'id': i, 'name': f'User {i}'} for i in range(1, 251)]
total = len(all_users)
total_pages = (total + per_page - 1) // per_page
start = (page - 1) * per_page
end = start + per_page
users = all_users[start:end]
return jsonify({
'data': users,
'pagination': {
'page': page,
'per_page': per_page,
'total': total,
'total_pages': total_pages,
'has_next': page < total_pages,
'has_prev': page > 1
},
'_links': {
'self': f'/users?page={page}&per_page={per_page}',
'first': f'/users?page=1&per_page={per_page}',
'last': f'/users?page={total_pages}&per_page={per_page}',
'next': f'/users?page={page+1}&per_page={per_page}' if page < total_pages else None,
'prev': f'/users?page={page-1}&per_page={per_page}' if page > 1 else None
}
})from functools import wraps
from flask import Flask, jsonify, request
app = Flask(__name__)
# Mock user roles
users_db = {
1: {'name': 'Alice', 'owner_id': 1},
2: {'name': 'Bob', 'owner_id': 2}
}
def require_permission(permission):
"""Decorator for permission checking"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# Get current user from auth (simplified)
user_id = request.headers.get('X-User-ID', type=int)
if not user_id:
return jsonify({'error': 'Unauthorized'}), 401
# Check permission
resource_id = kwargs.get('user_id')
resource = users_db.get(resource_id)
if not resource:
return jsonify({'error': 'Not found'}), 404
if permission == 'read':
# Anyone can read
pass
elif permission == 'write':
# Only owner or admin can write
if user_id != resource['owner_id']:
return jsonify({'error': 'Forbidden'}), 403
return f(*args, **kwargs)
return decorated_function
return decorator
@app.route('/users/<int:user_id>', methods=['GET'])
@require_permission('read')
def get_user(user_id):
return jsonify(users_db.get(user_id))
@app.route('/users/<int:user_id>', methods=['PUT'])
@require_permission('write')
def update_user(user_id):
data = request.get_json()
users_db[user_id].update(data)
return jsonify(users_db[user_id])from flask import Flask, jsonify, request
from datetime import datetime
import uuid
app = Flask(__name__)
class APIError(Exception):
"""Custom API error"""
def __init__(self, message, status_code=400, error_code=None):
self.message = message
self.status_code = status_code
self.error_code = error_code or 'API_ERROR'
@app.errorhandler(APIError)
def handle_api_error(error):
"""Handle custom API errors"""
error_id = str(uuid.uuid4())
response = {
'error': {
'code': error.error_code,
'message': error.message,
'id': error_id,
'timestamp': datetime.utcnow().isoformat()
}
}
return jsonify(response), error.status_code
@app.route('/users/<int:user_id>')
def get_user(user_id):
if user_id < 0:
raise APIError(
'Invalid user ID',
status_code=400,
error_code='INVALID_USER_ID'
)
user = None # Simulate not found
if not user:
raise APIError(
'User not found',
status_code=404,
error_code='USER_NOT_FOUND'
)
return jsonify(user)
# Response format:
# {
# "error": {
# "code": "USER_NOT_FOUND",
# "message": "User not found",
# "id": "550e8400-e29b-41d4-a716-446655440000",
# "timestamp": "2024-02-23T10:30:00"
# }
# }import requests
from jsonschema import validate, ValidationError
# Define expected schema
user_schema = {
'type': 'object',
'properties': {
'id': {'type': 'integer'},
'name': {'type': 'string'},
'email': {'type': 'string', 'format': 'email'}
},
'required': ['id', 'name', 'email']
}
def test_api_contract(url):
"""Verify API response matches contract"""
response = requests.get(url)
data = response.json()
try:
validate(instance=data, schema=user_schema)
print("✓ API response matches contract")
return True
except ValidationError as e:
print(f"✗ Contract violation: {e.message}")
return False
# Test
test_api_contract('https://api.example.com/users/1')from flask import Flask, jsonify, make_response
from functools import wraps
from datetime import datetime, timedelta
app = Flask(__name__)
def cache_response(max_age=3600):
"""Decorator for HTTP caching"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
response = make_response(f(*args, **kwargs))
# Set cache headers
response.headers['Cache-Control'] = f'public, max-age={max_age}'
response.headers['ETag'] = hash(response.get_data()).__str__()
response.headers['Last-Modified'] = datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')
return response
return decorated_function
return decorator
@app.route('/users/<int:user_id>')
@cache_response(max_age=3600)
def get_user(user_id):
"""Cached for 1 hour"""
return jsonify({'id': user_id, 'name': 'Alice'})
# Client can use conditional requests
response = requests.get(
'https://api.example.com/users/1',
headers={'If-None-Match': 'previous-etag'}
)
if response.status_code == 304: # Not Modified
print("Use cached version")
else:
print("Fresh data:", response.json())| Concept | Implementation |
|---|---|
| HATEOAS | Include links in responses for discoverability |
| Content Negotiation | Support multiple formats via Accept headers |
| Versioning | Use URL paths or headers, plan deprecation |
| Pagination | Include total, page, per_page, has_next/prev |
| Permissions | Check authorization at resource level |
| Error Handling | Use standard error response format with codes |
| Caching | Set Cache-Control and ETag headers |
| API Contract | Document and test response schemas |
Explore advanced HTTP request patterns and optimization techniques.
Next: Advanced HTTP Requests →
Ready for advanced challenges? Try advanced challenges
Resources
Ojasa Mirai
Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.
Learn Deep • Build Real • Verify Skills • Launch Forward