Fetch Architecture for Next.js

VerifiedSafe

Provides client and server-side fetch utilities for Next.js applications, including API route proxying to FastAPI backends. Handles authentication token refresh, server-side data fetching, and error handling across layers. Use this skill when asked to set up fetch utilities or implement data fetching with auth in Next.js.

Sby Skills Guide Bot
DevelopmentIntermediate
406/2/2026
Claude CodeCursor
#nextjs#fetch-utilities#api-proxy#authentication#fastapi

Recommended for

Our review

Sets up client and server-side fetch utilities for Next.js with API route proxying to FastAPI backends and authentication token handling.

Strengths

  • Clear separation of client and server fetch logic
  • Automatic token refresh handling
  • Easy-to-configure API route proxying

Limitations

  • Requires a FastAPI backend specifically
  • Presumes a predefined project structure
  • May be overkill for simple projects without authentication
When to use it

Use this skill when you need to build a robust fetch architecture for a Next.js application paired with a FastAPI backend and token-based authentication.

When not to use it

Do not use this skill for projects without a backend or when you do not need API proxying or authentication token management.

Security analysis

Safe
Quality score85/100

The skill provides legitimate client and server-side fetch utility code for Next.js with standard authentication handling. It includes no destructive commands, obfuscated payloads, or instruction to exfiltrate data. All network operations are within the application's normal flow.

No concerns found

Examples

Set up fetch utilities
Set up client and server fetch utilities for my Next.js app that proxies to a FastAPI backend at http://localhost:8000/api/v1, with auth token refresh.
Add API route proxy
Create an API route in Next.js that proxies requests to the FastAPI backend, including authentication headers.
Implement auth refresh
Implement automatic token refresh in the client-side fetch for my Next.js app when the access token expires.

Fetch Architecture Skill

Client and server-side fetch utilities for Next.js applications with API route proxying to FastAPI backends.

When to Use This Skill

Use this skill when asked to:

  • Set up fetch utilities for Next.js
  • Configure client-side API calls with auth refresh
  • Implement server-side data fetching
  • Create API route proxies to backend services
  • Handle authentication tokens across layers

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                    Browser (Client)                          │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  Client Components                                   │    │
│  │  • fetchClient.get/post/put/delete                  │    │
│  │  • SWR hooks with fetcher                           │    │
│  └──────────────────────────┬──────────────────────────┘    │
└─────────────────────────────┼───────────────────────────────┘
                              │ HTTP (cookies)
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    Next.js Server                            │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  API Routes (app/api/...)                           │    │
│  │  • withAuth() wrapper                               │    │
│  │  • backendGet/Post/Put/Delete helpers               │    │
│  └──────────────────────────┬──────────────────────────┘    │
│                             │                                │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  Server Actions                                     │    │
│  │  • serverGet/Post/Put/Delete                        │    │
│  │  • Forwards cookies to API routes                   │    │
│  └──────────────────────────┬──────────────────────────┘    │
│                             │                                │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  Server Components (pages)                          │    │
│  │  • auth() session check                             │    │
│  │  • Call server actions for SSR data                 │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────┼───────────────────────────────┘
                              │ HTTP (Bearer token)
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    FastAPI Backend                           │
│  /api/v1/...                                                │
└─────────────────────────────────────────────────────────────┘

Directory Structure

lib/
├── fetch/
│   ├── index.ts              # Exports
│   ├── client.ts             # Client-side fetch (browser)
│   ├── server.ts             # Server-side fetch (actions, routes)
│   ├── api-route-helper.ts   # API route wrappers
│   ├── errors.ts             # Error classes
│   └── types.ts              # TypeScript types
└── auth/
    ├── server-auth.ts        # Server authentication
    └── auth-service.ts       # Client auth (token refresh)

Core Files

1. Error Classes

// lib/fetch/errors.ts
export class ApiError extends Error {
  constructor(
    message: string,
    public status: number,
    public data?: unknown
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

export function extractErrorMessage(data: unknown): string {
  if (typeof data === 'string') return data;
  if (typeof data === 'object' && data !== null) {
    const obj = data as Record<string, unknown>;
    if (typeof obj.detail === 'string') return obj.detail;
    if (typeof obj.message === 'string') return obj.message;
    if (typeof obj.error === 'string') return obj.error;
  }
  return 'An error occurred';
}

2. Type Definitions

// lib/fetch/types.ts
export interface FetchOptions {
  headers?: Record<string, string>;
  timeout?: number;
}

export interface FetchRequestOptions extends FetchOptions {
  method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
  body?: unknown;
}

3. Client Fetch (Browser)

// lib/fetch/client.ts
"use client";

import { AuthService } from '@/lib/auth/auth-service';
import { ApiError, extractErrorMessage } from './errors';
import type { FetchOptions, FetchRequestOptions } from './types';

const DEFAULT_TIMEOUT = 30000;
const MAX_RETRIES = 2;

async function clientFetch<T>(
  url: string,
  options: FetchRequestOptions = {},
  attempt = 1,
  isRetryAfterRefresh = false
): Promise<T> {
  const controller = new AbortController();
  const timeoutId = setTimeout(
    () => controller.abort(),
    options.timeout || DEFAULT_TIMEOUT
  );

  try {
    const response = await fetch(url, {
      method: options.method || 'GET',
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      },
      body: options.body ? JSON.stringify(options.body) : undefined,
      signal: controller.signal,
      credentials: 'include',  // Include cookies
    });

    const data = await response.json().catch(() => ({}));

    if (!response.ok) {
      // Handle 401 - try token refresh
      if (response.status === 401 && !isRetryAfterRefresh) {
        clearTimeout(timeoutId);
        const newToken = await AuthService.refreshAccessToken();
        if (newToken) {
          return clientFetch<T>(url, options, attempt, true);
        }
        window.location.href = '/login';
        throw new ApiError('Session expired', 401);
      }

      // Retry on 429/503
      if ((response.status === 429 || response.status === 503) && attempt < MAX_RETRIES) {
        clearTimeout(timeoutId);
        await new Promise(r => setTimeout(r, 1000 * attempt));
        return clientFetch<T>(url, options, attempt + 1);
      }

      throw new ApiError(extractErrorMessage(data), response.status, data);
    }

    return data as T;
  } finally {
    clearTimeout(timeoutId);
  }
}

// Legacy wrapper (returns { data: T })
export const fetchClient = {
  get: async <T>(url: string, opts?: FetchOptions) => {
    const data = await clientFetch<T>(url, { ...opts, method: 'GET' });
    return { data };
  },
  post: async <T>(url: string, body?: unknown, opts?: FetchOptions) => {
    const data = await clientFetch<T>(url, { ...opts, method: 'POST', body });
    return { data };
  },
  put: async <T>(url: string, body?: unknown, opts?: FetchOptions) => {
    const data = await clientFetch<T>(url, { ...opts, method: 'PUT', body });
    return { data };
  },
  delete: async <T>(url: string, opts?: FetchOptions) => {
    const data = await clientFetch<T>(url, { ...opts, method: 'DELETE' });
    return { data };
  },
};

4. Server Fetch (Actions & Routes)

// lib/fetch/server.ts
"use server";

import { cookies, headers } from 'next/headers';
import { ApiError, extractErrorMessage } from './errors';
import type { FetchRequestOptions } from './types';

// Server → Next.js API routes
export async function serverFetch<T>(
  url: string,
  options: FetchRequestOptions = {}
): Promise<T> {
  const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
  const cookieStore = await cookies();
  const cookieHeader = cookieStore.getAll().map(c => `${c.name}=${c.value}`).join('; ');

  const response = await fetch(`${baseUrl}${url}`, {
    method: options.method || 'GET',
    headers: {
      'Content-Type': 'application/json',
      ...(cookieHeader && { Cookie: cookieHeader }),
      ...options.headers,
    },
    body: options.body ? JSON.stringify(options.body) : undefined,
  });

  const data = await response.json().catch(() => ({}));
  if (!response.ok) {
    throw new ApiError(extractErrorMessage(data), response.status, data);
  }
  return data as T;
}

// API routes → FastAPI backend
export async function backendFetch<T>(
  url: string,
  token: string,
  options: FetchRequestOptions = {}
): Promise<T> {
  const baseUrl = process.env.NEXT_PUBLIC_BACKEND_API_URL || 'http://localhost:8000';

  const response = await fetch(`${baseUrl}${url}`, {
    method: options.method || 'GET',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`,
      ...options.headers,
    },
    body: options.body ? JSON.stringify(options.body) : undefined,
  });

  const data = await response.json().catch(() => ({}));
  if (!response.ok) {
    throw new ApiError(extractErrorMessage(data), response.status, data);
  }
  return data as T;
}

// Convenience methods
export const serverGet = <T>(url: string) => serverFetch<T>(url, { method: 'GET' });
export const serverPost = <T>(url: string, body: unknown) => serverFetch<T>(url, { method: 'POST', body });
export const serverPut = <T>(url: string, body: unknown) => serverFetch<T>(url, { method: 'PUT', body });
export const serverDelete = <T>(url: string) => serverFetch<T>(url, { method: 'DELETE' });

5. API Route Helper

// lib/fetch/api-route-helper.ts
import { NextResponse } from 'next/server';
import { auth } from '@/lib/auth/server-auth';
import { backendFetch } from './server';
import { ApiError } from './errors';

export async function withAuth<T>(
  handler: (token: string) => Promise<T>
): Promise<NextResponse> {
  try {
    const session = await auth();
    if (!session?.accessToken) {
      return NextResponse.json({ detail: 'Unauthorized' }, { status: 401 });
    }
    const data = await handler(session.accessToken);
    return NextResponse.json(data);
  } catch (error) {
    if (error instanceof ApiError) {
      return NextResponse.json({ detail: error.message }, { status: error.status });
    }
    return NextResponse.json({ detail: 'Internal server error' }, { status: 500 });
  }
}

export const backendGet = <T>(url: string, token: string) =>
  backendFetch<T>(url, token, { method: 'GET' });
export const backendPost = <T>(url: string, token: string, body: unknown) =>
  backendFetch<T>(url, token, { method: 'POST', body });
export const backendPut = <T>(url: string, token: string, body: unknown) =>
  backendFetch<T>(url, token, { method: 'PUT', body });
export const backendDelete = <T>(url: string, token: string) =>
  backendFetch<T>(url, token, { method: 'DELETE' });

Request Flow

Client-Side (Mutations)

Component → fetchClient → API Route → withAuth → backendFetch → FastAPI

Server-Side (SSR)

Page → Server Action → serverFetch → API Route → withAuth → backendFetch → FastAPI

SWR (Data Fetching)

useSWR(url, fetcher) → fetchClient.get → API Route → withAuth → backendFetch → FastAPI

Key Patterns

  1. Client includes cookies - credentials: 'include'
  2. Server forwards cookies - Cookie header to API routes
  3. API routes use Bearer token - Extract from session
  4. Auto token refresh - On 401, try refresh once
  5. Consistent error format - ApiError class
  6. Retry on rate limit - 429/503 with backoff
Related skills