← スキル一覧に戻る
fetch-architecture
adelabdelgawad / support-center
⭐ 0🍴 0📅 2026年1月18日
SKILL.md
# 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
```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>;
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
```typescript
// 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)
```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 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)
```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>(
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
```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>(
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