Working with APIs
Learn how to integrate external APIs into your Craft projects. This guide covers fetching data, authentication, error handling, and best practices.
API Integration Basics#
Server-Side Fetching#
Fetch data in Server Components:
// app/weather/page.tsx
async function WeatherPage() {
const response = await fetch("https://api.weather.com/current", {
headers: {
Authorization: `Bearer ${process.env.WEATHER_API_KEY}`,
},
next: { revalidate: 3600 }, // Cache for 1 hour
});
const weather = await response.json();
return <WeatherDisplay data={weather} />;
}
Client-Side Fetching#
For interactive data loading:
"use client";
import { useState, useEffect } from "react";
export function UserSearch() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!query) return;
setLoading(true);
fetch(`/api/search?q=${encodeURIComponent(query)}`)
.then((res) => res.json())
.then((data) => setResults(data))
.finally(() => setLoading(false));
}, [query]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search users..."
/>
{loading ? <Spinner /> : <ResultsList results={results} />}
</div>
);
}
API Route Proxy#
Create internal API routes to proxy external APIs:
// app/api/weather/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const city = searchParams.get("city");
const response = await fetch(`https://api.weather.com/current?city=${city}`, {
headers: {
Authorization: `Bearer ${process.env.WEATHER_API_KEY}`,
},
});
if (!response.ok) {
return Response.json(
{ error: "Weather service unavailable" },
{ status: 502 }
);
}
const data = await response.json();
return Response.json(data);
}
Benefits of Proxying#
- Keep API keys secure on the server
- Add rate limiting and caching
- Transform response data
- Handle errors consistently
Authentication Methods#
API Key Authentication#
const response = await fetch("https://api.example.com/data", {
headers: {
"X-API-Key": process.env.API_KEY,
},
});
Bearer Token#
const response = await fetch("https://api.example.com/data", {
headers: {
Authorization: `Bearer ${process.env.ACCESS_TOKEN}`,
},
});
OAuth 2.0#
// lib/oauth.ts
export async function getAccessToken() {
const response = await fetch("https://oauth.example.com/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: process.env.CLIENT_ID!,
client_secret: process.env.CLIENT_SECRET!,
}),
});
const { access_token } = await response.json();
return access_token;
}
Error Handling#
Comprehensive Error Handling#
async function fetchWithErrorHandling(url: string) {
try {
const response = await fetch(url);
if (!response.ok) {
switch (response.status) {
case 401:
throw new Error("Unauthorized - check API key");
case 403:
throw new Error("Forbidden - insufficient permissions");
case 404:
throw new Error("Resource not found");
case 429:
throw new Error("Rate limit exceeded");
default:
throw new Error(`HTTP error: ${response.status}`);
}
}
return await response.json();
} catch (error) {
if (error instanceof TypeError) {
throw new Error("Network error - check connection");
}
throw error;
}
}
User-Friendly Error Display#
function DataDisplay() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
if (error) {
return (
<div className="p-4 bg-red-50 text-red-700 rounded-lg">
<p className="font-medium">Something went wrong</p>
<p className="text-sm">{error.message}</p>
<button onClick={retry} className="mt-2 underline">
Try again
</button>
</div>
);
}
return <div>{/* Display data */}</div>;
}
Caching Strategies#
Static Data (Revalidate Periodically)#
const data = await fetch("https://api.example.com/static", {
next: { revalidate: 86400 }, // Revalidate daily
});
Dynamic Data (No Cache)#
const data = await fetch("https://api.example.com/realtime", {
cache: "no-store",
});
On-Demand Revalidation#
// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
export async function POST() {
revalidateTag("products");
return Response.json({ revalidated: true });
}
// Fetching with tags
const products = await fetch("https://api.example.com/products", {
next: { tags: ["products"] },
});
Common API Integrations#
Stripe (Payments)#
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const { priceId } = await request.json();
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
line_items: [{ price: priceId, quantity: 1 }],
mode: "subscription",
success_url: `${process.env.NEXT_PUBLIC_URL}/success`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/cancel`,
});
return Response.json({ url: session.url });
}
SendGrid (Email)#
import sgMail from "@sendgrid/mail";
sgMail.setApiKey(process.env.SENDGRID_API_KEY!);
export async function sendWelcomeEmail(email: string, name: string) {
await sgMail.send({
to: email,
from: "hello@yourapp.com",
subject: "Welcome to Our App!",
html: `<h1>Hello ${name}!</h1><p>Thanks for signing up.</p>`,
});
}
OpenAI (AI)#
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
export async function POST(request: Request) {
const { prompt } = await request.json();
const completion = await openai.chat.completions.create({
model: "gpt-4",
messages: [{ role: "user", content: prompt }],
});
return Response.json({
response: completion.choices[0].message.content,
});
}
Rate Limiting#
Implementing Rate Limits#
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "60 s"),
});
export async function GET(request: Request) {
const ip = request.headers.get("x-forwarded-for") ?? "127.0.0.1";
const { success, limit, remaining } = await ratelimit.limit(ip);
if (!success) {
return Response.json(
{ error: "Rate limit exceeded" },
{
status: 429,
headers: {
"X-RateLimit-Limit": limit.toString(),
"X-RateLimit-Remaining": remaining.toString(),
},
}
);
}
// Process request...
}
Testing APIs#
Mock Responses#
// __mocks__/api.ts
export const mockWeatherResponse = {
temperature: 72,
condition: "sunny",
humidity: 45,
};
// In tests
jest.mock("./api", () => ({
fetchWeather: jest.fn().mockResolvedValue(mockWeatherResponse),
}));
API Testing Route#
// app/api/test/route.ts (development only)
export async function GET() {
if (process.env.NODE_ENV !== "development") {
return Response.json({ error: "Not available" }, { status: 404 });
}
return Response.json({
apis: {
weather: await testWeatherAPI(),
payments: await testStripeAPI(),
},
});
}
Best Practices#
Use Environment Variables#
Never hardcode API keys:
// ❌ Bad
const API_KEY = "sk_live_abc123";
// ✅ Good
const API_KEY = process.env.API_KEY;
Handle Loading States#
Always show loading feedback:
if (loading) {
return <Skeleton className="h-40 w-full" />;
}
Validate Responses#
Don't trust external data blindly:
import { z } from "zod";
const WeatherSchema = z.object({
temperature: z.number(),
condition: z.string(),
});
const data = WeatherSchema.parse(await response.json());
Next Steps#
- Environment Variables - Secure API keys
- Database Integration - Store API data
- Best Practices - Production tips