$fetch
The $fetch method is a low-level authenticated fetch wrapper that automatically adds the Bearer token to requests. It provides a convenient way to make API calls without manually managing authentication headers.
This method is accessed via authClient.$fetch() and returns a standard Fetch API Response object.
Method Signature
$fetch(path: string, init?: RequestInit): Promise<Response>
Parameters
path
- type:
string- Required - The API endpoint path to fetch
init
- type:
RequestInit(optional)- Optional fetch configuration (headers, method, body, etc.)
Return Value
Returns Promise<Response> - Standard Fetch API Response object with all methods like .json(), .text(), .blob(), etc.
How It Works
- Retrieves the access token from localStorage
- Merges request configuration with provided init options
- Adds Authorization header automatically:
Bearer <access_token> - Makes the fetch request using the standard Fetch API
- Returns the response without modification
Usage
Basic GET Request
import { authClient } from "@/auth";
async function fetchUserProfile() {
const response = await authClient.$fetch("/api/user/profile");
if (response.ok) {
const data = await response.json();
console.log(data);
} else {
console.error("Failed to fetch profile:", response.status);
}
}
POST Request with JSON Body
import { authClient } from "@/auth";
async function updateProfile(data: { name: string; email: string }) {
const response = await authClient.$fetch("/api/user/profile", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (response.ok) {
return await response.json();
} else {
throw new Error(`Failed to update profile: ${response.statusText}`);
}
}
With Error Handling
import { authClient } from "@/auth";
async function fetchWithErrorHandling<T>(path: string): Promise<T> {
try {
const response = await authClient.$fetch(path);
if (!response.ok) {
if (response.status === 401) {
throw new Error("Unauthorized - please sign in again");
} else if (response.status === 403) {
throw new Error("Forbidden - insufficient permissions");
} else if (response.status === 404) {
throw new Error("Resource not found");
} else {
throw new Error(`Request failed: ${response.statusText}`);
}
}
return await response.json();
} catch (error) {
console.error("Fetch error:", error);
throw error;
}
}
File Upload
import { authClient } from "@/auth";
async function uploadFile(file: File) {
const formData = new FormData();
formData.append("file", file);
const response = await authClient.$fetch("/api/upload", {
method: "POST",
body: formData,
// Note: Don't set Content-Type header - browser will set it automatically with boundary
});
if (response.ok) {
return await response.json();
} else {
throw new Error("Upload failed");
}
}
DELETE Request
import { authClient } from "@/auth";
async function deleteItem(itemId: string) {
const response = await authClient.$fetch(`/api/items/${itemId}`, {
method: "DELETE",
});
if (response.ok) {
console.log("Item deleted successfully");
return true;
} else {
console.error("Failed to delete item");
return false;
}
}
With Query Parameters
import { authClient } from "@/auth";
async function searchUsers(query: string, limit: number = 10) {
const params = new URLSearchParams({
q: query,
limit: limit.toString(),
});
const response = await authClient.$fetch(`/api/users/search?${params}`);
if (response.ok) {
return await response.json();
} else {
throw new Error("Search failed");
}
}
React Hook Pattern
import { authClient } from "@/auth";
import { useState, useEffect } from "react";
function useApiData<T>(path: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await authClient.$fetch(path);
if (response.ok) {
const result = await response.json();
setData(result);
} else {
throw new Error(`Request failed: ${response.statusText}`);
}
} catch (err) {
setError(err instanceof Error ? err : new Error("Unknown error"));
} finally {
setLoading(false);
}
};
fetchData();
}, [path]);
return { data, loading, error };
}
// Usage
function UserProfile() {
const { data, loading, error } = useApiData<User>("/api/user/profile");
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!data) return <div>No data</div>;
return <div>{data.name}</div>;
}
Comparison with Standard fetch
Using $fetch (Recommended)
// Automatically adds Bearer token
const response = await authClient.$fetch("/api/user");
Using Standard fetch
// Must manually add token
const token = authClient.token();
const response = await fetch("/api/user", {
headers: {
Authorization: `Bearer ${token?.access_token}`,
},
});
Benefits of $fetch:
- Automatically adds Bearer token
- Cleaner code, less boilerplate
- Consistent authentication handling
- Easier to maintain
Advanced Patterns
Retry with Token Refresh
import { authClient } from "@/auth";
async function fetchWithRetry(path: string, init?: RequestInit) {
let response = await authClient.$fetch(path, init);
if (response.status === 401) {
// Token might be expired, try refreshing
const newToken = await authClient.refreshToken();
if (newToken) {
// Retry with new token
response = await authClient.$fetch(path, init);
}
}
return response;
}
Request Interceptor Pattern
import { authClient } from "@/auth";
async function interceptedFetch(path: string, init?: RequestInit) {
// Add custom headers
const customInit: RequestInit = {
...init,
headers: {
...init?.headers,
"X-Client-Version": "1.0.0",
"X-Request-ID": crypto.randomUUID(),
},
};
const response = await authClient.$fetch(path, customInit);
// Log response for debugging
console.log(`[${response.status}] ${path}`);
return response;
}
Type-Safe API Client
import { authClient } from "@/auth";
interface ApiResponse<T> {
data: T;
message?: string;
}
async function apiGet<T>(path: string): Promise<T> {
const response = await authClient.$fetch(path);
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`);
}
const result: ApiResponse<T> = await response.json();
return result.data;
}
async function apiPost<T, D>(path: string, data: D): Promise<T> {
const response = await authClient.$fetch(path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`);
}
const result: ApiResponse<T> = await response.json();
return result.data;
}
// Usage
interface User {
id: string;
name: string;
email: string;
}
const user = await apiGet<User>("/api/user/profile");
const updatedUser = await apiPost<User, Partial<User>>("/api/user/profile", {
name: "New Name",
});
Authentication Requirement
This method requires a valid access token to be available. If no token is stored, the Authorization header will not be added, and the request will likely fail with a 401 error.
Always ensure the user is authenticated before using $fetch:
const token = authClient.token();
if (!token) {
// Redirect to login
window.location.href = "/login";
return;
}
// Safe to use $fetch now
const response = await authClient.$fetch("/api/data");
Error Handling
The method returns a standard Response object. Check response.ok or response.status to handle errors:
const response = await authClient.$fetch("/api/data");
if (!response.ok) {
if (response.status === 401) {
// Token expired or invalid
await authClient.refreshToken();
} else if (response.status === 403) {
// Forbidden
console.error("Insufficient permissions");
} else if (response.status >= 500) {
// Server error
console.error("Server error occurred");
}
}
See Also
- token function - Retrieve stored tokens
- refreshToken function - Refresh expired tokens
- getUserinfo function - Example of API call using $fetch