On this page
- What an API is in a design interview
- Choosing a protocol: REST, GraphQL, gRPC or push
- REST, part 1: modelling resources
- REST, part 2: HTTP methods and what they promise
- REST, part 3: where each input belongs
- REST, part 4: status codes and response shapes
- Worked example: the food-delivery API
- GraphQL: one endpoint, client-shaped responses
- RPC and gRPC: fast, typed calls between services
- Design principles that keep an API usable
- Pagination in depth (members only)
- Filtering, sorting, field selection and search (members only)
- Idempotency keys: making POST safe to retry (members only)
- Concurrency control with ETags (members only)
- Long-running operations: 202 Accepted (members only)
- Error responses clients can act on (members only)
- Versioning and evolving an API (members only)
- Security: authentication, authorization and trust (members only)
- Rate limiting and throttling (members only)
- The 5-minute API step: checklist and whiteboard (members only)
- Interview questions (members only)
Every system design round has a moment, usually five or ten minutes in, where the interviewer says "Okay, what does the API look like?" It is a short step, but it sets the vocabulary for everything after it: the endpoints you name become the arrows on your diagram, the ids you choose become your partition keys, and the one detail you forget (pagination, idempotency, who is allowed to call what) is exactly where the follow-up questions land. This guide covers the whole surface: how to pick between REST, GraphQL and gRPC, how to model resources, which status codes matter, how pagination really breaks, idempotency keys, ETags, long-running jobs, versioning, auth and rate limits. We use one product the whole way through, a food-delivery app, so every idea lands on something concrete.
After reading this article you will be able to
- Pick REST, GraphQL, gRPC or a push channel for a given client in under a minute, and defend the choice with one trade-off.
- Turn a list of core entities into plural-noun resources, choose the right HTTP method for each operation and put every input in the right place (path, query, header or body).
- Return the right status code, with the right headers, from 201 Created to 429 Too Many Requests, and a consistent error body.
- Explain why offset pagination repeats and skips rows on live data, and design cursor (keyset) pagination with a stable sort.
- Make a payment-style POST safe to retry with an idempotency key, and stop lost updates with ETags and
If-Match. - Handle long-running operations with 202 Accepted, version an API without breaking clients, and secure it with tokens, roles and rate limits.
- Present a complete API on the whiteboard in about five minutes, and answer the follow-ups interviewers ask most.
01What an API is in a design interview
API (application programming interface) Definition
The contract between a client and your system: which operations exist, what each one accepts, what it returns and what can go wrong. It says nothing about how the work is done behind it.
The best analogy I know is a restaurant menu. The menu lists what you can order, in what sizes and with which options. It does not tell you how the kitchen is laid out, how many cooks are on shift or where the rice is stored. The kitchen can be rebuilt overnight and, as long as the menu stays the same, no diner notices. An API is the menu; your services and databases are the kitchen.
That framing tells you what good API design optimises for. It should be easy for clients to use correctly, hard to use incorrectly, and stable while everything behind it changes. Notice that "mirrors my database tables" is not on that list. We'll come back to that.
The API is the contract between clients and your system
Where the API step sits in the interview
Most design rounds follow a similar arc: clarify requirements, list the core entities, define the API, draw the high-level design, then go deep on one or two hard parts. The API step comes right after the entities because it is the bridge between them: each functional requirement becomes one or two endpoints, and each endpoint touches one or two entities.
Here is the part many candidates get wrong in both directions. Most interviewers are not grading API perfection. They want a reasonable, consistent API in about five minutes, so you can get to the scaling and consistency problems where the real signal is. Spending 20 minutes debating PUT vs PATCH costs you the deep dive. Skipping the API entirely costs you the shared vocabulary.
Two exceptions. For frontend, mobile, platform or product-engineering roles, the API is the job, so expect more time on it and sharper questions about payload shape, pagination and errors. For junior roles, interviewers expect less distributed-systems depth, so more of the hour often goes to the API and data model. Ask early: "How much time would you like me to spend on the API?"
For this guide we'll design the public API of a food-delivery app: customers browse restaurants and menus, place and track orders, pay, and leave reviews; restaurant owners edit menus; couriers update deliveries; support staff issue refunds. We'll cover the interview-sized version first in each section, then the production detail you can reach for when the interviewer pushes.
02Choosing a protocol: REST, GraphQL, gRPC or push
You are choosing between three request/response styles, plus one family of push channels for real-time features. Here is each in one breath.
- REST models your system as resources (nouns) addressed by URLs and manipulated with standard HTTP methods. It rides on everything the web already has: caches, proxies, browsers, curl, status codes. It is the default for public and client-facing APIs.
- GraphQL exposes one endpoint and a typed schema. The client sends a query naming exactly the fields it wants, across related objects, and gets exactly that back. It shines when many different clients need differently shaped data.
- RPC, today mostly gRPC, models the system as procedures (verbs) you call like local functions:
Quote(orderId),CheckPermission(user, resource). Messages are binary Protocol Buffers over HTTP/2, which makes it compact and fast. It is the usual choice between your own services. - WebSockets and Server-Sent Events (SSE) are not request/response APIs at all; they are long-lived connections that let the server push. You add them alongside one of the above for live order tracking, chat or notifications.
The decision rarely needs more than three questions. Before reading on, try answering them for our food app's courier-tracking screen.
Picking a protocol in three questions
For courier tracking, the answers are: external client, yes the server must push, so the tracking screen opens an SSE stream (or a WebSocket) while everything else on that screen is REST. SSE is the simpler fit when only the server talks (location updates flowing one way); WebSockets earn their extra complexity when both sides send messages, as in chat.
| Aspect | REST | GraphQL | gRPC |
|---|---|---|---|
| Mental model | Resources (nouns) + HTTP methods | A typed graph you query | Remote functions (verbs) |
| Endpoints | Many URLs | One, usually POST /graphql | One method per procedure |
| Payload | Usually JSON, human readable | JSON, shaped by the query | Binary Protocol Buffers |
| Transport | HTTP/1.1 or HTTP/2 | HTTP (WebSocket for subscriptions) | HTTP/2 only; browsers need a gRPC-Web proxy |
| HTTP caching | Excellent (GET + ETag + CDNs) | Hard (one POST URL); needs persisted queries or app-level caches | None built in |
| Contract and types | Optional (OpenAPI) | Built in (schema) | Built in (.proto) with code generation |
| Streaming | No (pair it with SSE or WebSockets) | Subscriptions | Server, client and bidirectional streams |
| Best fit | Public APIs, CRUD, web and mobile | Many clients with different data needs | Internal, high-volume service-to-service calls |
My default, and the one I'd recommend you say out loud: "I'll use REST for the client-facing API, and gRPC between internal services if latency matters." REST is well understood by every interviewer, has tooling everywhere, and fits the large majority of product APIs. Reach for GraphQL or gRPC only when you can name the specific problem it solves. If you're unsure, say REST and move on; nobody fails a round for picking REST.
Then the interviewer asks how they'll cache the restaurant page at the CDN, or stop a client from requesting a query nested ten levels deep, and the answer is not ready. Pick the tool whose costs you can explain. GraphQL is a great answer when you can say "we have a web dashboard, two mobile apps and a partner widget, all needing different slices of the same data".
03REST, part 1: modelling resources
REST design starts from your core entities, not from your screens or your tables. A resource is any thing a client can name and act on: a restaurant, a menu item, an order, a review. Resources come in two shapes: a collection (/restaurants) and an item inside it (/restaurants/81).
Four naming rules cover almost every interview.
- Nouns, not verbs. The HTTP method is the verb.
POST /orders, notPOST /createOrder;GET /orders/42, notGET /getOrder?id=42. - Plural collections.
/restaurantsand/restaurants/81read as "the restaurants collection" and "restaurant 81 in it". Mixing/restaurant/81and/orders/42is a small inconsistency clients trip over forever. - Ids in the path identify; everything else goes elsewhere. We'll make this precise in a moment.
- Nest only to show ownership, and at most two levels.
/restaurants/81/menu-itemssays "the menu of restaurant 81". Deeper paths get brittle fast.
Here's the resource map I'd sketch for the food app.
Resources for a food-delivery API
/reviews/{id} works on its own and carries restaurantId and menuItemId as fields.Nested or flat?
Nesting reads nicely and makes ownership checks obvious in routing, but it forces the client to know every parent id. If a support agent has a review id from a complaint ticket, they should not have to look up its restaurant and menu item first to build the URL. A good compromise is nest for creation and listing, flat for the item itself: POST /restaurants/81/reviews and GET /restaurants/81/reviews, but GET /reviews/9013 and DELETE /reviews/9013.
Your API is not your schema
A tempting shortcut is one endpoint per table: /order_headers, /order_lines, /order_status_history. It leaks your storage design into every client, so you can never refactor the tables without breaking someone, and it makes clients chatty: rendering one order takes three calls. Model resources around what clients think about. An order resource can include its line items and current status even if those live in three tables, or even three services.
The opposite failure is also real: one giant resource that returns everything about a restaurant (200 menu items, 3,000 reviews) because "it saves round trips". Aim for the middle: resources the size of one screen's main object, with related collections reachable by their own paginated endpoint.
Actions that aren't CRUD
Sooner or later you need "cancel this order" or "reorder my last meal". Three reasonable options, in the order I'd suggest them:
- Change state with PATCH when the action is a field change:
PATCH /orders/42with{"status": "cancelled"}. Works when the server can validate the transition. - Create a noun for the action when it has its own data or lifecycle:
POST /orders/42/cancellationwith a reason, returning a cancellation you can laterGET. Refunds work the same way:POST /orders/42/refunds. - A custom verb as a last resort:
POST /orders/42:cancel. Some large API style guides allow this colon form. It's fine in an interview if you say why the first two didn't fit.
04REST, part 2: HTTP methods and what they promise
Each method makes a promise to every cache, proxy, retry library and client between you and the user. Two properties matter most.
- Safe: the request does not change server state. Crawlers, prefetchers and caches may send safe requests freely.
- Idempotent: sending the request N times leaves the server in the same state as sending it once. The response may differ (a second
DELETEmight return 404), but the state does not. Clients and proxies may retry idempotent requests after a timeout without asking.
| Method | Use it to | Safe | Idempotent | Typical success | Food app example |
|---|---|---|---|---|---|
GET | Read a resource or list | Yes | Yes | 200 (304 if unchanged) | GET /restaurants/81/menu-items |
POST | Create in a collection, or trigger a process | No | No | 201 + Location, or 202 | POST /orders |
PUT | Replace a resource completely (or create at a known URL) | No | Yes | 200 or 204 (201 if created) | PUT /users/me/address |
PATCH | Change some fields | No | Not guaranteed | 200 or 204 | PATCH /menu-items/7 |
DELETE | Remove a resource | No | Yes | 204 (404 on a repeat is fine) | DELETE /reviews/9013 |
HEAD / OPTIONS | Headers only / capabilities and CORS preflight | Yes | Yes | 200 / 204 | Browser preflight before a cross-origin PATCH |
The table is easier to remember as a story about retries. Imagine the network drops right after each request reaches the server, and the client sends it again.
What happens when a retry sends the request twice
PUT vs PATCH, and the two PATCH formats
PUT sends the whole new representation; any field you leave out is reset or removed. That's why it's idempotent, and also why it's awkward for big objects edited by different people. PATCH sends only the changes, and there are two standard formats worth naming:
| Format | Content-Type | Body looks like | Good at | Watch out for |
|---|---|---|---|---|
| JSON Merge Patch (RFC 7396) | application/merge-patch+json | {"price": 260, "description": null} | Simple field updates; looks like the resource | null means "delete the field"; arrays are replaced whole |
| JSON Patch (RFC 6902) | application/json-patch+json | [{"op": "add", "path": "/tags/-", "value": "vegan"}] | Array edits, conditional test ops, atomic multi-step changes | More verbose; add to an array end is not idempotent |
In an interview, "PATCH with a merge-patch body" is plenty. If a client sends a Content-Type you don't support, answer 415 Unsupported Media Type.
Many candidates expose something like GET /orders/42/cancel because it's easy to test in a browser. Link prefetchers, crawlers and caches all assume GET is safe, so one preview of that link can cancel a real order. Anything that changes state is POST, PUT, PATCH or DELETE.
05REST, part 3: where each input belongs
A request has four places to carry data, and putting an input in the wrong one is a small mistake interviewers notice immediately. Here's one request with all four.
Where each input belongs in an HTTP request
| Place | Put here | Food app examples | Don't put here |
|---|---|---|---|
| Path | Required identifiers of the resource | /restaurants/81, /orders/42 | Filters, options, secrets |
| Query string | Optional filters, sorting, pagination, field selection | ?cuisine=thai&sort=-rating&limit=20 | Tokens or personal data (URLs end up in logs, history and referrers) |
| Headers | Metadata about the call: auth, idempotency, content type, caching, version | Authorization, Idempotency-Key, If-Match, Accept | Business data the resource owns |
| Body | The representation being created or changed; anything large or structured | {"items": [...], "deliveryAddress": {...}} | Bodies on GET (many proxies drop them) |
A handy test for path vs query: if removing the value makes the request mean a different resource, it's path; if it only narrows or reshapes the same collection, it's query. Removing 81 from /restaurants/81 changes what you're talking about. Removing cuisine=thai still returns restaurants, only more of them.
POST /orders with {"userId": "u-7", ...} lets anyone order on someone else's account by changing one field. The user id comes from the verified token on the server side. If you need "my" resources, use /users/me/orders or plain /orders scoped to the token. We return to this in the security section, because it is the most common real-world API vulnerability.
06REST, part 4: status codes and response shapes
The status code is the first thing every client, retry library, load balancer and dashboard reads. Getting the family right matters more than getting the exact code right: 2xx means it worked, 4xx means the caller must change something, 5xx means we failed and a retry might help.
Status codes worth knowing, by family
A few pairs come up again and again in interviews:
- 401 vs 403. 401 Unauthorized really means unauthenticated: no token, expired token, bad signature. Log in and retry. 403 Forbidden means we know who you are and you still may not do this. Some APIs return 404 instead of 403 for resources the caller can't see, so they don't confirm the resource exists.
- 400 vs 422. 400 for requests you can't parse (broken JSON, wrong types). 422 Unprocessable Content for well-formed requests that break a rule ("quantity must be between 1 and 20"). Plenty of APIs use 400 for both; pick one convention and keep it.
- 409 vs 412. 409 Conflict: the request clashes with current state (ordering from a restaurant that closed a minute ago, a duplicate username). 412 Precondition Failed: the client's
If-Matchcondition no longer holds, which we'll use for optimistic locking. - 502, 503, 504. Bad gateway (an upstream sent nonsense), unavailable (overloaded or in maintenance; add
Retry-After), gateway timeout (an upstream was too slow). All are retryable with backoff.
Headers that belong on responses
Status codes come with companions. 201 Created should carry Location: /orders/42 so the client knows the new resource's URL. 202 Accepted carries a Location for a status resource. 204 No Content has no body at all. 429 and 503 carry Retry-After. Every cacheable GET carries an ETag and a Cache-Control policy.
Envelopes or bare resources?
For a single resource, return the resource itself. For lists, wrap the items, because you need room for pagination metadata and you can never add it later to a bare JSON array without breaking clients.
{
"data": [
{ "id": "rv_9013", "rating": 4, "text": "Great biryani", "createdAt": "2026-10-17T09:12:00Z" },
{ "id": "rv_9008", "rating": 5, "text": "Fast delivery", "createdAt": "2026-10-17T08:40:00Z" }
],
"page": { "nextCursor": "eyJ0IjoiMjAyNi0xMC0xN1QwODo0MDowMFoiLCJpZCI6OTAwOH0", "hasMore": true }
}Content negotiation lets one resource have several representations. The client says what it accepts (Accept: application/json), the server answers with a matching Content-Type, or 406 Not Acceptable if it can't. In practice, most APIs speak JSON only, and that's a fine interview answer.
07Worked example: the food-delivery API
Let's put parts 1 to 4 together. Our functional requirements were: browse restaurants near me, view a menu, place and pay for an order, track it, review the restaurant, and let owners edit their menu. Before reading on, try writing the endpoints yourself, then compare.
# Discovery
GET /v1/restaurants?near=12.97,77.59&cuisine=thai&sort=-rating&limit=20 -> 200, paginated
GET /v1/restaurants/{restaurantId} -> 200
GET /v1/restaurants/{restaurantId}/menu-items -> 200, paginated
# Ordering (customer; user comes from the token)
POST /v1/orders Idempotency-Key: <uuid> -> 201 + Location
GET /v1/orders/{orderId} -> 200 (403 if not yours)
GET /v1/orders?status=active&cursor=... -> 200, my orders only
POST /v1/orders/{orderId}/cancellation -> 201 or 409 if already picked up
GET /v1/orders/{orderId}/tracking (SSE stream of courier location) -> 200 text/event-stream
# Reviews
POST /v1/restaurants/{restaurantId}/reviews -> 201 + Location
GET /v1/restaurants/{restaurantId}/reviews?limit=20&cursor=... -> 200, paginated
DELETE /v1/reviews/{reviewId} -> 204
# Restaurant owners
PATCH /v1/menu-items/{menuItemId} If-Match: "v5" -> 200 or 412
# Reports
POST /v1/exports -> 202 + Location: /v1/jobs/{jobId}
GET /v1/jobs/{jobId} -> 200 or 303 to the resultNotice a few choices. The order body contains restaurantId, the items and the delivery address, but never the user id. List endpoints all paginate. The two writes that must not happen twice (placing an order, and implicitly paying for it) take an idempotency key. The owner's menu edit is protected against concurrent edits. And the one slow operation, the export, returns immediately with 202. We'll build each of those in its own section.
You could expose POST /orders/{id}/payments as its own resource, and real systems often do, because payments have their own lifecycle (authorised, captured, refunded). In a 45-minute interview, I'd fold payment into order creation (the order body carries a payment-method token from the client-side payment SDK) and mention the split as a refinement. Never put raw card numbers in your API; the client tokenises them with the payment provider first.
08GraphQL: one endpoint, client-shaped responses
GraphQL exists to fix two REST pains that show up when many clients share one API. Over-fetching: the mobile home screen needs a restaurant's name, rating and five dishes, but GET /restaurants/81 returns 40 fields and GET /menu-items returns all 120 items. Under-fetching: the screen needs data from three resources, so it makes three sequential calls, each paying a full mobile round trip.
With GraphQL, the server publishes a typed schema, and the client sends one query naming exactly what it wants:
type Restaurant {
id: ID!
name: String!
rating: Float
menuItems(first: Int = 20, after: String): MenuItemConnection!
reviews(first: Int = 10, after: String): ReviewConnection!
}
type Query {
restaurant(id: ID!): Restaurant
}
type Mutation {
placeOrder(input: PlaceOrderInput!, idempotencyKey: String!): Order!
}
# The client sends this as POST /graphql
query HomeScreen {
restaurant(id: "81") {
name
rating
menuItems(first: 5) { edges { node { name price } } }
reviews(first: 3) { edges { node { rating text } } }
}
}On the server, every field has a resolver: a function that knows how to fetch that field for its parent object. The engine walks the query, calls the resolvers it needs (in parallel where it can), and assembles a JSON response with exactly the requested shape. Reads are queries, writes are mutations, and server push is a subscription, usually over a WebSocket.
Three REST round trips vs one GraphQL query
The costs you must be ready to talk about
The flexibility moves work and risk to the server. Interviewers who mention GraphQL usually probe at least one of these.
- The N+1 problem. A query for 20 restaurants and each one's owner runs one query for the list, then the owner resolver runs once per restaurant: 21 database calls. The fix is a DataLoader: resolvers ask the loader for owner ids, it waits until the end of the current tick, then loads all of them in one
IN (...)query, and caches results for the rest of the request. - Caching. Every query is a POST to the same URL, so CDNs and browser caches can't help by default. Teams use persisted queries (the client sends a hash of a pre-registered query, which can be a cacheable GET) plus caching inside the resolvers or a normalised client-side cache.
- Abuse and cost. One query can ask for restaurants, their reviews, each reviewer's orders, each order's restaurant... Limit query depth, compute a cost score per query and reject expensive ones, and rate limit by cost rather than by request count.
- Errors. GraphQL commonly returns HTTP 200 with an
errorsarray next to partialdata. Monitoring that only watches status codes will miss failures. - Authorization per field. Because any field can appear in any query, access checks must live in the resolvers (or the business layer they call), not at the endpoint.
The N+1 problem and the DataLoader fix
IN query, which is the standard first answer to "what's the N+1 problem in GraphQL?".When I'd choose it in an interview: several client types (web dashboard, two mobile apps, a partner widget) with genuinely different data needs, a product team that ships screens faster than a backend team can add endpoints, or an aggregation layer over many microservices. When I'd avoid it: a simple CRUD product, public APIs where HTTP caching matters, or file uploads and downloads.
09RPC and gRPC: fast, typed calls between services
RPC lets one service call a function on another as if it were local. Instead of "create an order resource", you think "call Pricing.Quote(orderId)". gRPC is the dominant modern implementation, and it brings three things.
- A contract first. You write a
.protofile describing services and messages. Theprotoccompiler generates client stubs and server base classes in Java, Go, Python, C++ and more, so both sides share one source of truth and type errors show up at compile time. - Compact binary messages. Protocol Buffers encode fields by number, not by name, so a message is typically several times smaller than the same JSON and much cheaper to parse.
- HTTP/2. One long-lived connection carries many concurrent calls (multiplexed streams), headers are compressed, and calls can stream in either direction.
From a .proto file to a call over HTTP/2
syntax = "proto3";
service Pricing {
rpc Quote(QuoteRequest) returns (QuoteResponse);
rpc WatchSurge(SurgeRequest) returns (stream SurgeUpdate); // server streaming
}
message QuoteRequest {
string order_id = 1;
string city = 2;
reserved 3; // removed field: never reuse the number
}
message QuoteResponse {
int64 subtotal_paise = 1; // money as integer minor units, never float
int64 delivery_fee_paise = 2;
int64 total_paise = 3;
}gRPC has its own status codes (NOT_FOUND, INVALID_ARGUMENT, UNAVAILABLE, DEADLINE_EXCEEDED...) and a feature worth mentioning in any interview about microservices: deadlines. The caller says "I'll wait 300 ms", that budget propagates down the call chain, and every service stops work the moment the original caller has given up.
When I'd choose it: internal, high-volume service-to-service calls, polyglot teams that want generated clients, and streaming between services. When I'd avoid it: public APIs for third parties (they expect REST and JSON), and direct browser clients, which can't speak native gRPC and need a gRPC-Web proxy. The common production shape is the one in Fig. 1: REST at the edge, gRPC inside.
"Clients talk REST to the gateway. Behind it, the order service calls pricing and inventory over gRPC because those calls happen on every checkout, at a few thousand per second, and we want typed contracts and deadlines between teams."
10Design principles that keep an API usable
Protocols aside, the APIs people enjoy using share a handful of habits. These are the ones I listen for.
- Consistency beats cleverness. One naming style for paths (
kebab-case), one for JSON fields (camelCaseorsnake_case, never both), one pagination scheme, one error format, one timestamp format (ISO 8601 in UTC). A client that has learned one endpoint should be able to guess the next. - Least surprise. GET never changes anything, DELETE deletes, a 2xx means success. Money is integer minor units (paise, cents) with a currency code, never a float. Ids are opaque strings, so you can change their format later.
- Design for the client, not the tables. Avoid chatty APIs where one screen needs ten calls, and avoid mirroring storage. Aggregate where it's natural, for example an order that includes its line items.
- Backwards compatibility by default. Adding an optional field or a new endpoint is safe. Removing or renaming a field, changing a type, tightening validation or changing a default is breaking. Clients must ignore unknown fields; that rule alone makes most evolution painless.
- Predictable limits. Every list is paginated with a capped page size, every request has a maximum body size, every caller has a rate limit, and all of it is documented.
- Self-describing where it's cheap. Returning links such as
Location,nextpage URLs and status-resource URLs is the useful part of HATEOAS (hypermedia as the engine of application state). Full hypermedia-driven clients are rare; don't spend interview time on them.
One more for multi-tenant products (say, the restaurant-owner dashboard used by thousands of restaurant chains): decide where the tenant comes from. A subdomain (chainA.api.example.com), a path segment (/tenants/{id}/...) or a header all work, but the safest source is the authenticated token, so a caller can never reach another tenant by editing a URL.
Members-only article
Keep reading with the Article Pass
You've read the free part. The other 11 sections of this guide, with every diagram and worked example, are for members. One Article Pass unlocks every members-only article on Engineering Atlas: the ones live today and every one I publish while your pass is active.
- Pagination in depth
- Filtering, sorting, field selection and search
- Idempotency keys: making POST safe to retry
- Concurrency control with ETags
- Long-running operations: 202 Accepted
- Error responses clients can act on
- Versioning and evolving an API
- Security: authentication, authorization and trust
- Rate limiting and throttling
- The 5-minute API step: checklist and whiteboard
- Interview questions
From ₹49 · one-time payment, no auto-renewal · separate from course passes