npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@captigo/nextjs

v0.2.0

Published

Next.js App Router helpers for Captigo — request parsing, client IP, and route-handler verification

Readme

@captigo/nextjs

Next.js App Router helpers for Captigo — parse Request, resolve client IP, verify with any CaptchaAdapter.

Uses the Web Request API only (no runtime dependency on next). Pair with @captigo/react and an adapter such as @captigo/turnstile on the client.


Installation

npm install @captigo/nextjs @captigo/turnstile

@captigo/core is installed transitively. next ≥ 14 is an optional peer (declared for version clarity; helpers work anywhere Request is available).


App Router — route handler

The most common pattern: a POST endpoint that receives the token from a form and verifies it before taking any action.

// app/api/submit/route.ts
import { verifyCaptchaFromRequest, CaptchaError } from "@captigo/nextjs";
import { turnstile } from "@captigo/turnstile";

const adapter = turnstile({ siteKey: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY! });

export async function POST(request: Request) {
  let result;
  try {
    result = await verifyCaptchaFromRequest(
      request,
      adapter,
      process.env.TURNSTILE_SECRET!,
    );
  } catch (err) {
    if (err instanceof CaptchaError) {
      // Token was missing from the body, or the provider network call failed.
      return Response.json({ error: err.message }, { status: 400 });
    }
    throw err;
  }

  if (!result.success) {
    return Response.json({ error: "CAPTCHA verification failed" }, { status: 400 });
  }

  // Token is verified — proceed with the protected action.
  return Response.json({ ok: true });
}

Note on field names. verifyCaptchaFromRequest looks for a "token" field by default. If your form uses a different name (e.g. "cf-turnstile-response" from Turnstile's auto-injected hidden input), pass it via options.fieldName.


Client-side (App Router)

Use @captigo/react and @captigo/turnstile on the client as usual. No changes are needed — @captigo/nextjs is a server-only package.

// app/contact/page.tsx
"use client";
import { Captcha } from "@captigo/react";
import { turnstile } from "@captigo/turnstile";
import { useState } from "react";

const adapter = turnstile({ siteKey: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY! });

export default function ContactPage() {
  const [token, setToken] = useState<string | null>(null);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    if (!token) return;

    await fetch("/api/submit", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ token, message: "Hello" }),
    });
  }

  return (
    <form onSubmit={handleSubmit}>
      <Captcha adapter={adapter} onSuccess={(t) => setToken(t.value)} />
      <button type="submit" disabled={!token}>Send</button>
    </form>
  );
}

API

verifyCaptchaFromRequest(request, adapter, secretKey, options?)

Extracts the CAPTCHA token from the request body, resolves the client IP from standard headers, and calls adapter.verify().

import { verifyCaptchaFromRequest } from "@captigo/nextjs";

const result = await verifyCaptchaFromRequest(request, adapter, secretKey, {
  fieldName: "token",   // default — the body field that holds the token
  forwardIp: true,      // default — passes client IP to the provider
});

Throws CaptchaError when:

  • The token field is absent from the request body.
  • The provider's network request fails.

Returns VerifyResult — check result.success to decide whether to accept the submission.


captchaTokenFromRequest(request, fieldName?)

Extracts the raw token string from a request body. Tries JSON first, then FormData/URL-encoded. Returns null when the field is absent or empty — never throws on parse failure.

import { captchaTokenFromRequest } from "@captigo/nextjs";

// Use this when you need the token separately before calling adapter.verify().
const token = await captchaTokenFromRequest(request, "cf-turnstile-response");
if (!token) {
  return Response.json({ error: "Missing CAPTCHA token" }, { status: 400 });
}

const result = await adapter.verify(token, process.env.TURNSTILE_SECRET!);

clientIpFromRequest(request)

Resolves the client IP from standard CDN and proxy headers. Checks in order:

  1. CF-Connecting-IP (Cloudflare)
  2. X-Forwarded-For (first address in the list)
  3. X-Real-IP (nginx)

Returns undefined when none are present.

import { clientIpFromRequest } from "@captigo/nextjs";

const ip = clientIpFromRequest(request);
const result = await adapter.verify(token, secretKey, ip ? { remoteip: ip } : undefined);

Server Actions

verifyCaptchaFromRequest expects a Request object. Server Actions receive form data directly, not a Request, so use captchaTokenFromRequest with the raw FormData instead — or verify via a route handler.

// app/actions.ts
"use server";
import { CaptchaError } from "@captigo/nextjs";
import { turnstile } from "@captigo/turnstile";

const adapter = turnstile({ siteKey: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY! });

export async function submitAction(formData: FormData) {
  const token = formData.get("token");
  if (typeof token !== "string" || !token) {
    throw new Error("Missing CAPTCHA token");
  }

  const result = await adapter.verify(token, process.env.TURNSTILE_SECRET!);
  if (!result.success) {
    throw new Error("CAPTCHA verification failed");
  }

  // ... proceed
}

Notes

  • No next runtime dependency. The helpers work with the standard Web Request API (available natively in Next.js App Router and Node.js 18+).
  • Server-only. Never call adapter.verify() or expose your secret key on the client.
  • Provider-agnostic. Pass any captigo adapter — turnstile(config), hcaptcha(config), recaptchaV2(config), etc.

Documentation

@captigo/react · Repository · Issues