Academy25 Apr 202513 min read

REST API Design Best Practices: Build Developer-Friendly APIs

Design REST APIs developers love using resource-based URLs, consistent error handling, versioning, pagination, and OpenAPI documentation for scalable integrations.

MB
Max Beech
Head of Content
Person coding on laptop for web development project

TL;DR

  • Use resource-based URLs (/users/123, not /getUser?id=123); HTTP verbs express actions.
  • Return structured JSON with status codes; errors include error.code, error.message, error.details.
  • Version APIs via URL (/v1/users) or header (Accept: application/vnd.api+json; version=1).

Jump to URL design · Jump to Request/response format · Jump to Error handling · Jump to Versioning

REST API Design Best Practices: Build Developer-Friendly APIs

Your API is your product's interface to the world -poor design creates friction, support burden, and limits adoption. These REST API design best practices create intuitive, scalable APIs developers actually enjoy using.

Key takeaways

  • Resource-based URLs + HTTP verbs = self-documenting API surface.
  • Consistent error responses (codes, messages, details) reduce integration time 50%.
  • OpenAPI documentation enables auto-generated SDKs and interactive testing.

URL design principles

Use nouns for resources, verbs for actions

Pattern: /resources/{id}/subresources/{id}

Good URLs:

GET    /users              # List users
GET    /users/123          # Get user 123
POST   /users              # Create user
PUT    /users/123          # Update user 123
DELETE /users/123          # Delete user 123
GET    /users/123/posts    # Get posts by user 123

Bad URLs:

GET /getUser?id=123         # Verb in URL (use GET /users/123)
POST /users/delete          # DELETE method exists
GET /user-posts?userId=123  # Use /users/123/posts

Plural vs singular

Recommendation: Always plural (/users, /posts), even for singleton resources.

Why: Consistency. Exception: Singleton resources like /me (current user), /status (health check).

Query parameters for filtering/sorting

GET /users?status=active&sort=created_at:desc&limit=50&offset=100

Standard params:

  • Filtering: ?status=active&role=admin
  • Sorting: ?sort=created_at:desc,name:asc
  • Pagination: ?limit=50&offset=100 or ?cursor=abc123
  • Field selection: ?fields=id,name,email (reduce payload)
REST API URL Structure /users (collection) /users/123 (item) /users/123/posts (nested)
Resource-based URLs: collections, individual items, nested subresources.

"The developer experience improvements we've seen from AI tools are the most significant since IDEs and version control. This is a permanent shift in how software gets built." - Emily Freeman, VP of Developer Relations at AWS

Request/response format

Standard JSON structure

Success response (200 OK):

{
  "data": {
    "id": "usr_123",
    "name": "Alice Chen",
    "email": "alice@example.com",
    "created_at": "2025-04-25T10:30:00Z"
  }
}

Collection response (200 OK):

{
  "data": [
    { "id": "usr_123", "name": "Alice Chen" },
    { "id": "usr_456", "name": "Bob Smith" }
  ],
  "meta": {
    "total": 1247,
    "limit": 50,
    "offset": 100
  },
  "links": {
    "next": "/users?limit=50&offset=150",
    "prev": "/users?limit=50&offset=50"
  }
}

Why wrap in data: Allows adding meta, links, included fields without breaking clients.

HTTP status codes

CodeMeaningUse case
200 OKSuccessGET, PUT, PATCH succeeded
201 CreatedResource createdPOST succeeded
204 No ContentSuccess, no bodyDELETE succeeded
400 Bad RequestClient errorValidation failed, malformed JSON
401 UnauthorizedAuth failedMissing/invalid API key
403 ForbiddenInsufficient permissionsUser lacks access to resource
404 Not FoundResource doesn't existGET /users/999 (no user 999)
429 Too Many RequestsRate limit exceededClient hit 100 req/min limit
500 Internal Server ErrorServer errorUnhandled exception

Rule: 2xx = success, 4xx = client error, 5xx = server error.

Error handling framework

Consistent error response

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Email address is invalid",
    "details": [
      {
        "field": "email",
        "issue": "Must be valid email format"
      }
    ],
    "request_id": "req_abc123"
  }
}

Fields:

  • code: Machine-readable (e.g., RATE_LIMIT_EXCEEDED, RESOURCE_NOT_FOUND).
  • message: Human-readable description.
  • details: Field-level validation errors (for 400 responses).
  • request_id: Trace logs for debugging.

Common error codes

CodeHTTP StatusExample message
INVALID_REQUEST400"Missing required field: name"
UNAUTHORIZED401"API key invalid or expired"
FORBIDDEN403"Insufficient permissions to delete user"
NOT_FOUND404"User usr_123 not found"
RATE_LIMIT_EXCEEDED429"Rate limit: 100 requests/minute exceeded"
INTERNAL_ERROR500"An unexpected error occurred"

Example (Stripe-style):

{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_invalid_integer",
    "message": "Invalid integer: abc",
    "param": "limit"
  }
}

For integration best practices, see /blog/zapier-vs-make-vs-n8n-ai-ops.

API versioning strategy

Option 1: URL versioning

GET /v1/users/123
GET /v2/users/123  # Breaking change: response schema differs

Pros: Explicit, easy to route, clear deprecation path.
Cons: Version in every URL; harder to evolve incrementally.

When to use: Major versions (v1, v2); breaking changes (field removed, renamed).

Option 2: Header versioning

GET /users/123
Accept: application/vnd.myapi.v1+json

Pros: Clean URLs, version decoupled from resource.
Cons: Less discoverable; clients must set headers.

When to use: SaaS platforms with long-lived API contracts.

Option 3: No versioning (additive changes only)

Strategy: Never break compatibility; only add fields, endpoints, query params.

Example:

  • ✅ Add new field phone_number (clients ignore unknown fields).
  • ❌ Rename emailemail_address (breaking).

When to use: Internal APIs, rapid iteration pre-GA.

Recommendation: Start with URL versioning (/v1); transition to header-based for mature APIs.

API Versioning Lifecycle v1 (stable) v2 (current) v3 (beta) v1 (deprecated)
Maintain 2 versions simultaneously; deprecate old versions with 12-month notice.

Pagination & rate limiting

Pagination strategies

Offset-based:

GET /users?limit=50&offset=100
  • Pros: Simple, stateless.
  • Cons: Slow for large offsets; inconsistent if data changes during pagination.

Cursor-based:

GET /users?limit=50&cursor=usr_123_encoded
  • Pros: Consistent results, performant for large datasets.
  • Cons: Can't jump to arbitrary page.

Recommendation: Offset for small datasets (<10K records), cursor for large or real-time data.

Rate limiting headers

HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1714041600

When limit exceeded:

HTTP/1.1 429 Too Many Requests
Retry-After: 60

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit: 100 requests/minute exceeded. Retry after 60 seconds."
  }
}

Tiers:

  • Free: 100 req/min
  • Pro: 1,000 req/min
  • Enterprise: Custom limits

Documentation with OpenAPI

Why OpenAPI (Swagger):

  • Auto-generate SDKs (TypeScript, Python, Go).
  • Interactive API explorer (Swagger UI, Redocly).
  • Validation: Test requests match schema.

Example OpenAPI spec:

openapi: 3.0.0
info:
  title: Users API
  version: 1.0.0
paths:
  /v1/users:
    get:
      summary: List users
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type: string

Tools: Stoplight Studio (visual editor), Redocly (docs hosting), Postman (import OpenAPI).

Call-to-action (API design) Audit existing API endpoints against these principles; refactor top 3 inconsistencies before adding new endpoints.

FAQs

REST vs GraphQL -which to choose?

REST: Simpler, cacheable, better for public APIs, CRUD operations.
GraphQL: Flexible queries, reduces over-fetching, better for complex UIs with varied data needs.

Recommendation: Start with REST; add GraphQL if frontend has 10+ bespoke queries.

Should you use HATEOAS (hypermedia links)?

HATEOAS example:

{
  "data": {
    "id": "usr_123",
    "name": "Alice"
  },
  "links": {
    "self": "/users/123",
    "posts": "/users/123/posts"
  }
}

Pros: Discoverability, decouples clients from URL construction.
Cons: Verbose, rarely implemented fully.

Recommendation: Include links for pagination (next, prev); skip for every resource (overkill).

How to handle file uploads?

Multipart form-data:

POST /users/123/avatar
Content-Type: multipart/form-data

--boundary
Content-Disposition: form-data; name="file"; filename="avatar.jpg"
...

Alternative: Direct upload to S3, return URL to API:

  1. GET /uploads/presigned-url{url, fields}
  2. POST to S3 with file
  3. POST /users/123/avatar with {url: "s3://..."}

What about webhooks?

Design: POST to customer-configured URL with event payload.

Best practices:

  • Include event.type (user.created, payment.succeeded).
  • Retry failed webhooks (exponential backoff: 1s, 5s, 25s, 2m, 10m).
  • Sign payloads (HMAC) for verification.
  • Support webhook logs (customer can debug delivery).

Summary and next steps

Design REST APIs with resource-based URLs, consistent JSON responses, structured errors, versioning, and OpenAPI documentation for excellent developer experience.

Next steps

  1. Define URL schema for your core resources (users, posts, etc.).
  2. Standardise error response format and document common error codes.
  3. Generate OpenAPI spec and publish interactive docs (Swagger UI, Redocly).

Internal links

External references

Crosslinks