Skip to main content

Designing REST APIs

As engineers, we need to create programming interfaces all the time. A well-designed API balances usability, performance, and maintainability; making it easier for other engineers to integrate and use in their applications. Clear documentation, consistent design principles, security, and error handling are crucial to ensuring the API meets both functional and non-functional requirements effectively.

Principles of a well-designed API

Simplicity and Clarity

  • Instinctive - It should be obvious/easy to guess what each endpoint does.
  • Minimalistic - Provide only the features that are necessary and avoid overloading the API with redundant or confusing functionality.
  • Consistent Naming - Use clear, consistent, and descriptive names for endpoints, methods, and parameters.

Consistency

  • Uniform Structure - Maintain consistency across the API in terms of endpoint patterns, naming conventions, response formats, and error handling.
  • Behavioral Consistency - Similar actions should be performed in the same way. e.g. All endpoints throw HTTP 400 error when client passes invalid data to the endpoint.

Clear Documentation

  • Comprehensive Docs - Provide clear, up-to-date documentation for every endpoint, method, and parameter.
  • Examples - Provide example requests and responses, along with use cases to help developers quickly understand how to integrate the API into their applications.

Security

  • Authentication and Authorization - Secure the API by requiring proper authentication (e.g., JWT, OAuth, API keys) and ensure ACLs are in place for accessing specific resources.
  • Data Encryption - Use HTTPS (SSL/TLS) for secure communication to prevent data interception during transit.
  • Rate Limiting / Throttling - Protect your API from abuse by implementing rate limiting, quotas, and throttling mechanisms to ensure that it can handle load while preventing DDoS attacks or accidental overuse.

Error Handling

  • Clear Error Messages - Provide meaningful and human-readable error messages. e.g. avoid generic messages like "Something went wrong".
  • Standardized Error Codes - Use standard HTTP status codes (e.g. - 2xx:success, 4xx:client request error, 5xx:server error)
  • Error Documentation - Document the possible errors and their meanings in the API documentation, so developers can quickly troubleshoot.

Idempotency

  • Safe Operations - Ensure that repeated requests for the same action produce the same result (e.g., GET requests should always return the same data)
  • Mutating Methods - For non-idempotent actions be clear about the changes they will make.

Statelessness

  • Statelessness - Each request should contain all the information needed to process the request, and the server should not store any session state between requests.

REST API Standardization

This section talks through specific guidelines to follow when designing and implementing REST APIs.

Request and Response Formats

Using a consistent and standardized format like JSON ensures that your API is easy to understand and integrates well with a wide range of tools and frameworks. Clear structure and meaningful field names reduce confusion for developers and make debugging more straightforward. Including all necessary information in a concise manner improves the usability and reliability of your API.

Request: Creating a new order
Endpoint: POST /orders

{
"customerId": "12345",
"items": [
{ "productId": "56789", "quantity": 2 },
{ "productId": "98765", "quantity": 1 }
],
"shippingAddress": {
"street": "123 Main St",
"city": "Springfield",
"state": "IL",
"zip": "62701"
},
"paymentMethod": "credit_card"
}
Response: Successful order creation
Status: 201 Created

{
"orderId": "67890",
"status": "pending",
"orderDate": "2024-12-26T10:30:00Z",
"totalAmount": 120.50
}

Endpoint Paths (Nouns vs Verbs)

Designing endpoints with nouns focuses on the resources your API exposes, not the actions being performed. This makes your API intuitive and aligns with REST principles. Using verbs often leads to redundant and inconsistent naming, making the API harder to maintain and understand. With nouns, the HTTP method naturally conveys the intended action (e.g., GET for retrieval, POST for creation).

<CheckCircle className="w-4 h-4 mr-1 inline text-green-600" /> Good:
GET /orders – Retrieve all orders
POST /orders – Create a new order
GET /orders/{orderId} – Retrieve a specific order
<XCircle className="w-4 h-4 mr-1 inline text-red-600" /> Bad:
GET /getOrders
POST /createOrder
GET /getOrderToEditByID

Resource Representation

A consistent and meaningful representation of resources makes your API predictable, which is crucial for developers to adopt and use it efficiently. It also ensures that future enhancements to the resource schema can be handled in a backward-compatible manner. For example, using clear field names and consistent types avoids confusion about what a value represents and how it should be processed.

When retrieving an order:
Request: GET /orders/67890
Response:
{
"orderId": "67890",
"customerId": "12345",
"items": [
{ "productId": "56789", "name": "Wireless Mouse", "quantity": 2, "price": 25.00 },
{ "productId": "98765", "name": "Keyboard", "quantity": 1, "price": 70.00 }
],
"shippingAddress": {
"street": "123 Main St",
"city": "Springfield",
"state": "IL",
"zip": "62701"
},
"status": "pending",
"orderDate": "2024-12-26T10:30:00Z",
"totalAmount": 120.50
}

Logical Nesting of Resources

Logical nesting reflects the relationships between resources in a straightforward and organized way. This makes your API easier to navigate and reduces ambiguity about how to access related data. By using nested paths sparingly and logically, you avoid overwhelming developers with complex or redundant endpoint structures.

Retrieve items for a specific order:
GET /orders/{orderId}/items

Add a new item to an order:
POST /orders/{orderId}/items
{
"productId": "11123",
"quantity": 3
}

Retrieve a specific item from an order:
GET /orders/{orderId}/items/{itemId}

Avoid Cyclic Resource Nesting

Avoiding deep or cyclic nesting ensures that your API remains clean, performant, and user-friendly. Overly nested endpoints can be difficult to document, use, and debug. Flattening the structure where possible makes your API more accessible, as developers can retrieve related data without making multiple deeply nested calls or managing excessive path complexity.

<XCircle className="w-4 h-4 mr-1 inline text-red-600" /> Bad:
GET /customers/{customerId}/orders/{orderId}/items/{itemId}/product
This is overly nested and makes the API difficult to use.

<CheckCircle className="w-4 h-4 mr-1 inline text-green-600" /> Good: Flatten the structure:
GET /orders/{orderId} (includes customer details and items in the response)
GET /products/{productId} (fetch product details if needed)