Skip to content

API Design Standards

Overview

This document outlines the design standards and principles for building consistent, maintainable, and developer-friendly APIs. All API endpoints must adhere to these standards.

RESTful Principles

Resource-Oriented Design

Design APIs around resources (nouns) rather than actions (verbs):

Good:

GET    /users
POST   /users
GET    /users/123
PUT    /users/123
DELETE /users/123

Bad:

POST /getUsers
POST /createUser
POST /updateUser
POST /deleteUser

HTTP Methods

Use HTTP methods correctly to indicate the action:

MethodPurposeIdempotentSafe
GETRetrieve resourcesYesYes
POSTCreate new resourcesNoNo
PUTReplace entire resourcesYesNo
PATCHPartially update resourcesNoNo
DELETERemove resourcesYesNo

Idempotency

Ensure PUT and DELETE are idempotent - calling them multiple times should have the same effect as calling them once:

php
// PUT - Idempotent
PUT /users/123
{
  "name": "John Doe",
  "email": "john@example.com"
}
// Calling this multiple times results in the same state

// POST - Not Idempotent
POST /users
{
  "name": "John Doe"
}
// Calling this multiple times creates multiple users

URL Design

URL Structure

Follow this consistent URL pattern:

https://api.example.com/{version}/{resource}/{id}/{sub-resource}/{sub-id}

Examples:

GET /v1/users/123
GET /v1/users/123/posts
GET /v1/users/123/posts/456
GET /v1/posts/456/comments

Naming Conventions

Use Plural Nouns

Always use plural nouns for collections:

Good:

GET /users
GET /posts
GET /comments

Bad:

GET /user
GET /post
GET /comment

Use Kebab-Case for URLs

Use lowercase with hyphens for multi-word resources:

Good:

GET /user-profiles
GET /blog-posts
GET /api-keys

Bad:

GET /userProfiles
GET /BlogPosts
GET /API_Keys

Avoid Deep Nesting

Limit URL nesting to 2-3 levels:

Good:

GET /users/123/posts
GET /posts?user_id=123

Bad:

GET /users/123/posts/456/comments/789/likes

For deep relationships, use query parameters or separate endpoints:

GET /comments?post_id=456
GET /likes?comment_id=789

Query Parameters

Filtering

Use query parameters for filtering collections:

GET /users?status=active
GET /users?role=admin&status=active
GET /posts?published=true&category=tech

Searching

Use q or search parameter for full-text search:

GET /users?q=john
GET /posts?search=api+design

Sorting

Use sort parameter with comma-separated fields:

GET /users?sort=created_at        # Ascending
GET /users?sort=-created_at       # Descending (prefix with -)
GET /users?sort=-created_at,name  # Multiple fields

Field Selection

Allow clients to request specific fields:

GET /users?fields=id,name,email

Response:

json
{
  "data": [
    {
      "id": 1,
      "name": "John Doe",
      "email": "john@example.com"
    }
  ]
}

Pagination

Always include pagination parameters:

GET /users?page=2&per_page=20
GET /users?cursor=abc123&limit=20

See Pagination for detailed guidelines.

Request Design

Request Headers

Required Headers

All requests should include:

http
Content-Type: application/json
Accept: application/json
Authorization: Bearer {token}

Optional Headers

http
Accept-Language: en-US
User-Agent: MyApp/1.0
X-Request-ID: unique-request-id

Request Body

Use JSON

Always use JSON for request bodies:

http
POST /users
Content-Type: application/json

{
  "name": "John Doe",
  "email": "john@example.com"
}

Snake Case for Properties

Use snake_case for JSON property names:

Good:

json
{
  "first_name": "John",
  "last_name": "Doe",
  "date_of_birth": "1990-01-01"
}

Bad:

json
{
  "firstName": "John",
  "LastName": "Doe",
  "DateOfBirth": "1990-01-01"
}

Nested Objects

Structure nested objects logically:

json
{
  "name": "John Doe",
  "email": "john@example.com",
  "address": {
    "street": "123 Main St",
    "city": "Springfield",
    "country": "US"
  },
  "preferences": {
    "newsletter": true,
    "theme": "dark"
  }
}

Arrays

Use arrays for collections:

json
{
  "tags": ["api", "design", "rest"],
  "roles": ["admin", "editor"]
}

Response Design

Response Structure

Standard Success Response

Wrap data in a consistent structure:

json
{
  "data": {
    "id": 1,
    "name": "John Doe",
    "email": "john@example.com"
  }
}

For collections:

json
{
  "data": [
    { "id": 1, "name": "User 1" },
    { "id": 2, "name": "User 2" }
  ],
  "meta": {
    "total": 100,
    "page": 1,
    "per_page": 20
  },
  "links": {
    "first": "/users?page=1",
    "last": "/users?page=5",
    "prev": null,
    "next": "/users?page=2"
  }
}

Standard Error Response

Use a consistent error format:

json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The given data was invalid.",
    "details": {
      "email": [
        "The email field is required."
      ],
      "password": [
        "The password must be at least 8 characters."
      ]
    }
  }
}

HTTP Status Codes

Use appropriate status codes:

Success Codes (2xx)

CodeUsage
200 OKSuccessful GET, PUT, PATCH, DELETE
201 CreatedSuccessful POST that creates a resource
202 AcceptedRequest accepted for async processing
204 No ContentSuccessful request with no response body

Client Error Codes (4xx)

CodeUsage
400 Bad RequestInvalid request format
401 UnauthorizedAuthentication required or failed
403 ForbiddenAuthenticated but not authorized
404 Not FoundResource doesn't exist
405 Method Not AllowedHTTP method not supported
409 ConflictResource conflict (duplicate)
422 Unprocessable EntityValidation errors
429 Too Many RequestsRate limit exceeded

Server Error Codes (5xx)

CodeUsage
500 Internal Server ErrorUnexpected server error
502 Bad GatewayInvalid upstream response
503 Service UnavailableServer temporarily unavailable
504 Gateway TimeoutUpstream timeout

Response Examples

Create Resource (201)

http
POST /users
Content-Type: application/json

{
  "name": "John Doe",
  "email": "john@example.com"
}

Response:

http
HTTP/1.1 201 Created
Location: /users/123
Content-Type: application/json

{
  "data": {
    "id": 123,
    "name": "John Doe",
    "email": "john@example.com",
    "created_at": "2024-01-01T00:00:00Z"
  }
}

Update Resource (200)

http
PATCH /users/123
Content-Type: application/json

{
  "name": "Jane Doe"
}

Response:

http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "data": {
    "id": 123,
    "name": "Jane Doe",
    "email": "john@example.com",
    "updated_at": "2024-01-01T12:00:00Z"
  }
}

Delete Resource (204)

http
DELETE /users/123

Response:

http
HTTP/1.1 204 No Content

Validation Error (422)

http
POST /users
Content-Type: application/json

{
  "email": "invalid-email"
}

Response:

http
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The given data was invalid.",
    "details": {
      "name": ["The name field is required."],
      "email": ["The email must be a valid email address."]
    }
  }
}

Data Types and Formats

Dates and Times

Always use ISO 8601 format in UTC:

json
{
  "created_at": "2024-01-01T00:00:00Z",
  "updated_at": "2024-01-01T12:30:45Z",
  "published_at": "2024-01-02T08:00:00Z"
}

Booleans

Use true and false (not 1/0 or "true"/"false"):

json
{
  "is_active": true,
  "is_verified": false,
  "has_premium": true
}

Numbers

Use appropriate numeric types:

json
{
  "id": 123,                    // Integer
  "price": 19.99,               // Float
  "quantity": 5,                // Integer
  "rating": 4.5                 // Float
}

Null Values

Include null values when a field exists but has no value:

json
{
  "name": "John Doe",
  "middle_name": null,
  "bio": null
}

Optionally omit null values if documented:

json
{
  "name": "John Doe"
}

Enumerations

Use consistent string values for enums:

json
{
  "status": "active",           // Not "Active" or "ACTIVE"
  "role": "admin",              // Not "ADMIN"
  "priority": "high"            // Not "HIGH"
}

Document allowed values:

Status: draft | published | archived
Role: user | admin | moderator
Priority: low | medium | high

Versioning

Version in URL

Include version in the URL path:

https://api.example.com/v1/users
https://api.example.com/v2/users

See API Versioning for detailed guidelines.

Relationships

Embedded Resources

For simple relationships, embed related data:

json
{
  "id": 1,
  "title": "Blog Post",
  "author": {
    "id": 123,
    "name": "John Doe",
    "email": "john@example.com"
  }
}

For complex relationships, use links:

json
{
  "id": 1,
  "title": "Blog Post",
  "author_id": 123,
  "links": {
    "author": "/users/123",
    "comments": "/posts/1/comments"
  }
}

Include Parameter

Allow clients to request related resources:

GET /posts/1?include=author,comments

Response:

json
{
  "data": {
    "id": 1,
    "title": "Blog Post",
    "author": {
      "id": 123,
      "name": "John Doe"
    },
    "comments": [
      { "id": 1, "text": "Great post!" }
    ]
  }
}

Batch Operations

Bulk Create

http
POST /users/batch
Content-Type: application/json

{
  "users": [
    { "name": "User 1", "email": "user1@example.com" },
    { "name": "User 2", "email": "user2@example.com" }
  ]
}

Response:

json
{
  "data": [
    { "id": 1, "name": "User 1", "email": "user1@example.com" },
    { "id": 2, "name": "User 2", "email": "user2@example.com" }
  ],
  "meta": {
    "created": 2,
    "failed": 0
  }
}

Bulk Update

http
PATCH /users/batch
Content-Type: application/json

{
  "updates": [
    { "id": 1, "status": "active" },
    { "id": 2, "status": "inactive" }
  ]
}

Bulk Delete

http
DELETE /users/batch
Content-Type: application/json

{
  "ids": [1, 2, 3, 4, 5]
}

Asynchronous Operations

For long-running operations, return 202 Accepted:

http
POST /exports/users

Response:

http
HTTP/1.1 202 Accepted
Location: /jobs/abc123
Content-Type: application/json

{
  "data": {
    "job_id": "abc123",
    "status": "processing",
    "created_at": "2024-01-01T00:00:00Z"
  },
  "links": {
    "status": "/jobs/abc123"
  }
}

Check status:

http
GET /jobs/abc123

Response:

json
{
  "data": {
    "job_id": "abc123",
    "status": "completed",
    "result": {
      "download_url": "/downloads/users-export.csv"
    },
    "completed_at": "2024-01-01T00:05:00Z"
  }
}

File Uploads

Single File Upload

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

[Binary data]

Response:

json
{
  "data": {
    "url": "https://cdn.example.com/avatars/123.jpg",
    "size": 102400,
    "mime_type": "image/jpeg"
  }
}

Multiple File Upload

http
POST /posts/456/attachments
Content-Type: multipart/form-data

[Multiple binary files]

Response:

json
{
  "data": [
    {
      "id": 1,
      "url": "https://cdn.example.com/files/file1.pdf",
      "name": "document.pdf"
    },
    {
      "id": 2,
      "url": "https://cdn.example.com/files/file2.jpg",
      "name": "image.jpg"
    }
  ]
}

Search Endpoints

GET /users?q=john
GET /posts?search=api+design
http
POST /search/users
Content-Type: application/json

{
  "query": "john",
  "filters": {
    "status": "active",
    "role": ["admin", "editor"]
  },
  "sort": "-created_at",
  "page": 1,
  "per_page": 20
}

Webhooks

Webhook Payload

json
{
  "event": "user.created",
  "data": {
    "id": 123,
    "name": "John Doe",
    "email": "john@example.com"
  },
  "timestamp": "2024-01-01T00:00:00Z",
  "webhook_id": "wh_abc123"
}

Webhook Headers

http
POST https://client.example.com/webhook
Content-Type: application/json
X-Webhook-Signature: sha256=abc123...
X-Webhook-Event: user.created
X-Webhook-ID: wh_abc123

Best Practices

1. Be Consistent

  • Use the same patterns across all endpoints
  • Maintain consistent naming conventions
  • Use the same error format everywhere

2. Be Explicit

  • Clearly name endpoints and parameters
  • Document all possible values
  • Provide examples in documentation

3. Be Backward Compatible

  • Don't remove fields without deprecation
  • Add new fields as optional
  • Version breaking changes

4. Be Secure

  • Always validate input
  • Use authentication and authorization
  • Rate limit all endpoints
  • Sanitize output

5. Be Efficient

  • Implement pagination for all collections
  • Support field selection
  • Enable caching with ETags
  • Use compression

6. Be Helpful

  • Provide clear error messages
  • Include request IDs for debugging
  • Document all endpoints thoroughly
  • Provide SDKs when possible

Validation

Input Validation

Validate all input data:

php
// Example validation rules
[
    'name' => 'required|string|max:255',
    'email' => 'required|email|unique:users',
    'age' => 'nullable|integer|min:0|max:150',
    'status' => 'required|in:active,inactive,pending'
]

Output Validation

Use response schemas to ensure consistent output:

php
// User resource schema
[
    'id' => 'integer',
    'name' => 'string',
    'email' => 'string',
    'created_at' => 'datetime',
    'updated_at' => 'datetime'
]

Documentation

OpenAPI/Swagger

Document all endpoints using OpenAPI specification:

yaml
paths:
  /users:
    get:
      summary: List all users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserList'

Inline Examples

Provide examples for all endpoints:

markdown
## Create User

POST /v1/users

Example request:
{
  "name": "John Doe",
  "email": "john@example.com"
}

Example response:
{
  "data": {
    "id": 123,
    "name": "John Doe"
  }
}

Testing

Test All Endpoints

php
public function test_can_create_user()
{
    $response = $this->postJson('/api/v1/users', [
        'name' => 'John Doe',
        'email' => 'john@example.com',
    ]);

    $response->assertStatus(201)
        ->assertJsonStructure([
            'data' => ['id', 'name', 'email']
        ]);
}

Test Error Cases

php
public function test_validation_fails_with_invalid_email()
{
    $response = $this->postJson('/api/v1/users', [
        'name' => 'John Doe',
        'email' => 'invalid-email',
    ]);

    $response->assertStatus(422)
        ->assertJsonValidationErrors(['email']);
}

CPR - Clinical Patient Records