How Many API Calls Does Your Home Page Make?


Whenever I open a web application for the first time, I do the same thing after logging in: I open DevTools, switch to the Network tab, and count the API calls on the home page.

It’s a small habit, but it tells me a lot. Every API call can trigger several database queries behind it, so the number of calls on the first page is an early hint about performance. More interestingly, it often shows how the system was designed: where the business logic lives, how the backend and frontend split their responsibilities, and whether the architecture fits the product.

But the raw number alone doesn’t tell you much. Ten calls can be perfectly healthy, and four can be a red flag. What matters is why each call is there.

Case 1: Many calls, and that’s fine

Imagine a dashboard with four widgets:

  • a list of recent news
  • the current weather
  • the user’s pending tasks
  • a small chart of monthly activity

These are four independent pieces of the business. They don’t share data, they change at different rates, and they could be owned by different teams. Loading them with four separate API calls is the natural result of how the product is shaped.

In fact, keeping them separate has real benefits:

  • Progressive rendering. Each widget appears as soon as its data arrives. The weather doesn’t wait for the chart.
  • Independent failure. If the news service is down, the other three widgets still work.
  • Independent caching. Weather might be cached for ten minutes, tasks not at all.

Could you merge them into a single “dashboard endpoint” later? Maybe. But that’s a separate optimization with its own trade-offs. A single endpoint is only as fast as its slowest part, and it couples features that have nothing to do with each other. Four calls for four independent components is not a design problem.

Case 2: One view, many calls — that’s the smell

Now imagine the home page is a single table. Say, a list of accounts.

To render that one table correctly, the frontend calls:

  1. /api/accounts to get the raw rows
  2. /api/managers to show the manager’s name for each account
  3. /api/accounts/{id}/children to show the sub-accounts
  4. /api/statuses to translate status codes into labels
  5. /api/permissions to decide which actions are enabled per row
  6. …and a couple more

Then the frontend stitches everything together:

forkJoin({
  accounts: this.api.getAccounts(),
  managers: this.api.getManagers(),
  statuses: this.api.getStatuses(),
  permissions: this.api.getPermissions(),
}).pipe(
  switchMap(({ accounts, managers, statuses, permissions }) =>
    forkJoin(
      accounts.map((account) =>
        this.api.getChildren(account.id).pipe(
          map((children) => ({
            ...account,
            managerName: managers.find((m) => m.id === account.managerId)?.name,
            statusLabel: statuses.find((s) => s.code === account.status)?.label,
            canEdit: permissions.some((p) => p.accountId === account.id && p.edit),
            children,
          })),
        ),
      ),
    ),
  ),
);

This is one business concept — “the accounts table” — built from six or seven requests. The number of calls is a symptom. The underlying problems are more specific.

Frontend joins

The client is matching managers to accounts by ID, statuses by code, and permissions by account. That is a join. The database is built to do joins efficiently, with indexes and a query planner. The browser is not. The logic is also in the wrong place: it’s hard to test, hard to reuse, and every other client (mobile app, export job, report) has to reimplement it.

Waterfalls

Look at the switchMap: the children requests can’t start until the accounts arrive. Dependent calls run in sequence, and their latencies add up. Four parallel calls cost roughly one round trip; four dependent calls cost four.

N+1 over HTTP

The children are fetched once per row. With 50 rows, that’s 50 extra requests. It’s the same N+1 problem you know from ORMs, except every “query” is now a full HTTP round trip with headers, authentication, and serialization.

Fixing it

The fix depends on one question: where does the data actually live?

The data lives in one database or service

If accounts, managers, statuses, children, and permissions all live in the same database, the table should come from one view-specific endpoint backed by one well-written query:

GET /api/accounts-overview

The backend does the joins where the data lives and returns exactly the rows the table needs:

[
  {
    "id": 42,
    "name": "Main Portfolio",
    "managerName": "J. Silva",
    "statusLabel": "Active",
    "canEdit": true,
    "children": [{ "id": 43, "name": "Sub Portfolio A" }]
  }
]

The frontend becomes simple again: one call, one model, render it. The composition logic lives in one place on the server, where it can be tested and optimized. And there’s one query to tune instead of seven endpoints each running their own.

Some people call this “coupling the API to the UI.” I’d argue that a screen your users see every day deserves an endpoint designed for it. Generic resource endpoints are great building blocks, but they shouldn’t force every view to assemble itself in the browser.

The data lives in separate services

Sometimes one query isn’t possible. Accounts come from one microservice, permissions from an identity service, and statuses from a third-party API. In that case, a Backend-for-Frontend (BFF) is the right tool.

A BFF is a thin server layer built for a specific frontend. The browser makes one call to it. The BFF calls the internal services, combines the results, and returns the shape the view needs.

This helps because:

  • One round trip from the browser. Browser-to-server latency is the expensive part, especially on mobile. Server-to-server calls inside the same data center take a few milliseconds.
  • The join leaves the frontend. The composition logic is on the server, not buried in RxJS chains.
  • Waterfalls get cheaper. Dependent calls still run in sequence, but each step is fast.
  • The internal structure stays hidden. If services are split or merged later, the frontend contract doesn’t change.

But be honest about what a BFF does not do: it doesn’t reduce the total work. The internal calls still happen, and each still hits its database. A BFF moves the composition to a better place; it doesn’t make the queries better. If all the data lives in one database, adding a BFF on top of seven endpoints is a band-aid. Fix the endpoint instead.

GraphQL?

GraphQL lets the client ask for exactly the shape it needs in one request, so it can look like the answer here. It helps with over-fetching and round trips, but it doesn’t remove the need for efficient resolvers. A naive GraphQL server will happily run the same N+1 queries behind a single request. It’s a tool, not a fix by itself.

When client-side composition is fine

Combining data in the frontend isn’t always wrong. It’s reasonable when:

  • the sources are truly independent and you don’t control them (e.g. two third-party APIs)
  • the combined data is small and already cached (e.g. a list of status labels loaded once at startup)
  • you’re prototyping, and you know you’ll move it later

The problem isn’t composition itself. It’s making the browser do the core data work for a view on every page load.

Audit your own app in two minutes

  1. Log in and land on the home page.
  2. Open DevTools → Network, filter by Fetch/XHR, and reload.
  3. Count the requests.
  4. Look at the waterfall column: are some requests waiting for others to finish?
  5. For each request, ask: which component uses this?

If several requests feed the same component, you’ve found a view that’s assembling itself in the browser. If you see the same endpoint repeated with different IDs, you’ve found an N+1.

Conclusion

The number of API calls on your home page is a quick, useful smell test for performance, clean code, and architecture. But the number alone isn’t the verdict.

  • Several calls for several independent features is healthy.
  • Several calls for one view usually means the data logic is in the wrong place.

And when you find that second case, ask where the data lives. If it’s in one place, write one good endpoint. If it’s spread across services, compose it on the server with a BFF. Either way, the browser should render the view, not build it.