A Durable Axios Client: Choosing, Typing, and Operating the Contract
Axios is durable when it is treated as a request contract rather than as shorthand for GET and POST. The central design question is how to carry one application intent across configuration, lifecycle hooks, data representation, and runtime transport without hiding the boundaries that affect reliability or security. A good Axios client makes those choices explicit enough to design, review, type, and maintain.
The result is not one universal configuration. A browser client for a fixed API, a Node service that fetches external URLs, an edge function using a framework-provided Fetch implementation, and a local daemon client using a Unix socket have different trust and capability requirements. They can still share Axios’s request model, but they should not pretend that the same defaults, adapter, payload representation, or failure policy fit all four.
Start with the contract, not the adapter
A durable design begins by stating what the request means. The application should know its destination relationship, the data it is sending, the response it expects, the conditions under which the result is still useful, and the status outcomes it wants to handle as ordinary application states. These are contract decisions. They should be expressible before choosing whether the request uses XHR, Node HTTP, or Fetch.
For a known company API, the contract may be: send JSON updates to one origin, attach credentials only for that API, encode filters in the server’s expected query grammar, accept a defined response shape, and stop a request when the user leaves the page. For a file service, the contract may instead be: submit multipart fields and files, stream a large response, report progress where possible, and enforce a byte ceiling. The method call can look similar, but the meaning and resource policy are different.
Axios’s value is that both clients can express their intent through the same broad shape: a URL or base URL, method, headers, params, data, response type, lifetime controls, and adapter policy. The request then passes through a shared lifecycle of merging, normalization, interception, transformation, transport, settlement, and response handling. The stable contract is the language of those decisions; the runtime supplies the capabilities used to honor it.
This explains why the adapter should not be the first design choice. Starting with “use Fetch everywhere” or “use the Node adapter” can force the application’s representation and failure model to fit the transport. Starting with the contract allows a more useful comparison: which adapter can satisfy the required stream, progress, proxy, redirect, response-type, and cancellation behavior in the actual runtime?
Give each decision an owner
Once the contract is clear, assign each decision to the narrowest owner that can reuse it safely. The following map is a design aid, not a replacement for the application’s specific trust model:
| Decision | Natural owner | Why the boundary matters |
|---|---|---|
| API destination, shared timeout, common response convention | Scoped Axios instance | Reusable for one coherent service without becoming global ambient state |
| One request’s filter, body, response type, or longer deadline | Request config | Keeps an exception visible and avoids changing future calls |
| Repeated authentication or observability policy for one client | Instance interceptor | Runs at a known lifecycle point, but remains privileged caller code |
| JSON, form, or domain-specific wire translation | Request/response transform or serializer | Changes representation and must preserve the adapter’s accepted value contract |
| Connection, proxy, redirect, stream, and runtime behavior | Selected adapter | Depends on the environment and may not have cross-runtime parity |
| HTTP status acceptance and application fallback | validateStatus and caller logic |
Gives meaning to a response without changing the status returned by the server |
An instance is usually the center of gravity. It can hold a destination’s base URL, explicit timeout, headers, adapter choice, and interceptors. A request can override only what is genuinely local. This structure avoids the two common extremes: a global client whose credentials and transformations reach unrelated hosts, and a collection of one-off calls whose shared policy exists only in developers’ memory.
The body deserves special attention. Axios does not inherit or deep-merge request data from defaults. That behavior prevents one endpoint’s fields from appearing in another endpoint’s body. If an application needs a shared field, it should add it through a deliberately scoped transform or interceptor that checks the endpoint and representation. The extra code is not ceremony for its own sake; it records who owns the coupling.
Interceptors and transforms should also be assigned different jobs. An interceptor is lifecycle middleware: it can reject a request, add a header, normalize a response, or change control flow. A transform translates data: it can turn an object into JSON, URL-encoded fields, or FormData, or parse a textual response. An adapter performs runtime transport.