@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-formsEl 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=xxxxxUso 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.
