$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

  1. Retrieves the access token from localStorage
  2. Merges request configuration with provided init options
  3. Adds Authorization header automatically: Bearer <access_token>
  4. Makes the fetch request using the standard Fetch API
  5. 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

// 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