$api Plugin

The $api plugin provides a pre-configured $fetch client instance exposed globally as useNuxtApp().$api. It centralizes API baseURL resolution, automatic session Bearer token attachment on { auth: true }, and global auto-logout on 401 unauthenticated responses.

While useApi is designed for declarative data fetching during component setup, $api is built for imperative actions such as form submissions, event handlers, and data mutations.

Prerequisites

1. External Modules & Libraries

  • nuxt-auth-utils: Required for session state and Bearer token management via useUserSession().
    pnpm add nuxt-auth-utils
    
    Register in nuxt.config.ts:
    export default defineNuxtConfig({
      modules: ["nuxt-auth-utils"],
    });
    

2. Environment Variables & Runtime Config

  • Session Secret: Add a 32+ character secret to .env:
    NUXT_SESSION_PASSWORD="a-long-random-string-at-least-32-characters"
    
  • Runtime Config: Configure API base URL and proxy toggle in nuxt.config.ts:
    export default defineNuxtConfig({
      runtimeConfig: {
        public: {
          apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || "https://api.example.com",
          enableProxy: process.env.NUXT_PUBLIC_ENABLE_PROXY === "true",
        },
      },
    });
    

3. Server Proxy (Optional)

  • Catch-all proxy route (server/api/proxy/[...].ts) or Nitro routeRules proxy if routing through enableProxy: true.

Features

  • Singleton $fetch Instance: Created once during plugin initialization instead of rebuilding options on every invocation.
  • Dynamic Base URL: Automatically routes through the same-origin /api/proxy endpoint when enableProxy is active, or defaults to public.apiBaseUrl.
  • Opt-in Bearer Authentication: Pass { auth: true } to automatically inject Authorization: Bearer <token> from useUserSession().
  • Nuxt Context Preservation: Interceptors run outside the synchronous Vue lifecycle, so composable operations (fetchSession, clearSession, navigateTo) are executed via nuxtApp.runWithContext().
  • Global 401 Auto-Logout: Clears the user session and redirects to / whenever an unauthenticated response is encountered.

Usage Example

1. Imperative Client-Side Request (Mutations / Form Submission)

const { $api } = useNuxtApp();

// POST request with credentials
const res = await $api<{ token: string; user: User }>("/login", {
  method: "POST",
  body: { email: form.email, password: form.password },
});

// Authenticated request
await $api("/user/profile", {
  method: "PATCH",
  auth: true,
  body: { name: "Updated Name" },
});

2. SSR-Safe Fetching with Custom $fetch

You can pass $api directly to Nuxt's native useFetch via the $fetch option:

const { data: courses, status } = await useFetch("/courses", {
  $fetch: $api,
  auth: true,
});

Implementation Code

1. Plugin Implementation (app/plugins/api.ts)

Design Details & Rationale

  • Interceptors Outside Context: Fetch interceptors (onRequest, onResponseError) execute asynchronously outside Nuxt's current instance context. Any composable that depends on context—such as fetchSession(), clearSession(), or navigateTo()—must be wrapped in nuxtApp.runWithContext().
  • Plain Ref Reads: Reading session.value is a reactive Vue ref read and does not require runWithContext().
  • Headers Mutability: Sets headers directly on options.headers using options.headers.set("Authorization", ...) rather than replacing the headers object, preserving any custom headers passed by the caller.
export default defineNuxtPlugin((nuxtApp) => {
  const {
    session,
    fetch: fetchSession,
    clear: clearSession,
  } = useUserSession();
  const config = useRuntimeConfig();

  // When ENABLE_PROXY is set, route requests through the same-origin server
  // proxy (/api/proxy) instead of hitting the external API directly.
  const baseURL = config.public.enableProxy
    ? "/api/proxy"
    : (config.public.apiBaseUrl as string);

  const api = $fetch.create({
    baseURL,
    headers: { Accept: "application/json" },

    async onRequest({ options }) {
      const { auth } = options as { auth?: boolean };
      if (!auth) return;

      if (!session.value) {
        await nuxtApp.runWithContext(() => fetchSession());
      }

      if (session.value?.token) {
        options.headers.set("Authorization", `Bearer ${session.value.token}`);
      }
    },

    async onResponseError({ response }) {
      // if 401 (e.g. token invalidated by login from another device) then logout
      if (
        response.status === 401 ||
        (response._data as { message?: string })?.message === "Unauthenticated"
      ) {
        await nuxtApp.runWithContext(async () => {
          await clearSession();
          await navigateTo("/");
        });
      }
    },
  });

  return { provide: { api } };
});

2. Configuration (nuxt.config.ts)

export default defineNuxtConfig({
  modules: ["nuxt-auth-utils"],
  runtimeConfig: {
    public: {
      apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || "https://api.example.com",
      enableProxy: process.env.NUXT_PUBLIC_ENABLE_PROXY === "true",
    },
  },
});