Guides/Working with APIs

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#