REST API Design
REST uses resource-oriented URLs and HTTP verbs for stateless APIs. It’s the standard for public APIs and service-to-service communication.
REST Principles
Resources Use nouns, not verbs: /orders not /getOrders
HTTP Verbs GET (read), POST (create), PUT (replace), PATCH (update), DELETE (remove)
Stateless Each request contains all information needed to process it
Status Codes 2xx (success), 3xx (redirect), 4xx (client error), 5xx (server error)
RESTful URL Design
GET /api/v1/orders # List orders
GET /api/v1/orders/123 # Get specific order
POST /api/v1/orders # Create new order
PUT /api/v1/orders/123 # Replace order (full update)
PATCH /api/v1/orders/123 # Partial update
DELETE /api/v1/orders/123 # Delete order
# Nested resources
GET /api/v1/orders/123/items # Get order items
POST /api/v1/orders/123/items # Add item to order
HTTP Status Codes
2xx Success
3xx Redirection
4xx Client Error
5xx Server Error
200 OK : Request succeeded
201 Created : Resource created (return Location header)
204 No Content : Success with no response body
301 Moved Permanently : Resource permanently moved
302 Found : Temporary redirect
304 Not Modified : Cached version is still valid
400 Bad Request : Invalid request format
401 Unauthorized : Authentication required
403 Forbidden : Not authorized to access resource
404 Not Found : Resource doesn’t exist
409 Conflict : Resource conflict (duplicate, version mismatch)
422 Unprocessable Entity : Validation failed
429 Too Many Requests : Rate limit exceeded
500 Internal Server Error : Unexpected server error
502 Bad Gateway : Invalid response from upstream server
503 Service Unavailable : Temporary unavailability
504 Gateway Timeout : Upstream timeout
Cursor-based pagination scales better than offset for large datasets.
// Cursor-based pagination (recommended)
GET /api/v 1 /orders?limit= 20 &cursor=eyJpZCI 6 MTIzfQ
{
"data" : [ ... ],
"pagination" : {
"next_cursor" : "eyJpZCI6MTQzfQ" ,
"has_more" : true
}
}
// Offset-based pagination (simple but doesn't scale)
GET /api/v 1 /orders?limit= 20 &offset= 40
Ignore pagination for collection endpoints leads to memory + latency issues. Always paginate collections.
API Versioning
URL Path (Recommended)
Query Parameter
/api/v1/orders
/api/v2/orders
Clear, cacheable, easy to route Simple but breaks caching
Version REST APIs from day one. Use URL path versioning for simplicity and cache-ability.
OpenAPI Specification
Generate machine-readable API contracts using OpenAPI (formerly Swagger).
openapi : 3.0.0
info :
title : Order API
version : 1.0.0
paths :
/orders :
get :
summary : List orders
parameters :
- name : limit
in : query
schema :
type : integer
default : 20
responses :
'200' :
description : Success
content :
application/json :
schema :
type : array
items :
$ref : '#/components/schemas/Order'
Generate OpenAPI specs from code and validate them in CI to keep documentation in sync with implementation.
GraphQL API Design
GraphQL provides a query language that allows clients to request exactly the data they need, solving the over-fetching and under-fetching problems of REST.
GraphQL Schema
# Type definitions
type Order {
id : ID !
total : Float !
status : OrderStatus !
customer : Customer !
items : [ OrderItem ! ] !
createdAt : DateTime !
}
type Customer {
id : ID !
name : String !
email : String !
}
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
}
type Query {
order ( id : ID ! ): Order
orders ( limit : Int , cursor : String ): OrderConnection !
}
type Mutation {
placeOrder ( input : PlaceOrderInput ! ): PlaceOrderPayload !
cancelOrder ( id : ID ! ): Order !
}
GraphQL Queries
# Client fetches only what it needs
query GetOrder ( $id : ID ! ) {
order ( id : $id ) {
total
status
customer {
name
email
}
items {
product {
name
}
quantity
price
}
}
}
Solving the N+1 Problem
Use DataLoader for batched resolver execution to avoid N+1 database queries.
import DataLoader from 'dataloader' ;
// Batch load customers by ID
const customerLoader = new DataLoader ( async ( ids : string []) => {
const customers = await db . customers . findMany ({
where: { id: { in: ids } }
});
// Return in same order as requested IDs
return ids . map ( id => customers . find ( c => c . id === id ));
});
// Resolver uses DataLoader
const resolvers = {
Order: {
customer : ( order ) => customerLoader . load ( order . customerId )
}
};
GraphQL N+1 problem: use DataLoader for batched resolver execution to avoid one database query per item.
GraphQL Best Practices
Do
Use DataLoader for all resolvers accessing a database
Implement query depth limiting to prevent abuse
Use persisted queries for production clients
Add input validation at the resolver level
Don't
Expose your internal domain model directly in the schema
Use GraphQL mutations for fire-and-forget async operations
Allow unlimited query depth or complexity
Use GraphQL for internal BFF (Backend for Frontend) layers serving mobile/SPA clients with complex data needs; use REST for stable public APIs with broad consumer compatibility.
gRPC API Design
gRPC is a high-performance RPC framework using Protocol Buffers for binary serialization and HTTP/2 for multiplexed transport. It’s the standard for internal microservice communication.
Protocol Buffers
syntax = "proto3" ;
package orders.v1 ;
service OrderService {
// Unary RPC
rpc GetOrder ( GetOrderRequest ) returns ( Order );
// Server streaming
rpc ListOrders ( ListOrdersRequest ) returns ( stream Order );
// Client streaming
rpc PlaceOrders ( stream PlaceOrderRequest ) returns ( PlaceOrdersResponse );
// Bidirectional streaming
rpc ProcessOrders ( stream OrderUpdate ) returns ( stream OrderResult );
}
message GetOrderRequest {
string order_id = 1 ;
}
message Order {
string id = 1 ;
double total = 2 ;
OrderStatus status = 3 ;
string customer_id = 4 ;
repeated OrderItem items = 5 ;
google.protobuf.Timestamp created_at = 6 ;
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0 ;
ORDER_STATUS_PENDING = 1 ;
ORDER_STATUS_CONFIRMED = 2 ;
ORDER_STATUS_SHIPPED = 3 ;
}
message OrderItem {
string product_id = 1 ;
int32 quantity = 2 ;
double price = 3 ;
}
gRPC Features
Binary Protocol Protocol Buffers: 5x smaller than JSON, schema-enforced
HTTP/2 Multiplexing Multiple streams on a single connection
4 RPC Types Unary, server-streaming, client-streaming, bidirectional
Code Generation Strongly typed client/server stubs in any language
Protobuf Evolution Rules
Protobuf field numbers must never change — add new fields, deprecate old ones. Changing field numbers breaks all consumers.
message Order {
string id = 1 ; // Never change field number
double total = 2 ;
OrderStatus status = 3 ;
string customer_id = 4 ;
// Add new fields with new numbers
string tracking_number = 5 ; // Safe: new optional field
// Deprecate fields, don't remove
reserved 6 ; // Previously used field
reserved "old_field_name" ;
}
Enable gRPC server reflection in non-production environments — it dramatically speeds up development and debugging without distributing .proto files to every team.
gRPC Best Practices
Do
Use gRPC for high-frequency internal service-to-service calls
Enforce backward-compatible Protobuf evolution (only add fields, never remove)
Use buf.build for Protobuf linting and breaking change detection
Implement proper error handling with status codes
Don't
Expose gRPC directly to browsers without a gRPC-Web proxy
Change field numbers in Protobuf definitions
Use gRPC for external public APIs — REST/GraphQL has wider client support
Messaging & Event-Driven APIs
Message queues (RabbitMQ, SQS) and event streams (Kafka) decouple producers from consumers, enabling async, fault-tolerant, horizontally scalable communication.
Queue vs Topic
Queue (Point-to-Point)
Topic (Pub-Sub)
Each message delivered to one consumer Producer → Queue → Consumer 1
→ Consumer 2 (competing)
→ Consumer 3 (competing)
# Use cases:
- Task distribution
- Work queues
- Job processing
Each message delivered to all subscribers Producer → Topic → Subscriber 1
→ Subscriber 2
→ Subscriber 3
# Use cases:
- Event broadcasting
- Notifications
- Change data capture
Kafka Event Streaming
Kafka is a distributed log, durable, replayable, with consumer groups for parallel processing.
// Kafka producer
const producer = kafka . producer ();
await producer . connect ();
await producer . send ({
topic: 'orders' ,
messages: [
{
key: order . customerId , // Partition key
value: JSON . stringify ({
type: 'OrderPlaced' ,
orderId: order . id ,
total: order . total ,
timestamp: Date . now ()
})
}
]
});
// Kafka consumer with offset commit
const consumer = kafka . consumer ({ groupId: 'order-processor' });
await consumer . subscribe ({ topic: 'orders' });
await consumer . run ({
eachMessage : async ({ topic , partition , message }) => {
await processIdempotently ( message );
// Offset auto-committed after eachMessage resolves
}
});
Idempotent Consumers
Consumer idempotency is mandatory — at-least-once delivery guarantees duplicates will arrive.
// Idempotent message processing
async function processIdempotently ( message : Message ) {
const messageId = message . headers [ 'message-id' ];
// Check if already processed
if ( await db . processedMessages . exists ( messageId )) {
console . log ( `Skipping duplicate message ${ messageId } ` );
return ;
}
// Process in transaction
await db . transaction ( async ( tx ) => {
await processOrder ( message . value , tx );
await tx . processedMessages . insert ({ id: messageId });
});
}
Dead Letter Queue (DLQ)
Always configure a Dead Letter Queue for every consumer — production message queues will eventually receive messages that fail repeatedly.
// RabbitMQ with DLQ
const queue = 'orders' ;
const dlq = 'orders.dlq' ;
await channel . assertQueue ( dlq , { durable: true });
await channel . assertQueue ( queue , {
durable: true ,
arguments: {
'x-dead-letter-exchange' : '' ,
'x-dead-letter-routing-key' : dlq ,
'x-message-ttl' : 86400000 , // 24 hours
}
});
Messaging Best Practices
Do
Make all message consumers idempotent
Configure DLQs and monitor them for failures
Use Kafka consumer groups for parallel processing with ordering guarantees
Include correlation IDs for distributed tracing
Don't
Process messages without idempotency (at-least-once means duplicates)
Block the consumer thread during slow downstream calls
Ignore DLQ depth as an operational metric
Kafka enables event sourcing, audit logs, and real-time data pipelines at massive scale. Use consumer groups for parallel processing with ordering guarantees per partition.