API Design
API Design Interview Questions and Review Checklist
A checklist to review any API, and the questions interviewers ask with model answers.
On this page
In one line: a good API review is a short list of questions you ask in the same order every time: names, methods, shapes, errors, lists, change, retries, security, docs, and style. This lesson turns the whole series into one checklist, an automatic checker you can run, a worked review of a bad API, and 20 interview questions with answers you can say out loud.
You have now gone from "what is an API" to idempotency keys, OAuth and gRPC. That is a lot of pieces. In a real review or an interview, nobody asks you to recite them. They show you an endpoint list or a problem statement and ask, "What would you change?" or "How would you design this?"
This lesson gives you a way to answer. It is lesson 9 of the API design series, the last one. It leans on everything before: resources and URLs, methods and status codes, request and response design, pagination, versioning, retries, webhooks and rate limits, authentication and authorization, OpenAPI, and GraphQL and gRPC. If a checklist item looks strange, the lesson number beside it tells you where to find the full story.
What I ran / checked. The one program in this lesson, a small API review script, ran on Linux with Node.js v22.22.0 and @apidevtools/swagger-parser 13.1.0 (the same parser as in lesson 7). I pasted its real output. The checklist and the answers come from the earlier lessons, so they rest on the sources listed there: the OpenAPI 3.1.0 text, the OWASP REST Security Cheat Sheet, the OWASP OAuth 2.0 Cheat Sheet, the OWASP API Security Top 10 (2023) summary table, the OWASP GraphQL Cheat Sheet, the IETF drafts for Idempotency-Key and RateLimit headers, the Standard Webhooks spec, the GraphQL spec and GraphQL over HTTP draft, and the gRPC status code and HTTP/2 protocol documents. Not reachable from my machine: the IETF RFC pages (RFC 9110, 9457, 6749 and so on), Stripe and GitHub docs, and owasp.org itself, so when an answer below names an RFC, it is from the lessons in this series and general knowledge, not from me re-reading the RFC for this lesson. Where I say what a big provider does, I did not check their docs. I did not verify any GitHub star counts.
Words used in this lesson
- Review checklist is a fixed list of questions you ask about every API, so you do not forget the boring but important ones.
- Smell is a sign that something is probably wrong, even if you cannot prove it yet.
- Backward compatible means old clients keep working after a change.
- Blast radius means how much damage one failure or one stolen credential can cause.
- Design question is an interview task such as "design an API for a booking system".
- Trade-off is a choice where you gain one thing and give up another. Good answers always name both sides.
Part 1: the ten-minute review checklist
Print this, or copy it into your pull request template. Each group has the lesson that explains it. A "no" to any line is not always a bug. It is a place to stop and ask "is this on purpose?".
1. Resources and URLs (lesson 0)
- URLs name things (nouns), in the plural:
/orders,/orders/42. Not/getOrders. - Names are consistent: one case style, one word per concept (not
customershere andclientsthere). - Nesting is shallow (at most two levels), and every nested thing can also be reached by its own id.
- Actions that are not CRUD have a clear home (a sub-resource such as
POST /orders/42/cancellation, or a documented action), not a random verb in the path. - There is a stated rule for the version (path, header or date) and it is written down.
2. Methods and status codes (lesson 1)
- GET never changes anything. It is safe to call, cache and retry.
- PUT replaces and is idempotent; PATCH changes a few fields; DELETE is idempotent; POST creates or runs an action.
201with aLocationheader for creation.204for success with no body.202for work that finishes later.- Errors use the right class:
400/422for bad input,401for not logged in,403for not allowed,404for missing,409for a conflict,429for too many requests. Never200with an error inside. - Concurrent edits are handled (ETag with
If-Match, or a version field) where lost updates matter.
3. Request and response design (lesson 2)
- JSON shapes are consistent: the same field means the same thing everywhere; one naming style.
- Dates are ISO 8601 in UTC (
2026-10-11T10:00:00Z). Money is in the smallest unit (cents) with a currency code, never a float. - Responses never include secrets, password hashes, internal ids or fields the caller should not see (API3).
- Requests are validated at the edge with a schema. Unknown fields are rejected on input (stops mass assignment).
- One error format everywhere (problem details, RFC 9457), with a stable
type, a humantitle/detail, and field-level errors for validation. - Error messages do not leak stack traces, SQL or other users' data.
4. Lists, filtering and sorting (lesson 3)
- No endpoint returns an unbounded list. There is a default and a maximum page size.
- Cursor (keyset) pagination for big or changing data; offset only where the data is small and stable.
- Sorting is stable and has a tie-breaker (for example
created_atthenid). - Filters and sorts that clients may use are listed and indexed. Unknown ones give a clear
400. - A total count is offered only if it is cheap or clearly marked as approximate.
5. Change and compatibility (lesson 4)
- Adding a field, an endpoint or an optional parameter is treated as safe. Removing, renaming, tightening or changing meaning is treated as breaking.
- Clients are told to ignore unknown fields and unknown enum values where possible.
- A change policy exists: expand-then-contract, deprecation notices, a
Sunsetdate, and a way to see who still uses the old thing. - Old versions have an owner and an end date (API9: improper inventory management).
6. Reliability (lesson 5)
- Every non-idempotent POST that matters (payments, orders, emails) accepts an
Idempotency-Key. The server stores key, fingerprint and result; in-progress gives409; a different body gives422. - The key is claimed atomically (a database unique constraint), not with a check-then-save gap.
- Clients retry only temporary errors, with backoff and jitter, and obey
Retry-After. - Slow work returns
202with a job URL, not a held connection. - Webhooks: signed with HMAC over the raw body, a timestamp checked, constant-time compare, event ids for de-duplication, retries over days, secret rotation, delivery logs. The sender defends against SSRF.
- Rate limits exist per key or user, answer
429withRetry-After, and are cost-aware on expensive endpoints. Headers are documented.
7. Security (lesson 6)
- HTTPS only. No credentials in URLs.
Authorizationheaders are not logged. - 401 versus 403 is used correctly, with
WWW-Authenticate. - Tokens are short-lived. JWTs are verified with a fixed algorithm and checked for
iss,audandexp. API keys are stored hashed and shown once. - OAuth: authorization code with PKCE for user apps; exact
redirect_urimatch; no implicit or password grants. - Object-level check on every id (BOLA), property-level allow-lists on input and output (mass assignment, over-exposure), function-level checks on admin routes. Default deny.
- Scopes limit the app; ownership checks limit the user. Both exist.
8. Documentation and contract (lesson 7)
- An OpenAPI file (or equivalent) is the source of truth, validated and linted in CI.
- Every operation has
operationId,summary, examples, and its error responses. - A contract test runs against the real server and fails the build when responses drift from the file.
- A breaking-change diff runs between the main branch and each pull request.
- Idempotency, rate limit, pagination and webhook behaviour is documented in words, not only in the schema.
9. Style fit (lesson 8)
- The style (REST, GraphQL, gRPC) was chosen for a stated reason.
- If GraphQL: batching for N+1, depth and cost limits, field-level auth, introspection off for private APIs.
- If gRPC: field numbers are never reused, deadlines are set on every call, TLS is on, and browser access is solved.
10. Operations (touched in lessons 5 and 8, deeper in the Backend series)
- Every request has a request id in logs and in an error body, so support can find it.
- Logs are structured and do not hold secrets. Latency and error rate are measured per endpoint.
- There are timeouts on every outgoing call, and a plan for what the API does when a dependency is down.
See Backend lesson 5 for logging and request ids.
The OWASP API Top 10 as a final pass
The 2023 list is a good last pass because it is what attackers try first. Read each name and ask "where could this be true for us?": API1 broken object level authorization, API2 broken authentication, API3 broken object property level authorization, API4 unrestricted resource consumption, API5 broken function level authorization, API6 unrestricted access to sensitive business flows, API7 server side request forgery, API8 security misconfiguration, API9 improper inventory management, API10 unsafe consumption of APIs. (I read the summary table of this list in the OWASP repository. As with all Top 10 lists, say which year's edition you mean.)
Part 2: make part of the checklist automatic
A checklist that lives in someone's head is skipped on busy days. The parts that a machine can check should be checked by a machine. Here is a short script that reads an OpenAPI file (through the parser from lesson 7) and applies eight rules from the list above. It is deliberately small, so you can read it in two minutes and change it for your own team.
// review.mjsimport SwaggerParser from "@apidevtools/swagger-parser";const api = await SwaggerParser.dereference(process.argv[2]);const problems = [];const add = (where, msg) => problems.push(`${where}: ${msg}`);const METHODS = ["get", "put", "post", "patch", "delete"];if (!api.servers?.length) add("root", "no servers listed");if (!api.security && !Object.values(api.paths).some(p => METHODS.some(m => p[m]?.security))) add("root", "no security declared anywhere");for (const [path, item] of Object.entries(api.paths)) { // resource naming: lower-case, no verbs in the path for (const s of path.split("/").filter(s => s && !s.startsWith("{"))) { if (/[A-Z_]/.test(s)) add(path, `segment "${s}" should be lower-case kebab-case`); if (/^(get|create|delete|update|do|run)/i.test(s)) add(path, `segment "${s}" looks like a verb`); } for (const m of METHODS) { const op = item[m]; if (!op) continue; const where = `${m.toUpperCase()} ${path}`; const codes = Object.keys(op.responses ?? {}); if (!op.operationId) add(where, "missing operationId"); if (!op.summary) add(where, "missing summary"); if (!codes.some(c => c.startsWith("4"))) add(where, "no 4xx response documented"); const secured = (op.security ?? api.security ?? []).length > 0; if (secured && !codes.includes("401")) add(where, "secured but no 401 documented"); if (m === "post") { const hasKey = (op.parameters ?? []).some(p => p.in === "header" && p.name.toLowerCase() === "idempotency-key"); if (!hasKey) add(where, "POST without Idempotency-Key"); if (!codes.includes("429")) add(where, "POST without 429 documented"); if (codes.includes("201") && !op.responses["201"].headers?.Location) add(where, "201 without Location header"); } if (m === "get" && !path.endsWith("}")) { const names = (op.parameters ?? []).map(p => p.name); if (!names.includes("limit") && !names.includes("page_size")) add(where, "collection GET without a page-size parameter"); } for (const [code, r] of Object.entries(op.responses ?? {})) if (code.startsWith("4") || code.startsWith("5")) if (!Object.keys(r.content ?? {}).includes("application/problem+json")) add(`${where} ${code}`, "error is not application/problem+json"); }}console.log(`${process.argv[2]}: ${problems.length} finding(s)`);for (const p of problems) console.log(" - " + p);I ran it on two files. The first is the orders API file from lesson 7. The second, bad.yaml, is a small shop API written the way many first drafts look:
openapi: 3.1.0info: { title: Shop, version: "1" }paths: /getOrders: get: responses: "200": { description: ok } /Orders_List: post: summary: make order responses: "201": { description: created } "500": description: boom content: application/json: schema: { type: object }Real output:
../l7/openapi.yaml: 0 finding(s)bad.yaml: 15 finding(s) - root: no servers listed - root: no security declared anywhere - /getOrders: segment "getOrders" should be lower-case kebab-case - /getOrders: segment "getOrders" looks like a verb - GET /getOrders: missing operationId - GET /getOrders: missing summary - GET /getOrders: no 4xx response documented - GET /getOrders: collection GET without a page-size parameter - /Orders_List: segment "Orders_List" should be lower-case kebab-case - POST /Orders_List: missing operationId - POST /Orders_List: no 4xx response documented - POST /Orders_List: POST without Idempotency-Key - POST /Orders_List: POST without 429 documented - POST /Orders_List: 201 without Location header - POST /Orders_List 500: error is not application/problem+jsonAn honest note: the good file passes cleanly because I wrote it to follow these rules in lesson 7, so a zero is not proof that it is perfect. And the script cannot judge what it cannot see. It does not know if /Orders_List should be /orders, or if the 500 should exist at all. It catches the mechanical slips. Humans must still judge the design. Use the script to free review time for the questions only a person can answer, such as "is this the right resource model?" and "who may call this?".
Do not turn a checklist into a ritual. If every finding is silenced or every rule is ignored with a comment, the checklist is decoration. Each rule should have a reason you can explain in a sentence. If you cannot explain it, remove the rule.
Part 3: a worked review of a bad API
Here is a design as it might appear in a pull request for a small booking service. Read it first and write down what you would change before you read my list.
POST /createBooking body: {userId, roomId, start, end, price: 49.99}GET /getBookings?userId=7 returns every booking (could be 100000) as an arrayPOST /bookings/cancel body: {id}GET /deleteBooking/12 (deletes it)PUT /booking/12 body: {status: "paid"} (only status sent)Every response: 200 OK. Errors look like {"success": false, "msg": "Something went wrong"}Auth: the client sends header X-User-Id: 7Dates: "10/11/26 3pm"My review, in order of the checklist:
| # | Smell | Why it hurts | Fix |
|---|---|---|---|
| 1 | Verbs in URLs: /createBooking, /getBookings, /deleteBooking | The method already says the verb; names become inconsistent (lesson 0). | POST /bookings, GET /bookings, DELETE /bookings/12. |
| 2 | Singular and plural mixed: /booking/12 and /bookings/cancel | Clients cannot guess URLs. | Always plural: /bookings/12. |
| 3 | GET /deleteBooking/12 deletes | GET must be safe: crawlers, prefetchers and link scanners will delete bookings (lesson 1). | DELETE /bookings/12. |
| 4 | PUT with only status | PUT means replace the whole resource; a partial body wipes other fields or is ambiguous. | PATCH /bookings/12 with {status}. Better, make "pay" an action tied to a payment resource. |
| 5 | POST /bookings/cancel with id in body | The id belongs in the URL; the action is unclear. | POST /bookings/12/cancellation, or PATCH status to cancelled if the rules allow. |
| 6 | Price as 49.99 (a float) | Floating point rounding, no currency (lesson 2). | price_cents: 4999, currency: "EUR"; or better, compute the price on the server. |
| 7 | Client sends price | The client must never decide the price (a business-flow and integrity bug). | Server calculates from the room and dates. |
| 8 | Date "10/11/26 3pm" | Ambiguous (10 November or 11 October?), no timezone. | ISO 8601: 2026-10-11T15:00:00Z. |
| 9 | Always 200, errors in body | Clients, caches, monitors and retry code cannot tell success from failure (lesson 1). | Real status codes: 201, 400/422, 404, 409 (room already booked), and so on. |
| 10 | Error is {"success": false, "msg": ...} | No stable machine-readable code; no field errors (lesson 2). | Problem details with type, title, detail, errors[] and a request id. |
| 11 | GET /getBookings returns everything | Unbounded list: slow, expensive, a denial of service (lesson 3). | GET /bookings?limit=20&cursor=... with a maximum of 100. |
| 12 | userId in the query | Anyone can change it and read others' bookings (BOLA, lesson 6). | Take the user from the token. Filter by it on the server. |
| 13 | Auth is X-User-Id: 7 | The client claims who it is. This is not authentication at all (lesson 6). | Bearer token (OAuth with PKCE or a session) verified by the server. |
| 14 | POST /createBooking retried after a timeout | Double booking (lesson 5). | Idempotency-Key header, plus a database constraint so one room cannot have two overlapping bookings. |
| 15 | No version, no docs | You cannot evolve safely (lessons 4 and 7). | Put the version policy in info, write the OpenAPI file, add contract tests. |
Count how many of the fifteen are about style (1, 2, 8) and how many are about correctness or safety (3, 4, 6, 7, 9, 12, 13, 14). The safety ones are the ones to say first in an interview, because they cost real money.
Number 14 has a second layer. The idempotency key stops duplicate requests. It does not stop two different customers booking the same room at the same time. That needs a rule in the database (an exclusion or unique constraint on room and time range) or a transaction with a lock. When you review, always ask "what protects the business rule, not only the HTTP call?".
Part 4: a design problem, step by step
Interviewers love: "Design the API for a booking system" (or a library, a to-do app, a ride-hailing service). They are not hunting for the perfect answer. They watch how you think. Use this order. It is the series in seven steps.
Step 1: clarify (two minutes). Ask who calls the API (a mobile app, partners, internal services), what the main actions are, how big it is (users, requests per second), and what must never go wrong (double booking, double charge). Write down two or three answers. This also shows you think about the problem before the solution.
Step 2: find the resources. Nouns: rooms, bookings, customers, payments. Draw how they relate: a booking belongs to one customer and one room and has one payment.
Step 3: list the operations. For each resource: list, create, read, change, delete, and the special actions.
GET /rooms?city=...&available_from=...&available_to=...&limit=20&cursor=...GET /rooms/{roomId}POST /bookings (Idempotency-Key required) -> 201 + LocationGET /bookings?status=confirmed&limit=20&cursor=... (only my own)GET /bookings/{bookingId}PATCH /bookings/{bookingId} (change dates, If-Match with ETag)POST /bookings/{bookingId}/cancellation -> 201 or 200POST /bookings/{bookingId}/payments (Idempotency-Key required)Step 4: shapes and errors. Show one booking JSON: id, status (enum: pending, confirmed, cancelled), room_id, starts_at, ends_at (UTC), total_cents, currency. State the error format (problem details) and name the important error cases: 409 room not available, 422 end before start, 404 not found (also for someone else's booking), 429.
Step 5: the hard parts. This is where you win the interview. Say: (a) retries and double booking: idempotency key plus a database constraint on room and time range; (b) payment: the payment provider calls me back, so I need a webhook receiver that verifies signatures and de-duplicates by event id; (c) slow things such as sending confirmation emails happen in the background; (d) lists are paginated by cursor; (e) rate limits per user and per IP, stricter on POST /bookings.
Step 6: security. Say who calls and how: users log in through OAuth, and the API checks scope and then ownership on every booking id. Staff have a role and different scopes. Input is validated with a schema. Responses include only fields the caller may see.
Step 7: evolution and contract. Say you would write the OpenAPI file first, run contract tests in CI, add new fields without a version bump, and use a Sunset header when something must go.
Finish with the trade-off you made and what you would watch in production: "I chose cursor pagination, so we lose jump-to-page-37. I'd watch the p95 latency of the availability search, because that is the heaviest query."
This answer fits in ten minutes. If you have fifteen, add the data model (the overlap constraint), caching of room data, and the notification flow.
Part 5: how to talk in an API interview
A few habits that separate strong answers from weak ones:
- Say the trade-off. "Offset pagination is simple, but it gets slower with big offsets and can skip rows when data changes. Cursor pagination fixes both, but you cannot jump to a page." Two sides every time.
- Start with the failure, not the happy path. "What happens if the client times out after we charged the card?" is the question that shows experience.
- Name the status code. Do not say "an error". Say "409 Conflict because the room is already taken".
- Use a number or a real result when you have one. From this series: three parallel requests created three charges until the key was claimed atomically; a token bucket allowed 5 requests where a fixed window allowed 10 at a boundary; a naive GraphQL query did 11 database calls and a batched one did 2; a protobuf message was 16 bytes against 48 in JSON. A small true example beats a long abstract one.
- Admit what you have not used. "I have not run gRPC in production. I have built a small service and I know the status codes and the browser limits" is better than a bluff.
- Connect layers. Rate limits protect the service; authorization protects data; idempotency protects money. Say which one you are using and why.
Try it
- Take an API you built or use at work. Run the checklist. Find three items that fail. For each, write one sentence: "Fix now", "Fix next quarter" or "Accept, because...".
- Run the review script from Part 2 on an OpenAPI file you own, or write a small one for two endpoints. Which finding surprised you?
- Redesign the bad booking API from Part 3 into the final list of endpoints. Check that every URL is a noun, every POST that matters has an idempotency key, and no URL contains
userId. - Practise the seven-step design out loud for a library system (books, members, loans) in ten minutes. Record yourself. Did you mention a failure case before the happy path ended?
- Write the three headers a
429response should carry or may carry (hint: lesson 5). Then write the two headers a201should carry.
Answers
- There is no single answer. If many fail on one theme (for example no idempotency, no pagination, no ownership checks), start with the theme that can lose money or data. Security and reliability items usually come before style items.
- Common surprises: missing
operationIds, no 4xx response documented on POST, and collection endpoints without a page-size parameter. These are the cheap, mechanical ones. - A reasonable list:
GET /bookings,POST /bookings,GET /bookings/{id},PATCH /bookings/{id},POST /bookings/{id}/cancellation. The user comes from the token.POST /bookingstakesIdempotency-Keyand returns201withLocation. - Resources:
books,members,loans. Hard parts: one copy cannot be loaned twice (unique constraint on active loan per copy), overdue notices (background jobs, webhooks or email), fines (money in cents). Failure case: the librarian's tablet loses connection after scanning, so the check-out is retried and needs an idempotency key. 429 Too Many RequestswithRetry-After, and optionallyRateLimitandRateLimit-Policy(from the IETF draft) so clients can slow down early. A201should carryLocationpointing to the new resource, andContent-Type: application/json(and anETagif you use optimistic concurrency).
Interview questions
These cover the whole series. Answers are first-person and short enough to say aloud. Where a question points back to a lesson, the answer is the summary of it.
What makes an API good? (Must know)
A good API is predictable. Resources are named with nouns and used the same way everywhere, the HTTP methods and status codes mean what they say, there is one error format, lists are paginated, and changes do not break existing clients. It is also safe to retry, secure by default (every object access is checked) and documented in a file that is tested against the real server. If a new developer can guess the next URL and handle its errors without reading much, the design is good.
When would you use POST, PUT and PATCH? (Trap)
POST creates a resource when the server picks the id, or runs an action; it is not idempotent. PUT replaces the whole resource at a known URL and is idempotent. PATCH changes only some fields. It is not guaranteed idempotent, but I try to design it so repeating gives the same result. For DELETE, repeating gives the same end state, so it is idempotent too. The safe-to-retry difference is why POST for payments needs an idempotency key.
Explain the difference between 401, 403 and 404 for a protected resource. (Must know)
401 means I do not know who you are, so log in again. 403 means I know who you are and you are not allowed. For other people's objects I often return 404, so that attackers cannot find out which ids exist. I choose one rule and document it. A 401 carries a WWW-Authenticate header.
How do you make a POST endpoint safe to retry? (Must know)
I require an Idempotency-Key header. The server stores the key, a fingerprint of the request, and the response. A repeat after completion gets the saved response; a repeat while the first is running gets 409; the same key with a different body gets 422. The important detail is to claim the key atomically, with a unique constraint in the database, before doing the work. In my test, a check-then-save version let three parallel requests create three charges.
Offset or cursor pagination? (Must know)
Offset is simple and lets users jump to a page, but it gets slower with a big offset (the database still walks past all skipped rows) and can duplicate or skip items when rows are inserted. Cursor, or keyset, pagination uses a pointer such as (created_at, id) of the last item, which is fast and stable, but you cannot jump to page 37. I default to cursor for large or fast-changing data, use a stable sort with a tie-breaker, cap the page size, and avoid exact counts unless they are cheap.
What counts as a breaking change? (Must know)
Anything that can make an existing client fail: removing or renaming a field or endpoint, making an optional request field required, changing a type, narrowing an enum or a limit, changing the meaning of a value, or changing an error code that clients handle. Adding an optional field, a new endpoint or a new enum value is usually safe, if clients ignore unknown fields. I check changes with a diff of the OpenAPI file in CI.
How do you version an API? (Senior signal)
First I try not to need a new version: I add things, and use expand-and-contract to change behaviour in steps. When a real break is unavoidable I version on purpose. A version in the URL (/v2) is simple and visible; a header or date-based version keeps URLs stable but is harder to see and cache. Whatever I choose, I document it, deprecate the old one with notices and a Sunset date, measure who still uses it, and only turn it off after that.
How do you design webhooks? (Senior signal)
Each event has an id, a type, a timestamp and a fixed payload per type. I sign the raw body with HMAC-SHA256 using a per-customer secret, over the id, timestamp and body, and send it in headers. The receiver compares in constant time, checks that the timestamp is recent, and de-duplicates by event id. I retry for days with exponential backoff and jitter, treat only 2xx as success, give customers a delivery log and a replay button, support secret rotation by sending two signatures, and block internal addresses to avoid SSRF.
How would you rate limit a public API? (Must know)
I choose the key first: the API key or user for authenticated calls, IP plus username for login. I pick an algorithm. A token bucket allows short bursts at a steady average, while a fixed window is simpler but allows a double burst at the boundary: in my test it let 10 requests through in 0.2 seconds against a limit of 5 per 10 seconds. The counters live in a shared store so all servers agree. Over the limit I return 429 with Retry-After, and advertise limits in headers. Expensive endpoints get their own, lower limits.
Explain OAuth 2.0 authorization code with PKCE. (Must know)
The client creates a random verifier and sends its hash, the challenge, with the authorization request. The user logs in at the authorization server and approves. The server redirects back with a short-lived single-use code and the state. The client checks state and posts the code and the original verifier to the token endpoint. The server hashes the verifier and compares. If someone steals the code, they cannot use it without the verifier. I use it for all user-facing apps, including single-page and mobile apps, and I require an exact redirect URI match.
What is BOLA and how do you test for it? (Must know)
Broken Object Level Authorization: an endpoint takes an object id and returns the object without checking that the caller may access it. It is first in the OWASP API Top 10 for 2023. To prevent it, I check ownership or permission in every handler that uses a client-supplied id, ideally by scoping the query (WHERE id = ? AND owner_id = ?). To test, I create two users and verify that user A gets 404 or 403 on every id belonging to user B, for every method. Unguessable ids help a little but are not the fix.
What is mass assignment? (Trap)
It is when the server copies all fields from the request into a record, so a client can set fields it should not, such as role or balance. The fix is an allow-list of settable fields, a schema that rejects unknown properties on input, and separate input and output models. Its mirror image is excessive data exposure: returning every field of a record. OWASP combines both as broken object property level authorization.
What do you put in an OpenAPI file and why test against it? (Must know)
Paths and operations with parameters, bodies, responses and errors, reusable schemas in components, the security scheme, and webhooks. The file drives docs, request validation, mocks, generated clients and breaking-change checks. I test the real server against it with contract tests because otherwise the file drifts: in my demo, a changed server still returned 201, but the contract test found four wrong fields. I also lint it, since a file can be valid but poor.
REST, GraphQL or gRPC: how do you choose? (Must know)
For public APIs and simple products I start with REST: it has the best caching, tooling and docs. I consider GraphQL when many clients need different shapes of the same connected data, and I have the capacity for batching, query cost limits and field-level authorization. I use gRPC between internal services where strict types, streaming, deadlines and efficiency matter, and expose REST or GraphQL to the outside. A system can use all three in different places.
What is the N+1 problem, and how does it show up in an API? (Senior signal)
It is one query for a list followed by one query per item for related data. It shows up in ORMs and, very easily, in GraphQL resolvers. In my test, 10 posts caused 11 database calls; with DataLoader batching it was 2. In REST it also happens when an endpoint loops over rows and fetches details one by one. The fixes are joins, IN (...) batching, DataLoader and caches, plus an indexed query design.
How would you protect a GraphQL API from expensive queries? (Senior signal)
I add a depth limit and list-size limits as validation rules so bad queries are rejected before any resolver runs. I add timeouts, cost analysis for heavy fields, and rate limits by cost rather than only per request. I limit how many operations or aliases one HTTP request may hold, since batching can hide brute-force attempts. I turn off introspection for private APIs and authorize every field. In my toy server a depth-9 query caused 9,841 resolver calls.
A client reports that a payment was charged twice. How do you investigate? (Senior signal)
I start with the request ids and logs for both charges. Did they have the same idempotency key, or different ones? If the same key charged twice, the key claim was not atomic or it expired too early. If different keys, the client generated a new key on retry. That is a client bug. I also check for webhook duplicates: was the same event handled twice because it was not de-duplicated by event id? Then I fix the specific gap, add a unique constraint where it was missing, and write a test that sends the request twice in parallel.
An endpoint is slow. What do you look at in the API design? (Senior signal)
I measure first: latency per endpoint, and where the time goes. Then the usual API-level causes: an unbounded list or a large offset (add cursor pagination and a page size cap), N+1 queries, missing indexes for the filters and sorts the API allows, returning huge objects when a client needs a few fields, no caching headers for data that rarely changes, and work that should be asynchronous (return 202 and a job). I fix the biggest cause first and measure again.
How do you deprecate an endpoint without upsetting clients? (Must know)
I announce it in the docs and the changelog, mark it deprecated in OpenAPI, and send a Deprecation or Sunset header on responses with the date, so tools and humans can see it. I measure who still calls it, by API key, and contact the heavy users directly. I offer the replacement first and a migration guide, give a long enough window, and only shut it off after usage is near zero, often with a brief "brownout" (a planned short outage) to find forgotten callers.
Walk me through designing a booking API in five minutes. (Must know)
I clarify who calls it and what must never go wrong, which is a double booking or a double charge. Resources are rooms, bookings and payments. Endpoints: GET /rooms with filters and cursor pagination, POST /bookings with an idempotency key returning 201 and Location, GET /bookings/{id}, PATCH with an ETag, POST /bookings/{id}/cancellation. Errors use problem details, with 409 for a taken room. The room-overlap rule is a database constraint, not only an HTTP check. Payment confirmation arrives through a signed webhook, de-duplicated by event id. Auth is OAuth with PKCE, scopes plus ownership checks, rate limits per user. I write the OpenAPI file first, add contract tests, and say what I would watch in production.
Key takeaways
- Review in the same order every time: names, methods, shapes, errors, lists, change, retries, security, docs, style fit, operations.
- Say the money-and-data risks first: no authorization on objects, no idempotency, unbounded lists, wrong status codes.
- A checklist helps most when machines do the mechanical parts (lint, contract tests, diff) and people judge the design.
- The strongest interview answers name the failure case, the status code and the trade-off, and use one small real number or example.
- Protect the business rule at the lowest layer that can enforce it (a database constraint), not only at the HTTP edge.
- REST, GraphQL and gRPC are tools. The decisions in this series (names, errors, retries, auth, docs, change) apply to all of them.
What's next
This is the end of the API design series. To keep going:
- Put these ideas into a working service with the Backend in Depth series: folders, validation, error handling, logging, auth and testing.
- Study the attack side in the Security for Developers series, especially access control (lesson 4) and APIs and servers (lesson 7).
- Practise out loud with Full-Stack lesson 30 (tRPC, REST and OpenAPI) and Full-Stack lesson 22 (auth libraries, OAuth and OIDC).
Your homework: pick one real API, run the checklist, write down the three most important fixes, and explain them to a friend in under two minutes. If you can do that clearly, you are ready for the interview.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related lessons
- Backend and API Scenario Bank · Scenario Debugging
- Route Handlers, Middleware (Proxy) and APIs · Next.js App Router
- APIs and Servers: SSRF, File Uploads, Rate Limits and Misconfiguration · Security for Developers
Part of the study guide
Related series