REST (Representational State Transfer)
REST is the most widely used API style for web services. It uses HTTP methods and standard conventions to expose resources.
Core Principles
- Resources: Everything is a resource identified by a URL.
/users/123is the resource for user 123. - HTTP Methods: Use GET to read, POST to create, PUT to replace, PATCH to update, DELETE to remove.
- Stateless: Each request contains all the information needed to process it. No server-side session state.
- Representations: Resources can have multiple representations (JSON, XML, HTML). JSON is the de facto standard.
URL Design Best Practices
GET /users # List all users
GET /users/123 # Get user 123
POST /users # Create a new user
PUT /users/123 # Replace user 123
PATCH /users/123 # Partially update user 123
DELETE /users/123 # Delete user 123
GET /users/123/orders # List orders for user 123
GET /users/123/orders/456 # Get order 456 for user 123- Use nouns, not verbs:
/usersnot/getUsers. - Use plural nouns:
/usersnot/user. - Use kebab-case for multi-word resources:
/user-profiles. - Nest resources to express relationships:
/users/123/orders. - Do not nest more than two levels deep. Beyond that, use query parameters or separate endpoints.
Pagination
Large collections must be paginated. Common approaches:
| Style | Parameters | Pros | Cons |
|---|---|---|---|
| Offset-based | ?offset=20&limit=10 | Simple. Allows jumping to any page. | Slow for large offsets. Inconsistent if data changes between requests. |
| Cursor-based | ?cursor=abc123&limit=10 | Consistent pagination. Fast regardless of position. | Cannot jump to arbitrary pages. |
| Keyset-based | ?after_id=123&limit=10 | Uses indexed column. Very efficient. | Requires a unique, sequential key. |
Idempotency
Idempotent endpoints produce the same result regardless of how many times they are called. GET, PUT, and DELETE are idempotent by design. POST is not.
For non-idempotent operations (e.g., payment processing), use an idempotency key: the client sends a unique key with the request. If the server sees the same key again, it returns the cached result instead of processing again.
GraphQL
GraphQL is a query language for APIs developed by Facebook. Instead of fixed endpoints returning fixed data, the client specifies exactly what fields it needs.
# Client requests exactly the fields it needs
query {
user(id: "123") {
name
email
orders(last: 5) {
id
total
status
}
}
}Advantages
- No over-fetching: Clients get exactly the data they ask for, nothing more.
- No under-fetching: A single request can fetch related data (user + orders) that would require multiple REST calls.
- Strong typing: The schema defines all available types and fields. Validated at build time.
- Introspection: The API is self-documenting. Tools can auto-generate clients and documentation.
Disadvantages
- Complexity: More complex server-side implementation (resolvers, data loaders).
- Caching: HTTP-level caching does not work naturally (all requests are POST to the same URL). Requires application-level caching.
- N+1 Problem: Naive resolvers can generate too many database queries. Mitigated with DataLoader batching.
- Security: Arbitrary query depth can be exploited for denial-of-service. Must enforce query complexity limits.
gRPC
gRPC is a high-performance RPC framework using HTTP/2 and Protocol Buffers. Ideal for service-to-service communication.
syntax = "proto3";
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (stream User);
}
message GetUserRequest {
string id = 1;
}
message User {
string id = 1;
string name = 2;
string email = 3;
}When to Use gRPC
- Internal microservice-to-microservice calls where performance matters.
- Streaming use cases (server- or bi-directional streaming).
- When you need strong typing across multiple languages.
- Not ideal for browser clients (limited browser support for HTTP/2 trailers, though gRPC-Web bridges the gap).
API Versioning
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL Path | /v1/users, /v2/users | Clear, explicit. Easy to route. | Breaks URL semantics (version is not a resource). |
| Query Parameter | /users?version=2 | URL remains clean. Optional. | Easy to forget. Routing is harder. |
| Header | Accept: application/vnd.api.v2+json | URL stays the same. No coupling to version in URL. | Less discoverable. Harder to test in browser. |
/v1/) is the most common and practical choice. It is explicit, easy to understand, and well-supported by load balancers and API gateways. Use it unless you have a specific reason not to.
Rate Limiting in APIs
APIs should communicate rate limits to clients via response headers:
X-RateLimit-Limit: 100 # Max requests per window
X-RateLimit-Remaining: 42 # Requests remaining
X-RateLimit-Reset: 1612345678 # Unix timestamp when window resets
Retry-After: 30 # Seconds to wait (on 429 response)REST vs. GraphQL vs. gRPC
| Aspect | REST | GraphQL | gRPC |
|---|---|---|---|
| Protocol | HTTP/1.1 or HTTP/2 | HTTP (usually POST) | HTTP/2 |
| Data Format | JSON | JSON | Protocol Buffers (binary) |
| Typing | Weak (OpenAPI spec optional) | Strong (schema) | Strong (protobuf) |
| Caching | HTTP caching works naturally | Requires custom caching | No HTTP caching |
| Best For | Public APIs, CRUD operations | Flexible client-driven queries | Internal service-to-service |
| Learning Curve | Low | Medium | Medium-High |
Key Takeaways
- REST is the pragmatic default for public-facing APIs. Follow conventions strictly.
- GraphQL shines when clients need flexible, efficient data fetching: especially mobile apps with bandwidth constraints.
- gRPC is the performance champion for internal services. Use it for server-to-server communication.
- Always version your APIs from day one. URL path versioning is the most straightforward approach.
- Design for idempotency, especially for write operations that may be retried on failure.
- Use cursor-based pagination for large, dynamic datasets.