Skip to content
Fetch Architecture Skill logo

Fetch Architecture Skill

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

SKILL.md

Full skill instructions

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

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View