On this page

What the round is testing

  1. Build CRUD for a resource
  1. Add authentication to a route
  1. Support more than one API version
  1. Return proper errors and status codes
  1. Add rate limiting
  1. Paginate a list endpoint
  1. Add filtering and sorting
  1. 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)

Image
Arslan Ahmad
Eight REST API coding questions with working Flask answers: CRUD, auth, versioning, errors, rate limiting, pagination, filtering, and idempotent writes.
Image

What the round is testing

  1. Build CRUD for a resource
  1. Add authentication to a route
  1. Support more than one API version
  1. Return proper errors and status codes
  1. Add rate limiting
  1. Paginate a list endpoint
  1. Add filtering and sorting
  1. 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 likeWhat 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.

Image

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.

  1. 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.
  2. Get the happy path working. One endpoint, end to end, returning real data.
  3. Add the error cases. Missing field, not found, wrong state. This is where most of the marks are.
  4. Then the extras. Auth, pagination, rate limiting, in whatever order the brief emphasised.
  5. 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 200 for everything, including errors.
  • Verbs in paths, such as /getUser or /createOrder.
  • Using PUT for a partial update when PATCH is meant.
  • Forgetting the Location header on 201.
  • No cap on page size, so a caller can request everything.
  • Leaking a raw exception message in a 500 response.
  • Skipping input validation and letting a missing field raise a KeyError.
  • Reaching for 400 when the correct answer is 409 or 422.

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.

API
System Design Interview
System Design Fundamentals

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!

More From Designgurus
Annual Subscription
Get instant access to all current and upcoming courses for one year.

Access to 50+ courses

New content added monthly

Certificate of completion

$31.08

/month

Billed Annually

Recommended Course
Grokking the Object Oriented Design Interview

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
Join our Newsletter

Get the latest system design articles and interview tips delivered to your inbox.

Read More

System Design Interview Guide – 7 Steps to Ace It

Arslan Ahmad

Arslan Ahmad

Mastering the System Design Interview: Landing Your Dream Job

Arslan Ahmad

Arslan Ahmad

Designing an E-Ticketing/Booking System

Arslan Ahmad

Arslan Ahmad

Top System Design Interview FAQs – Key Answers Explained

Arslan Ahmad

Arslan Ahmad

Design Gurus logo
One-Stop Portal For Tech Interviews.
Copyright © 2026 Design Gurus, LLC. All rights reserved.