Python REST Principles Practice Problems & Exercises
Practice: REST Principles
← Back to lessonWrite a function that normalizes URL paths to follow REST resource naming conventions.Solution
def normalize_resource_url(url):
"""Apply REST resource naming conventions to a URL path.
Rules:
- Resources should be plural nouns (user -> users, post -> posts)
- Lowercase only
- No verbs in path (getUser -> users, createPost -> posts)
- Words separated by hyphens (not camelCase or underscores)
Return the corrected path string.
"""
pass
test_cases = [
('/getUser/1', '/users/1'),
('/Post/42', '/posts/42'),
('/blog_post/5/getComments', '/blog-posts/5/comments'),
('/UserProfile/me', '/user-profiles/me'),
]
for original, expected in test_cases:
result = normalize_resource_url(original)
status = 'PASS' if result == expected else 'FAIL'
print(f"{status}: {original} -> {result}")
Expected Output
PASS: /getUser/1 -> /users/1
PASS: /Post/42 -> /posts/42
PASS: /blog_post/5/getComments -> /blog-posts/5/comments
PASS: /UserProfile/me -> /user-profiles/meHints
Hint 1: Process each path segment independently, skipping numeric segments and 'me'.
Hint 2: Remove verb prefixes like 'get', 'create', 'update', 'delete' from the start of a segment.
Hint 3: Convert camelCase to hyphen-case, replace underscores with hyphens, then pluralize.
Map CRUD operation names to their corresponding HTTP method and URL pattern.Solution
def crud_to_http(operation, has_id=False):
"""Return the correct HTTP method and URL pattern for a CRUD operation.
operations: 'list', 'create', 'read', 'update', 'partial_update', 'delete'
has_id: whether the operation targets a specific resource (True) or collection (False)
Return a dict with 'method' and 'url_pattern' keys.
Example: crud_to_http('list') -> {'method': 'GET', 'url_pattern': '/resources'}
"""
pass
operations = [
('list', False),
('create', False),
('read', True),
('update', True),
('partial_update', True),
('delete', True),
]
for op, has_id in operations:
result = crud_to_http(op, has_id)
print(f"{op} (id={has_id}): {result['method']} {result['url_pattern']}")
Expected Output
list (id=False): GET /resources
create (id=False): POST /resources
read (id=True): GET /resources/{id}
update (id=True): PUT /resources/{id}
partial_update (id=True): PATCH /resources/{id}
delete (id=True): DELETE /resources/{id}Hints
Hint 1: List and create operate on the collection URL (/resources), not individual items.
Hint 2: Read, update, partial_update, and delete operate on individual items (/resources/{id}).
Hint 3: partial_update uses PATCH; full update uses PUT.
Write a validator that determines whether a request follows the REST statelessness constraint.Solution
def is_stateless_request(request):
"""Determine if a request follows REST statelessness.
A request is stateless if it carries all needed auth/context itself
(e.g., Authorization header with token) rather than relying on server-side session.
Return True if stateless, False if session-dependent.
request is a dict with optional keys: 'headers', 'cookies', 'session_id'
"""
pass
requests = [
{'headers': {'Authorization': 'Bearer eyJhbGci...'}, 'cookies': {}},
{'headers': {'Authorization': 'Basic dXNlcjpwYXNz'}, 'cookies': {}},
{'headers': {}, 'cookies': {'session_id': 'abc123'}},
{'headers': {}, 'session_id': 'xyz789', 'cookies': {}},
{'headers': {'X-API-Key': 'key-abc123'}, 'cookies': {}},
]
for i, req in enumerate(requests):
print(f"Request {i+1}: stateless={is_stateless_request(req)}")
Expected Output
Request 1: stateless=True
Request 2: stateless=True
Request 3: stateless=False
Request 4: stateless=False
Request 5: stateless=TrueHints
Hint 1: Stateless = authentication carried in the request itself (Authorization header, API key header).
Hint 2: Session cookies or a session_id field imply server-side state — not stateless.
Hint 3: Check headers for Authorization or X-API-Key; check cookies/fields for session indicators.
Implement Richardson Maturity Model level detection based on API characteristics.Solution
def richardson_level(api_description):
"""Determine the Richardson Maturity Model level (0-3) of an API.
Level 0: Single URI, one HTTP method (RPC-style)
Level 1: Multiple URIs but still single method or no verb semantics
Level 2: Proper HTTP verbs + multiple resources
Level 3: Level 2 + hypermedia controls (HATEOAS links in response)
api_description keys: 'uses_multiple_uris', 'uses_http_verbs', 'includes_links'
Return an int 0-3.
"""
pass
apis = [
{'uses_multiple_uris': False, 'uses_http_verbs': False, 'includes_links': False},
{'uses_multiple_uris': True, 'uses_http_verbs': False, 'includes_links': False},
{'uses_multiple_uris': True, 'uses_http_verbs': True, 'includes_links': False},
{'uses_multiple_uris': True, 'uses_http_verbs': True, 'includes_links': True},
]
for api in apis:
level = richardson_level(api)
print(f"Level {level}: {api}")
Expected Output
Level 0: {'uses_multiple_uris': False, 'uses_http_verbs': False, 'includes_links': False}
Level 1: {'uses_multiple_uris': True, 'uses_http_verbs': False, 'includes_links': False}
Level 2: {'uses_multiple_uris': True, 'uses_http_verbs': True, 'includes_links': False}
Level 3: {'uses_multiple_uris': True, 'uses_http_verbs': True, 'includes_links': True}Hints
Hint 1: The levels build on each other: Level 3 requires all three features.
Hint 2: You can sum the boolean values to get the level directly.
Hint 3: Level 3 (HATEOAS) means responses contain links to related actions/resources.
Implement both offset-based and cursor-based pagination envelope builders for a REST API.Solution
def paginate_offset(items, page, per_page):
"""Return a pagination envelope using offset/limit style.
Result dict keys: data, page, per_page, total, total_pages, has_next, has_prev
"""
pass
def paginate_cursor(items, cursor, limit):
"""Return a pagination envelope using cursor-based style.
cursor: the ID of the last seen item (None for first page).
Items are assumed sorted by id.
Result dict keys: data, next_cursor (id of last item in page, or None), has_more
Each item is a dict with at least an 'id' key.
"""
pass
items = [{'id': i, 'value': f'item_{i}'} for i in range(1, 21)]
page2 = paginate_offset(items, page=2, per_page=5)
print(f"Page 2: {[i['id'] for i in page2['data']]}")
print(f"total={page2['total']}, total_pages={page2['total_pages']}, has_next={page2['has_next']}")
cursor_page = paginate_cursor(items, cursor=5, limit=5)
print(f"Cursor page: {[i['id'] for i in cursor_page['data']]}")
print(f"next_cursor={cursor_page['next_cursor']}, has_more={cursor_page['has_more']}")
Expected Output
Page 2: [6, 7, 8, 9, 10]
total=20, total_pages=4, has_next=True
Cursor page: [6, 7, 8, 9, 10]
next_cursor=10, has_more=TrueHints
Hint 1: Offset pagination: start = (page - 1) * per_page, slice items[start:start+per_page].
Hint 2: total_pages = ceil(total / per_page) — use math.ceil or integer math.
Hint 3: Cursor pagination: find the item after the cursor ID, then slice the next 'limit' items.
Build a HATEOAS-compliant response envelope that adds hypermedia links to a REST resource.Solution
def build_hateoas_response(resource_type, resource_id, data, base_url):
"""Wrap a resource in a HATEOAS envelope.
Add a '_links' key containing:
- self: GET URL for this resource
- collection: GET URL for the collection
- update: PUT URL for this resource
- delete: DELETE URL for this resource
Each link is a dict with 'href' and 'method' keys.
"""
pass
user = {'id': 42, 'name': 'Alice', 'email': '[email protected]'}
response = build_hateoas_response('users', 42, user, 'https://api.example.com')
import json
print(json.dumps(response, indent=2))
Expected Output
{
"id": 42,
"name": "Alice",
"email": "[email protected]",
"_links": {
"self": {
"href": "https://api.example.com/users/42",
"method": "GET"
},
"collection": {
"href": "https://api.example.com/users",
"method": "GET"
},
"update": {
"href": "https://api.example.com/users/42",
"method": "PUT"
},
"delete": {
"href": "https://api.example.com/users/42",
"method": "DELETE"
}
}
}Hints
Hint 1: Construct the base resource URL as base_url + '/' + resource_type.
Hint 2: The 'self' and item-level links append '/' + str(resource_id).
Hint 3: Copy the data dict and add the '_links' key to avoid mutating the original.
Implement an API version router that extracts the version from URL path, Accept header, or custom header.Solution
def route_api_version(request):
"""Determine the API version from a request.
Check in order:
1. URL path prefix: /v1/, /v2/ etc.
2. Accept header: application/vnd.api+json;version=2
3. Custom header: X-API-Version: 3
4. Default to version 1 if none found.
Return an int representing the version number.
"""
pass
requests = [
{'path': '/v2/users', 'headers': {}},
{'path': '/users', 'headers': {'Accept': 'application/vnd.api+json;version=3'}},
{'path': '/users', 'headers': {'X-API-Version': '2'}},
{'path': '/users', 'headers': {}},
{'path': '/v1/posts', 'headers': {'X-API-Version': '3'}}, # URL takes precedence
]
for req in requests:
print(f"version={route_api_version(req)}: path={req['path']}")
Expected Output
version=2: path=/v2/users
version=3: path=/users
version=2: path=/users
version=1: path=/users
version=1: path=/v1/postsHints
Hint 1: Use a regex like r'^/v(\d+)/' to extract version from the URL path.
Hint 2: Parse the Accept header for ';version=N' using a regex or split.
Hint 3: Check URL path first — it has the highest precedence.
Implement an idempotency key store that prevents duplicate processing of POST requests.Solution
class IdempotencyStore:
"""Simulate an idempotency key store for POST requests.
If a request with the same Idempotency-Key was already processed,
return the cached response instead of processing again.
"""
def __init__(self):
self._store = {}
def process(self, idempotency_key, handler, *args, **kwargs):
"""If key seen before, return cached result.
Otherwise call handler(*args, **kwargs), cache result, return it.
"""
pass
call_count = 0
def create_payment(amount, currency):
global call_count
call_count += 1
return {'payment_id': 'pay_001', 'amount': amount, 'currency': currency, 'call': call_count}
store = IdempotencyStore()
key = 'idem-key-abc123'
r1 = store.process(key, create_payment, 100, currency='USD')
r2 = store.process(key, create_payment, 100, currency='USD') # duplicate
r3 = store.process('different-key', create_payment, 200, currency='EUR')
print(f"r1: {r1}")
print(f"r2: {r2}")
print(f"r3: {r3}")
print(f"handler called {call_count} times (should be 2)")
Expected Output
r1: {'payment_id': 'pay_001', 'amount': 100, 'currency': 'USD', 'call': 1}
r2: {'payment_id': 'pay_001', 'amount': 100, 'currency': 'USD', 'call': 1}
r3: {'payment_id': 'pay_001', 'amount': 200, 'currency': 'EUR', 'call': 2}
handler called 2 times (should be 2)Hints
Hint 1: Check if the key exists in self._store before calling the handler.
Hint 2: If the key is new, call the handler, store the result, then return it.
Hint 3: The cached result for r2 should be identical to r1 (same call count).
Build an analyzer that quantifies REST over-fetching and under-fetching problems for a given scenario.Solution
def analyze_rest_requests(scenario):
"""Analyze a REST scenario for over-fetching and under-fetching.
scenario: dict with:
'needed_fields': set of fields the client actually needs
'available_fields': set of fields the endpoint returns
'endpoints_needed': int (how many HTTP calls needed to get all data)
Return a dict with:
'over_fetching': bool (endpoint returns fields client doesn't need)
'under_fetching': bool (client needs more than one endpoint)
'wasted_fields': set of extra fields returned
'efficiency_score': float 0.0-1.0 (needed/available * 1/endpoints)
"""
pass
scenarios = [
{
'name': 'Profile page',
'needed_fields': {'id', 'name', 'avatar'},
'available_fields': {'id', 'name', 'avatar', 'email', 'phone', 'address', 'created_at'},
'endpoints_needed': 1,
},
{
'name': 'Blog post with author',
'needed_fields': {'title', 'content', 'author_name', 'author_avatar'},
'available_fields': {'title', 'content', 'author_id'},
'endpoints_needed': 2,
},
]
for s in scenarios:
result = analyze_rest_requests(s)
print(f"\n{s['name']}:")
print(f" over_fetching: {result['over_fetching']}")
print(f" under_fetching: {result['under_fetching']}")
print(f" wasted_fields: {sorted(result['wasted_fields'])}")
print(f" efficiency_score: {result['efficiency_score']:.2f}")
Expected Output
Profile page:
over_fetching: True
under_fetching: False
wasted_fields: ['address', 'created_at', 'email', 'phone']
efficiency_score: 0.43
Blog post with author:
over_fetching: False
under_fetching: True
wasted_fields: []
efficiency_score: 0.17Hints
Hint 1: over_fetching: available_fields has fields not in needed_fields.
Hint 2: under_fetching: endpoints_needed > 1.
Hint 3: efficiency_score = (len(needed) / len(available)) * (1 / endpoints_needed).
Build an in-memory REST resource store with full CRUD support and ETag generation.Solution
import hashlib
import json
class RESTResourceStore:
"""A simple in-memory REST resource store with ETag support.
Supports: create, read, list, update (full), patch (partial), delete.
ETags are MD5 hashes of the serialized resource.
"""
def __init__(self):
self._store = {}
self._next_id = 1
def create(self, data):
"""POST /resources — create and return (id, resource, etag)."""
pass
def read(self, resource_id):
"""GET /resources/{id} — return (resource, etag) or raise KeyError."""
pass
def update(self, resource_id, data):
"""PUT /resources/{id} — full replace, return (resource, etag)."""
pass
def patch(self, resource_id, partial_data):
"""PATCH /resources/{id} — merge update, return (resource, etag)."""
pass
def delete(self, resource_id):
"""DELETE /resources/{id} — remove, return True or raise KeyError."""
pass
def _etag(self, resource):
return hashlib.md5(json.dumps(resource, sort_keys=True).encode()).hexdigest()[:8]
store = RESTResourceStore()
rid, user, etag1 = store.create({'name': 'Alice', 'role': 'user'})
print(f"Created: id={rid}, etag={etag1}")
user2, etag2 = store.update(rid, {'name': 'Alice', 'role': 'admin'})
print(f"Updated: role={user2['role']}, etag_changed={etag1 != etag2}")
user3, etag3 = store.patch(rid, {'email': '[email protected]'})
print(f"Patched: email={user3.get('email')}, role={user3['role']}")
store.delete(rid)
try:
store.read(rid)
except KeyError:
print("Deleted: resource not found")
Expected Output
Created: id=1, etag=4d2e4c3b
Updated: role=admin, etag_changed=True
Patched: [email protected], role=admin
Deleted: resource not foundHints
Hint 1: Store resources as dicts keyed by integer ID; increment self._next_id on each create.
Hint 2: update() replaces the entire resource; patch() merges using dict.update().
Hint 3: Recompute the ETag after every write operation using self._etag().
Write a REST API design linter that checks routes for common best-practice violations.Solution
def lint_rest_routes(routes):
"""Check a list of route definitions against REST best practices.
Each route is a dict: {'method': str, 'path': str}
Rules to check:
1. No verbs in resource names (get, create, fetch, delete, update, list)
2. Resources should be plural (not /user/{id}, should be /users/{id})
3. No uppercase in paths
4. DELETE and PUT should always include an ID parameter
5. POST on collection URL (no ID) is correct — warn if POST has /{id}
Return a list of (route, [list of violation strings]) tuples.
Only include routes with at least one violation.
"""
pass
routes = [
{'method': 'GET', 'path': '/users'},
{'method': 'POST', 'path': '/user'},
{'method': 'GET', 'path': '/getUsers'},
{'method': 'DELETE', 'path': '/articles'},
{'method': 'PUT', 'path': '/posts'},
{'method': 'POST', 'path': '/orders/{id}'},
{'method': 'GET', 'path': '/Users/Profile'},
]
violations = lint_rest_routes(routes)
for route, issues in violations:
print(f"{route['method']} {route['path']}:")
for issue in issues:
print(f" - {issue}")
Expected Output
POST /user:
- Resource 'user' should be plural
GET /getUsers:
- Path contains verb 'get'
DELETE /articles:
- DELETE should include an ID parameter e.g. /{id}
PUT /posts:
- PUT should include an ID parameter e.g. /{id}
POST /orders/{id}:
- POST should target a collection URL, not an individual resource
GET /Users/Profile:
- Path should be lowercaseHints
Hint 1: Check for verb segments by splitting the path on '/' and testing each non-ID segment.
Hint 2: Detect plural by checking if a segment ends in 's' (simple heuristic).
Hint 3: ID parameters look like '{id}', '{user_id}', etc. — check if any segment starts with '{'.
