Skip to main content

Python REST Principles Practice Problems & Exercises

Practice: REST Principles

11 problems4 Easy4 Medium3 Hard50–70 min
← Back to lesson

#1Normalize REST Resource URLsEasy
resource namingRESTURL conventions

Write a function that normalizes URL paths to follow REST resource naming conventions.

Solution
import re

def normalize_resource_url(url):
def fix_segment(seg):
if not seg or seg.isdigit() or seg in ('me', 'v1', 'v2', 'api'):
return seg
# Remove verb prefixes
for verb in ('get', 'create', 'update', 'delete', 'fetch', 'list'):
if seg.lower().startswith(verb) and len(seg) > len(verb):
seg = seg[len(verb):]
break
# camelCase to hyphen
seg = re.sub(r'([A-Z])', r'-\1', seg).lstrip('-')
# underscores to hyphens
seg = seg.replace('_', '-')
seg = seg.lower()
# pluralize simple nouns
if not seg.endswith('s') and not seg.endswith('-'):
seg = seg + 's'
return seg

parts = url.split('/')
return '/'.join(fix_segment(p) for p in parts)

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}")
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/me
Hints

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.


#2CRUD to HTTP Verb MapperEasy
CRUDHTTP verbsREST mapping

Map CRUD operation names to their corresponding HTTP method and URL pattern.

Solution
def crud_to_http(operation, has_id=False):
mapping = {
'list': ('GET', '/resources'),
'create': ('POST', '/resources'),
'read': ('GET', '/resources/{id}'),
'update': ('PUT', '/resources/{id}'),
'partial_update': ('PATCH', '/resources/{id}'),
'delete': ('DELETE', '/resources/{id}'),
}
method, pattern = mapping[operation]
return {'method': method, 'url_pattern': pattern}

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


#3Stateless Request ValidatorEasy
statelessRESTsessionJWT

Write a validator that determines whether a request follows the REST statelessness constraint.

Solution
def is_stateless_request(request):
headers = request.get('headers', {})
cookies = request.get('cookies', {})

# Stateful indicators
if 'session_id' in request:
return False
if any('session' in k.lower() for k in cookies):
return False

# Stateless auth indicators
if 'Authorization' in headers:
return True
if any(k.lower().startswith('x-api') for k in headers):
return True

return False

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)}")
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=True
Hints

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.


#4Richardson Maturity Level DetectorEasy
Richardson Maturity ModelRESTHATEOAS

Implement Richardson Maturity Model level detection based on API characteristics.

Solution
def richardson_level(api_description):
level = 0
if api_description.get('uses_multiple_uris'):
level = 1
if level == 1 and api_description.get('uses_http_verbs'):
level = 2
if level == 2 and api_description.get('includes_links'):
level = 3
return level

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


#5Pagination Response BuilderMedium
paginationRESTcursoroffset

Implement both offset-based and cursor-based pagination envelope builders for a REST API.

Solution
import math

def paginate_offset(items, page, per_page):
total = len(items)
total_pages = math.ceil(total / per_page)
start = (page - 1) * per_page
data = items[start:start + per_page]
return {
'data': data,
'page': page,
'per_page': per_page,
'total': total,
'total_pages': total_pages,
'has_next': page < total_pages,
'has_prev': page > 1,
}

def paginate_cursor(items, cursor, limit):
if cursor is None:
start = 0
else:
ids = [item['id'] for item in items]
start = ids.index(cursor) + 1 if cursor in ids else 0
data = items[start:start + limit]
has_more = (start + limit) < len(items)
next_cursor = data[-1]['id'] if data else None
return {
'data': data,
'next_cursor': next_cursor,
'has_more': has_more,
}

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']}")
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=True
Hints

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.


#6HATEOAS Link BuilderMedium
HATEOAShypermediaREST Level 3links

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):
collection_url = f"{base_url}/{resource_type}"
item_url = f"{collection_url}/{resource_id}"
response = dict(data)
response['_links'] = {
'self': {'href': item_url, 'method': 'GET'},
'collection': {'href': collection_url, 'method': 'GET'},
'update': {'href': item_url, 'method': 'PUT'},
'delete': {'href': item_url, 'method': 'DELETE'},
}
return response

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


#7API Version RouterMedium
API versioningURL versioningAccept header versioning

Implement an API version router that extracts the version from URL path, Accept header, or custom header.

Solution
import re

def route_api_version(request):
path = request.get('path', '')
headers = request.get('headers', {})

# 1. URL path prefix
match = re.match(r'^/v(\d+)/', path)
if match:
return int(match.group(1))

# 2. Accept header versioning
accept = headers.get('Accept', '')
match = re.search(r'version=(\d+)', accept)
if match:
return int(match.group(1))

# 3. Custom header
if 'X-API-Version' in headers:
return int(headers['X-API-Version'])

return 1

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'}},
]

for req in requests:
print(f"version={route_api_version(req)}: path={req['path']}")
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/posts
Hints

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.


#8Idempotency Key Middleware SimulatorMedium
idempotencyPOSTdeduplicationmiddleware

Implement an idempotency key store that prevents duplicate processing of POST requests.

Solution
class IdempotencyStore:
def __init__(self):
self._store = {}

def process(self, idempotency_key, handler, *args, **kwargs):
if idempotency_key in self._store:
return self._store[idempotency_key]
result = handler(*args, **kwargs)
self._store[idempotency_key] = result
return result

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


#9REST vs GraphQL Query AnalyzerHard
RESTGraphQLover-fetchingunder-fetchingN+1

Build an analyzer that quantifies REST over-fetching and under-fetching problems for a given scenario.

Solution
def analyze_rest_requests(scenario):
needed = scenario['needed_fields']
available = scenario['available_fields']
endpoints = scenario['endpoints_needed']

wasted = available - needed
over_fetching = len(wasted) > 0
under_fetching = endpoints > 1
efficiency = (len(needed) / len(available)) * (1 / endpoints) if available else 0.0

return {
'over_fetching': over_fetching,
'under_fetching': under_fetching,
'wasted_fields': wasted,
'efficiency_score': efficiency,
}

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}")
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.17
Hints

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


#10In-Memory REST Resource StoreHard
REST CRUDin-memory storeETagsconcurrency

Build an in-memory REST resource store with full CRUD support and ETag generation.

Solution
import hashlib
import json

class RESTResourceStore:
def __init__(self):
self._store = {}
self._next_id = 1

def create(self, data):
resource_id = self._next_id
self._next_id += 1
resource = dict(data)
resource['id'] = resource_id
self._store[resource_id] = resource
return resource_id, resource, self._etag(resource)

def read(self, resource_id):
if resource_id not in self._store:
raise KeyError(resource_id)
resource = self._store[resource_id]
return resource, self._etag(resource)

def update(self, resource_id, data):
if resource_id not in self._store:
raise KeyError(resource_id)
resource = dict(data)
resource['id'] = resource_id
self._store[resource_id] = resource
return resource, self._etag(resource)

def patch(self, resource_id, partial_data):
if resource_id not in self._store:
raise KeyError(resource_id)
self._store[resource_id].update(partial_data)
resource = self._store[resource_id]
return resource, self._etag(resource)

def delete(self, resource_id):
if resource_id not in self._store:
raise KeyError(resource_id)
del self._store[resource_id]
return True

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")
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 found
Hints

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


#11REST API Design LinterHard
RESTAPI designlintingbest practices

Write a REST API design linter that checks routes for common best-practice violations.

Solution
import re

def lint_rest_routes(routes):
verbs = {'get', 'create', 'fetch', 'delete', 'update', 'list', 'post', 'put', 'add', 'remove'}
results = []

for route in routes:
method = route['method'].upper()
path = route['path']
issues = []

if path != path.lower():
issues.append("Path should be lowercase")

segments = [s for s in path.split('/') if s]
has_id_param = any(re.match(r'^\{.+\}$', s) for s in segments)

for seg in segments:
if re.match(r'^\{.+\}$', seg):
continue
for verb in verbs:
if seg.lower().startswith(verb) and (len(seg) == len(verb) or not seg[len(verb)].isalpha()):
issues.append(f"Path contains verb '{verb}'")
break
# Check plural (simplified)
clean = seg.lower().rstrip('s')
if seg.lower() and not seg.lower().endswith('s') and not re.match(r'^\{.+\}$', seg):
issues.append(f"Resource '{seg}' should be plural")

if method in ('DELETE', 'PUT') and not has_id_param:
issues.append(f"{method} should include an ID parameter e.g. /{{id}}")

if method == 'POST' and has_id_param:
issues.append("POST should target a collection URL, not an individual resource")

if issues:
results.append((route, issues))

return results

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}")
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 lowercase
Hints

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 '{'.

© 2026 EngineersOfAI. All rights reserved.