Coding

Fetch Architecture Skill

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

promptBeginner5 min to valuemarkdown
0 views
Jan 15, 2026

Sign in to like and favorite skills

Prompt Playground

1 Variables

Fill Variables

Preview

# Fetch Architecture Skill

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

## When to Use [T>]his 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                           │    │
│  └──────────────────────────┬──────────────────────────┘    │
└─────────────────────────────┼───────────────────────────────┘
                              │ H[T>][T>]P (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                 │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────┼───────────────────────────────┘
                              │ H[T>][T>]P (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              # [T>]ypeScript types
└── auth/
    ├── server-auth.ts        # Server authentication
    └── auth-service.ts       # Client auth (token refresh)
```

## Core Files

### 1. Error Classes

```typescript
// 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[T>];
    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. [T>]ype Definitions

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

export interface FetchRequestOptions extends FetchOptions {
  method?: 'GE[T>]' | 'POS[T>]' | 'PU[T>]' | 'PA[T>]CH' | 'DELE[T>]E';
  body?: unknown;
}
```

### 3. Client Fetch (Browser)

```typescript
// 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 DEFAUL[T>]_[T>]IMEOU[T>] = 30000;
const MAX_RE[T>]RIES = 2;

async function clientFetch<[T>][T>](
  url: string,
  options: FetchRequestOptions = {},
  attempt = 1,
  isRetryAfterRefresh = false
): Promise<[T>][T>] {
  const controller = new AbortController();
  const timeoutId = set[T>]imeout(
    () =[T>] controller.abort(),
    options.timeout || DEFAUL[T>]_[T>]IMEOU[T>]
  );

  try {
    const response = await fetch(url, {
      method: options.method || 'GE[T>]',
      headers: {
        'Content-[T>]ype': '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(() =[T>] ({}));

    if (!response.ok) {
      // Handle 401 - try token refresh
      if (response.status === 401 && !isRetryAfterRefresh) {
        clear[T>]imeout(timeoutId);
        const new[T>]oken = await AuthService.refreshAccess[T>]oken();
        if (new[T>]oken) {
          return clientFetch<[T>][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_RE[T>]RIES) {
        clear[T>]imeout(timeoutId);
        await new Promise(r =[T>] set[T>]imeout(r, 1000 * attempt));
        return clientFetch<[T>][T>](url, options, attempt + 1);
      }

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

    return data as [T>];
  } finally {
    clear[T>]imeout(timeoutId);
  }
}

// Legacy wrapper (returns { data: [T>] })
export const fetchClient = {
  get: async <[T>][T>](url: string, opts?: FetchOptions) =[T>] {
    const data = await clientFetch<[T>][T>](url, { ...opts, method: 'GE[T>]' });
    return { data };
  },
  post: async <[T>][T>](url: string, body?: unknown, opts?: FetchOptions) =[T>] {
    const data = await clientFetch<[T>][T>](url, { ...opts, method: 'POS[T>]', body });
    return { data };
  },
  put: async <[T>][T>](url: string, body?: unknown, opts?: FetchOptions) =[T>] {
    const data = await clientFetch<[T>][T>](url, { ...opts, method: 'PU[T>]', body });
    return { data };
  },
  delete: async <[T>][T>](url: string, opts?: FetchOptions) =[T>] {
    const data = await clientFetch<[T>][T>](url, { ...opts, method: 'DELE[T>]E' });
    return { data };
  },
};
```

### 4. Server Fetch (Actions & Routes)

```typescript
// 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>][T>](
  url: string,
  options: FetchRequestOptions = {}
): Promise<[T>][T>] {
  const baseUrl = process.env.NEX[T>]_PUBLIC_SI[T>]E_URL || 'http://localhost:3000';
  const cookieStore = await cookies();
  const cookieHeader = cookieStore.getAll().map(c =[T>] `${c.name}=${c.value}`).join('; ');

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

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

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

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

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

// Convenience methods
export const serverGet = <[T>][T>](url: string) =[T>] serverFetch<[T>][T>](url, { method: 'GE[T>]' });
export const serverPost = <[T>][T>](url: string, body: unknown) =[T>] serverFetch<[T>][T>](url, { method: 'POS[T>]', body });
export const serverPut = <[T>][T>](url: string, body: unknown) =[T>] serverFetch<[T>][T>](url, { method: 'PU[T>]', body });
export const serverDelete = <[T>][T>](url: string) =[T>] serverFetch<[T>][T>](url, { method: 'DELE[T>]E' });
```

### 5. API Route Helper

```typescript
// 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>][T>](
  handler: (token: string) =[T>] Promise<[T>][T>]
): Promise<NextResponse[T>] {
  try {
    const session = await auth();
    if (!session?.access[T>]oken) {
      return NextResponse.json({ detail: 'Unauthorized' }, { status: 401 });
    }
    const data = await handler(session.access[T>]oken);
    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>][T>](url: string, token: string) =[T>]
  backendFetch<[T>][T>](url, token, { method: 'GE[T>]' });
export const backendPost = <[T>][T>](url: string, token: string, body: unknown) =[T>]
  backendFetch<[T>][T>](url, token, { method: 'POS[T>]', body });
export const backendPut = <[T>][T>](url: string, token: string, body: unknown) =[T>]
  backendFetch<[T>][T>](url, token, { method: 'PU[T>]', body });
export const backendDelete = <[T>][T>](url: string, token: string) =[T>]
  backendFetch<[T>][T>](url, token, { method: 'DELE[T>]E' });
```

## 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
Share: