QuizCluster
Backend EngineeringBackend Engineer I to Senior / Staff15 min read

REST API Design Interview Guide: Authentication, Pagination, Versioning & Rate Limiting

From Resource Modeling to OAuth2, Cursor Pagination & Token Bucket Rate Limiters at Scale

Priya Nair
Ex-FAANG Staff Backend Engineer & API Platform Lead
12+ Years Building Public & Internal REST Platforms
Prep Timeline
3 to 5 Weeks
Format
3 Rounds (API Design, Backend Coding, System Design Deep-Dive)
Conversion
+64% Offer Conversion
REST API Design Interview Guide: Authentication, Pagination, Versioning & Rate Limiting
Executive Summary & Key Takeaways

What You Must Master to Clear This Track

  • REST is a set of architectural constraints (statelessness, uniform interface, cacheability), not just 'JSON over HTTP' — interviewers probe whether you actually understand the trade-offs behind each one.
  • Know all three authentication models cold: API keys for service-to-service trust, JWTs for stateless session claims, and OAuth2 Authorization Code + PKCE for delegated third-party access.
  • Cursor (keyset) pagination beats offset pagination at scale because it avoids re-scanning skipped rows and stays stable when rows are inserted or deleted mid-page.
  • Version APIs defensively from day one (URI or header versioning) and treat every field addition, removal, or type change as a backward-compatibility decision, not an afterthought.
  • Idempotency keys and a standardized error envelope are what separate a toy CRUD API from a production-grade contract that survives retries, timeouts, and partial failures.
Structured Preparation Timeline

Step-by-Step Study Plan

Follow this sequential roadmap designed to take you from core foundations to advanced architecture and mock interviews.

Phase 1 (Week 1)

Architectural Constraints, Resource Design & HTTP Semantics

REST Fundamentals & Resource Modeling

Master Roy Fielding's constraints (statelessness, uniform interface, layered system), correct HTTP verb/status code usage, and clean noun-based resource URLs.

Key Milestones
  • Design resource hierarchies for a nested domain (e.g. /orders/{id}/line-items) with correct plural nouns and no verbs in URLs.
  • Map every CRUD operation to the correct HTTP method and idempotency semantics (GET, PUT, PATCH, DELETE, POST).
  • Practice explaining statelessness and its trade-off against convenience features like server-side sessions.
Recommended Actions
  • Redesign a legacy RPC-style API ('/getUserById?id=5') into RESTful resource form as a drill.
  • Write out the full HTTP status code table (2xx/3xx/4xx/5xx) from memory with one real use case per code.
Phase 2 (Week 2-3)

Security Boundaries, Data Slicing & Long-Term Contract Evolution

Auth, Pagination & Versioning

Deep-dive OAuth2 Authorization Code flow with PKCE, JWT structure and revocation trade-offs, offset vs keyset pagination, and the three major versioning strategies.

Key Milestones
  • Trace the full OAuth2 Authorization Code + PKCE handshake end-to-end, including token refresh.
  • Implement a cursor-based pagination query using a composite (timestamp, id) key with a supporting index.
  • Compare URI versioning, header/Accept versioning, and content negotiation with real backward-compatibility scenarios.
Recommended Actions
  • Decode a real JWT on jwt.io and identify which claims are safe to trust without server-side verification.
  • Write a migration plan for renaming a field in a v1 API response without breaking existing mobile clients.
Phase 3 (Week 4-5)

Production Hardening for Abuse, Retries & Partial Failure

Resilience: Rate Limiting, Idempotency & Error Contracts

Token bucket vs sliding window rate limiting, idempotency keys for safe retries, standardized error envelopes, and mock interview drills tying it all together.

Key Milestones
  • Implement a distributed token bucket limiter backed by Redis and reason about race conditions across app instances.
  • Design an idempotency-key mechanism for a payment/charge endpoint that is safe under network retries.
  • Draft a single standardized error response shape covering validation, auth, and server errors consistently.
Recommended Actions
  • Run 3 mock interviews designing a public API (e.g. a bookings API) end-to-end in 45 minutes.
  • Review real-world API docs (Stripe, GitHub, Twilio) and reverse-engineer their pagination, versioning, and error conventions.
Deep-Dive Architecture & Concepts

1. REST Architectural Constraints & Resource Modeling

REST is not 'HTTP plus JSON' — it is a specific set of architectural constraints defined by Roy Fielding's dissertation. Interviewers use resource modeling questions to separate candidates who copy convention from those who understand why the convention exists.

Statelessness

Every request must contain all information needed to process it; the server holds no client session state between requests, which is what enables horizontal scaling behind any load balancer.

Uniform Interface & Resource Nouns

Resources are identified by nouns (/orders/{id}, not /getOrder), manipulated through standard HTTP verbs, and represented in a consistent, self-descriptive media type such as application/json.

Cacheability

Responses explicitly declare whether they are cacheable (Cache-Control, ETag) so clients and intermediary proxies can reduce redundant round-trips without violating correctness.

Layered System & Uniform HTTP Verbs

Clients cannot tell (and shouldn't need to know) whether they are talking directly to the origin server or through a gateway/CDN/cache layer in between.

HATEOAS (Hypermedia as the Engine of Application State)

The most-ignored constraint: responses should include links to valid next actions (e.g. a 'cancel' link on a pending order), letting clients navigate the API dynamically instead of hardcoding URLs.

Interviewer Insights & Pro Tips
  • When asked to 'design a REST API' in an interview, start by listing resources (nouns) and their relationships before touching a single endpoint path.
  • Justify verb choices explicitly: PUT is a full idempotent replace, PATCH is a partial (often non-idempotent) update, POST is for creation or non-idempotent actions.
Red Flags & Common Pitfalls
  • Using verbs in URLs (/createOrder, /getUserDetails) instead of resource nouns with HTTP methods carrying the verb semantics.
  • Returning 200 OK for every response regardless of outcome, which forces clients to parse the body just to know if something failed.
Deep-Dive Architecture & Concepts

2. Authentication & Authorization: API Keys, JWT & OAuth2

Auth questions test whether you can pick the right trust model for the right relationship: machine-to-machine, first-party session, or third-party delegated access — and whether you understand what each mechanism does NOT protect against.

API Keys

A static secret identifying the calling application (not a user); simple to implement but offers no per-user scoping and must be rotated manually if leaked.

JWT (JSON Web Tokens)

A signed, self-contained token carrying claims (sub, exp, scope) that a resource server can verify statelessly without a database lookup — but that statelessness makes revocation hard before expiry.

OAuth 2.0 Authorization Code Flow + PKCE

The standard for delegated third-party access: a user grants a client application scoped access without ever sharing their password, and PKCE closes the authorization-code-interception hole for public clients (SPAs, mobile apps).

Refresh Tokens & Rotation

Long-lived refresh tokens let a client obtain new short-lived access tokens without re-prompting the user; rotating refresh tokens on every use detects replay/theft.

OAuth 2.0 Authorization Code Flow with PKCE

The end-to-end delegated-authorization handshake used by SPAs, mobile apps, and third-party integrations.

1
Client Generates Challenge & Redirects
The client app generates a random code_verifier, derives a code_challenge (SHA-256), and redirects the user's browser to the Authorization Server with response_type=code, client_id, redirect_uri, scope, state, and code_challenge.
2
User Authenticates & Consents
The Authorization Server prompts the user to log in and approve the requested scopes; nothing here ever exposes the resource owner's password to the client application.
3
Authorization Code Returned
The Authorization Server redirects back to redirect_uri with a short-lived, single-use authorization code and the original state parameter (verified by the client to prevent CSRF).
4
Client Exchanges Code for Tokens
The client's backend POSTs the code plus the original code_verifier to the token endpoint; the server recomputes the challenge and, on match, issues an access_token, refresh_token, and optionally an id_token.
5
Client Calls Resource Server
Subsequent API calls attach the access_token as a Bearer credential in the Authorization header; the resource server validates its signature and expiry (or introspects it) before serving the request.
Interviewer Insights & Pro Tips
  • In interviews, explicitly say why PKCE exists: without it, a malicious app on the same device could intercept the authorization code redirect and exchange it for tokens itself.
  • Distinguish authentication ('who is this?') from authorization ('what can they do?') — scopes and roles belong to authorization, not the authentication mechanism.
Red Flags & Common Pitfalls
  • Storing JWTs in localStorage on the client, which is directly readable by any injected XSS script (httpOnly, Secure, SameSite cookies are safer for browser clients).
  • Treating JWT signature verification as proof the token hasn't been revoked — a stolen-but-unexpired JWT remains valid until a denylist or short expiry catches up.
Deep-Dive Architecture & Concepts

3. Pagination Strategies: Offset vs Cursor/Keyset

Pagination looks trivial until the dataset is billions of rows and users are scrolling an infinite feed — this is one of the highest-signal 'have you actually run this in production' questions in backend interviews.

Offset/Limit Pagination

Simple to implement (OFFSET n LIMIT m) and allows random page jumps, but the database must scan and discard all skipped rows, making deep pages progressively slower.

Cursor / Keyset Pagination

Uses the last seen row's sort key (typically a composite of timestamp + unique id) as an opaque cursor in a WHERE clause, so the database seeks directly to the next page via an index instead of scanning.

Stability Under Concurrent Writes

Offset pagination can skip or duplicate rows if items are inserted/deleted between page requests; keyset pagination stays consistent because each page is anchored to a specific row, not a row count.

Total Count Trade-Offs

Keyset pagination typically cannot cheaply return 'total pages' or support random page jumps — most infinite-scroll UIs (feeds, chat, notifications) don't need that anyway.

Keyset (Cursor) Pagination with a Composite Index
sql
-- Keyset pagination on a composite, unique, indexed key.
  -- The opaque cursor the client holds encodes the last row's (created_at, id).
  SELECT id, title, created_at
  FROM articles
  WHERE (created_at, id) < ($1::timestamptz, $2::uuid)  -- cursor from previous page
  ORDER BY created_at DESC, id DESC
  LIMIT 20;
  
  -- This index turns the WHERE + ORDER BY above into a fast index range scan
  -- instead of a full table scan followed by an in-memory sort.
  CREATE INDEX idx_articles_created_at_id
    ON articles (created_at DESC, id DESC);
Why it matters: Comparing the tuple (created_at, id) directly lets Postgres seek to the correct starting position using the composite index, giving O(log n + page size) performance regardless of how deep the client pages — unlike OFFSET, which degrades linearly with depth. The unique id tiebreaker prevents ambiguity when multiple rows share the same timestamp.
Interviewer Insights & Pro Tips
  • When asked 'how would you paginate a social feed', immediately reach for cursor pagination and justify it with the insert-during-scroll consistency argument.
  • Base64-encode the cursor (e.g. {created_at, id}) so it's opaque to clients — this lets you change the underlying sort key later without breaking the API contract.
Red Flags & Common Pitfalls
  • Using OFFSET on a table with millions of rows for deep pages, causing full scans that get slower with every page the user requests.
  • Exposing raw internal IDs or unencoded sort keys as the cursor, which leaks implementation details and breaks if the schema changes.
Deep-Dive Architecture & Concepts

4. Versioning, Rate Limiting, Idempotency & Error Contracts

The last mile of API design is what keeps a contract alive for years: how you evolve it without breaking clients, how you protect it from abuse, and how you make it safe to retry when the network fails halfway through a request.

URI vs Header vs Content-Negotiation Versioning

URI versioning (/v1/orders) is explicit and cache-friendly but clutters URLs; header versioning (Accept: application/vnd.api.v2+json) keeps URLs stable but is less discoverable and harder to test in a browser.

Backward-Compatible Evolution

Adding optional fields is safe; renaming, removing, or changing the type of an existing field is a breaking change that requires a new version or a carefully staged dual-write/dual-read migration.

Rate Limiting Algorithms

Token bucket allows controlled bursts while enforcing an average rate; sliding window log/counter gives smoother, more precise limits than fixed windows, which suffer from boundary burst spikes.

Idempotency Keys

A client-generated key (e.g. Idempotency-Key header) lets a server safely deduplicate retried POST requests — critical for payments, order creation, and any non-idempotent side effect over an unreliable network.

Standardized Error Envelope

A consistent { error: { code, message, details, requestId } } shape across every endpoint lets client SDKs handle errors generically instead of branching per-endpoint.

Distributed Token Bucket Rate Limiter (Redis + Lua)
typescript
// Atomic token bucket implemented as a Redis Lua script so concurrent
  // requests across many stateless app instances never race on read-modify-write.
  import { Redis } from "ioredis";
  
  const redis = new Redis(process.env.REDIS_URL);
  
  const TOKEN_BUCKET_SCRIPT = `
  local key = KEYS[1]
  local capacity = tonumber(ARGV[1])
  local refillRatePerSec = tonumber(ARGV[2])
  local now = tonumber(ARGV[3])
  local requested = tonumber(ARGV[4])
  
  local bucket = redis.call("HMGET", key, "tokens", "timestamp")
  local tokens = tonumber(bucket[1])
  local lastRefill = tonumber(bucket[2])
  
  if tokens == nil then
    tokens = capacity
    lastRefill = now
  end
  
  local elapsed = math.max(0, now - lastRefill)
  tokens = math.min(capacity, tokens + elapsed * refillRatePerSec)
  
  local allowed = tokens >= requested
  if allowed then
    tokens = tokens - requested
  end
  
  redis.call("HMSET", key, "tokens", tokens, "timestamp", now)
  redis.call("EXPIRE", key, 3600)
  
  return allowed and 1 or 0
  `;
  
  export async function isRequestAllowed(
    clientId: string,
    capacity = 100,
    refillRatePerSecond = 10,
  ): Promise<boolean> {
    const now = Date.now() / 1000;
    const result = await redis.eval(
      TOKEN_BUCKET_SCRIPT,
      1,
      `rate_limit:${clientId}`,
      capacity,
      refillRatePerSecond,
      now,
      1,
    );
    return result === 1;
  }
Why it matters: Running the read-refill-check-decrement logic as a single Lua script makes the whole operation atomic on the Redis server, eliminating the race condition where two app instances both read 'tokens remaining' before either decrements it. The bucket refills continuously based on elapsed time rather than resetting on a fixed clock boundary, which avoids the burst-at-window-edge problem of naive fixed-window counters.
Interviewer Insights & Pro Tips
  • When comparing rate limiting algorithms, lead with the trade-off: fixed window is cheapest but allows 2x burst at window boundaries; sliding window log is most accurate but costs more memory per key; token bucket is the pragmatic middle ground most APIs (Stripe, GitHub) actually ship.
  • Always return 429 Too Many Requests with a Retry-After header (and ideally X-RateLimit-Remaining/X-RateLimit-Reset) so well-behaved clients can back off automatically.
Red Flags & Common Pitfalls
  • Storing rate limit counters in local process memory in a horizontally scaled fleet, which lets each instance independently allow up to the limit — multiplying the effective limit by the instance count.
  • Making a payment or order-creation endpoint retry-unsafe by relying only on client-side 'don't double click' UX instead of a server-side idempotency key.
  • Breaking existing clients by silently changing a field's type or removing it in the current API version instead of introducing a new version or an additive migration.
Real-World Example

Migrating a Booking Platform's API from Offset Pagination to Cursor Pagination Under Load

A mid-size travel booking platform's public search API used simple OFFSET/LIMIT pagination on a `listings` table that had grown from 200K to 40M rows over two years. During peak season, partner integrations paginating deep into search results (page 200+) were timing out and causing database CPU spikes that degraded the primary booking flow.

  • 1The team instrumented slow query logs and confirmed OFFSET-heavy queries on deep pages were responsible for over 30% of database CPU during peak traffic windows.
  • 2They introduced a new cursor-based pagination parameter (`after` cursor encoding a base64 {sort_score, id} tuple) alongside the existing offset parameter, keeping the old behavior available under a deprecation notice rather than breaking partners immediately.
  • 3A composite index on (sort_score DESC, id DESC) was added to support the new keyset queries as fast index range scans instead of full sorts.
  • 4Partner-facing documentation and SDKs were updated with a 90-day migration window, and a `Deprecation` response header was added to every offset-paginated response to nudge automated clients.
  • 5The team monitored per-partner API version usage via a dashboard, reaching out directly to the last handful of integrations still on offset pagination before the sunset date.
  • 6Old OFFSET-based endpoints were removed only after usage dropped below 1% of total traffic for two consecutive weeks.
Outcome: Peak-season database CPU attributable to pagination queries dropped by roughly 70%, and deep-page search latency for partner integrations improved from several seconds to consistently under 100ms.
Real-World Interview Questions

Top Must-Know Interview Questions & Model Answers

REST PrinciplesMust-Know

Q1: What makes an API 'RESTful'? Explain Roy Fielding's core architectural constraints.

Executive Answer:REST is defined by a set of constraints — statelessness, a uniform interface, cacheability, layered system, client-server separation, and optionally code-on-demand — not by URL style or JSON usage alone.
Deep Dive Analysis:
  • Statelessness means each request carries all context needed to process it; the server stores no per-client session between calls.
  • Uniform interface means resources are addressed by URIs and manipulated through a small, standard set of HTTP verbs with self-descriptive representations.
  • Cacheability and layered system let intermediaries (CDNs, gateways) sit transparently between client and origin server without breaking correctness.
Interviewer Takeaway: An API using JSON over HTTP with verbs in the URL and server-side sessions is not RESTful — it's RPC wearing REST's clothes.
REST PrinciplesMust-Know

Q2: Why does statelessness matter for a REST API, and what does it cost you?

Executive Answer:Statelessness lets any server instance handle any request, enabling trivial horizontal scaling and load balancing, at the cost of sending more context (auth tokens, pagination cursors) on every request.
Deep Dive Analysis:
  • Because no session lives on a specific server, a load balancer can round-robin requests freely and instances can be added/removed without session affinity.
  • The trade-off is repeated overhead — every request re-sends auth tokens and any state the server would otherwise have remembered.
  • Session-like features (shopping carts, wizards) must be pushed to the client or a shared store (Redis, DB) rather than in-process memory.
Interviewer Takeaway: Never store per-user state in application server memory between requests — if you need 'session', put it in a token or a shared cache.
REST PrinciplesMedium

Q3: How should you model and name resources in REST URLs?

Executive Answer:Use plural nouns for collections, nest sub-resources under their parent, and let HTTP verbs carry the action instead of embedding verbs in the path.
Deep Dive Analysis:
  • /orders and /orders/{id} represent the collection and a single resource; /orders/{id}/line-items represents a nested sub-collection.
  • Actions that don't map cleanly to CRUD (e.g. 'cancel an order') are modeled as a sub-resource or state transition: POST /orders/{id}/cancel, not /cancelOrder.
  • Query parameters handle filtering, sorting, and pagination (?status=shipped&sort=-created_at) rather than being baked into the path.
Interviewer Takeaway: If your URL has a verb in it, you're probably describing an RPC call, not a REST resource.
REST PrinciplesHard

Q4: What is HATEOAS, and why do most production APIs skip it?

Executive Answer:HATEOAS means responses include hypermedia links describing valid next actions, letting clients navigate the API dynamically instead of hardcoding URLs — but it's largely skipped because it adds payload size and client complexity for marginal benefit in most internal/B2B APIs.
Deep Dive Analysis:
  • A HATEOAS response for a pending order might include links like { rel: 'cancel', href: '/orders/42/cancel' } only when cancellation is currently valid.
  • It decouples clients from hardcoded URL structures, in theory allowing the server to change routes without breaking clients that follow links.
  • In practice, most teams pair versioned, well-documented REST endpoints with generated SDKs instead, since true hypermedia-driven clients are rare outside specific ecosystems (e.g. Stripe's expandable objects, PayPal's HAL-based APIs).
Interviewer Takeaway: Know HATEOAS well enough to discuss it, but be honest in interviews that most 'REST APIs' in production are pragmatically level 2 on the Richardson Maturity Model, not level 3.
Authentication & AuthorizationMust-Know

Q5: Walk through the OAuth 2.0 Authorization Code flow with PKCE end-to-end.

Executive Answer:The client generates a code_verifier/code_challenge pair, redirects the user to authenticate and consent, receives a short-lived authorization code, then exchanges that code plus the verifier for access and refresh tokens at the token endpoint.
Deep Dive Analysis:
  • PKCE (Proof Key for Code Exchange) prevents a malicious app from stealing the authorization code mid-redirect and exchanging it itself, since only the original client holds the code_verifier.
  • The state parameter round-trips through the redirect to prevent CSRF against the callback endpoint.
  • The final token exchange happens server-to-server (or with PKCE, even from a public client) and returns an access_token for API calls plus a refresh_token for renewing access without re-prompting the user.
Interviewer Takeaway: PKCE is now recommended for ALL OAuth2 clients, not just mobile/SPA — it closes a real interception vulnerability at negligible cost.
Authentication & AuthorizationMedium

Q6: What's the difference between authentication and authorization?

Executive Answer:Authentication answers 'who is making this request'; authorization answers 'what is this identity allowed to do', and the two are handled by different mechanisms even when they appear bundled in a single token.
Deep Dive Analysis:
  • A JWT's signature verification is authentication (proving the token was issued by a trusted party for a specific subject); the scopes/roles inside it drive authorization decisions.
  • You can authenticate successfully and still be denied — e.g. a valid logged-in user hitting an admin-only endpoint returns 403 Forbidden, not 401 Unauthorized.
  • 401 means 'who are you, please authenticate'; 403 means 'I know who you are, and you're not allowed to do this.'
Interviewer Takeaway: Confusing 401 and 403 in an interview is a quick signal of shallow REST knowledge — 401 is identity, 403 is permission.
Authentication & AuthorizationMust-Know

Q7: How do JWTs work, and what are the risks of storing them client-side?

Executive Answer:A JWT is a base64-encoded header.payload.signature structure where the signature (HMAC or RSA/ECDSA) lets any resource server verify the token's integrity and issuer without a database round-trip, but the payload is only encoded, not encrypted, and storing it in localStorage exposes it to XSS.
Deep Dive Analysis:
  • The payload (claims like sub, exp, scope, iat) is readable by anyone who has the token — never put secrets in it.
  • Signature verification proves the token wasn't tampered with and came from a trusted issuer, but says nothing about whether it's been revoked since issuance.
  • For browser clients, httpOnly + Secure + SameSite cookies are safer than localStorage because JavaScript (and therefore an XSS payload) cannot read httpOnly cookies.
Interviewer Takeaway: JWTs trade a database lookup for a forgery-proof but revocation-hard token — mitigate with short expiries plus refresh tokens.
Authentication & AuthorizationMedium

Q8: When would you use a simple API key instead of OAuth2, and what are its limitations?

Executive Answer:API keys are appropriate for server-to-server or machine-to-machine calls where there's no individual human user to delegate on behalf of, but they offer no per-user scoping, expire only via manual rotation, and are all-or-nothing if leaked.
Deep Dive Analysis:
  • A weather API or internal microservice calling another microservice typically just needs to prove 'which application is calling', which an API key does cheaply.
  • Unlike OAuth2 tokens, API keys usually don't carry scopes or expiry by default, so a leaked key often grants full access until someone notices and rotates it.
  • Best practice: hash API keys at rest (like passwords), support multiple active keys per client for zero-downtime rotation, and log key usage for anomaly detection.
Interviewer Takeaway: Reach for API keys for trusted service-to-service traffic; reach for OAuth2 the moment a human user needs to delegate access to a third party.
Authentication & AuthorizationHard

Q9: How do you handle JWT revocation given that tokens are stateless by design?

Executive Answer:True instant revocation requires reintroducing some server-side state (a denylist or a token version/generation counter), so most systems instead minimize the revocation window with short-lived access tokens (5-15 min) plus revocable refresh tokens.
Deep Dive Analysis:
  • A denylist (e.g. in Redis, keyed by jti with TTL = remaining token lifetime) lets you invalidate specific tokens early, at the cost of a lookup on every request — partially defeating JWT's statelessness benefit.
  • A cheaper pattern: store a 'token version' per user; bump it on logout/password-change, and embed the version in the JWT — any mismatch on verification means the token is stale.
  • Refresh tokens are stored server-side anyway (so they CAN be revoked instantly), and keeping access tokens short-lived bounds the blast radius of a compromised access token.
Interviewer Takeaway: You can't truly revoke a stateless JWT before expiry without adding state back — so shrink the expiry window instead of fighting the model.
PaginationMust-Know

Q10: Compare offset-based pagination and cursor/keyset pagination.

Executive Answer:Offset pagination (OFFSET/LIMIT) is simple and supports random page jumps but scans and discards skipped rows, getting slower with depth; cursor/keyset pagination anchors each page to the last seen row's sort key, giving consistent performance and stability under concurrent writes at the cost of losing random page-jump and cheap total-count support.
Deep Dive Analysis:
  • OFFSET 100000 LIMIT 20 still requires the database to walk through 100,020 rows before returning the last 20 — an O(offset) operation.
  • Keyset pagination (WHERE (created_at, id) < (cursor_ts, cursor_id) ORDER BY ... LIMIT n) uses the index to seek directly to the right starting point, independent of how deep the page is.
  • Offset pagination can skip or duplicate rows if items are inserted/deleted between requests; keyset pagination is immune because each page is defined relative to a specific row, not a row count.
Interviewer Takeaway: Default to cursor pagination for any feed-like, high-growth, or infinite-scroll resource; offset pagination is fine for small, rarely-changing admin tables.
PaginationHard

Q11: Why does offset pagination degrade in performance on large tables?

Executive Answer:The database has to count and skip every row before the offset even when an index exists on the sort column, because OFFSET is a post-filter on an ordered stream, not a seek — so cost grows linearly with page depth.
Deep Dive Analysis:
  • Even with an index on created_at, the engine must traverse the index (or table) from the start of the ordering to reach row #100,020 before it can apply LIMIT 20.
  • Deep pagination (e.g. page 5,000 of search results) can turn a sub-millisecond query into a multi-second one purely from the discarded-row scan cost.
  • This is invisible in dev/staging with small seed data and only shows up under production data volume — a classic interview 'gotcha' to flag proactively.
Interviewer Takeaway: Offset cost is O(offset + limit), not O(limit) — always ask about expected table size and access depth before choosing pagination style.
PaginationHard

Q12: How do you design a stable pagination cursor when the sort key isn't unique?

Executive Answer:Pair the non-unique sort column with a guaranteed-unique tiebreaker (typically the primary key) and compare both as a composite tuple, so rows sharing the same primary sort value still have a total order.
Deep Dive Analysis:
  • Sorting purely by created_at can produce duplicate or skipped rows across pages if multiple rows share the same timestamp (common with bulk inserts or coarse timestamp precision).
  • WHERE (created_at, id) < (cursor_created_at, cursor_id) ORDER BY created_at DESC, id DESC establishes a strict total order even with duplicate timestamps.
  • The composite (column, id) index must match the ORDER BY direction exactly for the database to use it as an efficient range scan instead of a sort.
Interviewer Takeaway: Any sortable field used for keyset pagination needs a unique tiebreaker — never paginate on a non-unique column alone.
PaginationMedium

Q13: How would you support 'jump to page N' in a UI while primarily using cursor pagination internally?

Executive Answer:Offer both: use cursor pagination as the default fast path for sequential 'next page' navigation, and layer an approximate offset-based jump (often with a cached/estimated row count) only for UIs that truly need random page access, accepting the extra scan cost as a rare operation.
Deep Dive Analysis:
  • Most infinite-scroll or 'load more' UIs never need random jumps, so keep the default API cursor-based for performance.
  • For admin dashboards that do need 'go to page 47', a secondary offset-style endpoint (or a periodically materialized snapshot of page boundaries) can serve that rarer use case without slowing down the primary feed.
  • Total counts on huge tables are also expensive to compute exactly — many APIs return an approximate count (e.g. from table statistics) rather than a live COUNT(*).
Interviewer Takeaway: Don't force one pagination style to serve every UI need — pick the cheap default for the common case and accept a slower path for the rare one.
VersioningMust-Know

Q14: Compare URI versioning, header versioning, and content-negotiation versioning.

Executive Answer:URI versioning (/v1/orders) is explicit, cache-friendly, and easy to test in a browser but clutters the URL and implies the whole API version-locks together; header versioning (Accept-Version or a custom header) keeps URLs stable and allows per-resource versioning but is less discoverable; content-negotiation (Accept: application/vnd.api.v2+json) is the most 'correct' REST-purist approach but the least ergonomic for API consumers.
Deep Dive Analysis:
  • URI versioning is by far the most common in practice (Stripe, Twilio, GitHub) because it's trivially cacheable by URL and requires no special client tooling.
  • Header versioning avoids URL churn, which matters if URLs are used as durable identifiers (e.g. stored/bookmarked), but makes manual testing and caching by URL harder.
  • Content negotiation via media type is the most RESTful in spirit but adds friction for API consumers who now must set nonstandard Accept headers correctly.
Interviewer Takeaway: There's no universally 'correct' answer — justify your choice against your actual caching, discoverability, and client-tooling constraints.
VersioningMust-Know

Q15: How do you evolve an API without breaking existing clients?

Executive Answer:Treat additive, optional changes as safe (new optional fields, new endpoints, new optional query params) and treat anything that changes existing client expectations (removing/renaming a field, changing a type, tightening validation) as breaking, requiring a new version or a staged migration.
Deep Dive Analysis:
  • Additive changes: adding a new optional response field, adding a new optional request parameter with a sensible default, adding new endpoints.
  • Breaking changes: removing a field, renaming a field, changing a field's type (string to number), making an optional field required, or changing error response shapes.
  • For unavoidable breaking changes, use expand-and-contract: add the new field alongside the old one, migrate clients over a deprecation window, then remove the old field in the next major version.
Interviewer Takeaway: When in doubt, ask: 'would existing client code that ignores unknown fields still work correctly?' — if yes, it's additive; if no, it's breaking.
VersioningMedium

Q16: What's the practical difference between a breaking and non-breaking API change?

Executive Answer:A non-breaking change preserves every guarantee existing clients already depend on (fields present, types, required-ness, status codes for known scenarios); a breaking change violates at least one of those guarantees, even if it 'seems small' like renaming a field.
Deep Dive Analysis:
  • Renaming a field from user_id to userId looks trivial but breaks every client parsing the old key.
  • Changing a numeric ID field from an integer to a string (common when migrating to UUIDs) breaks strongly-typed clients even if the JSON still 'looks like' a valid value.
  • Tightening validation (e.g. making an email field require a valid format when it previously accepted any string) can break existing integrations sending data that used to pass.
Interviewer Takeaway: Assume every existing field, type, and status code is a public contract the moment it ships — changing it later is a versioning decision, not a bug fix.
VersioningMedium

Q17: How do you deprecate an API version safely in production?

Executive Answer:Announce deprecation with a clear timeline, surface it programmatically (Deprecation and Sunset response headers), monitor real usage of the old version, and only remove it once traffic has dropped to near zero or the sunset date has passed with adequate client outreach.
Deep Dive Analysis:
  • Return a Deprecation: true header (and ideally a Sunset: <date> header) on every response from the deprecated version so automated client tooling can detect it.
  • Instrument per-version request metrics so you know exactly which clients/API keys are still on the old version before cutting it off.
  • Provide a migration guide and, where possible, a compatibility shim so most clients can move with minimal code changes rather than a rewrite.
Interviewer Takeaway: Deprecation is a communication and observability problem as much as a technical one — never remove a version 'quietly'.
Rate LimitingMust-Know

Q18: Compare token bucket, leaky bucket, fixed window, and sliding window rate limiting algorithms.

Executive Answer:Fixed window is cheapest but allows up to 2x the limit in bursts at window boundaries; sliding window (log or counter) smooths that out at higher memory/compute cost; token bucket allows controlled bursts up to bucket capacity while enforcing a steady average rate; leaky bucket enforces a strictly constant output rate, smoothing bursts into a steady queue.
Deep Dive Analysis:
  • Fixed window counters reset a counter every N seconds — trivial to implement but a client can send the limit right before AND right after a window boundary, doubling the effective burst.
  • Sliding window log stores individual request timestamps for exact accuracy; sliding window counter approximates it cheaply by weighting the previous and current window counts.
  • Token bucket refills tokens continuously at a fixed rate up to a capacity ceiling, letting clients burst up to that ceiling then throttling to the refill rate — this is what most production APIs (Stripe, GitHub) actually use.
Interviewer Takeaway: Token bucket is the pragmatic default: it tolerates legitimate bursts (a client catching up after being offline) while still bounding sustained throughput.
Rate LimitingHard

Q19: How do you implement rate limiting consistently across a fleet of stateless API servers?

Executive Answer:Centralize the rate limit counters in a shared, low-latency store (Redis is standard) and perform the read-check-update as a single atomic operation (a Lua script or Redis's built-in atomic commands) so concurrent requests hitting different app instances can't race past the limit.
Deep Dive Analysis:
  • In-memory per-instance counters fail because each of N instances independently enforces the full limit, multiplying the effective ceiling by N.
  • A naive GET-then-SET against Redis from multiple instances still races; wrapping the whole check in a Lua script (or using Redis's atomic INCR + EXPIRE pattern) makes it a single indivisible operation.
  • At extreme scale, some systems trade perfect accuracy for lower latency using local approximate counters that periodically sync to a central store (eventually-consistent rate limiting).
Interviewer Takeaway: Any rate limiter that isn't backed by shared, atomically-updated state is not actually enforcing a fleet-wide limit.
Rate LimitingMedium

Q20: What HTTP status code and headers should a rate-limited API return?

Executive Answer:Return 429 Too Many Requests along with a Retry-After header telling the client how long to wait, and ideally X-RateLimit-Limit/X-RateLimit-Remaining/X-RateLimit-Reset headers so well-behaved clients can self-throttle before even hitting the limit.
Deep Dive Analysis:
  • 429 is the standard, purpose-built status for this scenario — using 403 or 503 instead loses semantic meaning and breaks client libraries that specifically handle 429 with backoff.
  • Retry-After can be a number of seconds or an HTTP date, giving the client a concrete signal instead of guessing a backoff interval.
  • Exposing remaining-quota headers on every response (not just when limited) lets clients proactively slow down before ever seeing a 429.
Interviewer Takeaway: A good rate limit contract tells clients how to behave, not just that they misbehaved.
Rate LimitingMedium

Q21: How do you rate-limit fairly per-user vs per-IP, and why does it matter?

Executive Answer:Per-user (or per-API-key) limiting ties quota to an authenticated identity so it scales fairly regardless of network topology; per-IP limiting is a necessary fallback for unauthenticated endpoints but risks over-throttling many legitimate users sharing one IP (corporate NAT, mobile carrier-grade NAT) or under-throttling an attacker rotating IPs.
Deep Dive Analysis:
  • Public login or signup endpoints have no authenticated identity yet, so per-IP (or per-IP + fingerprint) limiting is the only option there, accepting its imprecision.
  • Once a request is authenticated, keying the limiter on user/API-key id is strictly fairer and harder to evade than per-IP.
  • Layered limiting is common in practice: a loose per-IP limit at the edge (basic abuse protection) plus a stricter per-user/per-key limit at the application layer (fair quota enforcement).
Interviewer Takeaway: Rate limit on the most specific reliable identity you have available at that layer of the stack — IP is a fallback, not a default.
Idempotency & ErrorsMust-Know

Q22: What does idempotency mean for HTTP methods, and which methods are idempotent by spec?

Executive Answer:An idempotent operation produces the same server state whether it's executed once or multiple times; GET, PUT, DELETE, HEAD, OPTIONS are idempotent by the HTTP spec, while POST and PATCH are not guaranteed idempotent.
Deep Dive Analysis:
  • PUT /users/5 {name: 'Alex'} run twice leaves the same end state both times — idempotent by definition, even though the second call still does work.
  • POST /orders run twice (without extra safeguards) creates two separate orders — this is exactly the 'double-submit on a flaky network' problem idempotency keys solve.
  • PATCH is context-dependent: {status: 'shipped'} is idempotent, but {increment_stock: 1} is not, since repeating it changes the result each time.
Interviewer Takeaway: Idempotency is about the RESULT of repeating a call, not about whether the call has side effects at all.
Idempotency & ErrorsHard

Q23: How do you make a non-idempotent endpoint like a payment charge safe to retry?

Executive Answer:Require the client to send a unique Idempotency-Key header per logical operation; the server stores the key alongside the result of the first successful execution and, on any retry with the same key, returns the stored result directly instead of re-executing the charge.
Deep Dive Analysis:
  • The client generates the idempotency key once per user action (e.g. once per 'Pay Now' click) and resends the same key on every retry of that same action, including across network timeouts.
  • The server persists {idempotency_key -> response, status} typically in the same transaction as the side effect, so a crash between charging the card and recording the key can't cause a silent duplicate charge.
  • Keys are usually scoped per endpoint and expired after a reasonable window (e.g. 24 hours) to bound storage growth.
Interviewer Takeaway: Idempotency keys move retry-safety from 'hope the client doesn't double-click' to a guaranteed server-side contract — this is exactly how Stripe's Payment Intents API works.
Idempotency & ErrorsMedium

Q24: Design a standardized error response format for a REST API.

Executive Answer:Return a consistent JSON envelope across every endpoint — such as { error: { code, message, details, requestId } } — paired with the correct HTTP status code, so client SDKs can branch on `code` programmatically instead of parsing human-readable messages.
Deep Dive Analysis:
  • `code` should be a stable, machine-readable string (e.g. VALIDATION_ERROR, RATE_LIMITED, RESOURCE_NOT_FOUND) that won't change even if the human-readable `message` copy is edited.
  • `details` carries structured, field-level information for validation errors (e.g. an array of {field, issue} objects) so forms can highlight the exact broken input.
  • `requestId` (echoed from a request-tracing header) lets support/engineering correlate a client-reported error with server-side logs instantly.
Interviewer Takeaway: A good error contract is designed for programmatic consumption first, human readability second.
Idempotency & ErrorsMedium

Q25: What status code should you return for a validation failure versus a business rule failure?

Executive Answer:Malformed or missing input (fails schema validation) should return 400 Bad Request; a syntactically valid request that violates a business rule (e.g. 'insufficient inventory') is better modeled as 422 Unprocessable Entity or a domain-specific 409 Conflict, depending on the nature of the conflict.
Deep Dive Analysis:
  • 400 signals 'the request itself is malformed' — wrong types, missing required fields, invalid JSON.
  • 422 signals 'the request is well-formed and understood, but the server can't act on it' — e.g. an order request for a valid product ID that's out of stock.
  • 409 Conflict is best reserved for state-conflict scenarios specifically, like a concurrent edit conflict (optimistic locking version mismatch) rather than general business-rule violations.
Interviewer Takeaway: Match the status code to WHERE the failure lives — malformed request (400) vs valid-but-unactionable request (422) vs conflicting state (409) are three distinct failure modes.
Idempotency & ErrorsHard

Q26: How do you handle partial failures in a batch API endpoint?

Executive Answer:Return 207 Multi-Status (or a 200 with a structured per-item results array) listing the outcome of every individual item in the batch, rather than forcing an all-or-nothing success/failure for the whole request.
Deep Dive Analysis:
  • A batch of 100 'create user' operations where 97 succeed and 3 fail validation should not roll back the 97 successes just because the request-level status can't be a single clean code.
  • The response body should include a per-item array like [{ index: 0, status: 'success', id: '...' }, { index: 1, status: 'error', error: {...} }] so the client can retry only the failed subset.
  • Document clearly whether the batch is transactional (all-or-nothing) or best-effort (partial success allowed) — this is a business decision, not just an API detail, and must be explicit in the contract.
Interviewer Takeaway: Never force a single HTTP status to represent N independent outcomes — give the client a structured per-item result instead.
Common Mistakes

Mistakes That Sink Otherwise Strong Candidates

Designing URLs around verbs and actions instead of nouns (/getOrders, /createUser).

Why it happens: Developers coming from RPC-style or SOAP backgrounds default to function-call thinking, mapping each server operation directly to a URL.

The fix: Model resources as nouns and let the HTTP method carry the verb: GET /orders, POST /users. Reserve action-style sub-paths (POST /orders/{id}/cancel) only for state transitions that don't map cleanly to CRUD.

Using OFFSET/LIMIT pagination on a large, frequently-growing table without ever revisiting the decision.

Why it happens: Offset pagination is the simplest thing to implement and works fine in development with small seed data, so the performance cliff only appears once the table reaches production scale.

The fix: Default to cursor/keyset pagination for any resource expected to grow past a few hundred thousand rows or be paged deeply, backed by a composite index matching the sort order.

Storing JWTs in browser localStorage for a single-page app.

Why it happens: localStorage is the easiest storage API to reach for and 'just works' in a quick demo, without the developer considering the XSS attack surface.

The fix: Store tokens in httpOnly, Secure, SameSite cookies so client-side JavaScript (and any injected XSS payload) cannot read them directly.

Rate limiting using an in-memory counter inside each application server process.

Why it happens: It's the fastest thing to prototype locally, and the bug is invisible until the service is actually scaled to multiple instances in production.

The fix: Back the rate limiter with a shared store (Redis) and update counters atomically (Lua script or atomic commands) so the limit is enforced fleet-wide, not per-instance.

Treating a field rename or type change as a 'minor fix' shipped directly into the current API version.

Why it happens: The change looks small and internally consistent to the team making it, without accounting for external clients already depending on the old shape.

The fix: Classify every response/request shape change as additive or breaking before shipping; breaking changes go through a new version or an explicit expand-and-contract migration with a deprecation window.

Allowing a POST endpoint that charges a card or creates an order to be re-executed freely on client retry.

Why it happens: The unhappy path (network timeout right after the server processed the request but before the client got the response) is rarely tested, so double-execution only surfaces in production incident reports.

The fix: Require an Idempotency-Key on any non-idempotent, side-effecting endpoint, and store the key-to-result mapping so retries return the original result instead of re-executing.

Returning 200 OK with an `{ success: false }` body instead of the correct 4xx/5xx status code.

Why it happens: It feels 'simpler' to always return 200 and let clients check a boolean, especially when a frontend team drives the API contract informally.

The fix: Use accurate HTTP status codes as the primary signal of outcome (400/401/403/404/409/422/429/500) so infrastructure (caches, load balancers, monitoring, client libraries) can react correctly without inspecting the body.

Inventing a different, inconsistent error response shape per endpoint or per team.

Why it happens: Endpoints are often built by different engineers or teams over time without a shared contract enforced up front.

The fix: Define one standardized error envelope ({ error: { code, message, details, requestId } }) at the platform level and enforce it via shared middleware or a schema/lint check in CI.

Assuming JWT signature verification means the token is still valid and unrevoked.

Why it happens: Signature checks are the most visible part of JWT handling, so it's easy to treat 'signature valid' as equivalent to 'access still authorized'.

The fix: Keep access token lifetimes short (minutes, not days), verify a token-version or denylist claim for sensitive operations, and rely on revocable refresh tokens for the actual logout/revocation guarantee.

Cheat Sheet

Quick-Reference Cheat Sheet

HTTP Status Codes — When to Use Which
200 OKSuccessful GET, PUT, or PATCH with a response body.
201 CreatedSuccessful POST that created a new resource; include a Location header.
204 No ContentSuccessful DELETE or action with no response body to return.
400 Bad RequestMalformed syntax, missing required fields, invalid types.
401 UnauthorizedMissing or invalid authentication credentials — 'who are you?'
403 ForbiddenAuthenticated but not permitted to perform this action.
409 ConflictState conflict, e.g. optimistic-locking version mismatch.
422 Unprocessable EntityWell-formed request that violates a business rule.
Pagination Strategy Comparison
Offset/LimitSimple, supports random page jumps; O(offset) cost, unstable under concurrent writes.
Cursor/KeysetAnchors to last row's key; O(log n) via index seek, stable, no random page jump.
Best for offsetSmall or rarely-changing tables (admin panels, config lists).
Best for cursorFeeds, infinite scroll, high-growth tables, chat/notification history.
Cursor encodingBase64-encode the composite (sort_key, id) tuple to keep it opaque.
Rate Limiting Algorithm Comparison
Fixed WindowCheapest; allows up to 2x burst at window boundaries.
Sliding Window LogMost accurate; higher memory cost storing per-request timestamps.
Sliding Window CounterApproximates sliding log cheaply by weighting adjacent windows.
Token BucketAllows bounded bursts up to capacity, enforces steady average rate; most common production choice.
Leaky BucketSmooths bursts into a strictly constant output rate via a processing queue.
API Versioning Strategy Comparison
URI Versioning (/v1/...)Explicit, cache-friendly, easiest to test manually; most widely used in practice.
Header VersioningKeeps URLs stable; less discoverable, harder to test in a plain browser.
Content Negotiation (Accept: vnd.api.v2+json)Most 'RESTfully pure'; least ergonomic for typical API consumers.
Additive ChangeNew optional field/endpoint/param — safe without a version bump.
Breaking ChangeField removed/renamed/retyped, or required-ness tightened — needs a new version.
Auth Mechanism Comparison
API KeyBest for service-to-service trust; no per-user scope; manual rotation on leak.
JWTStateless, self-verifying claims; hard to revoke before expiry.
OAuth2 + PKCEDelegated third-party access without sharing credentials; standard for user-consent flows.
Refresh TokenLong-lived, server-revocable; used to mint new short-lived access tokens.
401 vs 403401 = not authenticated; 403 = authenticated but not authorized.
Idempotency & Error Contract Essentials
Idempotent MethodsGET, PUT, DELETE, HEAD, OPTIONS — same result no matter how many times repeated.
Non-Idempotent MethodsPOST, and PATCH in the general case — must be explicitly guarded for retries.
Idempotency-Key HeaderClient-generated unique key per logical action; server stores first result and replays it on retry.
Error Envelope{ error: { code, message, details, requestId } } — consistent across every endpoint.
429 ResponsePair with Retry-After and X-RateLimit-* headers so clients can self-throttle.
Assessment Integration

Recommended Practice Quizzes on QuizCluster

Test your retention and prepare for timed live coding and MCQ technical screening rounds:

Frequently Asked Questions

Is GraphQL replacing REST in interviews?

No — REST remains the dominant interview topic because most companies' production APIs are still REST-based, and REST design questions (auth, pagination, versioning, rate limiting) test fundamentals that transfer to GraphQL and gRPC as well. Expect GraphQL to come up as a comparison question, not a replacement topic.

Do I need to memorize every HTTP status code for interviews?

No, but you should know the common ones cold: 200/201/204 for success variants, 400/401/403/404/409/422 for client errors, and 429/500/503 for rate limiting and server failure. Interviewers care more about WHY you'd pick 422 over 400 than whether you've memorized all 63 official codes.

Should I bring up rate limiting and idempotency even if not explicitly asked?

Yes — proactively mentioning rate limiting, idempotency, and error contracts when designing any endpoint that writes data (especially payments or order creation) is a strong signal of production experience and is exactly what separates mid-level from senior-level answers.

How deep should I go on OAuth2 if the role isn't security-focused?

Know the Authorization Code + PKCE flow well enough to draw it from memory and explain what each step prevents (CSRF via state, code interception via PKCE) — you don't need to memorize every OAuth2 grant type (implicit, client credentials, device code) in equal depth unless the role is specifically security or platform-focused.

Explore Other Preparation Guides

Software Engineering
How to Prepare for SDE Interview: Complete 2026 Roadmap
16 min readRead →
Java Ecosystem
How to Prepare for Java Developer Interview: Core to Spring Boot & JVM
18 min readRead →
Microservices & Distributed Systems
How to Prepare for Microservices Developer Interview: Distributed Architecture & Cloud
17 min readRead →
System Design
System Design Interview Guide: Complete 2026 Roadmap
21 min readRead →
Databases
SQL Interview Questions & Preparation Guide: Beginner to Advanced
17 min readRead →
Programming Languages
Python Interview Preparation: Complete Guide for 2026
17 min readRead →
Frontend Engineering
React Interview Preparation: React 19 & Next.js Guide
17 min readRead →
Cloud & DevOps
Kubernetes Interview Guide: Architecture, Pods, Networking & Troubleshooting
17 min readRead →
Cloud & DevOps
AWS Solutions Architect Interview Guide: Real Architecture Scenarios
17 min readRead →
Databases
Database System Design: SQL vs NoSQL, Sharding, Replication & Indexing
19 min readRead →
Microservices & Distributed Systems
Kafka Interview Guide: Architecture, Consumers, Partitions & Exactly-Once Semantics
17 min readRead →
Cloud & DevOps
Docker Interview Guide: Images, Containers, Networking & Production Debugging
15 min readRead →
Programming Languages
JavaScript & TypeScript Interview Guide: From Closures to the Event Loop
17 min readRead →
Backend Engineering
Node.js Backend Interview Guide: Event Loop, Streams, APIs & Scaling
17 min readRead →
Databases
Redis System Design Guide: Caching, Eviction, Persistence & Distributed Locks
17 min readRead →
Software Engineering
Concurrency Interview Guide: Threads, Locks, Race Conditions & Deadlocks
17 min readRead →
Software Engineering
Dynamic Programming Patterns: How to Recognize and Solve DP Problems
17 min readRead →