name: add-api-route
updated: 2026-02-10
description: Create Next.js API routes for backend proxies, server-side aggregation, or third-party API integrations with caching and error handling.
argument-hint: [routePath] [backendEndpoint]
allowed-tools: Read, Edit, Write, Glob
Add API Route
Create a new Next.js API route that proxies to the backend with proper authentication and error handling.
Arguments
$0 - Route path relative to pages/api (e.g., voters/stats creates pages/api/voters/stats.ts)
$1 - Backend endpoint path (e.g., /api/voters/statistics)
Instructions
Step 1: Create API Route
Create file at src/pages/api/${$0}.ts (or src/pages/api/${$0}/index.ts for directory routes):
import type { NextApiRequest, NextApiResponse } from "next";
import { callApi } from "@/utils/apiHelper";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "GET") {
return res.status(405).json({ error: "Method not allowed" });
}
try {
const response = await callApi({
endpoint: "${$1}",
method: "GET",
});
const data = await response.json();
res.setHeader("Cache-Control", "public, s-maxage=60, stale-while-revalidate=300");
return res.status(response.status).json(data);
} catch (error) {
console.error("${$0} API error:", error);
return res.status(500).json({ error: "Failed to fetch data" });
}
}
Step 2: For Dynamic Routes
If the route has parameters (e.g., voters/[id].ts):
import type { NextApiRequest, NextApiResponse } from "next";
import { callApi } from "@/utils/apiHelper";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "GET") {
return res.status(405).json({ error: "Method not allowed" });
}
const { id } = req.query;
if (!id || typeof id !== "string") {
return res.status(400).json({ error: "Invalid ID parameter" });
}
try {
const response = await callApi({
endpoint: `/api/voters/${id}`,
method: "GET",
});
const data = await response.json();
res.setHeader(, );
res.(response.).(data);
} (error) {
.(, error);
res.().({ : });
}
}
Step 3: Add Service Function (Optional)
If this data will be used by components, add a service function in src/services/api.ts:
export async function fetch${PascalCaseName}(): Promise<${TypeName}> {
return fetchApi<${TypeName}>("/api/${$0}");
}
Cache Header Guidelines
Adjust s-maxage based on data freshness needs:
- 60s - Standard for most governance data
- 300s - For slowly changing data (NCL, historical)
- 10s - For frequently updated data (live votes)
- 3600s - For immutable blockchain data (tx timestamps, block hashes) — add
stale-while-revalidate=86400
File Structure Examples
| Route | Creates |
|---|
voters | src/pages/api/voters.ts |
voters/stats | src/pages/api/voters/stats.ts |
voters/[id] | src/pages/api/voters/[id].ts |
proposals/[hash]/votes | src/pages/api/proposals/[hash]/votes.ts |
After Creation
- Test the endpoint with
curl http://localhost:3000/api/${$0}
- Check backend connectivity
- Verify caching headers in browser DevTools
Server-Side Aggregation Pattern
When a frontend chart needs data from N+1 API calls (list all items, then fetch detail for each), move the aggregation server-side. This prevents hundreds of client-side calls and lets you cache the result for all users.
When to use: Any chart that would otherwise make O(N) detail fetches from the browser.
Create Aggregation Endpoint
import type { NextApiRequest, NextApiResponse } from "next";
import { callApi } from "@/utils/apiHelper";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "GET") return res.status(405).json({ error: "Method not allowed" });
try {
const pageSize = 100;
const firstRes = await callApi({ endpoint: `/items?page=1&pageSize=${pageSize}`, method: "GET" });
const firstData = await firstRes.json();
const allItems = [...firstData.items];
if (firstData.pagination.totalPages > 1) {
const remaining = Array.from({ : firstData.. - }, i + );
pages = .(
remaining.( (pg) => {
r = ({ : , : });
( r.()).;
})
);
( page pages) allItems.(...page);
}
batchSize = ;
results = [];
( i = ; i < allItems.; i += batchSize) {
batch = allItems.(i, i + batchSize);
details = .(
batch.( (item) => {
{
r = ({ : , : });
d = r.();
{ : item., };
} {
{ : item., };
}
})
);
results.(...details);
}
res.(, );
res.().({ : results });
} (error) {
.(, error);
res.().({ : });
}
}
Pair with SWR Hook
export function useAggregatedData() {
const { data, error, isLoading, mutate } = useSWR(
"/api/items/aggregated", fetcher,
{ revalidateOnFocus: false, dedupingInterval: 300000 }
);
return { items: data?.items || [], isLoading, error: error?.message || null, refresh: () => mutate() };
}
Cache Duration Guidelines for Aggregation
| Data Type | s-maxage | stale-while-revalidate |
|---|
| Expensive aggregation (100+ backend calls) | 300s | 600s |
| Moderate aggregation (10-50 calls) | 120s | 300s |
| Simple proxy | 60s | 300s |
Third-Party API Integration (Alternative Pattern)
For routes that call external APIs (not the backend), use this pattern with server-side caching:
Step 1: Create Route with In-Memory Cache
import type { NextApiRequest, NextApiResponse } from "next";
const EXTERNAL_API_URL = "https://api.example.com/v1/endpoint";
const serverCache = new Map<string, string>();
const inFlightRequests = new Map<string, Promise<string>>();
function hashKey(input: string): string {
let hash = 0;
for (let i = 0; i < input.length; i++) {
hash = ((hash << 5) - hash) + input.charCodeAt(i);
hash = hash & hash;
}
return String(Math.abs(hash));
}
function pruneCache() {
if (serverCache.size > 1000) {
const keysToDelete = .(serverCache.()).(, );
keysToDelete.( serverCache.(key));
}
}
() {
(req. !== ) {
res.().({ : });
}
apiKey = process..;
(!apiKey) {
res.().({ : });
}
{ input } = req.;
cacheKey = (input);
cached = serverCache.(cacheKey);
(cached) {
res.().({ : cached, : });
}
inFlight = inFlightRequests.(cacheKey);
(inFlight) {
{
result = inFlight;
res.().({ result, : });
} {
res.().({ : });
}
}
apiPromise = (input, apiKey);
inFlightRequests.(cacheKey, apiPromise);
{
result = apiPromise;
serverCache.(cacheKey, result);
();
inFlightRequests.(cacheKey);
res.().({ result });
} (error) {
inFlightRequests.(cacheKey);
.(, error);
res.().({ : });
}
}
(): <> {
response = (, {
: ,
: {
: ,
: ,
},
: .({ input }),
});
(!response.) {
();
}
data = response.();
data.;
}
Key Benefits of This Pattern
| Feature | Benefit |
|---|
| Server-side cache | All users share same cache (1 API call per unique input) |
| In-flight deduplication | Parallel requests for same input share one API call |
| Cache pruning | Prevents memory issues on long-running servers |
| Env var for API key | Keeps secrets server-side |
When to Use This Pattern
- External translation APIs (DeepL, Google Translate)
- AI/ML inference APIs
- Rate-limited third-party services
- Any API where multiple users might request the same data