Articles › Software Architecture

REST API Design Best Practices: URLs, Methods, Status Codes and Versioning

Design APIs developers love: resource naming, HTTP methods, status codes, error format, pagination, filtering, versioning, idempotency and security — with concrete examples.

An API is a user interface for developers. A well-designed API is predictable: once you've used two endpoints, you can guess how the third works. A poorly designed one forces everyone to read documentation for every call — and breaks clients whenever it changes. These are the conventions that make REST APIs a pleasure to consume.

Think in resources, not actions

REST models your system as resources (nouns) manipulated with a small set of HTTP methods (verbs).

❌ RPC-style ✅ Resource-oriented
POST /getProducts GET /products
POST /createOrder POST /orders
GET /deleteUser?id=5 DELETE /users/5
POST /updateProductPrice PATCH /products/42

Naming rules

  • Use plural nouns: /products, /orders.
  • Use kebab-case for multi-word paths: /order-items.
  • Nest only for real ownership, and not deeper than one level: /customers/7/orders.
  • Keep URLs lowercase; never put verbs in them except for genuine operations that aren't CRUD (POST /orders/42/cancel is acceptable and clear).

Use HTTP methods correctly

Method Purpose Safe Idempotent
GET read a resource or collection ✅ ✅
POST create a resource / trigger an operation ❌ ❌
PUT replace a resource entirely ❌ ✅
PATCH partially update a resource ❌ usually ❌
DELETE delete a resource ❌ ✅
  • Safe means it doesn't change state — GET must never modify data (crawlers and prefetchers call GETs freely).
  • Idempotent means repeating the request has the same effect as doing it once — important when networks retry.

Return meaningful status codes

Code When
200 OK successful GET / PUT / PATCH with a body
201 Created resource created — include a Location header
204 No Content success without a body (DELETE, some updates)
400 Bad Request malformed request or validation failure
401 Unauthorized not authenticated (missing/invalid token)
403 Forbidden authenticated but not allowed
404 Not Found resource doesn't exist
409 Conflict state conflict (duplicate email, version mismatch)
422 Unprocessable Entity well-formed but semantically invalid (some teams use 400 instead)
429 Too Many Requests rate limit exceeded
500 Internal Server Error unexpected server failure

Never return 200 OK with { "success": false } — it breaks HTTP tooling, caching and monitoring.

A consistent error format

Use the standard Problem Details format (RFC 9457), which ASP.NET Core supports out of the box:

{
  "type": "https://example.com/problems/validation",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "email": ["Email is required."],
    "price": ["Price must be greater than zero."]
  },
  "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00"
}

One error shape across the whole API means clients write error handling once. Include a traceId so support can find the request in your logs — but never leak stack traces in production.

Pagination, filtering and sorting

Collections must be paginated:

GET /products?page=2&pageSize=20
GET /products?category=laptops&minPrice=500&sort=-price,name
{
  "items": [ ... ],
  "page": 2,
  "pageSize": 20,
  "totalCount": 438
}
  • Offset pagination (page/pageSize) is simple and supports jumping to a page.
  • Cursor pagination (?after=eyJpZCI6MTAwfQ) is stable and fast for large or frequently changing data, like feeds.
  • Enforce a maximum pageSize on the server.
  • A common sorting convention: sort=-createdAt for descending.

Versioning

APIs evolve; clients don't update instantly. Plan versioning from day one:

GET /api/v1/orders/42        # URL versioning — simple and visible
GET /api/orders/42
    api-version: 2.0         # header versioning — cleaner URLs

Rules that avoid most version bumps:

  • Additive changes are safe: new endpoints, new optional fields.
  • Breaking changes need a new version: removing or renaming fields, changing types or meaning.
  • Announce deprecations with a Deprecation/Sunset header and a timeline.

Idempotency for POST

Mobile networks retry. If a "create payment" request times out and is retried, you might charge twice. Accept an idempotency key:

POST /payments
Idempotency-Key: 6f1c9c0e-8f4b-4c6e-9b4a-2f9b1d1e7a10

The server stores the key with the result; a repeated request with the same key returns the original result instead of executing again.

Shape responses for clients

  • Return DTOs, not database entities.
  • Use consistent casing (camelCase in JSON is the norm).
  • Use ISO 8601 for dates in UTC: "2026-02-07T09:30:00Z".
  • Represent money with decimal strings or integer minor units — never binary floats.
  • Use null deliberately, and don't change the meaning of a field between versions.

Caching and concurrency

GET /products/42
ETag: "a3f9c2"
Cache-Control: public, max-age=60
  • Clients can send If-None-Match: "a3f9c2" and get 304 Not Modified — saving bandwidth.
  • For updates, If-Match with the ETag prevents lost updates: if someone else changed the resource, return 412 Precondition Failed.

Security basics

  • HTTPS everywhere.
  • Authenticate with tokens (JWT/OAuth2) and authorize every endpoint — check ownership, not just login (prevent IDOR: /orders/43 must not be readable by user 42's token if it's not their order).
  • Validate all input; never trust client-provided IDs or prices.
  • Rate-limit public endpoints and return 429 with Retry-After.
  • Don't expose internal details (SQL errors, stack traces, server versions).

Document it

Generate an OpenAPI document (Swagger) and keep examples in it. Good documentation includes authentication instructions, error formats, pagination rules and realistic sample requests. Tools can then generate typed clients for TypeScript, C# and mobile apps.

Summary

Good REST APIs are resource-oriented, use HTTP methods and status codes as intended, return one consistent error format, paginate collections, version deliberately and protect every endpoint. These conventions aren't bureaucracy — they're what make an API predictable, evolvable and pleasant to integrate with.

FAQ

Should I use PUT or PATCH for updates?

Use PUT when the client sends the full representation, PATCH for partial updates. Many APIs only implement PATCH-style partial updates — just be consistent.

Is REST still relevant with GraphQL and gRPC?

Yes. REST is the simplest, most widely supported choice for public and web APIs. GraphQL suits complex client-driven data needs; gRPC suits internal service-to-service communication.

Should my API return 404 or 403 for resources the user can't access?

Returning 404 avoids revealing that the resource exists. Many APIs prefer that for private resources.