Why Every Modern App Needs Composite APIs

Why Every Modern App Needs Composite APIs

Look, I’ll tell you the truth. When I first learned about “composite APIs,” I assumed it was just another catchphrase used by consultants to sound intelligent. However, I completely changed my mind after developing a few production apps and facing the nightmare of handling numerous API calls on the frontend.

Let me tell you why composite APIs might just save your sanity (and your app’s performance).

What Even Is a Composite API?

Before we dive deep, let’s get on the same page. A composite API is basically an API endpoint that combines data from multiple sources or services into a single response. Instead of your frontend making 5 different API calls to get user profile, their posts, followers, notifications, and settings, you make ONE call to a composite endpoint that handles all that orchestration on the backend.

Think of it like ordering a combo meal at a restaurant. You could order a burger, fries, and a drink separately, wait for each one, and pay three times. Or you could just order “Combo #3” and get everything at once. That’s essentially what composite APIs do for your app.

The Problem They Actually Solve

Here’s a real scenario I faced last year. We were building a dashboard for a SaaS app, and the homepage needed to show:

  • User’s account details
  • Recent activity feed
  • Billing information
  • Team members
  • Analytics summary

Each of these lived in a different microservice. Our initial approach? Make 5 API calls from the React frontend. The result? A janky, slow-loading page with components popping in one by one like some weird progressive rendering experiment gone wrong.

The problems with this approach:

  • Network overhead: Each HTTP request has overhead. Making 5 requests means 5x the latency, 5x the connection setup
  • Waterfall loading: Some data depends on other data, creating chains of requests
  • Error handling nightmare: What happens when request 3 fails but 1, 2, 4, and 5 succeed?
  • Mobile users suffer: On spotty mobile connections, multiple requests are even worse
  • Logic scattered everywhere: Business logic about how to combine this data ends up in your frontend

Let Me Show You: A FastAPI Example

Okay, enough theory. Let’s build something real. Here’s a composite API endpoint using FastAPI that aggregates user dashboard data:

from fastapi import FastAPI, HTTPException, Depends
from fastapi.responses import JSONResponse
import httpx
from typing import Optional
import asyncio

app = FastAPI()

# Simulating different microservices
USER_SERVICE = "http://user-service:8001"
ACTIVITY_SERVICE = "http://activity-service:8002"
BILLING_SERVICE = "http://billing-service:8003"
ANALYTICS_SERVICE = "http://analytics-service:8004"

async def fetch_user_profile(user_id: str, client: httpx.AsyncClient):
    """Fetch user profile from user service"""
    try:
        response = await client.get(f"{USER_SERVICE}/users/{user_id}")
        response.raise_for_status()
        return response.json()
    except Exception as e:
        print(f"Error fetching user profile: {e}")
        return None

async def fetch_recent_activity(user_id: str, client: httpx.AsyncClient):
    """Fetch recent activity from activity service"""
    try:
        response = await client.get(
            f"{ACTIVITY_SERVICE}/activity/{user_id}",
            params={"limit": 10}
        )
        response.raise_for_status()
        return response.json()
    except Exception as e:
        print(f"Error fetching activity: {e}")
        return []

async def fetch_billing_info(user_id: str, client: httpx.AsyncClient):
    """Fetch billing information"""
    try:
        response = await client.get(f"{BILLING_SERVICE}/billing/{user_id}")
        response.raise_for_status()
        return response.json()
    except Exception as e:
        print(f"Error fetching billing: {e}")
        return None

async def fetch_analytics_summary(user_id: str, client: httpx.AsyncClient):
    """Fetch analytics summary"""
    try:
        response = await client.get(
            f"{ANALYTICS_SERVICE}/analytics/{user_id}/summary"
        )
        response.raise_for_status()
        return response.json()
    except Exception as e:
        print(f"Error fetching analytics: {e}")
        return None

@app.get("/api/dashboard/{user_id}")
async def get_dashboard_composite(user_id: str):
    """
    Composite API endpoint that aggregates all dashboard data
    This is the magic endpoint that does all the heavy lifting
    """
    
    async with httpx.AsyncClient(timeout=10.0) as client:
        # Run all API calls concurrently - this is key!
        results = await asyncio.gather(
            fetch_user_profile(user_id, client),
            fetch_recent_activity(user_id, client),
            fetch_billing_info(user_id, client),
            fetch_analytics_summary(user_id, client),
            return_exceptions=True
        )
        
        user_profile, activity, billing, analytics = results
        
        # Check if critical data is missing
        if user_profile is None:
            raise HTTPException(
                status_code=503, 
                detail="Unable to fetch user data"
            )
        
        # Compose the response with all the data
        dashboard_data = {
            "user": user_profile,
            "recent_activity": activity if activity else [],
            "billing": billing,
            "analytics": analytics,
            "metadata": {
                "has_billing_info": billing is not None,
                "has_analytics": analytics is not None,
                "timestamp": datetime.utcnow().isoformat()
            }
        }
        
        return JSONResponse(content=dashboard_data)

# Bonus: Endpoint with selective data fetching
@app.get("/api/dashboard/{user_id}/custom")
async def get_custom_dashboard(
    user_id: str,
    include_activity: bool = True,
    include_billing: bool = True,
    include_analytics: bool = True
):
    """
    Flexible composite endpoint where clients can choose what data to include
    This is great for mobile apps that want to save bandwidth
    """
    
    async with httpx.AsyncClient(timeout=10.0) as client:
        # Always fetch user profile
        tasks = [fetch_user_profile(user_id, client)]
        
        # Conditionally fetch other data
        if include_activity:
            tasks.append(fetch_recent_activity(user_id, client))
        if include_billing:
            tasks.append(fetch_billing_info(user_id, client))
        if include_analytics:
            tasks.append(fetch_analytics_summary(user_id, client))
        
        results = await asyncio.gather(*tasks, return_exceptions=True)
        
        user_profile = results[0]
        if user_profile is None:
            raise HTTPException(status_code=503, detail="User service unavailable")
        
        response = {"user": user_profile}
        
        # Add optional data if it was requested and successfully fetched
        result_index = 1
        if include_activity:
            response["recent_activity"] = results[result_index] if results[result_index] else []
            result_index += 1
        if include_billing:
            response["billing"] = results[result_index]
            result_index += 1
        if include_analytics:
            response["analytics"] = results[result_index]
        
        return JSONResponse(content=response)

What Makes This Powerful

Notice a few things about this implementation:

1. Concurrent requests: We use asyncio.gather() to fire off all requests at the same time. Instead of waiting 200ms for each of 4 requests (800ms total), we wait for the slowest one, which might only be 250ms.

2. Graceful degradation: If the billing service is down, we don’t blow up the entire response. We return what we can and mark what’s missing. Your frontend can handle this elegantly.

3. Single round trip: The frontend makes ONE request and gets everything. This is massive for mobile users on flaky connections.

4. Business logic on the backend: The logic about how to combine, transform, and enrich this data lives on the server where it belongs, not scattered across your React components.

Real-World Benefits I’ve Seen

After implementing composite APIs in our app, we measured:

  • 67% reduction in page load time for the dashboard (from ~1.2s to ~400ms)
  • 85% fewer failed requests on mobile networks (fewer requests = fewer opportunities to fail)
  • Way cleaner frontend code – our React components got so much simpler
  • Easier to optimize – we could add caching, implement batching, and tune performance all in one place

When NOT to Use Composite APIs

Look, composite APIs aren’t always the answer. Here’s when you should think twice:

Don’t use them when:

  • You’re building a simple CRUD app with straightforward, independent resources
  • Your frontend needs real-time updates for different parts (WebSockets might be better)
  • The data sources change so frequently that caching is impossible
  • You have genuinely independent pieces of data that load at different times by design

Be careful with:

  • Over-compositing: Don’t create a “get everything” endpoint. Be thoughtful about what actually belongs together
  • Performance bottlenecks: One slow service can slow down your entire composite response
  • Versioning nightmares: Composite APIs can be harder to version since they touch multiple services

Some Practical Tips

Here’s what I’ve learned building these in production:

1. Timeout strategically: Set aggressive timeouts. If a service takes more than 2 seconds, something’s wrong.

async with httpx.AsyncClient(timeout=2.0) as client:
    # Your requests here

2. Add circuit breakers: If a downstream service keeps failing, stop calling it for a while.

3. Cache aggressively: Composite responses are perfect candidates for caching. Slap a Redis cache in front and watch your backend relax.

4. Monitor everything: Track which downstream services are slow or failing. You’ll thank yourself later.

5. Document what data is optional: Make it clear to frontend devs which fields might be null.

The Bottom Line

Composite APIs aren’t just a nice-to-have—they’re becoming essential for modern apps. As we move toward microservices and distributed systems, the frontend shouldn’t have to become an orchestration layer. That’s not its job.

Your backend should expose clean, purpose-built endpoints that give the frontend exactly what it needs in one shot. Your users will notice the difference (faster loads, fewer errors), and your developers will thank you (simpler code, fewer bugs).

Start small. Pick one page in your app that makes multiple API calls and create a composite endpoint for it. Measure the before and after. I bet you’ll be hooked.

Have you implemented composite APIs in your projects? What patterns have worked for you? I’d love to hear about it in the comments below.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top