REST API Interview Questions and Answers

REST API questions test whether you understand HTTP itself: which methods are safe to retry, which status code means what, how clients page through data and avoid re-downloading it. Five questions are answered with a small working service over New York's 265 taxi zones, with every response shown.

GloryTecks Team

Training & Placement Faculty

6 May 2026 9 min read

Last updated: 9 October 2026

Share:

REST API interview questions are really questions about HTTP. Which methods can a client safely retry? Which status code tells the client what went wrong? How does a client page through a large collection, or avoid downloading something it already has? The answers below come from a small working service built with FastAPI over a real dataset, New York's taxi zone lookup table (NYC TLC: trip record data), and every response shown is real.

FastAPI's TestClient sends requests straight into the application without a network server (FastAPI: testing), so the whole exchange runs in one script.

python
import csv
import hashlib
import json

from fastapi import FastAPI, Header, HTTPException, Request, Response
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field

with open("taxi_zone_lookup.csv") as f:                        # NYC TLC taxi zone lookup, 265 zones
    ZONES = {int(r["LocationID"]): {"borough": r["Borough"], "zone": r["Zone"]} for r in csv.DictReader(f)}

app = FastAPI()
calls: dict[str, int] = {}


class Zone(BaseModel):
    borough: str = Field(min_length=1)
    zone: str = Field(min_length=1)


def etag(body: dict) -> str:
    return '"' + hashlib.sha256(json.dumps(body, sort_keys=True).encode()).hexdigest()[:16] + '"'


@app.middleware("http")
async def rate_limit(request: Request, call_next):              # 5 requests per client key, for the demo
    key = request.headers.get("x-api-key", "anonymous")
    calls[key] = calls.get(key, 0) + 1
    if key.startswith("limited") and calls[key] > 5:
        return Response(status_code=429, headers={"Retry-After": "60"})
    return await call_next(request)


@app.get("/zones")
def list_zones(borough: str | None = None, limit: int = 3, after: int = 0):
    ids = [i for i in sorted(ZONES) if i > after and (borough is None or ZONES[i]["borough"] == borough)]
    page = ids[:limit]
    return {"items": [{"id": i, **ZONES[i]} for i in page], "next_after": page[-1] if len(ids) > limit else None}


@app.get("/zones/{zone_id}")
def get_zone(zone_id: int, response: Response, if_none_match: str | None = Header(default=None)):
    if zone_id not in ZONES:
        raise HTTPException(404, "zone not found")
    tag = etag(ZONES[zone_id])
    if if_none_match == tag:
        return Response(status_code=304, headers={"ETag": tag})
    response.headers["ETag"] = tag
    return {"id": zone_id, **ZONES[zone_id]}


@app.put("/zones/{zone_id}", status_code=200)
def replace_zone(zone_id: int, body: Zone):
    ZONES[zone_id] = body.model_dump()
    return {"id": zone_id, **ZONES[zone_id]}


@app.post("/zones", status_code=201)
def create_zone(body: Zone):
    new_id = max(ZONES) + 1
    ZONES[new_id] = body.model_dump()
    return {"id": new_id, **ZONES[new_id]}


@app.delete("/zones/{zone_id}", status_code=204)
def delete_zone(zone_id: int, x_api_key: str = Header(default=""), x_role: str = Header(default="")):
    if not x_api_key:                                          # who are you? unknown -> 401
        raise HTTPException(401, "missing credentials", headers={"WWW-Authenticate": "ApiKey"})
    if x_role != "admin":                                      # known, but not allowed -> 403
        raise HTTPException(403, "admins only")
    ZONES.pop(zone_id, None)


api = TestClient(app)
show = lambda r: f"{r.status_code} {r.json() if r.content else ''}".strip()

print("R1 resources and status codes")
print("   GET /zones/132     ->", show(api.get("/zones/132")))
print("   GET /zones/999     ->", show(api.get("/zones/999")))
print("   GET /zones/abc     ->", api.get("/zones/abc").status_code, "| POST with empty zone ->", api.post("/zones", json={"borough": "Queens", "zone": ""}).status_code)

print("\nR2 POST is not idempotent, PUT is")
before = len(ZONES)
for _ in range(2):
    api.post("/zones", json={"borough": "Queens", "zone": "Test Zone"})
print(f"   two identical POSTs created {len(ZONES) - before} zones")
before = len(ZONES)
for _ in range(2):
    last = api.put("/zones/300", json={"borough": "Queens", "zone": "Test Zone"})
print(f"   two identical PUTs to /zones/300 created {len(ZONES) - before} zone, final state: {last.json()}")

print("\nR3 cursor pagination over the 69 Manhattan zones")
after, pages, first = 0, 0, None
while after is not None:
    page = api.get("/zones", params={"borough": "Manhattan", "limit": 20, "after": after}).json()
    first = first or [z["zone"] for z in page["items"]][:2]
    after, pages = page["next_after"], pages + 1
print(f"   {pages} pages of up to 20; first two zones: {first}")

print("\nR4 conditional GET with an ETag")
r = api.get("/zones/132")
again = api.get("/zones/132", headers={"If-None-Match": r.headers["etag"]})
print(f"   first GET {r.status_code} with {len(r.content)} bytes | repeat with If-None-Match: {again.status_code} with {len(again.content)} bytes")
api.put("/zones/132", json={"borough": "Queens", "zone": "JFK Airport (renamed)"})
print("   after the zone changes, the old ETag gets:", api.get("/zones/132", headers={"If-None-Match": r.headers["etag"]}).status_code)

print("\nR5 401 vs 403, and 429")
print("   DELETE with no credentials      ->", show(api.delete("/zones/300")))
print("   DELETE as an analyst            ->", show(api.delete("/zones/300", headers={"x-api-key": "k1", "x-role": "analyst"})))
print("   DELETE as an admin              ->", api.delete("/zones/300", headers={"x-api-key": "k1", "x-role": "admin"}).status_code)
codes = [api.get("/zones/1", headers={"x-api-key": "limited-client"}).status_code for _ in range(7)]
blocked = api.get("/zones/1", headers={"x-api-key": "limited-client"})
print("   seven calls from a limited key ->", codes, "| Retry-After:", blocked.headers.get("retry-after"))
text
R1 resources and status codes
   GET /zones/132     -> 200 {'id': 132, 'borough': 'Queens', 'zone': 'JFK Airport'}
   GET /zones/999     -> 404 {'detail': 'zone not found'}
   GET /zones/abc     -> 422 | POST with empty zone -> 422

R2 POST is not idempotent, PUT is
   two identical POSTs created 2 zones
   two identical PUTs to /zones/300 created 1 zone, final state: {'id': 300, 'borough': 'Queens', 'zone': 'Test Zone'}

R3 cursor pagination over the 69 Manhattan zones
   4 pages of up to 20; first two zones: ['Alphabet City', 'Battery Park']

R4 conditional GET with an ETag
   first GET 200 with 50 bytes | repeat with If-None-Match: 304 with 0 bytes
   after the zone changes, the old ETag gets: 200

R5 401 vs 403, and 429
   DELETE with no credentials      -> 401 {'detail': 'missing credentials'}
   DELETE as an analyst            -> 403 {'detail': 'admins only'}
   DELETE as an admin              -> 204
   seven calls from a limited key -> [200, 200, 200, 200, 200, 429, 429] | Retry-After: 60

R1. Which status code should an API return?

The one that tells the client what to do next. A zone that exists returns 200 with the resource: zone 132 is JFK Airport. A zone that does not exist returns 404. A malformed request, an ID of "abc" or a zone with an empty name, returns 422 from FastAPI's validation before any of your code runs; many APIs use 400 for the same thing. Creating a resource returns 201, and a successful delete with nothing to say returns 204. Returning 200 with an error message in the body breaks every client, proxy and monitoring tool that relies on the code.

R2. What is idempotency, and which methods have it?

A method is idempotent if repeating a request has the same effect as sending it once; PUT, DELETE and the safe methods such as GET are idempotent, POST is not (RFC 9110). Two identical POSTs created two new zones. Two identical PUTs to /zones/300 created one zone and left it in the same final state. This matters because networks fail: a client that gets no reply cannot know whether its request arrived, and can only retry safely if the method is idempotent. For POSTs that must not duplicate, such as payments, APIs accept an idempotency key from the client and ignore repeats.

R3. How do you paginate a large collection?

With a limit and a cursor. The service returns up to limit items and a next_after value; the client passes it back to get the next page. Paging through the 69 Manhattan zones in pages of 20 took 4 requests. A cursor based on the last ID seen stays correct when rows are added or deleted between requests. Offset-based paging can skip or repeat items when that happens, and gets slower as the offset grows, the same problem as OFFSET in SQL.

R4. How do clients avoid downloading the same data again?

Conditional requests. The service sends an ETag, a fingerprint of the resource. A client that sends it back in If-None-Match gets 304 Not Modified with no body if nothing changed (RFC 9110). The first request returned 50 bytes; the repeat returned 0. After zone 132 was updated, the old ETag no longer matched and the full resource came back with 200. On large resources and busy clients, this saves real bandwidth, and it lets caches between client and server do their job.

R5. What is the difference between 401 and 403, and when do you send 429?

401 means "I do not know who you are": the request lacks valid credentials, and the response must say how to authenticate in a WWW-Authenticate header. 403 means "I know who you are, and the answer is no" (RFC 9110). A delete with no API key got 401, an analyst got 403, and an admin got 204. 429 Too Many Requests is for rate limiting, ideally with Retry-After telling the client how long to wait (RFC 6585). The limited key's sixth and seventh calls got 429 with a 60-second wait. Well-behaved clients back off when they see it, which is why sending 500 or silently dropping requests instead is worse for everyone.

Other questions to be ready for

  • What makes an API RESTful? Resources identified by URLs, standard methods acting on them, representations such as JSON, and stateless requests: each request carries everything needed to process it, so any server can handle it.
  • How do you version an API? In the path (/v2/zones) or a header. The path is easier to see and route; either way, avoid breaking existing clients without notice.
  • PUT or PATCH? PUT replaces the whole resource, as replace_zone does; PATCH changes part of it and is not guaranteed to be idempotent.
  • How should errors look? The same structure everywhere: the status code, a machine-readable error type, a human-readable message, and for validation errors the field that failed. FastAPI's 422 responses already list each failing field, which is a good model to copy.

How to answer API questions

  • Name the status code and what the client should do with it.
  • Say whether each operation is safe to retry.
  • Design pagination for data that changes while it is being read.
  • Separate authentication, who you are, from authorisation, what you may do.

To build such a service step by step, see building REST APIs with Python and the FastAPI guide. For putting a model behind one, see serving ML models with FastAPI and Docker. Broader preparation is in Python interview questions: top 50 with answers.

What are common REST API interview questions?

HTTP methods and idempotency, status codes, PUT versus PATCH, pagination, caching and ETags, authentication versus authorisation, rate limiting, versioning, statelessness, and error formats.

Is POST idempotent?

No. Sending the same POST twice usually creates two resources. PUT, DELETE and safe methods such as GET are idempotent. APIs often add an idempotency key to make POST retries safe.

What is the difference between 401 and 403?

401 means the request has no valid credentials, so the client should authenticate. 403 means the client is known but not allowed to do this.

What should an API return when a client sends too many requests?

429 Too Many Requests, ideally with a Retry-After header saying how long to wait before trying again.

Building APIs in Python is part of the Python Programming course.

Sources

GloryTecks Team

Training & Placement Faculty

The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.

Want to master Interview Questions?

Join GloryTecks’ expert-led Python Programming training in Hyderabad with real-time projects and 100% placement support.

Explore course