Episode 3 — NodeJS MongoDB Backend Architecture / 3.9 — REST API Development

Interview Questions: REST API Development (Episode 3)

How to use this material (instructions)

  1. Read 3.9.a through 3.9.f.
  2. Answer aloud, then compare below.
  3. Pair with 3.9-Exercise-Questions.md.

Beginner Level

Q1: What is REST and what are the 6 constraints?

Why interviewers ask: Foundational knowledge for any backend role.

Model answer:

REST stands for REpresentational State Transfer -- it is an architectural style (not a protocol) defined by Roy Fielding in 2000. It describes six constraints that, when followed, produce scalable, loosely coupled systems. (1) Client-Server -- separation of UI concerns (client) from data/logic concerns (server), allowing each to evolve independently. (2) Stateless -- every request must carry all information needed to process it; the server stores no session state between requests. This enables horizontal scaling because any server can handle any request. (3) Cacheable -- responses must identify themselves as cacheable or non-cacheable so clients and intermediaries can reuse them. (4) Uniform Interface -- resources identified by URIs, manipulated through representations (JSON), self-descriptive messages, and HATEOAS. This is the most distinctive REST constraint. (5) Layered System -- the client cannot tell whether it talks to the origin server or an intermediary (CDN, load balancer, gateway). (6) Code on Demand (optional) -- servers can send executable code to extend client functionality.

Most real-world APIs implement constraints 1-5 and skip HATEOAS, making them "REST-like" or Level 2 on the Richardson Maturity Model. This is perfectly acceptable for most applications.


Q2: What HTTP methods map to CRUD operations, and what are the key differences between PUT and PATCH?

Why interviewers ask: Tests understanding of HTTP semantics in REST APIs.

Model answer:

HTTP MethodCRUDIdempotent?Request Body
GETReadYesNo
POSTCreateNoYes
PUTReplace (full update)YesYes (complete resource)
PATCHPartial updateNo*Yes (only changed fields)
DELETEDeleteYesUsually no

PUT replaces the entire resource -- you send the complete object, and the server overwrites the existing one. If you omit a field, it should be removed or set to null. PUT is idempotent: sending the same PUT request multiple times has the same result.

PATCH updates only the specified fields -- you send only what changed. It is more bandwidth-efficient and avoids accidentally clearing fields. Technically PATCH is not guaranteed to be idempotent (it depends on the operation), though in practice most PATCH implementations are.

In Express: app.put('/users/:id', ...) expects a full replacement body. app.patch('/users/:id', ...) expects a partial body with only the modified fields. Most modern APIs use PATCH for updates because full-resource replacement is rarely practical.


Q3: Explain the most important HTTP status codes used in REST APIs.

Why interviewers ask: Status codes are the language of API communication.

Model answer:

2xx Success: 200 OK for successful GET/PUT/PATCH. 201 Created for successful POST -- include a Location header pointing to the new resource. 204 No Content for successful DELETE -- no response body.

4xx Client Errors: 400 Bad Request for malformed syntax or invalid data. 401 Unauthorized means "not authenticated" -- the client has not identified itself (missing or invalid token). 403 Forbidden means "not authorized" -- the client is authenticated but lacks permission. 404 Not Found for non-existent resources. 409 Conflict for duplicates or state conflicts. 422 Unprocessable Entity when JSON is syntactically valid but fails business validation. 429 Too Many Requests when rate limit is exceeded.

5xx Server Errors: 500 Internal Server Error for unexpected server failures -- never expose internal details in production. 502 Bad Gateway when a proxy/gateway receives an invalid response from upstream. 503 Service Unavailable when the server is temporarily down (maintenance, overload).

Key distinction: 401 is "who are you?" (authentication). 403 is "you can't do this" (authorization). Getting this right signals maturity to interviewers.


Q4: What is API versioning and what strategies exist?

Why interviewers ask: Tests forward-thinking API design skills.

Model answer:

API versioning lets you evolve an API without breaking existing clients. When you make breaking changes (removing fields, changing response shapes, altering behavior), clients on the old version continue working while new clients use the updated version.

Three main strategies:

URL path versioning (/api/v1/users, /api/v2/users) -- the most common approach. Used by GitHub, Stripe, Twitter. Pros: explicit, easy to route, easy to cache. Cons: pollutes URLs, can lead to code duplication.

Header versioning (Accept: application/vnd.myapi.v2+json or custom X-API-Version: 2) -- cleaner URLs but harder to test in browsers. Used by GitHub (also supports this).

Query parameter versioning (/api/users?version=2) -- simplest to implement but hard to cache and feels hacky.

In Express, URL path versioning maps cleanly to Router modules: mount v1Router at /api/v1 and v2Router at /api/v2. Share common business logic in services/controllers, with version-specific response transformations in each router.


Q5: Why is server-side input validation mandatory, even with client-side validation?

Why interviewers ask: Tests security awareness -- a non-negotiable for backend developers.

Model answer:

Client-side validation exists for user experience -- it provides instant feedback without a round trip. But it offers zero security. Any attacker can bypass the client entirely using curl, Postman, a custom script, or browser DevTools. They can disable JavaScript, modify form data in transit, or craft any HTTP request they want.

Server-side validation is the only layer that is authoritative and unbypassable. It must validate: (1) Presence -- required fields exist. (2) Type -- values are the expected types (string, number, etc.). (3) Format -- email patterns, phone formats, date formats. (4) Range -- min/max values, string lengths. (5) Business rules -- start date before end date, valid enum values. (6) Security -- sanitize to prevent XSS (escape()), NoSQL injection (reject $ operators in user input), and SQL injection (parameterized queries).

In Express, the two most popular validation libraries are express-validator (middleware-based, field-by-field) and Zod (schema-based, TypeScript-friendly). Both produce structured error responses that the client can display.


Intermediate Level

Q6: How would you design a consistent error response format for a REST API?

Why interviewers ask: Tests API design maturity and developer experience thinking.

Model answer:

Every error response should follow the same structure so clients can parse errors programmatically:

{
  "error": {
    "status": 422,
    "code": "VALIDATION_ERROR",
    "message": "Input validation failed",
    "details": [
      { "field": "email", "message": "Must be a valid email address" },
      { "field": "age", "message": "Must be at least 13" }
    ]
  }
}

Key fields: status (HTTP code as number), code (machine-readable error type for client switch statements), message (human-readable summary), details (optional array of field-level errors for validation failures).

Implementation: create an ApiError class with static factory methods (ApiError.badRequest(), ApiError.notFound(), etc.). Throw these in route handlers. A global Express error handler catches them and formats the response. In production, never expose stack traces or internal error messages -- return generic messages for 500 errors. In development, include the stack for debugging.


Q7: What is CORS and why is it a security concern for APIs?

Why interviewers ask: CORS misconfigurations are a common security vulnerability.

Model answer:

CORS (Cross-Origin Resource Sharing) is a browser security mechanism that restricts web pages from making requests to a different origin (domain + port + protocol) than the one that served the page. When a frontend at https://app.example.com calls an API at https://api.example.com, the browser sends a preflight OPTIONS request to check if the API allows cross-origin access.

Why it matters: app.use(cors()) with no configuration sets Access-Control-Allow-Origin: * -- any website in the world can make authenticated requests to your API. An attacker's website could make API calls using a visitor's cookies or credentials.

Secure configuration:

app.use(cors({
  origin: ['https://app.example.com', 'https://admin.example.com'],
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400
}));

Whitelist specific origins, limit allowed methods and headers, and set credentials: true only if you use cookies. In development, you can use origin: 'http://localhost:3000' but never * with credentials: true (browsers reject this combination).


Q8: What does helmet() do in an Express application?

Why interviewers ask: Tests knowledge of security headers and defense-in-depth.

Model answer:

Helmet is Express middleware that sets HTTP security headers to protect against common web vulnerabilities. It is a collection of smaller middlewares, each setting one or more headers:

HeaderWhat It Prevents
Content-Security-PolicyXSS, injection, clickjacking (controls which resources can load)
X-Content-Type-Options: nosniffMIME-type sniffing (prevents browser from guessing content type)
X-Frame-Options: SAMEORIGINClickjacking (prevents embedding your site in iframes)
Strict-Transport-SecurityDowngrade attacks (forces HTTPS after first visit)
X-XSS-Protection: 0Disables flawed browser XSS filter (CSP is the replacement)
Referrer-PolicyReferrer leakage (controls what URL info is sent)
X-Permitted-Cross-Domain-PoliciesFlash/PDF cross-domain access
X-DNS-Prefetch-ControlDNS prefetch privacy leak

Setup is one line: app.use(helmet()). For production, customize CSP directives to match your application's needs. Helmet is not a complete security solution -- it is one layer in defense-in-depth alongside input validation, authentication, rate limiting, and CORS.


Q9: How does rate limiting protect an API and how would you implement it?

Why interviewers ask: Tests understanding of API abuse prevention.

Model answer:

Rate limiting restricts the number of requests a client can make within a time window. It protects against: brute-force attacks (trying thousands of passwords), DDoS (overwhelming the server), scraping (mass data extraction), and API abuse (exceeding fair-use limits).

In Express, use express-rate-limit:

const rateLimit = require('express-rate-limit');

const generalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  standardHeaders: true,
  message: { error: 'Too many requests, try again later' }
});

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10,
  skipSuccessfulRequests: true
});

app.use('/api', generalLimiter);
app.use('/api/auth/login', authLimiter);

Strategies: Apply stricter limits to authentication endpoints (10 attempts/15 min) than general endpoints (100/15 min). Use skipSuccessfulRequests for login so valid users are not penalized. For distributed systems, store rate-limit state in Redis (using rate-limit-redis) so all server instances share the same counters. Return 429 Too Many Requests with a Retry-After header.


Q10: Explain how you would prevent NoSQL injection in a MongoDB/Express API.

Why interviewers ask: Tests security depth specific to the MongoDB stack.

Model answer:

NoSQL injection occurs when an attacker sends a JSON object where a string is expected, exploiting MongoDB query operators:

// Normal login: { "email": "alice@test.com", "password": "secret" }
// Attack: { "email": { "$gt": "" }, "password": { "$gt": "" } }
// This matches ALL documents because every string is greater than ""

Prevention layers: (1) Type validation -- use express-validator's .isString() on all text inputs to reject objects and arrays. (2) Mongoose schema typing -- schema fields typed as String reject object values. (3) Sanitize input -- use express-mongo-sanitize middleware to strip any keys starting with $ or containing . from req.body, req.query, and req.params. (4) Never pass raw input to queries -- always validate and sanitize before using in find(), findOne(), etc. (5) Avoid $where -- it accepts arbitrary JavaScript strings.


Advanced Level

Q11: How would you design a REST API for a large-scale application with millions of users?

Why interviewers ask: Tests architectural thinking and production experience.

Model answer:

API design: Use URL-path versioning (/api/v1/). Keep resources focused (max 2 levels of nesting: /users/:id/posts, not deeper). Use pagination on all list endpoints (cursor-based for large datasets). Support sparse field selection via query params (?fields=name,email). Return consistent response envelopes ({ data, pagination, meta }).

Performance: Implement caching at multiple levels -- HTTP cache headers (Cache-Control, ETag) for GET requests, Redis for frequently accessed data (user sessions, configuration), CDN for static assets and cacheable API responses. Use database indexes on all queried fields. Use .lean() in Mongoose for read-only responses. Consider read replicas for heavy read traffic.

Reliability: Rate limiting per endpoint with Redis-backed stores. Circuit breakers for external service calls. Graceful degradation when dependencies fail. Health check endpoints (/health) for load balancers. Structured logging with correlation IDs for request tracing.

Security: Helmet for headers, strict CORS whitelist, input validation on every endpoint, authentication middleware (JWT), authorization middleware (roles/permissions), request body size limits, and NoSQL injection prevention. Separate rate limits for auth endpoints.

Scalability: Stateless design so any server can handle any request. Store sessions in Redis, not in memory. Use a message queue (RabbitMQ, SQS) for async tasks (email, image processing). Container orchestration (Kubernetes) for auto-scaling.


Q12: What are the OWASP API Security Top 10 risks and how do you mitigate them?

Why interviewers ask: Tests comprehensive security knowledge.

Model answer:

The OWASP API Security Top 10 (2023) addresses the most critical API risks:

#RiskExpress Mitigation
API1Broken Object Level AuthorizationCheck resource.ownerId === req.user.id on every endpoint
API2Broken AuthenticationStrong password requirements, bcrypt, JWT with short expiry, refresh tokens
API3Broken Object Property Level AuthorizationWhitelist allowed fields in updates ({ $set: { name, email } }, not req.body)
API4Unrestricted Resource ConsumptionRate limiting, express.json({ limit: '10kb' }), pagination limits
API5Broken Function Level AuthorizationRBAC middleware: requireRole('admin') before admin routes
API6Unrestricted Access to Sensitive Business FlowsRate limit + CAPTCHA on registration, password reset, purchases
API7Server-Side Request Forgery (SSRF)Validate and whitelist URLs before server-side fetching
API8Security MisconfigurationHelmet, disable X-Powered-By, production error handler (no stack traces)
API9Improper Inventory ManagementDocument all endpoints (OpenAPI/Swagger), sunset deprecated versions
API10Unsafe Consumption of APIsValidate responses from third-party APIs, timeout all external calls

Defense-in-depth: no single mitigation is sufficient. Layer authentication, authorization, validation, rate limiting, headers, and monitoring.


Q13: How would you implement API versioning in Express with shared business logic between versions?

Why interviewers ask: Tests code organization and maintainability thinking.

Model answer:

The goal is to version the API contract (request/response shape, URL structure) while sharing business logic (database queries, calculations, external service calls).

Architecture:

src/
├── services/           # Shared business logic (version-agnostic)
│   ├── userService.js  # createUser, findUser, updateUser
│   └── postService.js
├── routes/
│   ├── v1/
│   │   ├── users.js    # v1 request parsing, v1 response shape
│   │   └── posts.js
│   └── v2/
│       ├── users.js    # v2 request parsing, v2 response shape
│       └── posts.js
├── middleware/          # Shared middleware
└── app.js

Services contain pure business logic: userService.createUser(data) returns the user object. Version-specific route handlers call the same service but transform the response differently. v1 might return { id, name, email } while v2 returns { id, name, email, avatar, role, createdAt } with pagination metadata.

Mount with: app.use('/api/v1', v1Router) and app.use('/api/v2', v2Router). Add Sunset and Deprecation headers to v1 responses. Document the migration path from v1 to v2.


Q14: Compare express-validator and Zod for input validation in Express. When would you choose each?

Why interviewers ask: Tests practical knowledge of validation tooling.

Model answer:

Criteriaexpress-validatorZod
ApproachMiddleware chain, field-by-fieldSchema declaration, object-level
TypeScriptBasic typesInfers TS types from schema
Validation stylebody('email').isEmail()z.object({ email: z.string().email() })
SanitizationBuilt-in (trim(), escape())Manual (transform functions)
Error formatvalidationResult(req)schema.parse() throws ZodError
Custom validators.custom(async fn).refine(fn) or .superRefine(fn)
Learning curveLower for Express developersLower for TypeScript developers
ReusabilityTied to Express middlewareFramework-agnostic (frontend + backend)

Choose express-validator when: your team is Express-focused, you need built-in sanitization, or you prefer middleware-style validation close to route definitions.

Choose Zod when: you use TypeScript and want inferred types, you need to share validation schemas between frontend and backend, or you prefer declarative schema definitions.

Both can coexist: use Zod for schema definition and type inference, then wrap it in Express middleware for route integration.


Q15: How would you test a REST API comprehensively?

Why interviewers ask: Tests quality assurance mindset and testing knowledge.

Model answer:

Unit tests (Jest/Vitest): Test individual functions -- validation logic, service layer, utility functions. Mock database and external services. Fast, isolated, high coverage.

Integration tests (Supertest + Jest): Test Express routes end-to-end against a test database. Send HTTP requests, verify status codes, response bodies, headers, and database state.

const request = require('supertest');
const app = require('../app');

describe('POST /api/users', () => {
  it('returns 201 with valid data', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({ name: 'Alice', email: 'alice@test.com' });
    expect(res.status).toBe(201);
    expect(res.body.data.name).toBe('Alice');
  });

  it('returns 400 with invalid email', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({ name: 'Alice', email: 'invalid' });
    expect(res.status).toBe(400);
  });
});

What to test: Happy paths (all CRUD operations succeed), validation failures (missing fields, invalid types, out-of-range values), authentication/authorization (no token, expired token, insufficient role), edge cases (duplicate keys, non-existent resources, concurrent updates), security (XSS payloads, NoSQL injection, oversized bodies, rate limiting), and performance (response times under load).

Tools: Postman/Newman for manual + automated collection testing, Supertest for programmatic integration tests, k6 or Artillery for load testing.


Quick-fire

#QuestionOne-line
1REST is a...Architectural style (not a protocol)
2Optional REST constraintCode on Demand
3Stateless means...Each request carries all info; server stores no session
4201 Created should includeLocation header with new resource URL
5401 vs 403401 = not authenticated; 403 = not authorized
6204 response bodyNone (No Content)
7Most popular versioningURL path (/api/v1/)
8helmet() doesSets security HTTP headers
9cors() with no optionsAllows all origins (dangerous)
10Rate limit headerRetry-After on 429 responses
11HATEOAS meansResponse includes links to available actions
12Richardson Maturity Level 2HTTP verbs + resource URIs (most APIs)

<- Back to 3.9 -- REST API Development (README)