All docs
2 min read

Next.js recipe

Three ways to wire a Formspring endpoint into a Next.js project.

App Router — server action

// app/contact/page.tsx
'use server';

async function submit(formData: FormData) {
  await fetch(process.env.FORMSPRING_URL!, {
    method: 'POST',
    body: formData,
  });
}

export default function Contact() {
  return (
    <form action={submit}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button type="submit">Send</button>
    </form>
  );
}

Pages Router — direct POST

// pages/contact.tsx
export default function Contact() {
  return (
    <form action={process.env.NEXT_PUBLIC_FORMSPRING_URL} method="POST">
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button type="submit">Send</button>
    </form>
  );
}

Route handler that proxies + adds metadata

// app/api/contact/route.ts
export async function POST(request: Request) {
  const body = await request.json();
  const enriched = { ...body, _source: 'website-contact' };

  const r = await fetch(process.env.FORMSPRING_URL!, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(enriched),
  });

  return Response.json(await r.json(), { status: r.status });
}

Verify a webhook in a route handler

// app/api/formspring-webhook/route.ts
import { createHmac, timingSafeEqual } from 'node:crypto';

export async function POST(request: Request) {
  const sig = request.headers.get('x-formspring-signature') ?? '';
  const raw = await request.text();
  const expected = createHmac('sha256', process.env.WEBHOOK_SECRET!)
    .update(raw)
    .digest('hex');

  if (sig.length !== expected.length || !timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return new Response('unauthorized', { status: 401 });
  }

  const body = JSON.parse(raw);
  // …handle the submission
  return new Response('ok');
}

What's next