Client and server-side fetch utilities for Next.js applications with API route proxying to FastAPI backends.
Sets up fetch utilities for Next.js with API route proxying to FastAPI backends.
/plugin marketplace add adelabdelgawad/fullstack-agents/plugin install adelabdelgawad-fullstack-agents-plugins-fullstack-agents@adelabdelgawad/fullstack-agentsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
examples.mdreferences/api-route-helper-pattern.mdreferences/client-fetch-pattern.mdreferences/server-fetch-pattern.mdscripts/helper.pyClient and server-side fetch utilities for Next.js applications with API route proxying to FastAPI backends.
Use this skill when asked to:
┌─────────────────────────────────────────────────────────────┐
│ 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/... │
└─────────────────────────────────────────────────────────────┘
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)
// 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';
}
// 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;
}
// 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 };
},
};
// 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' });
// 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' });
Component → fetchClient → API Route → withAuth → backendFetch → FastAPI
Page → Server Action → serverFetch → API Route → withAuth → backendFetch → FastAPI
useSWR(url, fetcher) → fetchClient.get → API Route → withAuth → backendFetch → FastAPI
credentials: 'include'This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.