APIs communicate in different ways. Some request data and wait for responses. Others push updates continuously. Some connect briefly, others maintain persistent connections. Understanding these patterns helps you build better applications.
The Main Communication Patterns
Five primary patterns dominate API design. Each suits different scenarios.
Request-Response - The classic pattern. Client sends a request, server responds. Simple, predictable, widely supported.
Streaming - Server sends continuous data. Client listens and processes. Perfect for live updates.
Pub-Subscribe - Publishers send messages to topics. Subscribers receive from topics they follow. Decouples senders from receivers.
Webhooks - Server calls client when events occur. Event-driven, reactive architecture.
Polling - Client repeatedly asks for updates. Simple but inefficient.
Let's explore each in detail.
Request-Response: The Foundation
This is the most common pattern. It works like calling someone and waiting for an answer.
REST uses this pattern extensively:
// Client requests
const response = await fetch('https://api.petstoreapi.com/v1/pets/pet_123');
const pet = await response.json();
// Server responds
// { "id": "123", "name": "Buddy", "status": "available" }
The request completes, then the response arrives. Clean, straightforward.
GraphQL uses request-response too, just through a different interface:
query {
pet(id: "123") {
name
status
}
}
gRPC unary methods work the same way:
response = stub.GetPet(request)
Request-response is perfect for:
- CRUD operations
- One-time data fetches
- Operations that complete quickly
The pattern has limitations. It doesn't handle real-time updates well. Long-running operations block the client.
Streaming: Continuous Data Flow
When data changes frequently, request-response becomes inefficient. Streaming keeps a connection open and pushes data continuously.
Server-Sent Events stream from server to client:
const eventSource = new EventSource('/api/stream/pets');
eventSource.onmessage = (event) => {
const update = JSON.parse(event.data);
updatePetDisplay(update);
};
The connection stays open. Each pet status change arrives immediately.
WebSocket enables bidirectional streaming:
const ws = new WebSocket('wss://api.petstoreapi.com/v1/ws/chat?token=YOUR_JWT_TOKEN');
// Server sends updates
ws.onmessage = (event) => {
handleUpdate(JSON.parse(event.data));
};
// Client sends messages too
ws.send(JSON.stringify({ type: 'subscribe', channel: 'orders' }));
Both sides communicate freely. Great for chat, live collaboration, and gaming.
gRPC streaming handles large data or continuous updates:
# Server streams responses
for pet in stub.ListPets(request):
processPet(pet)
Streaming is ideal when:
- Data changes continuously
- Low latency matters
- Real-time features are required
Pub-Subscribe: Decoupled Messaging
Pub-sub separates message producers from consumers. Publishers don't know who's listening. Subscribers don't know who's sending.
MQTT exemplifies this pattern:
# Publisher sends to topic
client.publish('petstore/orders/new', json.dumps(order))
# Subscriber listens to topic
def on_message(client, userdata, msg):
order = json.loads(msg.payload)
processOrder(order)
client.subscribe('petstore/orders/new')
The publisher sends once. Any number of subscribers receive. They don't need to know each other.
Pub-sub works well for:
- IoT sensor networks
- Event-driven architectures
- Distributing updates to multiple systems
- Decoupling services
Webhooks: Event Notifications
Webhooks reverse the flow. Instead of the client asking, the server notifies.
When something important happens, the server calls your endpoint:
POST https://yourapp.com/webhooks/petstore
{
"event": "order.created",
"data": { "id": "12345", "total": 59.99 }
}
You process the event immediately. No polling, no delay.
Webhooks power:
- Payment notifications
- Order status updates
- Integration between services
- Keeping external systems in sync
Polling: Simple but Costly
Polling asks repeatedly: "Any updates yet?"
setInterval(async () => {
const response = await fetch('/api/pets/changes?since=' + lastCheck);
const updates = await response.json();
processUpdates(updates);
lastCheck = Date.now();
}, 5000);
It works, but it wastes resources. Constant requests even when nothing changes.
Polling is acceptable when:
- Real-time isn't critical
- You can't use other patterns
- Changes are infrequent
Otherwise, prefer webhooks or streaming.
Choosing Your Pattern
Start with request-response. It's the foundation. Add other patterns when needed.
| Need | Best Pattern |
|---|---|
| Basic CRUD operations | Request-Response |
| Multiple data shapes | Request-Response (GraphQL) |
| High performance microservices | Request-Response (gRPC) |
| Real-time updates | Streaming |
| Bidirectional real-time | WebSocket |
| Simple server-to-client | Server-Sent Events |
| IoT devices | Pub-Sub (MQTT) |
| Event notifications | Webhooks |
| Connect AI assistants | MCP |
Combining Patterns
Modern applications use multiple patterns.
A pet store might use:
- REST - For web and mobile app initial loads
- GraphQL - For flexible queries from the mobile app
- WebSocket - For real-time order tracking in the dashboard
- SSE - For live inventory updates
- Webhooks - For payment provider notifications
- MCP - For AI assistant integration
Different needs call for different patterns. That's fine.
Pet Store API: Multiple Patterns
The Pet Store API supports multiple communication patterns. Use what works for your specific use case.
- REST for standard operations
- GraphQL for flexible queries
- gRPC for high-performance needs
- WebSocket for real-time features
- SSE for server push
- MQTT for IoT
- Webhooks for event notifications
- MCP for AI integration
The documentation at docs.petstoreapi.com shows how each pattern works with the API.
Start with the simplest pattern that works. Evolve as your needs require. The right communication pattern makes all the difference.