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

@sergiogdev/landing-forms

v0.1.0

Published

Cliente y proxy server-side para el backend de formularios de contacto de sergiogdev, con honeypot anti-spam integrado.

Readme

@sergiogdev/landing-forms

Cliente y proxy server-side para el backend propio de formularios de contacto (NestJS + Prisma, multi-tenant). Centraliza el fetch, el manejo de errores y el honeypot anti-spam que hasta ahora se reimplementaba a mano en cada landing.

Regla de seguridad: la x-api-key del backend nunca debe llegar al navegador. Cada landing necesita un endpoint propio server-side que actúe de proxy (esta librería te da la pieza que arma ese proxy).

Instalación

npm install @sergiogdev/landing-forms

El paquete expone dos entrypoints:

  • @sergiogdev/landing-forms — core, sin dependencias de framework.
  • @sergiogdev/landing-forms/next — adaptador para Next.js App Router (route handler + componente React del honeypot).

Un adaptador de Astro llegará más adelante; el core ya está preparado para ello (no depende de React ni de Next).

Variables de entorno recomendadas

Cada landing que consuma la librería define, del lado del servidor:

SERGIOGDEV_FORMS_API_KEY=xxxxx   # x-api-key del tenant, nunca en el cliente
SERGIOGDEV_FORMS_API_URL=https://api.sergiogdev.com
SERGIOGDEV_FORMS_FORM_ID=xxxxx

Uso con Next.js (App Router)

1. Proxy server-side

// app/api/contact/route.ts
import { createNextFormsRouteHandler } from "@sergiogdev/landing-forms/next";

export const { POST } = createNextFormsRouteHandler({
  apiBaseUrl: process.env.SERGIOGDEV_FORMS_API_URL!,
  formId: process.env.SERGIOGDEV_FORMS_FORM_ID!,
  apiKey: process.env.SERGIOGDEV_FORMS_API_KEY!,
});

Este handler valida el body (email requerido/formato, límites de longitud, tamaño de payload), añade la x-api-key y reenvía Origin/Referer de la request entrante al backend — necesario para que la verificación de dominio del backend tenga efecto.

2. Formulario cliente

"use client";
import { useState } from "react";
import { submitForm } from "@sergiogdev/landing-forms";
import { HoneypotField } from "@sergiogdev/landing-forms/next";

export function ContactForm() {
  const [result, setResult] = useState<string | null>(null);

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const form = new FormData(e.currentTarget);
    const res = await submitForm(
      {
        email: String(form.get("email")),
        name: String(form.get("name") ?? ""),
        message: String(form.get("message") ?? ""),
        department: String(form.get("department") ?? ""), // valor del honeypot
      },
      { proxyUrl: "/api/contact" }
    );

    switch (res.status) {
      case "accepted":
        setResult("¡Gracias! Te responderemos pronto.");
        break;
      case "rate-limited":
        setResult(`Demasiados envíos, inténtalo en ${res.retryAfterSeconds ?? 60}s.`);
        break;
      case "invalid":
        setResult("Revisa los datos del formulario.");
        break;
      default:
        setResult("Algo ha fallado, inténtalo más tarde.");
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      <input name="name" />
      <textarea name="message" />
      <HoneypotField />
      <button type="submit">Enviar</button>
    </form>
  );
}

submitForm completa automáticamente sourceUrl, referrer y los UTMs (utm_source/utm_medium/utm_campaign de la query string) a partir de window.location/document.referrer cuando corre en el navegador y no se pasan explícitamente.

Resultado de submitForm

type SubmitFormResult =
  | { status: "accepted"; submissionId?: string }   // éxito real u honeypot disparado (indistinguibles a propósito)
  | { status: "invalid"; messages: string[] }        // 400
  | { status: "forbidden" }                          // 401/403
  | { status: "not-found" }                          // 404
  | { status: "rate-limited"; retryAfterSeconds?: number } // 429
  | { status: "network-error"; error: unknown };

Un honeypot disparado se responde con 201 sin submissionId, exactamente igual que un envío real desde el punto de vista del cliente — no hay (ni debe haber) forma de diferenciarlos del lado del navegador.

Honeypot

El campo department debe ser un <input> real (nunca type="hidden"), oculto con position: absolute; left: -9999px (nunca display:none ni visibility:hidden, que algunos bots detectan vía getComputedStyle), y con tabindex="-1", autocomplete="off" y aria-hidden="true" para no afectar a usuarios legítimos. HoneypotField (adaptador Next.js) ya lo renderiza así; si integras otro framework, usa honeypotFieldAttributes/honeypotFieldStyle exportados por el core.

Core (sin framework)

import {
  submitForm,
  createFormsProxyHandler,
  validateSubmitFormInput,
  honeypotFieldAttributes,
  honeypotFieldStyle,
} from "@sergiogdev/landing-forms";

createFormsProxyHandler(config) recibe y devuelve datos planos (PlainRequest/PlainResponse), sin nada de framework — es lo que createNextFormsRouteHandler envuelve por dentro, y lo mismo haría un futuro adaptador de Astro.

Fuera de alcance (esta versión)

  • Adaptador de Astro.
  • Notificaciones/mailing propios (el backend ya envía email al tenant).
  • Panel de administración.
  • Captcha/Turnstile.