API Composition Pattern
What is API Composition?
API Composition aggregates data from multiple microservices into a single response, typically implemented in an API Gateway or BFF.
Problem
// Client needs data from multiple services
// Without composition: Multiple requests
const user = await fetch('http://user-service/users/123');
const orders = await fetch('http://order-service/orders?userId=123');
const reviews = await fetch('http://review-service/reviews?userId=123');
// Client must combine data
const profile = {
user: user.data,
orders: orders.data,
reviews: reviews.data
};Solution: API Composition
// API Gateway composes response
app.get('/api/users/:id/profile', async (req, res) => {
const userId = req.params.id;
// Fetch from multiple services in parallel
const [user, orders, reviews] = await Promise.all([
axios.get(`http://user-service/users/${userId}`),
axios.get(`http://order-service/orders?userId=${userId}`),
axios.get(`http://review-service/reviews?userId=${userId}`)
]);
// Compose response
res.json({
user: user.data,
orders: orders.data,
reviews: reviews.data
});
});Nested Composition
app.get('/api/orders/:id/details', async (req, res) => {
const orderId = req.params.id;
// Get order
const order = await axios.get(`http://order-service/orders/${orderId}`);
// Get related data
const [user, items] = await Promise.all([
axios.get(`http://user-service/users/${order.data.userId}`),
Promise.all(
order.data.items.map(item =>
axios.get(`http://product-service/products/${item.productId}`)
)
)
]);
// Compose nested response
res.json({
order: {
...order.data,
user: user.data,
items: order.data.items.map((item, index) => ({
...item,
product: items[index].data
}))
}
});
});Error Handling
app.get('/api/users/:id/profile', async (req, res) => {
const userId = req.params.id;
try {
const [user, orders, reviews] = await Promise.allSettled([
axios.get(`http://user-service/users/${userId}`),
axios.get(`http://order-service/orders?userId=${userId}`),
axios.get(`http://review-service/reviews?userId=${userId}`)
]);
// Partial response if some services fail
res.json({
user: user.status === 'fulfilled' ? user.value.data : null,
orders: orders.status === 'fulfilled' ? orders.value.data : [],
reviews: reviews.status === 'fulfilled' ? reviews.value.data : [],
errors: {
user: user.status === 'rejected' ? user.reason.message : null,
orders: orders.status === 'rejected' ? orders.reason.message : null,
reviews: reviews.status === 'rejected' ? reviews.reason.message : null
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});Caching
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 300 });
app.get('/api/users/:id/profile', async (req, res) => {
const userId = req.params.id;
const cacheKey = `profile:${userId}`;
// Check cache
const cached = cache.get(cacheKey);
if (cached) {
return res.json(cached);
}
// Fetch and compose
const [user, orders, reviews] = await Promise.all([
axios.get(`http://user-service/users/${userId}`),
axios.get(`http://order-service/orders?userId=${userId}`),
axios.get(`http://review-service/reviews?userId=${userId}`)
]);
const profile = {
user: user.data,
orders: orders.data,
reviews: reviews.data
};
// Cache result
cache.set(cacheKey, profile);
res.json(profile);
});GraphQL Composition
const { ApolloServer, gql } = require('apollo-server-express');
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
orders: [Order!]!
reviews: [Review!]!
}
type Order {
id: ID!
total: Float!
items: [OrderItem!]!
}
type OrderItem {
product: Product!
quantity: Int!
}
type Product {
id: ID!
name: String!
price: Float!
}
type Review {
id: ID!
rating: Int!
comment: String!
}
type Query {
user(id: ID!): User
}
`;
const resolvers = {
Query: {
user: async (_, { id }) => {
const response = await axios.get(`http://user-service/users/${id}`);
return response.data;
}
},
User: {
orders: async (user) => {
const response = await axios.get(`http://order-service/orders?userId=${user.id}`);
return response.data;
},
reviews: async (user) => {
const response = await axios.get(`http://review-service/reviews?userId=${user.id}`);
return response.data;
}
},
OrderItem: {
product: async (item) => {
const response = await axios.get(`http://product-service/products/${item.productId}`);
return response.data;
}
}
};
const server = new ApolloServer({ typeDefs, resolvers });Data Loader (Batching)
const DataLoader = require('dataloader');
// Batch load products
const productLoader = new DataLoader(async (productIds) => {
const response = await axios.post('http://product-service/products/batch', {
ids: productIds
});
return productIds.map(id =>
response.data.find(p => p.id === id)
);
});
// Use in resolver
const resolvers = {
OrderItem: {
product: async (item) => {
return await productLoader.load(item.productId);
}
}
};Timeout Handling
async function fetchWithTimeout(url, timeout = 5000) {
return Promise.race([
axios.get(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
)
]);
}
app.get('/api/users/:id/profile', async (req, res) => {
const userId = req.params.id;
const [user, orders, reviews] = await Promise.allSettled([
fetchWithTimeout(`http://user-service/users/${userId}`, 3000),
fetchWithTimeout(`http://order-service/orders?userId=${userId}`, 3000),
fetchWithTimeout(`http://review-service/reviews?userId=${userId}`, 3000)
]);
res.json({
user: user.status === 'fulfilled' ? user.value.data : null,
orders: orders.status === 'fulfilled' ? orders.value.data : [],
reviews: reviews.status === 'fulfilled' ? reviews.value.data : []
});
});Benefits
- Reduced Network Calls: Single request from client
- Better Performance: Parallel fetching
- Simplified Client: No composition logic
- Consistent Response: Single format
- Centralized Logic: Easier to maintain
Challenges
- Increased Complexity: Gateway logic
- Single Point of Failure: Gateway down = all down
- Performance Bottleneck: All traffic through gateway
- Tight Coupling: Gateway knows all services
Best Practices
- Use parallel fetching: Promise.all
- Handle partial failures: Promise.allSettled
- Implement caching: Reduce backend calls
- Set timeouts: Prevent hanging
- Use batching: DataLoader for N+1 queries
- Monitor performance: Track composition time
Interview Tips
- Explain pattern: Aggregate multiple services
- Show parallel fetching: Promise.all
- Demonstrate error handling: Partial responses
- Discuss GraphQL: Declarative composition
- Mention batching: DataLoader for efficiency
- Show caching: Reduce backend load
Summary
API Composition aggregates data from multiple microservices into single responses. Implement in API Gateway or BFF. Use parallel fetching with Promise.all. Handle partial failures gracefully. Cache composed responses. Use GraphQL for flexible composition. Implement batching with DataLoader. Essential for simplifying client interactions with microservices.
Test Your Knowledge
Take a quick quiz to test your understanding of this topic.