Stop Hardcoding Your Axios Singleton: A Three-Layer Architecture for Maintainable Frontend Requests
A request layer that directly imports the router, a UI component library, or hardcodes localStorage becomes impossible to reuse across Electron, mobile webviews, or different apps without rewriting it. The three-layer pattern with dependency injection keeps the services package portable and testable, which matters the moment a project adds a second backend or a second runtime target.
Most frontend projects start with a simple Axios instance that reads tokens from localStorage, redirects on 401, and shows error toasts. As backends multiply and requirements diverge, that single file accumulates conditionals for different success codes, auth schemes, and UI behaviors until no one dares to touch it. The root cause is coupling: the request layer knows too much about the application's router, component library, and storage mechanism.
The fix is a three-layer architecture. A request kernel (an AxiosFactory) creates instances and mounts interceptors without knowing any business logic. Service protocol layers handle per-backend differences like success codes and data field names. An application assembly layer injects runtime capabilities — token retrieval, auth failure handling, error display — at startup through functions like `configureServiceAuth` and `setErrorMessenger`. This inverts dependencies so the services layer never imports the router or a UI library.
Request-level behavior switches such as `hiddenNotify` and `skipAuthRedirect` prevent one-size-fits-all error handling from degrading the user experience during polling, silent token refresh, or optional data loading. The approach scales from single apps to monorepos, and the article provides concrete directory structures and a seven-question self-check for evaluating any request layer design.
The request layer's decay is not a failure of Axios but of boundary discipline: when a module knows about routers, UI components, and storage, it stops being a service and becomes an application hub.
Conditional branches keyed on `serviceType` inside a single interceptor are a reliable signal that the abstraction has collapsed and per-service instances are overdue.
Dependency inversion in the request layer — injecting `onAuthenticationFailure` rather than importing the router — is the same principle that keeps clean architecture testable, applied to a place frontend teams rarely apply it.
The `hiddenNotify` and `skipAuthRedirect` flags are not cosmetic; they acknowledge that HTTP requests in a real app have different UX contracts, and a uniform error handler violates those contracts.
LYStack's approach treats the services package as a capability provider that the app layer configures, which means the same services code can serve a web admin, an Electron shell, and a mobile webview without modification.