How to Work with APIs in Frontend Dashboards
REST vs GraphQL, error handling, caching, rate limits, data transformation, and TypeScript types. Practical API patterns for React and Next.js dashboard development.
API fetching fundamentals for dashboards
Dashboards are API consumers first, UI renderers second. Every panel — balances, charts, tables — depends on reliable data fetching. The patterns you establish in the data layer determine whether your dashboard feels instant or broken.
REST vs GraphQL
**REST** — simple endpoints, easy caching, works well with Next.js API routes. Best for straightforward CRUD and aggregated dashboard endpoints.
**GraphQL** — flexible queries, single endpoint, ideal for complex data relationships. The Graph is the primary GraphQL source in Web3 for indexed on-chain events.
// REST: simple dashboard endpoint
// app/api/dashboard/[address]/route.ts
export async function GET(_: Request, { params }: { params: { address: string } }) {
const [balances, transactions, prices] = await Promise.all([
fetchBalances(params.address),
fetchRecentTransactions(params.address),
fetchPrices(),
]);
return Response.json({ balances, transactions, prices });
}// GraphQL: The Graph for indexed events
const SWAPS_QUERY = `
query GetSwaps($pool: String!, $first: Int!) {
swaps(where: { pool: $pool }, orderBy: timestamp, orderDirection: desc, first: $first) {
id timestamp amount0In amount1Out sender
}
}
`;
async function fetchSwaps(pool: string) {
const res = await fetch(THE_GRAPH_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: SWAPS_QUERY, variables: { pool, first: 50 } }),
});
const { data, errors } = await res.json();
if (errors) throw new GraphQLError(errors[0].message);
return data.swaps;
}Error handling
Wrap every API call with typed error handling. Distinguish network errors, rate limits, server errors, and validation failures.
// lib/api-client.ts
export class ApiError extends Error {
constructor(public status: number, message: string, public code?: string) {
super(message);
this.name = 'ApiError';
}
}
async function apiRequest<T>(url: string, options?: RequestInit): Promise<T> {
try {
const res = await fetch(url, options);
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new ApiError(res.status, body.message ?? res.statusText, body.code);
}
return res.json();
} catch (error) {
if (error instanceof ApiError) throw error;
throw new ApiError(0, 'Network error. Check your connection.');
}
}Loading states and caching
Use TanStack Query for all client-side API data. Configure staleTime based on data freshness requirements.
export function useDashboardData(address: string) {
return useQuery({
queryKey: ['dashboard', address],
queryFn: () => apiRequest<DashboardData>(`/api/dashboard/${address}`),
staleTime: 30_000,
refetchInterval: 60_000,
retry: (failureCount, error) => {
if (error instanceof ApiError && error.status === 429) return false;
return failureCount < 3;
},
});
}Rate limits
Respect upstream rate limits. Cache responses at the API route level. Implement client-side request deduplication via React Query. Add exponential backoff on 429 responses.
// app/api/prices/route.ts
export async function GET(request: Request) {
const ids = new URL(request.url).searchParams.get('ids')?.split(',') ?? [];
const data = await fetchCoinGeckoPrices(ids);
return Response.json(data, {
headers: { 'Cache-Control': 's-maxage=30, stale-while-revalidate=60' },
});
}Data transformation
Transform API responses at the boundary, not in components. Convert raw on-chain values to human-readable numbers in the service layer.
// lib/transformers.ts
import { formatUnits } from 'viem';
export function transformBalance(raw: bigint, decimals: number, price: number) {
const amount = parseFloat(formatUnits(raw, decimals));
return {
amount,
formatted: amount.toLocaleString(undefined, { maximumFractionDigits: 6 }),
usdValue: amount * price,
usdFormatted: `$${(amount * price).toLocaleString(undefined, { minimumFractionDigits: 2 })}`,
};
}TypeScript types
Define response types for every API endpoint. Never use any for API data.
// types/api.ts
export interface DashboardData {
balances: TokenBalance[];
transactions: Transaction[];
totalUsd: number;
lastUpdated: string;
}
export interface TokenBalance {
address: string;
symbol: string;
amount: number;
usdValue: number;
priceChange24h: number;
}
export interface Transaction {
hash: string;
type: 'send' | 'receive' | 'swap' | 'approve';
amount: number;
token: string;
timestamp: number;
status: 'confirmed' | 'pending' | 'failed';
}FAQ
**Should I call APIs from components or hooks?** Always hooks or server components. Never call APIs directly in presentation components.
**How do I handle API keys?** Server-side only. Proxy through Next.js API routes. Never expose keys with NEXT_PUBLIC_ prefix.
**REST or GraphQL for a new dashboard?** REST for your own API layer. GraphQL (The Graph) for indexed on-chain events. Use both where each excels.
**How do I test API integrations?** Mock API responses in tests. Use MSW (Mock Service Worker) for integration tests. Test error paths, not just happy paths.
Want to work together? I build Web3 dashboards and DeFi interfaces.