Replace Axios with native fetch; add global 401 session-expiry redirect
All API calls now go through a thin request() wrapper around native fetch. Removes the axios dependency entirely. The wrapper injects the JWT on every request and — the key fix — clears localStorage and redirects to /login on any 401 response, so expired sessions no longer leave users on broken pages. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+21
-9
@@ -22,7 +22,7 @@ frontend/
|
||||
├── src/
|
||||
│ ├── main.tsx ← React root, QueryClientProvider, BrowserRouter
|
||||
│ ├── App.tsx ← Route tree, PrivateRoute, AdminRoute
|
||||
│ ├── api/client.ts ← Axios instance + ALL API functions (single source of truth)
|
||||
│ ├── api/client.ts ← Native fetch wrapper (`request()`) + ALL API functions (single source of truth); no Axios
|
||||
│ ├── hooks/
|
||||
│ │ ├── useAuth.ts ← Token state (localStorage), login/logout
|
||||
│ │ └── useTheme.ts ← Theme toggle
|
||||
@@ -86,21 +86,33 @@ frontend/
|
||||
|
||||
### API client (`src/api/client.ts`)
|
||||
|
||||
Single Axios instance — **all** API calls live here, nowhere else:
|
||||
**No Axios** — uses a thin native `fetch` wrapper. All API calls live here, nowhere else.
|
||||
|
||||
The core `request()` function handles:
|
||||
- Prepends `/api` base URL
|
||||
- Injects `Authorization: Bearer {token}` from `localStorage` on every request
|
||||
- **Global 401 handler**: clears `localStorage` token and redirects to `/login` via `window.location.href` — this is the expired-session redirect
|
||||
- Throws `ApiError(status, detail)` on non-2xx responses (detail parsed from JSON body)
|
||||
- Returns `undefined` on 204 No Content
|
||||
- Supports `blob: true` for file download/preview responses
|
||||
|
||||
```typescript
|
||||
const api = axios.create({ baseURL: "/api" });
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`;
|
||||
return config;
|
||||
});
|
||||
// The internal api object — use these methods in exported functions:
|
||||
api.get<T>(path, params?) // GET with optional query params object
|
||||
api.post<T>(path, json?) // POST with JSON body
|
||||
api.postForm<T>(path, URLSearchParams) // POST with form-encoded body (login)
|
||||
api.postFile<T>(path, FormData) // POST with multipart body (file upload)
|
||||
api.patch<T>(path, json?) // PATCH with JSON body
|
||||
api.delete<T>(path) // DELETE
|
||||
api.getBlob(path) // GET → Blob (download / view)
|
||||
```
|
||||
|
||||
Adding a new API call:
|
||||
1. Define a TypeScript interface for the response if it's new.
|
||||
2. Add a named export function (`getX`, `createX`, `updateX`, `deleteX`).
|
||||
3. Use `api.get<T>(...)`, `api.post<T>(...)`, etc.; always `.then((r) => r.data)`.
|
||||
3. Use the appropriate `api.*` method — return the promise directly (no `.then((r) => r.data)`).
|
||||
|
||||
Error handling in components: catch blocks receive an `ApiError` instance with `.status` and `.message` (the detail string).
|
||||
|
||||
### TanStack Query conventions
|
||||
|
||||
|
||||
Reference in New Issue
Block a user