ADR-005: RTK Query for Server State Management
Status: Accepted
Context
The frontend needs to fetch, cache, and mutate server data with:
- Automatic cache invalidation when mutations succeed
- Token refresh on 401 without manual retry logic in every component
- Typed request / response shapes
- Consistent loading and error states across the app
Options considered:
| Library | Redux integration | Auto-refresh | Typed queries | Bundle size |
|---|---|---|---|---|
| RTK Query | Native (RTK) | Via base query | ✅ | ~10 KB (already using RTK) |
| React Query | Separate store | Manual | ✅ | ~13 KB |
| SWR | None | Manual | Partial | ~4 KB |
Raw fetch | Manual | Manual | Manual | 0 KB |
The project already uses Redux Toolkit for client-side state (auth token, theme, login form). Adding RTK Query means zero extra dependencies — the cache, actions, and middleware live inside the existing Redux store.
Decision
Use RTK Query (bundled with @reduxjs/toolkit) for all server data fetching and mutations.
Architecture:
- A single
baseApi(frontend/redux/api/baseApi.ts) is created withcreateApi. All feature APIs inject their endpoints withbaseApi.injectEndpoints(). - The
baseQuerywrapsfetchBaseQuerywith a re-auth interceptor: on any 401 response it firesPOST /api/auth/refresh, stores the new token in Redux, and retries the original request — transparently, in every query and mutation. - A concurrency guard prevents refresh storms: if multiple requests fail with 401 simultaneously, only the first triggers a refresh; the rest wait on a shared
refreshPromise.
baseApi
├── authApi (login, logout, setPassword, changePassword, refreshToken)
└── integrationsApi (integrations list + toggle)
Cache tag invalidation follows RTK Query conventions — mutations call invalidatesTags to trigger refetches of affected queries.
The baseApi.reducerPath ("api") is added to the Redux store alongside the feature slices.
Consequences
Positive
- Token refresh is centralised in one place (
baseQueryWithReauth) — components never needcatch (e) { if (401) refresh() }logic - Typed endpoints: each endpoint declares its
ResultandArgtypes; RTK Query generates hooks (useGetUsersQuery,useLoginMutation) with full TypeScript inference - Cache is part of the Redux store — Redux DevTools shows all cached data and pending requests
- No extra dependencies — RTK Query ships inside
@reduxjs/toolkit - Consistent loading/error shapes:
{ isLoading, isError, data, error }in every component
Negative / Risks
- RTK Query has a larger learning curve than a plain
useFetchhook; thecreateApi/injectEndpoints/ tag invalidation model takes time to internalise - The re-auth interceptor silently retries on 401 — a malformed token that consistently returns 401 would cause an infinite refresh loop (mitigated:
clearToken()is dispatched on refresh failure, redirecting to login) - RTK Query caching is in-memory; the cache is cleared on page reload (server state is refetched; client state like the auth token is recovered from
localStorage)