← All practice sets
BackendPractice · 12 questions

Backend APIs

API design questions that come up in almost every backend loop: protocols, HTTP semantics, pagination, auth, idempotency, rate limits, webhooks, caching, and N+1.

Answer out loud or on paper before revealing. If your answer misses a key point, revisit the linked guide or explainer.

  1. Q1Mid

    When would you choose REST, GraphQL, or gRPC for an API?

    Reveal model answer

    REST over HTTP is the default for public and resource-shaped APIs: it is universally supported and gets HTTP caching, status codes, and tooling for free. GraphQL fits when many clients (web, mobile) need different shapes of the same graph of data and you want to avoid over- and under-fetching, at the cost of harder HTTP caching, per-field authorization, and query-cost control. gRPC uses Protocol Buffers contracts and HTTP/2, so it suits internal service-to-service calls that need low latency, strong typing, and streaming, but browsers need a proxy such as gRPC-Web. Many systems mix them: REST or GraphQL at the edge, gRPC inside.

    WHAT A STRONG ANSWER COVERS

    • REST: resource-oriented, cacheable with standard HTTP semantics, easiest for third parties.
    • GraphQL: client-chosen shape; needs DataLoader-style batching, depth/complexity limits, and persisted queries for caching.
    • gRPC: protobuf schema, code generation, streaming; best for internal traffic.
    • The choice is driven by the consumers and the team, not by fashion.

    RED FLAGS

    • "GraphQL is always faster than REST." It moves work to the server and can be slower without batching.
    • Choosing gRPC for a public browser API without mentioning the proxy requirement.
    • Ignoring that GraphQL usually sends everything as POST to one endpoint, which bypasses CDN caching by default.

    LIKELY FOLLOW-UPS

    • How would you stop a malicious GraphQL query from taking down the database?
    • How do you version a protobuf contract without breaking old clients?

    SOURCES

  2. Q2Junior

    Which HTTP methods are safe and which are idempotent, and why does it matter?

    Reveal model answer

    Safe methods (GET, HEAD, OPTIONS, TRACE) are read-only by contract. Idempotent methods are the safe ones plus PUT and DELETE: sending the same request twice has the same intended effect on the server as sending it once, even if the response differs (a second DELETE may return 404). POST is neither, and PATCH (RFC 5789) is not idempotent by definition. It matters because clients, proxies, and SDKs may automatically retry idempotent requests after a network failure, so a GET that mutates state or a POST retried blindly can cause real damage.

    WHAT A STRONG ANSWER COVERS

    • Safe = no intended state change; idempotent = repeat has the same effect as once.
    • PUT replaces a resource at a known URI, so it is idempotent; POST creates or processes, so it is not.
    • Idempotency is about server state, not identical responses.
    • Retries are only automatically safe for idempotent methods; POST needs an idempotency key.

    RED FLAGS

    • Saying idempotent means the response is identical every time.
    • Using GET for actions like /delete?id=1 (crawlers and prefetchers will trigger it).
    • Claiming PATCH is always idempotent.

    LIKELY FOLLOW-UPS

    • How would you make a POST that creates a payment safe to retry?
    • Is incrementing a counter via PUT idempotent?

    SOURCES

  3. Q3Mid

    Offset pagination versus cursor (keyset) pagination: what are the trade-offs?

    Reveal model answer

    Offset pagination (LIMIT 20 OFFSET 4000) is simple and supports jumping to page N, but the database still computes and discards every skipped row, so deep pages get slower, and concurrent inserts or deletes shift rows so users see duplicates or miss items. Cursor pagination returns an opaque cursor encoding the last row's sort key (for example created_at plus id as a tie-breaker) and the next page queries WHERE (created_at, id) < (:c, :id) ORDER BY created_at DESC, id DESC LIMIT 20, which uses an index and stays stable under writes. The cost is no random page access and a cursor that is tied to one sort order. Either way you need a deterministic ORDER BY with a unique tie-breaker.

    WHAT A STRONG ANSWER COVERS

    • Large OFFSET is O(offset): skipped rows are still computed.
    • Offset pages drift when rows are inserted or deleted between requests.
    • Keyset needs a unique, indexed sort key (add id as a tie-breaker).
    • Cursors should be opaque so the server can change their encoding.
    • Use offset for small admin tables or when page numbers are a real requirement.

    RED FLAGS

    • Paginating without ORDER BY, or ordering by a non-unique column only.
    • Returning a total count on every page of a huge table without considering its cost.
    • Exposing raw SQL values in the cursor and trusting them without validation.

    LIKELY FOLLOW-UPS

    • How would you support "jump to page 50" with cursors?
    • How do you paginate when the user can change the sort column?

    SOURCES

  4. Q4Mid

    How do you version an API and evolve it without breaking clients?

    Reveal model answer

    First, avoid breaking changes: adding optional fields, new endpoints, and new enum values that clients are told to tolerate are backward compatible, while removing or renaming fields, changing types, or tightening validation are breaking. When a break is unavoidable, pick a visible scheme: a URL prefix (/v2) is easy to route and cache, while a header or date-based version (Stripe pins each account to a version and accepts a Stripe-Version override) lets you ship many small versions and translate old responses through compatibility layers. Run versions side by side, publish deprecation dates, measure who still calls the old version, and only then remove it.

    WHAT A STRONG ANSWER COVERS

    • Know what counts as breaking versus additive.
    • URL versioning versus header or date-based versioning, with trade-offs.
    • Pin clients to a version so behavior does not change under them.
    • Deprecation needs telemetry and a timeline, not just a changelog entry.

    RED FLAGS

    • Bumping the major version for every additive change.
    • "We just update the API and tell clients." Mobile apps stay on old builds for months.
    • Assuming clients ignore unknown fields or enum values without documenting it.

    LIKELY FOLLOW-UPS

    • How would you handle a mobile app version you cannot force users to upgrade?
    • What would you put in a response to warn a client it is using a deprecated version?

    SOURCES

  5. Q5Junior

    What is the difference between authentication and authorization, and between 401 and 403?

    Reveal model answer

    Authentication establishes who the caller is (session cookie, token, mTLS certificate); authorization decides whether that identity may perform this action on this specific resource. 401 Unauthorized means the request lacks valid authentication credentials, and the server should send a WWW-Authenticate challenge; 403 Forbidden means the server understood who you are and refuses. Authorization must be checked server-side on every request and on the specific object, not only the route: checking that a user is logged in but not that invoice 123 belongs to them is the classic IDOR bug. Deny by default.

    WHAT A STRONG ANSWER COVERS

    • Authn = identity; authz = permission for an action on a resource.
    • 401 = missing or invalid credentials; 403 = authenticated but not allowed.
    • Object-level checks prevent IDOR (insecure direct object references).
    • Deny by default and enforce on the server, never only in the UI.

    RED FLAGS

    • Relying on hidden buttons or client-side checks for authorization.
    • Treating a valid JWT as proof the caller may access any ID in the URL.
    • Returning 403 for a missing token and 401 for a permission failure (swapped).

    LIKELY FOLLOW-UPS

    • When might you return 404 instead of 403 for a resource the caller cannot see?
    • RBAC versus ABAC versus relationship-based access control: when does each fit?

    SOURCES

  6. Q6Mid

    Server-side sessions or JWTs: what are the trade-offs?

    Reveal model answer

    A server-side session stores state in a database or cache and gives the browser an opaque random ID in a Secure, HttpOnly, SameSite cookie; it is easy to revoke instantly and leaks nothing, at the cost of a lookup per request. A JWT is a signed, self-contained token that services can verify without a lookup, which helps across many services, but it cannot be revoked before expiry without a denylist, so you keep access tokens short-lived and pair them with refresh tokens that are revocable. JWT validation must pin the allowed algorithms and check issuer, audience, and expiry. For a single first-party web app, cookie sessions are usually the simpler and safer default.

    WHAT A STRONG ANSWER COVERS

    • Sessions: opaque ID, server state, instant revocation.
    • JWT: stateless verification, but revocation and logout are hard; keep them short-lived.
    • Store browser credentials in HttpOnly cookies rather than localStorage to reduce XSS token theft; cookies then need CSRF protection.
    • Validate alg against an allowlist, plus iss, aud, and exp.

    RED FLAGS

    • "JWTs are more secure than sessions." They are a different trade-off.
    • Putting sensitive data in a JWT payload thinking it is encrypted (it is only base64url-encoded).
    • Accepting whatever alg the token header says, including none.

    LIKELY FOLLOW-UPS

    • How do you log a user out everywhere when you use JWTs?
    • How would you rotate the signing key without logging everyone out?

    SOURCES

  7. Q7Senior

    Walk through the OAuth 2.0 authorization code flow with PKCE. What does PKCE protect against?

    Reveal model answer

    The client generates a random code_verifier, sends its SHA-256 hash as code_challenge (method S256) with the authorization request, and the user authenticates at the authorization server, which redirects back with a short-lived authorization code. The client then exchanges the code plus the original code_verifier at the token endpoint; the server hashes the verifier and only issues tokens if it matches the stored challenge. This defeats authorization code interception and injection: an attacker who steals the code cannot redeem it without the verifier, which never left the client. RFC 9700 (OAuth 2.0 Security BCP) requires PKCE for public clients, recommends it for confidential ones, and says not to use the implicit or password grants.

    WHAT A STRONG ANSWER COVERS

    • code_verifier is random per request; code_challenge = BASE64URL(SHA256(verifier)).
    • The code is exchanged at the token endpoint, never used as a credential directly.
    • Mitigates code interception (custom URL schemes, logs, referrers) and injection.
    • Still validate state (or rely on PKCE plus exact redirect URI matching) against CSRF.
    • Implicit and resource owner password grants are deprecated by RFC 9700.

    RED FLAGS

    • Saying PKCE replaces TLS or the client secret for confidential clients.
    • Using the plain challenge method when S256 is available.
    • Confusing OAuth (delegated authorization) with OpenID Connect (authentication and ID tokens).

    LIKELY FOLLOW-UPS

    • Where should a single-page app keep its tokens?
    • How do refresh token rotation and reuse detection work?

    SOURCES

  8. Q8Senior

    How would you implement idempotency keys for a POST endpoint that charges a card?

    Reveal model answer

    The client generates a unique key (a UUID) per logical operation and sends it in an Idempotency-Key header, reusing it on every retry. The server atomically inserts the key with a fingerprint of the request parameters and an in-progress status (a unique constraint makes concurrent duplicates lose), performs the work, then stores the final status code and body against the key in the same transaction as the business write where possible. A retry with the same key returns the stored response; one that arrives while the first is still running gets a 409 or is told to retry; one with the same key but different parameters is rejected. Keys expire after a retention window (Stripe uses at least 24 hours), and downstream calls to the payment provider pass their own idempotency key too.

    WHAT A STRONG ANSWER COVERS

    • Client-generated key per logical operation, reused across retries.
    • Atomic claim of the key (unique constraint or conditional insert) to handle concurrent retries.
    • Store and replay the original response, including failures that happened after execution began.
    • Reject the same key with different parameters.
    • Propagate idempotency to downstream side effects.

    RED FLAGS

    • Check-then-insert without a lock or unique constraint (race between two retries).
    • Deduplicating only in an in-memory cache on one instance.
    • Generating the key on the server, which makes client retries look like new requests.

    LIKELY FOLLOW-UPS

    • What happens if the process crashes after charging the card but before storing the response?
    • How long should keys be retained, and why?

    SOURCES

  9. Q9Mid

    How would you add rate limiting to a public API?

    Reveal model answer

    Decide the key (API key, user, or IP for anonymous traffic) and the limit, then pick an algorithm: a token bucket allows short bursts while enforcing an average rate, and a sliding-window counter avoids the burst-at-the-boundary problem of fixed windows. Keep counters in a shared store such as Redis with atomic operations so every instance sees the same count, and decide whether to fail open or closed if that store is down. Reject with 429 Too Many Requests and a Retry-After header, and expose remaining quota so well-behaved clients can back off. Request-rate limits are often paired with a concurrency limit and load shedding that protects the service itself.

    WHAT A STRONG ANSWER COVERS

    • Choose the limiting key and limits per tier or endpoint cost.
    • Token bucket versus fixed window versus sliding window trade-offs.
    • Shared, atomic counters (Redis INCR with TTL or a Lua script) across instances.
    • 429 plus Retry-After; clients use backoff with jitter.
    • Rate limits protect fairness; load shedding protects availability.

    RED FLAGS

    • Per-instance in-memory counters behind a load balancer, which multiplies the real limit.
    • Returning 500 or 503 for a client that exceeded its quota.
    • Read-then-write counter updates that race under concurrency.

    LIKELY FOLLOW-UPS

    • How would you rate limit across multiple regions?
    • How should your own service behave when a third-party API rate limits you?

    SOURCES

  10. Q10Senior

    How do you receive webhooks reliably?

    Reveal model answer

    Verify the signature over the raw request body with a constant-time comparison and reject stale timestamps to block replays. Persist the event (or enqueue it) and return 2xx quickly, then process asynchronously, because slow handlers time out and trigger redelivery. Deliveries are at-least-once and unordered, so deduplicate on the event ID with a unique constraint and never rely on arrival order or timestamps: treat the event as a hint and fetch the current object state from the provider, or apply only state transitions that move forward. Monitor failures and have a replay path for events you missed during an outage.

    WHAT A STRONG ANSWER COVERS

    • Signature verification on the raw body; timestamp tolerance against replay.
    • Acknowledge fast, process via a queue.
    • Idempotent processing keyed on event ID.
    • No ordering guarantee: refetch state or use monotonic state machines.
    • Reconciliation or replay for missed events.

    RED FLAGS

    • Doing all the business work synchronously before responding.
    • Parsing and re-serializing JSON before verifying the signature.
    • Assuming each event arrives exactly once and in order.

    LIKELY FOLLOW-UPS

    • How would you design the sending side of a webhook system?
    • What do you do with an event that keeps failing to process?

    SOURCES

  11. Q11Mid

    Explain Cache-Control, ETag, and conditional requests. How would you cache an API response?

    Reveal model answer

    Cache-Control sets freshness and who may store a response: max-age gives the freshness lifetime, s-maxage overrides it for shared caches like CDNs, private forbids shared caches from storing it, no-cache means it may be stored but must be revalidated before every reuse, and no-store means do not store it at all. An ETag is a validator for a representation; the client sends it back in If-None-Match and the server answers 304 Not Modified with no body if it still matches, saving bandwidth but not the round trip. For an API, public reference data can use max-age with stale-while-revalidate at the CDN, while per-user responses should be private or no-store and must vary correctly (for example on Authorization or Cookie).

    WHAT A STRONG ANSWER COVERS

    • no-cache is not no-store.
    • Freshness (max-age, s-maxage) versus validation (ETag, Last-Modified, 304).
    • private versus shared caches; never let a CDN cache per-user data.
    • ETags also enable optimistic concurrency with If-Match on writes (412 on mismatch).

    RED FLAGS

    • Saying no-cache disables caching.
    • Caching authenticated responses at a CDN without a cache key that includes the user.
    • Generating ETags from a timestamp that differs per server in a fleet.

    LIKELY FOLLOW-UPS

    • How do you invalidate a CDN-cached response after a write?
    • How would you use If-Match to prevent lost updates?

    SOURCES

  12. Q12Mid

    What is the N+1 query problem, and how do you fix it in a REST or GraphQL API?

    Reveal model answer

    N+1 happens when you run one query for a list and then one more query per item to load a relation, usually hidden behind an ORM's lazy loading or a per-field GraphQL resolver; 50 orders become 51 round trips. Fix it by loading relations in bulk: eager loading or a join in the ORM, a single WHERE id IN (...) query, or in GraphQL a DataLoader that collects keys during one tick and batches them into one query per type. Detect it with query counts per request in tests or APM traces rather than waiting for production latency.

    WHAT A STRONG ANSWER COVERS

    • Recognize the pattern: query count grows with result size.
    • Fix with eager loading, joins, or IN-list batching.
    • GraphQL: per-request DataLoader for batching and de-duplication.
    • Guard with tests that assert query counts or with tracing.

    RED FLAGS

    • "Add a cache" as the first fix.
    • Sharing a DataLoader cache across requests, leaking data between users.
    • Eager loading everything, which creates huge joins and row explosion.

    LIKELY FOLLOW-UPS

    • When is a join worse than two queries?
    • How would you spot N+1 in production traces?

    SOURCES