Ojasa Mirai

Ojasa Mirai

Python

Loading...

Learning Level

🟢 Beginner🔵 Advanced
REST API BasicsHTTP RequestsStatus CodesJSON SerializationError HandlingAPI AuthenticationRate LimitingBuilding APIsWeb Scraping Basics
Python/Apis Json/Rest Api Basics

🏛️ Advanced REST API Principles — Designing Scalable Web Services

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.


🎯 Richardson Maturity Model

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 - Hypermedia As The Engine Of Application State

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)

📝 Content Negotiation

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

🔄 API Versioning Strategies

URL Versioning

# 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'
    })

Header Versioning

# 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'
    })

Deprecation Strategy

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')}")

📊 Pagination Best Practices

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
        }
    })

🔐 Resource-Level Permissions

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])

🚨 Advanced Error Handling

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"
#     }
# }

📏 API Contract Testing

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')

⚡ Caching Strategies

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())

✅ Key Takeaways

ConceptImplementation
HATEOASInclude links in responses for discoverability
Content NegotiationSupport multiple formats via Accept headers
VersioningUse URL paths or headers, plan deprecation
PaginationInclude total, page, per_page, has_next/prev
PermissionsCheck authorization at resource level
Error HandlingUse standard error response format with codes
CachingSet Cache-Control and ETag headers
API ContractDocument and test response schemas

🔗 What's Next?

Explore advanced HTTP request patterns and optimization techniques.

Next: Advanced HTTP Requests →


Ready for advanced challenges? Try advanced challenges


Resources

Python Docs

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

Courses

PythonFastapiReactJSCloud

© 2026 Ojasa Mirai. All rights reserved.

TwitterGitHubLinkedIn