Microservices architectures require careful API design to ensure loose coupling, scalability, and maintainability. Well-designed APIs act as contracts between services, enabling independent development and deployment.
Always define API contracts before implementation. Use OpenAPI 3.1 specifications to document endpoints, request/response formats, and error handling.
# Example OpenAPI 3.1 snippet
openapi: 3.1.0
info:
title: Order Service API
version: 1.0.0
paths:
/orders:
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
responses:
'201':
description: Order created
content:
application/json:
schema:
$ref: '#/components/schemas/OrderResponse'
Structure APIs around business capabilities, not technical concerns. Each microservice should expose APIs that map to a specific bounded context.
Maintain API stability with versioning strategies:
/v1/orders)Accept: application/vnd.company.api.v1+json)Use an API gateway to provide a unified entry point for clients:
// Example gateway routing configuration
import { ExpressGateway } from 'express-gateway';
const gateway = new ExpressGateway();
gateway.routes
.route('/orders')
.to('http://order-service:3000')
.withCircuitBreaker({ threshold: 3, timeout: 5000 });
gateway.routes
.route('/payments')
.to('http://payment-service:3001')
.withRateLimiter({ max: 100, window: '1m' });
Combine data from multiple services to reduce client-side joins:
// Spring WebFlux aggregator example
@RestController
public class OrderAggregatorController {
private final WebClient orderClient;
private final WebClient userClient;
public OrderAggregatorController() {
this.orderClient = WebClient.create("http://order-service");
this.userClient = WebClient.create("http://user-service");
}
@GetMapping("/order-details/{orderId}")
public Mono<OrderDetails> getOrderDetails(@PathVariable String orderId) {
return Mono.zip(
orderClient.get().uri("/orders/{id}", orderId).retrieve().bodyToMono(Order.class),
userClient.get().uri("/users/{id}", userId).retrieve().bodyToMono(User.class)
).map(tuple -> new OrderDetails(tuple.getT1(), tuple.getT2()));
}
}
Separate read and write operations for better scalability:
// C# CQRS implementation example
public class OrderCommandController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderCommand command)
{
var result = await _mediator.Send(command);
return CreatedAtAction(nameof(GetOrder), new { id = result.OrderId }, result);
}
}
public class OrderQueryController : ControllerBase
{
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(Guid id)
{
var query = new GetOrderQuery { OrderId = id };
var result = await _mediator.Send(query);
return Ok(result);
}
}
Use async event streaming for eventual consistency:
# Python event producer using Kafka
from confluent_kafka import Producer
def publish_order_created_event(order):
producer = Producer({'bootstrap.servers': 'kafka:9092'})
producer.produce(
'order-events',
key=str(order.id),
value=json.dumps({
'event_type': 'OrderCreated',
'data': order.to_dict()
})
)
producer.flush()
For high-performance service-to-service calls:
// order_service.proto
syntax = "proto3";
service OrderService {
rpc CreateOrder (CreateOrderRequest) returns (OrderResponse);
rpc GetOrder (GetOrderRequest) returns (OrderResponse);
}
message CreateOrderRequest {
string user_id = 1;
repeated OrderItem items = 2;
}
message OrderResponse {
string order_id = 1;
OrderStatus status = 2;
}
Implement strict authentication and authorization:
// Go middleware example
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
claims, err := validateJWT(token)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "userClaims", claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
# Kubernetes mTLS configuration example
apiVersion: networking.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
spec:
mtls:
mode: STRICT
// Node.js with OpenTelemetry
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
const provider = new NodeTracerProvider();
provider.addSpanProcessor(
new SimpleSpanProcessor(
new JaegerExporter({ endpoint: 'http://jaeger:14268/api/traces' })
)
);
provider.register();
// Rust Actix health check endpoint
use actix_web::{get, App, HttpResponse, HttpServer, Responder};
#[get("/health")]
async fn health() -> impl Responder {
HttpResponse::Ok().json(json!({
"status": "healthy",
"version": env!("CARGO_PKG_VERSION"),
"dependencies": check_dependencies().await
}))
}
# GraphQL cursor-based pagination
query {
orders(first: 10, after: "cursor123") {
edges {
node {
id
total
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
# Nginx caching configuration
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m inactive=60m;
location /api/orders {
proxy_cache api_cache;
proxy_cache_valid 200 5m;
proxy_pass http://order-service;
}
By consistently applying these patterns and principles, teams can build microservices APIs that stand the test of time while enabling rapid evolution of business capabilities.