Fundamental principles of REST API design

A well-designed REST API is predictable, consistent, and easy to consume. It is not just about returning JSON from a server: it is about creating a clear contract between your backend and the clients that consume it.

Resource naming

Rules for URLs

Endpoints should be plural nouns representing resources, not actions:

GOOD:
GET    /api/v1/users          -> list users
GET    /api/v1/users/123      -> get a user
POST   /api/v1/users          -> create a user
PUT    /api/v1/users/123      -> update a full user
PATCH  /api/v1/users/123      -> partial update
DELETE /api/v1/users/123      -> delete a user

BAD:
GET    /api/v1/getUsers
POST   /api/v1/createUser
POST   /api/v1/deleteUser/123
GET    /api/v1/user/list

Nested resources

For relationships, use nesting up to two levels maximum:

GET /api/v1/users/123/courses           -> user's courses
GET /api/v1/courses/456/lessons         -> course lessons
GET /api/v1/courses/456/lessons/789     -> a specific lesson

# Avoid excessive nesting
BAD:  /api/v1/users/123/courses/456/lessons/789/comments
GOOD: /api/v1/lessons/789/comments

Naming conventions

Rule Example
Use kebab-case /learning-paths not /learningPaths
Plural nouns /courses not /course
Lowercase /blog-posts not /Blog-Posts
No trailing slash /users not /users/
No extensions /users not /users.json

Correct HTTP methods

Method Usage Idempotent Body
GET Read resource(s) Yes No
POST Create resource No Yes
PUT Replace full resource Yes Yes
PATCH Partial update No* Yes
DELETE Delete resource Yes No
HEAD Check existence Yes No
OPTIONS Get allowed methods Yes No

*PATCH can be idempotent depending on the implementation.

HTTP status codes

Using the correct codes is fundamental so that clients know how to handle each response:

Success (2xx)

Code Usage
200 OK Successful request with body
201 Created Resource created (POST)
204 No Content Success without body (DELETE, PUT)

Redirection (3xx)

Code Usage
301 Moved Permanently Resource permanently moved
304 Not Modified Valid cache, do not send body

Client error (4xx)

Code Usage
400 Bad Request Malformed JSON or invalid parameters
401 Unauthorized Not authenticated
403 Forbidden Authenticated but without permissions
404 Not Found Resource does not exist
409 Conflict Conflict (e.g., duplicate email)
422 Unprocessable Entity Validation failed
429 Too Many Requests Rate limit exceeded

Server error (5xx)

Code Usage
500 Internal Server Error Unhandled error
502 Bad Gateway Upstream service failed
503 Service Unavailable Service temporarily unavailable

Response structure

Successful response

{
  "data": {
    "id": "crs_abc123",
    "type": "course",
    "attributes": {
      "title": "HTML desde cero",
      "level": "beginner",
      "lessonsCount": 12
    }
  }
}

Paginated list response

{
  "data": [
    { "id": "crs_001", "title": "HTML desde cero" },
    { "id": "crs_002", "title": "CSS Fundamentals" }
  ],
  "pagination": {
    "page": 1,
    "perPage": 20,
    "totalPages": 5,
    "totalItems": 94,
    "hasNextPage": true,
    "hasPrevPage": false
  },
  "links": {
    "self": "/api/v1/courses?page=1",
    "next": "/api/v1/courses?page=2",
    "last": "/api/v1/courses?page=5"
  }
}

Standardized error response

{
  "error": {
    "status": 404,
    "code": "RESOURCE_NOT_FOUND",
    "message": "El curso solicitado no existe.",
    "details": []
  }
}

Pagination

There are three main strategies:

1. Offset-based (the simplest)

GET /api/v1/courses?page=2&per_page=20

Advantages: simple to implement. Disadvantages: inefficient on large tables, inconsistent results if data is inserted.

2. Cursor-based (the most robust)

GET /api/v1/courses?cursor=eyJpZCI6MTIzfQ&limit=20

Advantages: consistent performance, stable results. Disadvantages: cannot jump to a specific page.

3. Keyset pagination

GET /api/v1/courses?after_id=crs_123&limit=20

Similar to cursor but with an explicit field as key.

Filtering

GET /api/v1/courses?level=beginner&category=frontend
GET /api/v1/courses?created_after=2025-01-01
GET /api/v1/courses?tags=html,css

Sorting

GET /api/v1/courses?sort=created_at       # ascending
GET /api/v1/courses?sort=-created_at      # descending
GET /api/v1/courses?sort=-rating,title    # multiple fields
GET /api/v1/courses?q=angular+signals
GET /api/v1/search?q=javascript&type=course,blog

Versioning

GET /api/v1/courses
GET /api/v2/courses

Simple, explicit, and easy to cache. This is the strategy most used by APIs like Stripe, GitHub, and Twilio.

In headers (alternative)

GET /api/courses
Accept: application/vnd.bemorex.v1+json

More semantically "pure" but harder to use and test.

Security

Authentication with Bearer tokens

GET /api/v1/users/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Security headers

X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Security-Policy: default-src 'self'

Rate limiting

Include informational headers in every response:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1706810400
Retry-After: 60

Input validation

  • Always validate on the server, never trust the client alone
  • Sanitize inputs to prevent XSS and SQL injection
  • Limit body size (e.g., 1MB for JSON, 10MB for files)
  • Validate Content-Type

Documentation

A good API is a well-documented API. Use OpenAPI (Swagger) to generate interactive documentation:

openapi: 3.1.0
info:
  title: Bemore Learn API
  versión: 1.0.0
paths:
  /api/v1/courses:
    get:
      summary: Listar cursos
      parameters:
        - name: level
          in: query
          schema:
            type: string
            enum: [beginner, intermediate, advanced]
      responses:
        '200':
          description: Lista de cursos

Conclusion

Designing a good REST API requires discipline and consistency. Follow established conventions, use HTTP codes correctly, standardize your responses, and document everything. A well-designed API is a productivity multiplier for your entire team and your consumers. The patterns presented here are the same ones used by companies like Stripe, GitHub, and Twilio in their world-class APIs.