Skip to main content

/writing

Proxy Patterns for Multi-API Dashboards

· 3 min read

The dashboard I worked on talked to two APIs with different authentication, different error shapes and different data models. That leaves you with a choice. Either every component learns both, or one layer learns both and the rest of the code stops noticing.

The first option gets worse with every endpoint you add. The second only gets worse when the two APIs differ, and they differ in a handful of fixed ways. I built the layer.

This is one piece of the dashboard’s integration layer. It is what consumer code actually sees: one typed surface, fed by contract-first mocks during design and by the codegen pipeline once specs ship.

The problem

One API handled platform concerns: organizations, users, billing. The other handled product features, and it was only reachable through the platform API’s proxy endpoint, which added its own routing rules.

So every component that fetched data had to know which API it wanted, how to authenticate against it, and how to read its errors. That knowledge was copied all over the codebase.

The solution

I split data fetching into two families of hooks, each with its own fetch wrapper, error handling and types. The management hooks authenticate directly. The product hooks route through the proxy and inject the context it needs.

Both families share one query options builder for cache keys, stale times and retry rules. The type signatures stay separate, so a management endpoint cannot be passed to a product hook by mistake. The compiler catches it.

The piece I liked most was making the product hooks decide for themselves whether to run. If the context they need is not there yet, the query disables itself instead of firing and failing. That removed a whole class of race conditions during navigation.

Query options as a shared language

Each domain has an options file exporting functions that return queryOptions. The same options feed the hooks in components and the route loaders that prefetch.

One configuration drives both. Change how a query is cached and both the component fetch and the loader pick it up.

What it looks like at size

Every domain in the dashboard now has its own options module, around a hundred of them, and every route in the application pulls from that set. The two hook families still hold the only knowledge of which API is which and how each one authenticates.

The measure of whether the boundary worked is that the number of places knowing about the proxy never grew. New features arrived, routes multiplied, and the routing rules stayed in one layer.

What I learned

The proxy layer moved complexity out of every component and into one place. The rest of the codebase stopped needing to know anything about routing or authentication. Engineers pick the right hook, and the type system handles the rest.