| name | add-api-route |
| description | Create Next.js API routes for backend proxies, server-side aggregation, or third-party API integrations with caching and error handling. |
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("Cache-Control", "public, s-maxage=60, stale-while-revalidate=300");
return res.status(response.status).json(data);
} catch (error) {
console.error("Voter detail API error:", error);
return res.status(500).json({ error: "Failed to fetch voter data" });
}
}
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({ length: firstData.pagination.totalPages - 1 }, (_, i) => i + 2);
const pages = await Promise.all(
remaining.map(async (pg) => {
const r = await callApi({ endpoint: `/items?page=${pg}&pageSize=${pageSize}`, method: "GET" });
return (await r.json()).items;
})
);
for (const page of pages) allItems.push(...page);
}
const batchSize = 20;
const results = [];
for (let i = 0; i < allItems.length; i += batchSize) {
const batch = allItems.slice(i, i + batchSize);
const details = await Promise.all(
batch.map(async (item) => {
try {
const r = await callApi({ endpoint: `/items/${item.id}`, method: "GET" });
const d = await r.json();
return { id: item.id, };
} catch {
return { id: item.id, };
}
})
);
results.push(...details);
}
res.setHeader("Cache-Control", "public, s-maxage=300, stale-while-revalidate=600");
return res.status(200).json({ items: results });
} catch (error) {
console.error("Aggregation API error:", error);
return res.status(500).json({ error: "Failed to aggregate data" });
}
}
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 = Array.from(serverCache.keys()).slice(0, 200);
keysToDelete.forEach(key => serverCache.delete(key));
}
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
const apiKey = process.env.EXTERNAL_API_KEY;
if (!apiKey) {
return res.status(500).json({ error: "Service not configured" });
}
const { input } = req.body;
const cacheKey = hashKey(input);
const cached = serverCache.get(cacheKey);
if (cached) {
return res.status(200).json({ result: cached, cached: true });
}
const inFlight = inFlightRequests.get(cacheKey);
if (inFlight) {
try {
const result = await inFlight;
return res.status(200).json({ result, cached: true });
} catch {
return res.status(500).json({ error: "Request failed" });
}
}
const apiPromise = callExternalAPI(input, apiKey);
inFlightRequests.set(cacheKey, apiPromise);
try {
const result = await apiPromise;
serverCache.set(cacheKey, result);
pruneCache();
inFlightRequests.delete(cacheKey);
return res.status(200).json({ result });
} catch (error) {
inFlightRequests.delete(cacheKey);
console.error("External API error:", error);
return res.status(500).json({ error: "Service error" });
}
}
async function callExternalAPI(input: string, apiKey: string): Promise<string> {
const response = await fetch(EXTERNAL_API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ input }),
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
return data.result;
}
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