REST API design Interview Questions
REST API design interviews assess your ability to build scalable, maintainable, and user-friendly web APIs. These interviews are common for backend, full-stack, and platform engineering roles, especially at senior levels. Interviewers evaluate your understanding of REST principles, resource modeling, error handling, pagination, versioning, and security. You'll face both conceptual discussions and hands-on coding challenges.
What REST API design interviews cover
RESTful Principles & Constraints
Understanding statelessness, uniform interface, resource identification, and how to properly use HTTP methods (GET, POST, PUT, DELETE, PATCH) and status codes.
Resource Modeling & Naming
Designing intuitive URI patterns for nested resources, pluralized nouns, filtering, sorting, and avoiding verbs in endpoints.
Pagination, Filtering & Versioning
Implementing cursor-based vs offset pagination, strategies for filtering/sorting, and URL vs header versioning approaches.
Error Handling & Security
Designing consistent error response structures, proper use of HTTP status codes, and common security practices like authentication, rate limiting, and input validation.
Sample REST API design interview questions
- Design a REST API for a news article system. How would you model the resources and endpoints for articles, comments, and tags?What a strong answer covers
- Resources: articles, comments, tags.
- Endpoints: /articles, /articles/{id}, /articles/{id}/comments, /tags, /articles/{id}/tags.
- Relationships: comments nested under articles, tags can be associated via articles.
- HTTP verbs: GET, POST, PUT/PATCH, DELETE accordingly.
- Consideration: use of query parameters for filtering, pagination.
View a sample answer
For a news article system, I would model three main resources: articles, comments, and tags. Articles would be at /articles and support GET (list), POST (create), GET /articles/{id}, PUT/PATCH /articles/{id}, DELETE /articles/{id}. Comments are a sub-resource of articles, so endpoints would be /articles/{id}/comments for list/create, and /comments/{id} for individual operations (though they belong to an article). Tags would be a separate resource /tags for listing and creating tags, and to associate tags with articles, I could use a many-to-many relationship via an endpoint like /articles/{id}/tags (GET to list, POST to add) or include tags as a nested resource under article creation/update. This design follows RESTful principles where resources are identified by URIs, and relationships are expressed through URL hierarchy. A common pitfall is over-nesting; for example, if tags are commonly accessed independently, they should have their own top-level endpoint. Also, consider using query parameters for filtering articles by tag (e.g., /articles?tag=technology) instead of a separate path.
- Given a user CRUD API, how would you implement soft deletes and retrieval of both active and deleted users?What a strong answer covers
- Soft delete: add a deleted_at timestamp column instead of physical deletion.
- CRUD endpoints: keep standard but differentiate queries via query parameter.
- GET /users returns only active users by default.
- GET /users?include_deleted=true returns all users.
- Alternatively, separate endpoints like /users/deleted.
View a sample answer
To implement soft deletes in a user CRUD API, I would add a nullable 'deleted_at' timestamp field to the users table. When a DELETE request is made to /users/{id}, instead of removing the row, I set deleted_at to the current timestamp. The standard GET /users endpoint would filter out records where deleted_at is not null (active users only). To retrieve deleted users, I could use a query parameter like 'include_deleted=true', which would include both active and deleted. Alternatively, a dedicated endpoint like /users/deleted could list only soft-deleted users. For the retrieve single user endpoint, GET /users/{id} could return the user regardless of deletion status, or require a flag. The tradeoff is complexity in queries and potential performance impact if not indexed. A pitfall is forgetting to enforce soft delete across related operations (e.g., login should reject deleted users). Also, consider whether to expose the deleted_at field in responses.
- How would you design the pagination for a large collection of tweets? Compare offset vs cursor-based pagination and choose one.What a strong answer covers
- Offset pagination: using page/limit, simple but inconsistent with insertions/deletions.
- Cursor-based pagination: using a cursor (e.g., tweet ID), stable and efficient.
- For large collections, cursor-based is better due to offset scan performance.
- Cursor-based suits real-time data like tweets (new tweets added frequently).
- Implementation: clients get a 'next_cursor' in the response, pass it as parameter.
View a sample answer
For a large collection of tweets, I would choose cursor-based pagination over offset-based. Offset pagination, using page and limit parameters, is simple but becomes problematic with large datasets because the database must scan and skip 'offset' rows, leading to performance degradation. Also, if new tweets are inserted frequently, the offset can shift, causing duplicate or missing results. Cursor-based pagination uses a stable cursor (e.g., a tweet's timestamp or ID) to define the starting point. The API returns a next_cursor in the response, and clients pass it as a query parameter (e.g., '?cursor=abc123&limit=20'). This approach is consistent even with concurrent writes and allows efficient use of database indexes. The main downside is that the cursor must be opaque and typically requires encoding. Also, it's less intuitive for users to jump to a specific page. For real-time feeds like tweets, cursor-based is the industry standard (e.g., Twitter API).
- Design an API that allows clients to request specific fields (sparse fieldsets) and include related resources (e.g., posts with author details).What a strong answer covers
- Sparse fieldsets: use 'fields' query parameter to select specific fields.
- Example: GET /posts?fields=id,title,body.
- Inclusion of related resources: use 'include' query parameter.
- Example: GET /posts/{id}?include=author,comments.
- Response structure: separate included resources in a 'included' array or embed them.
View a sample answer
To allow clients to request specific fields, I would implement a 'fields' query parameter that accepts a comma-separated list of field names. For example, GET /posts?fields=id,title,body returns only those fields. The server should filter the resource attributes accordingly. For including related resources, I'd use an 'include' parameter. For example, GET /posts?include=author would embed the author object within the post response, or use a JSON:API style compound document with a top-level 'included' array. A common design is to have the main data in 'data' and related resources in 'included'. This reduces the number of requests but increases response size. Tradeoffs include complexity in serialization and potential for deep nesting. A pitfall is allowing arbitrary include combinations that could lead to performance issues (e.g., circular relationships). I'd enforce a maximum include depth and whitelist allowed relationships.
- How would you version a REST API? Explain the pros and cons of URI versioning vs custom request header versioning.What a strong answer covers
- URI versioning: include version in URL path, e.g., /v1/users.
- Header versioning: use custom header like Accept: application/vnd.myapi.v1+json.
- URI versioning: simple, explicit, cache-friendly; but pollutes URL space.
- Header versioning: cleaner URLs, supports content negotiation; but harder to test.
- Considerations: both are used in practice; choose based on ecosystem.
View a sample answer
API versioning is essential for maintaining backward compatibility. URI path versioning (e.g., /v1/users) is straightforward: the version is explicit in the URL, making it easy to route, cache, and test. However, it pollutes the URL space and can lead to multiple endpoints. Custom request header versioning uses the Accept header (e.g., Accept: application/vnd.myapi.v1+json) to specify the version. This keeps URLs clean and leverages HTTP content negotiation, but it's less visible and harder to debug manually (e.g., from a browser). Header versioning also requires clients to set the header properly. The pros of URI versioning are simplicity and discoverability; cons are that it implies a new deployment for each version. Header versioning allows the same endpoint to serve multiple versions, but it can make caching and documentation slightly more complex. I personally prefer URI versioning for public APIs because it's more intuitive and easier to enforce, but header versioning is common in internal or hypermedia-driven APIs.
- Write a simple Express.js middleware that validates an API key from the Authorization header and returns a 401 if missing or invalid.What a strong answer covers
- Middleware function signature: (req, res, next).
- Extract API key from Authorization header (Bearer).
- Validate against an environment variable or database.
- If missing or invalid, respond with 401 Unauthorized.
- If valid, call next() to pass to next middleware.
View a sample answer
The Express.js middleware should extract the API key from the Authorization header, typically as a Bearer token. It then checks if the key exists and matches a known value (e.g., from an environment variable or a store). If missing or invalid, it returns a 401 status with an error message. If valid, it calls next() to continue processing. A potential pitfall is not handling the case where the header is present but malformed (e.g., missing 'Bearer' prefix). Also, for security, avoid logging the key. The code should be modular and reusable. Below is an implementation.
Reference solutionjavascript // apiKeyMiddleware.js const API_KEY = process.env.API_KEY || 'default-key'; function apiKeyMiddleware(req, res, next) { const authHeader = req.headers.authorization; if (!authHeader) { return res.status(401).json({ error: 'Missing Authorization header' }); } const parts = authHeader.split(' '); if (parts.length !== 2 || parts[0] !== 'Bearer') { return res.status(401).json({ error: 'Invalid Authorization header format. Expected: Bearer <API_KEY>' }); } const apiKey = parts[1]; if (apiKey !== API_KEY) { return res.status(401).json({ error: 'Invalid API key' }); } // Optionally attach the key to the request for downstream use req.apiKey = apiKey; next(); } module.exports = apiKeyMiddleware; - Design an error response structure for a payment API. Include examples for validation errors, insufficient funds, and server errors.What a strong answer covers
- Consistent structure with fields: error, code, message, details.
- Validation errors: include field-level errors in 'errors' array.
- Insufficient funds: specific code (e.g., PAYMENT_INSUFFICIENT_FUNDS) and message.
- Server errors: generic message and allow hiding details in production.
- HTTP status codes: 400 for validation, 402 for insufficient funds, 500 for server errors.
View a sample answer
A well-designed error response structure for a payment API should be consistent across all error types. I'd use a JSON object with 'error' boolean set to true, a 'code' string for machine-readable error classification, a 'message' for human-readable description, and optional 'details' array for field-level errors. For validation errors (HTTP 400), the 'details' array would contain objects with 'field', 'message', and 'code'. For insufficient funds (I'd use HTTP 402 Payment Required), the code could be 'PAYMENT_INSUFFICIENT_FUNDS' and message 'Insufficient balance to complete transaction'. A server error (HTTP 500) would have code 'INTERNAL_SERVER_ERROR' and a generic message, without exposing stack traces. In development, you might include more info, but in production, hide internal details. Below are examples for each case.
Reference solutionjson // Validation Error (400) { "error": true, "code": "VALIDATION_ERROR", "message": "One or more fields are invalid.", "details": [ { "field": "amount", "message": "Amount must be a positive number.", "code": "INVALID_AMOUNT" } ] } // Insufficient Funds (402) { "error": true, "code": "PAYMENT_INSUFFICIENT_FUNDS", "message": "Insufficient balance to complete the transaction.", "details": { "available_balance": 50.00, "required": 100.00 } } // Server Error (500) { "error": true, "code": "INTERNAL_SERVER_ERROR", "message": "An unexpected error occurred. Please try again later." } - How would you implement rate limiting for a public API? Describe the algorithm and how you'd communicate limits to clients.What a strong answer covers
- Algorithm: Token Bucket or Sliding Window (e.g., Fixed Window Counter, Sliding Window Log).
- Token Bucket: refill tokens per minute, each request consumes a token.
- Store counts per user/IP using in-memory cache (Redis) or similar.
- Communicate limits via headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.
- Return 429 Too Many Requests with Retry-After header when exceeded.
View a sample answer
I would implement rate limiting using a Token Bucket algorithm, which is simple and efficient. For each user/client (identified by API key or IP), I maintain a bucket with a fixed capacity (e.g., 100 tokens) and a refill rate (e.g., 10 tokens per minute). When a request arrives, if a token is available, it's consumed and the request proceeds; otherwise, a 429 Too Many Requests error is returned. The state can be stored in a distributed cache like Redis, which supports atomic operations. To communicate limits to clients, I'd include response headers: X-RateLimit-Limit (max requests allowed), X-RateLimit-Remaining (tokens left), and X-RateLimit-Reset (Unix timestamp when bucket refills). On limit exceeded, also include a Retry-After header with seconds to wait. A common pitfall is not accounting for burst traffic; Token Bucket allows bursts up to capacity. Another consideration: use a sliding window log for more precise limits, but at the cost of higher memory.
How to prepare
- Always start by identifying resources and their relationships, then map them to endpoints with proper HTTP methods.
- Mock a simple API in your preferred language (e.g., Node.js/Express, Python/Flask) to practice CRUD operations and error handling.
- Study common API design patterns like HATEOAS, idempotency for PUT/DELETE, and safe/dempotent methods.
- Prepare to discuss trade-offs: e.g., why you'd choose a particular pagination approach or versioning strategy.
- Practice whiteboarding: explain your design decisions for resource naming, status codes, and error formats clearly.
Frequently asked questions
What is the most common mistake in REST API design?
Using verbs in endpoints (e.g., /users/getUser instead of GET /users/:id). Also, failing to use proper HTTP status codes for errors.
Should I always use HATEOAS?
HATEOAS is a constraint for full REST adherence, but for practical microservices, it's often omitted for simplicity. Focus on clear resource links.
How do I handle partial updates?
Use PATCH with a JSON Patch or Merge Patch format. For partial updates, include only the fields to change in the request body.
What's the best way to version an API?
URI versioning (e.g., /v1/users) is explicit and easy to cache, but header versioning keeps URLs clean. Choose based on your team's preferences.
How do I test a REST API design interview?
Practice with mock interviews on a whiteboard or using tools like Postman. Focus on explaining trade-offs and your thought process.
Practice REST API design questions with instant AI feedback
Upload your resume, get a personalized mock interview, and see exactly what to improve — free to start.