Definition
- Verbs in URLs – using
/getUseror/createTransferinstead of letting the HTTP method express the action on a noun resource likeGET /users/42. - Inconsistent naming – mixing
/accounts,/Transaction, and/user_profilesinstead of a uniform convention like lowercase plural nouns with hyphens. - No versioning – changing a response shape without a version breaks every existing client.
- Unpaginated lists – returning every record in one response collapses in production with millions of rows.
- Leaked internals – exposing auto-increment database IDs and raw JPA entities instead of stable public identifiers and DTOs.
- 200 for everything – returning HTTP 200 for errors forces clients to parse the body to distinguish success from failure.
Think of National Bank of India's service counters. A badly designed one has counters labelled "GoAndGetAccount" and "DoCloseAccount," no queue limits, forms that reveal internal file numbers, and a clerk who stamps every request "DONE" even when he rejected it. A well-designed one has counters labelled by what they handle (Accounts, Transfers), clear queue limits, forms showing only the public account number, and a clerk who honestly says "approved," "not found," or "denied."
Understanding the Concepts
What is wrong with verbs in URLs?
A URL like /getUser or /createTransfer reinvents what the HTTP method already expresses. In REST the URL names a resource (a noun) and the method says what to do.
// Wrong – verbs in URLs:
GET /getUser?id=42
POST /createTransfer
POST /deleteAccount
// Correct – nouns for resources, methods for actions:
GET /api/v1/users/42
POST /api/v1/transfers
DELETE /api/v1/accounts/SAV2001
Using methods on nouns makes the API predictable and lets caches and tools reason about safety and idempotency.
Why does inconsistent naming matter?
Mixing /accounts, /Transaction, and /user_profiles forces clients to memorize exceptions. A good API picks one convention and applies it everywhere.
// Wrong – mixed conventions:
/accounts (lowercase plural)
/Transaction (PascalCase singular)
/user_profiles (snake_case)
// Correct – consistent convention:
/api/v1/accounts
/api/v1/transactions
/api/v1/user-profiles
The standard convention is lowercase plural nouns with hyphens for multi-word resources.
Why must you version from day one?
If you change a response shape without a version, you break every existing client. A good API versions from day one, commonly in the path.
package com.bank.controller;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/accounts")
public class AccountControllerV1 {
@GetMapping("/{id}")
public AccountResponseV1 get(@PathVariable String id) { ... }
}
@RestController
@RequestMapping("/api/v2/accounts")
public class AccountControllerV2 {
@GetMapping("/{id}")
public AccountResponseV2 get(@PathVariable String id) { ... }
}
This lets v2 evolve while v1 keeps serving existing clients.
What happens when list endpoints are not paginated?
Returning every record works in testing with ten rows and collapses in production with ten million. Every collection endpoint must paginate.
// Wrong – returns all transactions:
@GetMapping("/api/v1/accounts/{id}/transactions")
public List<Transaction> getAll(@PathVariable String id) {
return transactionRepo.findByAccountId(id); // 10 million rows → OOM
}
// Correct – paginated with defaults and caps:
@GetMapping("/api/v1/accounts/{id}/transactions")
public Page<TransactionResponse> getPage(
@PathVariable String id,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
size = Math.min(size, 100); // cap at 100
return transactionService.findByAccount(id, PageRequest.of(page, size));
}
Why are internal IDs and raw entities dangerous?
Exposing the raw database auto-increment ID lets attackers enumerate resources and infer row counts. Returning the JPA entity leaks internal columns and couples the API to the schema.
// Wrong – exposes sequential database ID:
GET /api/v1/customers/1
GET /api/v1/customers/2 // attacker can enumerate all customers
// Correct – opaque public identifier:
GET /api/v1/customers/cust-a3f7b2c1-9e4d
// Wrong – returns the entity:
@GetMapping("/customers/{id}")
public Customer get(@PathVariable String id) {
return customerRepo.findById(id).orElseThrow(); // leaks passwordHash
}
// Correct – returns a DTO:
@GetMapping("/customers/{id}")
public CustomerResponse get(@PathVariable String id) {
return customerService.getById(id); // only safe fields
}
Why is 200-for-everything harmful?
If a not-found, a validation failure, and a server error all return 200, clients cannot rely on the status line. A good API uses the status code as the primary signal.
// Wrong – everything is 200:
HTTP/1.1 200 OK
{ "success": false, "error": "Account not found" }
// Correct – proper status codes:
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{ "type": "https://api.bank.com/errors/not-found",
"title": "Not Found", "status": 404,
"detail": "Account SAV9999 not found" }
What do rate-limit headers look like?
A good API returns rate-limit headers so clients can self-throttle, and 429 with Retry-After on breach.
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 997
X-RateLimit-Reset: 1730000000
package com.bank.filter;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import java.io.IOException;
public class RateLimitFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("X-RateLimit-Limit", "1000");
response.setHeader("X-RateLimit-Remaining", String.valueOf(remaining));
response.setHeader("X-RateLimit-Reset", String.valueOf(resetEpoch));
if (remaining <= 0) {
response.setStatus(429);
response.setHeader("Retry-After", "60");
return;
}
chain.doFilter(req, res);
}
}
What does a well-designed API look like? (complete example)
// Create a transfer – correct design:
POST /api/v1/transfers
Content-Type: application/json
Authorization: Bearer eyJhbG...
X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
{ "from": "SAV2001", "to": "CUR3001", "amount": 50000.00 }
201 Created
Location: /api/v1/transfers/TXF-001
X-RateLimit-Remaining: 996
{ "transferId": "TXF-001", "status": "COMPLETED", "amount": 50000.00 }
What should a senior engineer know?
These are not cosmetic preferences. Correct status codes let gateways and monitoring react automatically. Stable IDs and DTOs let you refactor the database without breaking clients. Versioning lets you evolve safely over years. Rate-limit headers reduce load from well-behaved clients. Good REST design is what makes an API operable at scale and safe to change, which in banking is the whole game.
Visual Overview
Common Mistakes vs Best Practices: Mistake │ Best Practice ─────────────────────────────────┼────────────────────────────────── /getUser (verb in URL) │ GET /users/42 (noun + method) /Transaction (inconsistent) │ /transactions (lowercase plural) No version in URL │ /api/v1/accounts No pagination on lists │ ?page=0&size=20 (default + cap) Auto-increment ID exposed │ Opaque UUID-based ID Entity returned directly │ DTO with safe fields only 200 for everything │ 201, 400, 404, 429, 500 No rate-limit headers │ X-RateLimit-Limit/Remaining/Reset
Key Takeaway
The common REST API design mistakes are verbs in URLs, inconsistent naming, no versioning, unpaginated list endpoints, exposed internal IDs and raw entities, missing content negotiation headers, omitted rate-limit headers, and returning 200 for every outcome. Each one creates a real operational problem: clients cannot predict the API, cannot tell success from failure, break when the response changes, and choke on huge unpaginated lists.
A well-designed API fixes each of these. URLs name resources as nouns and the HTTP method expresses the action. Naming is consistent lowercase plural nouns. The API is versioned from day one in the path. Every collection paginates with documented defaults and caps. The API returns DTOs with stable public identifiers, never raw entities or auto-increment IDs. It honors content negotiation, returns rate-limit headers, and uses the status code as the primary signal with a consistent error body like Problem Details.
At National Bank of India, the first accounts API had every one of these flaws. Endpoints were named getAccount and doTransfer, all using POST, and every response was a 200 even on failure. The list endpoint returned all transactions with no pagination and leaked the raw entity with an internal risk flag. There was no versioning, so a response change broke a partner overnight. The team rebuilt it with versioned noun resources, correct methods and status codes, pagination, DTOs, and rate-limit headers from a filter.
The point is that these are not cosmetic preferences. Correct status codes let gateways react automatically. Stable IDs and DTOs let you refactor the database without breaking clients. Versioning lets you evolve safely over years. Rate-limit headers reduce load from well-behaved clients.
Remember one sentence: a well-designed REST API uses nouns for resources, methods for actions, correct status codes for outcomes, and DTOs for safety, and it versions, paginates, and rate-limits from day one.
---