Automated summary
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
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.
Do not use this skill for projects without a backend or when you do not need API proxying or authentication token management.
Security analysis
SafeThe 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
How this analysis is madeExamples
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.Create an API route in Next.js that proxies requests to the FastAPI backend, including authentication headers.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
- Client includes cookies -
credentials: 'include' - Server forwards cookies - Cookie header to API routes
- API routes use Bearer token - Extract from session
- Auto token refresh - On 401, try refresh once
- Consistent error format - ApiError class
- Retry on rate limit - 429/503 with backoff
From the file to a first result
The download contains exactly the content displayed on this page. This copy may differ from the latest version in the source repository.
View the source and supporting filesThis download contains SKILL.md only. Any scripts, references, templates and dependencies are not included. Get the required files from the source, preserving their directory structure, before continuing.
1. Download the file
Download SKILL.mdFrom the root of a test project, save the file as .skill-downloads/fetch-architecture-for-next-js-mm5t8abr/SKILL.md, or use the command below. This folder is for inspecting the skill before installation.
Command for macOS/Linux or WSL: requires a POSIX shell and curl. On Windows without WSL, use the download button and create the folder manually.
mkdir -p '.skill-downloads/fetch-architecture-for-next-js-mm5t8abr' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/fetch-architecture-for-next-js-mm5t8abr/download' --output '.skill-downloads/fetch-architecture-for-next-js-mm5t8abr/SKILL.md.part' && mv '.skill-downloads/fetch-architecture-for-next-js-mm5t8abr/SKILL.md.part' '.skill-downloads/fetch-architecture-for-next-js-mm5t8abr/SKILL.md'2. Check prerequisites
Read SKILL.md before using it. If it references scripts or external services, inspect them and prepare the required access in your test environment.
No recognized prerequisites section in this file. This does not mean it is self-contained: libraries, tools and access requirements may be mentioned elsewhere in the content or source repository.
The platforms on this page are catalog indications. The paths below follow tool documentation; they do not prove this skill has been run in those tools. No minimum tool version is confirmed for this listing.
3. Load the skill
This file does not provide a usable name/description header for this native procedure. It remains available to download and inspect; use the manual method below.
Manual method or another tool: attach SKILL.md to a conversation, or open it in an agent that can read files. Include any required supporting resources. No universal folder is recognized by every tool.
4. Try a first task
Use a short, synthetic input: an API endpoint, a small document or an interface brief. In a new conversation, attach the file or give the agent its path, then adapt this request:
Read .skill-downloads/fetch-architecture-for-next-js-mm5t8abr/SKILL.md and apply its instructions to this example: [describe a test input]. Identify any missing files, dependencies or access. Produce the result requested by the skill without inventing data or running unreviewed scripts.Expected result: output matching the skill instructions, or a precise list of missing prerequisites. A read confirmation alone does not validate a first use. If an external resource is unavailable, that path remains untested.
React Hook Form Best Practices
Development
Master React Hook Form: avoid anti-patterns, manage subscriptions, and handle reset, dirty state, and controlled inputs correctly.
Emotion to Mantine Migration
Development
Migrate Emotion styled-components to Mantine components with style props and CSS modules. Use when converting .styled.tsx files or removing @emotion imports.
Scope Creep Detector
Development
Analyzes git diffs against a stated intent to detect scope creep, unrelated files, broad PRs, and oversized changes, then suggests keep, split, or justify actions.