On this page
What the round is testing
- Build CRUD for a resource
- Add authentication to a route
- Support more than one API version
- Return proper errors and status codes
- Add rate limiting
- Paginate a list endpoint
- Add filtering and sorting
- Make a write safe to retry
How to approach any REST coding question
Common mistakes
Frequently asked questions
Final thoughts
Related Reading
REST API Coding Interview Questions (With Python and Flask Answers)

On This Page
What the round is testing
- Build CRUD for a resource
- Add authentication to a route
- Support more than one API version
- Return proper errors and status codes
- Add rate limiting
- Paginate a list endpoint
- Add filtering and sorting
- Make a write safe to retry
How to approach any REST coding question
Common mistakes
Frequently asked questions
Final thoughts
Related Reading
A REST API coding question asks you to build a working endpoint, not describe one. You get a short brief, an editor, and about twenty minutes. The eight questions below come up most often. Each has a Flask answer you can read in a minute and adapt to any framework.
These are implementation questions. If you are being asked to design the contract instead, that is a different round, and how to design a RESTful API covers it.
What the round is testing
| The question looks like | What is actually graded |
|---|---|
| "Build CRUD for a to do list" | Do you know the method and status code conventions |
| "Add authentication to this route" | Can you protect a route without breaking the others |
| "Support two API versions" | Do you understand backward compatibility |
| "Return proper errors" | Do you separate client faults from server faults |
| "Add rate limiting" | Do you know what to return when a caller goes over |
| "Paginate this list" | Do you know why offset breaks on large tables |
Working code that returns the wrong status code scores worse than slightly rougher code that gets the contract right.
1. Build CRUD for a resource
Question: Create a Flask API for a to do list with create, read, update, and delete.
from flask import Flask, jsonify, request app = Flask(__name__) todos = {} next_id = 1 @app.route('/v1/todos', methods=['GET']) def list_todos(): return jsonify({'data': list(todos.values())}), 200 @app.route('/v1/todos', methods=['POST']) def create_todo(): global next_id body = request.get_json(silent=True) or {} if not body.get('title'): return jsonify({'error': {'code': 'title_required', 'message': 'title is required.'}}), 400 todo = {'id': next_id, 'title': body['title'], 'done': False} todos[next_id] = todo next_id += 1 return jsonify(todo), 201, {'Location': f'/v1/todos/{todo["id"]}'} @app.route('/v1/todos/<int:todo_id>', methods=['GET']) def get_todo(todo_id): todo = todos.get(todo_id) if not todo: return jsonify({'error': {'code': 'not_found', 'message': 'No todo with that id.'}}), 404 return jsonify(todo), 200 @app.route('/v1/todos/<int:todo_id>', methods=['PATCH']) def update_todo(todo_id): todo = todos.get(todo_id) if not todo: return jsonify({'error': {'code': 'not_found', 'message': 'No todo with that id.'}}), 404 body = request.get_json(silent=True) or {} todo.update({k: body[k] for k in ('title', 'done') if k in body}) return jsonify(todo), 200 @app.route('/v1/todos/<int:todo_id>', methods=['DELETE']) def delete_todo(todo_id): if todos.pop(todo_id, None) is None: return jsonify({'error': {'code': 'not_found', 'message': 'No todo with that id.'}}), 404 return '', 204
Four details are being watched. 201 on create with a Location header. 204 on delete with no body. PATCH for a partial update rather than PUT, since only some fields are sent. And a plural noun in the path with no verbs.
2. Add authentication to a route
Question: Protect one endpoint so only authenticated callers reach it.
from functools import wraps from flask import request, jsonify TOKENS = {'tok_live_abc': 'user_1'} def require_auth(f): @wraps(f) def wrapper(*args, **kwargs): header = request.headers.get('Authorization', '') if not header.startswith('Bearer '): return jsonify({'error': {'code': 'unauthenticated', 'message': 'Missing bearer token.'}}), 401 user = TOKENS.get(header.split(' ', 1)[1]) if not user: return jsonify({'error': {'code': 'unauthenticated', 'message': 'Invalid token.'}}), 401 request.user_id = user return f(*args, **kwargs) return wrapper @app.route('/v1/me', methods=['GET']) @require_auth def me(): return jsonify({'user_id': request.user_id}), 200
Say the 401 versus 403 distinction out loud, because it is a common follow up. 401 means we do not know who you are. 403 means we do, and you still may not do this.
Two more points score well. A token belongs in the Authorization header, never in a query string, because query strings land in logs. Real systems verify a signed token rather than reading a dictionary. Name JWT verification or an introspection call as the production version.
3. Support more than one API version
Question: Serve version 1 and version 2 of an endpoint at the same time.
from flask import Blueprint v1 = Blueprint('v1', __name__, url_prefix='/v1') v2 = Blueprint('v2', __name__, url_prefix='/v2') @v1.route('/users/<int:user_id>') def get_user_v1(user_id): return jsonify({'id': user_id, 'name': 'Ada Lovelace'}), 200 @v2.route('/users/<int:user_id>') def get_user_v2(user_id): # v2 splits the single name field into two. return jsonify({'id': user_id, 'first_name': 'Ada', 'last_name': 'Lovelace'}), 200 app.register_blueprint(v1) app.register_blueprint(v2)
Blueprints keep each version's handlers separate, so a change to v2 cannot break v1 by accident.
The interviewer usually asks a second question here: when do you cut a new version? Only when a change would break a working client. Splitting name into two fields is breaking, because a client reading name now gets nothing. Adding a nickname field is not breaking, so it belongs in v1.
4. Return proper errors and status codes
Question: Handle errors so clients can react to them programmatically.
class ApiError(Exception): def __init__(self, code, message, status): self.code, self.message, self.status = code, message, status @app.errorhandler(ApiError) def handle_api_error(e): return jsonify({'error': {'code': e.code, 'message': e.message}}), e.status @app.errorhandler(404) def handle_404(e): return jsonify({'error': {'code': 'not_found', 'message': 'No such endpoint.'}}), 404 @app.errorhandler(Exception) def handle_unexpected(e): app.logger.exception('unhandled error') return jsonify({'error': {'code': 'internal_error', 'message': 'Something went wrong.'}}), 500 @app.route('/v1/orders/<int:order_id>/cancel', methods=['POST']) def cancel_order(order_id): order = orders.get(order_id) if not order: raise ApiError('not_found', 'No order with that id.', 404) if order['status'] == 'shipped': raise ApiError('already_shipped', 'This order has shipped.', 409) order['status'] = 'cancelled' return jsonify(order), 200
One error envelope for the whole API means a client writes its error handling once. The code is the contract and must stay stable. The message is for humans and can be reworded.
Note the 409 on the shipped order. The request was well formed and the caller was allowed, but the resource is in the wrong state. Reaching for 400 there is the common mistake.
Never leak an exception message to the caller. Log the detail, return a generic sentence.
5. Add rate limiting
Question: Stop a single client from calling an endpoint too often.
import time from collections import defaultdict from flask import request, jsonify LIMIT, WINDOW = 100, 60 hits = defaultdict(list) def rate_limit(f): @wraps(f) def wrapper(*args, **kwargs): key = request.headers.get('Authorization') or request.remote_addr now = time.time() hits[key] = [t for t in hits[key] if now - t < WINDOW] remaining = LIMIT - len(hits[key]) if remaining <= 0: retry_after = int(WINDOW - (now - hits[key][0])) + 1 return jsonify({'error': {'code': 'rate_limited', 'message': 'Too many requests.'}}), 429, { 'Retry-After': str(retry_after), 'X-RateLimit-Limit': str(LIMIT), 'X-RateLimit-Remaining': '0'} hits[key].append(now) response = f(*args, **kwargs) return response return wrapper
The headers are the part most candidates skip, and they are the part that matters. A client that receives Retry-After waits. A client that receives a bare error retries immediately and makes the problem worse.
Expect the scaling follow up. This counter lives in one process, so it breaks the moment you run two servers. The production answer is a shared store such as Redis, with the trade off that Redis becomes a dependency on every request.
6. Paginate a list endpoint
Question: Return a large collection in pages.
import base64 import json def encode_cursor(last_id): return base64.urlsafe_b64encode(json.dumps({'id': last_id}).encode()).decode() def decode_cursor(cursor): try: return json.loads(base64.urlsafe_b64decode(cursor.encode()))['id'] except Exception: raise ApiError('invalid_cursor', 'The cursor is not valid.', 400) @app.route('/v1/posts', methods=['GET']) def list_posts(): limit = min(int(request.args.get('limit', 20)), 100) cursor = request.args.get('cursor') rows = sorted(posts.values(), key=lambda p: p['id'], reverse=True) if cursor: last_id = decode_cursor(cursor) rows = [p for p in rows if p['id'] < last_id] page = rows[:limit] return jsonify({ 'data': page, 'next_cursor': encode_cursor(page[-1]['id']) if len(rows) > limit else None, 'has_more': len(rows) > limit, }), 200
Two things score here. The limit is capped, so a caller cannot ask for a million rows. And the cursor is encoded, which signals to clients that it is opaque and not something to construct by hand.
Be ready to compare the two approaches. Offset pagination is simpler and supports jumping to any page, but the database reads and discards the skipped rows, so deep pages get slow. It also repeats and skips items when new rows arrive mid read. Cursor pagination stays fast at any depth and stays correct while the list grows.
7. Add filtering and sorting
Question: Let callers filter and sort a collection through query parameters.
SORTABLE = {'created_at', 'price', 'title'} @app.route('/v1/items', methods=['GET']) def list_items(): rows = list(items.values()) category = request.args.get('category') if category: rows = [r for r in rows if r['category'] == category] min_price = request.args.get('min_price', type=int) if min_price is not None: rows = [r for r in rows if r['price'] >= min_price] sort = request.args.get('sort', '-created_at') field, desc = (sort[1:], True) if sort.startswith('-') else (sort, False) if field not in SORTABLE: raise ApiError('invalid_sort', f'Cannot sort by {field}.', 400) rows.sort(key=lambda r: r[field], reverse=desc) return jsonify({'data': rows}), 200
The allow list on sortable fields is the detail interviewers look for. Passing a user supplied field straight into a query is how sort parameters turn into injection bugs and accidental full table scans.
Name filters after the fields they filter, so ?category=books rather than ?f=1. A guessable parameter is documentation you did not have to write.
8. Make a write safe to retry
Question: A client retries a create request after a timeout. Stop it from creating two records.
processed = {} @app.route('/v1/payments', methods=['POST']) @require_auth def create_payment(): key = request.headers.get('Idempotency-Key') if not key: raise ApiError('idempotency_key_required', 'Send an Idempotency-Key header.', 400) if key in processed: return jsonify(processed[key]), 200 body = request.get_json(silent=True) or {} payment = {'id': f'pay_{len(processed) + 1}', 'amount': body.get('amount'), 'status': 'succeeded'} processed[key] = payment return jsonify(payment), 201
This question appears more often than its reputation suggests, because it separates candidates who have run a service from candidates who have not.
The client generates a unique key and sends it on the write. The server stores the key with the result. A repeat of the same key returns the stored result rather than doing the work twice.
Two follow ups are common. How long do you keep keys? Long enough to cover realistic retries, usually about a day. What if the same key arrives with a different body? Reject it with 422, because that is a client bug and silently returning the old result would hide it.
How to approach any REST coding question
The clock is short, so the order matters more than the typing.
- Say the contract first. Name the paths, the methods, and the status codes before you write a line. It takes thirty seconds and it stops a rewrite later.
- Get the happy path working. One endpoint, end to end, returning real data.
- Add the error cases. Missing field, not found, wrong state. This is where most of the marks are.
- Then the extras. Auth, pagination, rate limiting, in whatever order the brief emphasised.
- Say what you would add with more time. Persistence, a shared counter for rate limits, contract tests. Naming what is missing reads as judgement, not as an excuse.
Keep the storage a dictionary unless a database was asked for. Interviewers are reading your API behaviour, not your ORM.
Common mistakes
- Returning
200for everything, including errors. - Verbs in paths, such as
/getUseror/createOrder. - Using
PUTfor a partial update whenPATCHis meant. - Forgetting the
Locationheader on201. - No cap on page size, so a caller can request everything.
- Leaking a raw exception message in a
500response. - Skipping input validation and letting a missing field raise a
KeyError. - Reaching for
400when the correct answer is409or422.
Frequently asked questions
What are the most common REST API coding interview questions?
CRUD for a resource, adding authentication, and supporting two versions. Then proper errors, rate limiting, pagination, filtering and sorting, and idempotent writes. CRUD and error handling appear in almost every loop.
Which language should I use for a REST API coding question?
Whichever you are fastest in. Python with Flask or FastAPI is common because it is short to write. Express, Spring Boot, and Go all work equally well. The conventions being graded are the same in every framework.
What do interviewers grade in a REST coding question?
Correct methods and status codes, sensible error responses, input validation, and clean resource naming. Working code with the wrong status codes scores worse than slightly rougher code with the right contract.
Do I need a real database?
Usually not. An in memory dictionary is fine unless persistence was part of the brief. Say that you would swap it for a database, and name the index you would need.
How is this different from an API design interview?
A coding question asks you to implement an endpoint in a language. A design question asks you to define the contract, with no code at all. Design rounds go deeper on resource modelling, versioning, and trade offs.
How long should a REST coding answer take?
Around twenty minutes for a full CRUD resource with validation and errors. If you are still on the happy path at fifteen minutes, say so. Move to error handling, because that is where the remaining marks are.
Final thoughts
The eight questions above cover almost every REST implementation round. The same short list carries all of them. Name the contract before you type. Use the status codes honestly. Validate the input. Treat the failure cases as first class work.
Does your next loop include a design round instead? Grokking Modern API Design Interview covers that side, from resource modelling through pagination, versioning, and idempotency. It works 15 designs end to end.
What our users say
Eric
I've completed my first pass of "grokking the System Design Interview" and I can say this was an excellent use of money and time. I've grown as a developer and now know the secrets of how to build these really giant internet systems.
KAUSHIK JONNADULA
Thanks for a great resource! You guys are a lifesaver. I struggled a lot in design interviews, and Grokking System Design gave me an organized process to handle a design problem. Please keep adding more questions.
Arijeet
Just completed the “Grokking the system design interview”. It's amazing and super informative. Have come across very few courses that are as good as this!
Access to 50+ courses
New content added monthly
Certificate of completion
$31.08
/month
Billed Annually
Recommended Course

Grokking the Object Oriented Design Interview
59,948+ students
3.9
Learn how to prepare for object oriented design interviews and practice common object oriented design interview questions. Master low level design interview.
View Course