Skip to main content

API Routes

Overview

API routes (app/api/**/route.ts) are used for:

  1. External service integrations that require credential proxying (Directus, Redis).
  2. Cache invalidation that the Edge Runtime cannot perform (revalidateTag).
  3. Heavy backend operations that should not run inline in a server component (chart generation).

Mutations driven by forms generally use Server Actions instead.

Route inventory

RouteMethodPurpose
/api/essearchPOSTVoter / person search via Directus, with per-request credentials
/api/essearch-commonPOSTSame as above with fixed env-based credentials
/api/duplicatePOSTPer-campaign duplicate-signer check via Redis
/api/charts/[id]POSTGenerate and persist a chart's data using a service-role Supabase client
/api/permissions/revalidatePOSTDrop the cached permissions snapshot for the currently signed-in user

See API Route Handlers for end-to-end implementation details.

Common pattern

// app/api/<feature>/route.ts
import { NextRequest, NextResponse } from "next/server";

export const runtime = "nodejs"; // most routes use the Node runtime (Redis / service-role)
export const dynamic = "force-dynamic"; // when you need fresh execution per request

export async function POST(req: NextRequest) {
try {
const body = await req.json();
// ...do work
return NextResponse.json({ ok: true });
} catch (err) {
return NextResponse.json(
{ error: (err as Error).message },
{ status: 500 },
);
}
}

Voter search APIs

/api/essearch

  • Accepts both query params and a credentials: { name, emailKey, endpointKey, passwordKey } block.
  • Reads Directus credentials from process.env[<key>] per request, falling back to the common envs.
  • Caches the Directus access token in an HTTP-only cookie (1-hour TTL, sameSite: strict).
  • Retries once after a 401 by deleting the cached cookie and re-authenticating.
  • Returns { persons, hitsCount, totalHits }.

/api/essearch-common

Same logic but reads DIRECTUS_API_EMAIL_SEARCH, DIRECTUS_API_PASSWORD_SEARCH, DIRECTUS_URL_SEARCH and uses a fixed cookie name (directus_token_search).

Duplicate detection

/api/duplicate performs a single Redis SISMEMBER against campaign:{campaignId}:signers:

const exists = await client.sIsMember(`campaign:${campaignId}:signers`, voterId);
return NextResponse.json({ exists: Boolean(exists) });

The Redis singleton (src/lib/redis.ts) lazily connects to Upstash with TLS. On Connection is closed / ECONNRESET, the route resets the singleton and returns 503 so the caller can retry.

Charts API

/api/charts/[id] loads the chart definition from charts.id == :id, runs generateChartData(config), and persists the result back. Uses createSpecialClient(SERVICE_ROLE_KEY) — must remain server-only. There is also an equivalent Supabase Edge Function (generate-chart-data) used by the refresh-all-charts job.

Permissions revalidation

/api/permissions/revalidate calls:

revalidateTag(`permissions-${user.id}`);

The user id is read from the session (never from the request body), so a client cannot invalidate someone else's cache.

Choosing API route vs Server Action

NeedUse
Form submission with progressive enhancementServer Action
Strongly typed, in-tree mutationServer Action
External service that needs credentials proxiedAPI Route
revalidateTag from outside RSC (e.g. middleware)API Route
Service-role / heavy jobAPI Route