Verificación de email con Clerk
Clerk admite webhooks para los eventos del ciclo de vida del usuario, incluido user.created. Clerk no tiene un bloqueo nativo previo a la creación, pero puedes combinar un webhook con los metadatos de usuario de Clerk para marcar los registros inválidos y borrarlos de inmediato, o usar el patrón de redirección previa al registro.
Este tutorial usa el patrón recomendado: verificar el email en un endpoint del servidor antes de llamar a signUp.create de Clerk.
Requisitos previos
- Una cuenta de Clerk y un proyecto de Next.js con
@clerk/nextjs - Una clave de API de Mailbeam (regístrate gratis)
Preparación
npm install svix # svix es la biblioteca de webhooks de ClerkEndpoint de verificación en el servidor
// app/api/verify-email/route.ts
import { NextResponse } from "next/server";
// Todavía no hay SDK: este es el envoltorio de 12 líneas de /docs/quickstart.
import { verifyEmail } from "./lib/mailbeam";
export async function POST(req: Request) {
const { email } = await req.json();
if (!email) {
return NextResponse.json({ error: "falta el email" }, { status: 400 });
}
try {
const { valid, score, reason } = await verifyEmail(email);
return NextResponse.json({ valid, score, reason });
} catch {
// Falla en abierto
return NextResponse.json({ valid: true, score: 50, reason: null });
}
}Formulario de registro propio
// app/signup/CustomSignupForm.tsx
"use client";
import { useSignUp } from "@clerk/nextjs";
import { useState } from "react";
export function CustomSignupForm() {
const { signUp, setActive } = useSignUp();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
setError(null);
// Paso 1: verifica el email en el servidor antes de llamar a Clerk
const verifyRes = await fetch("/api/verify-email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
const { valid, score, reason } = await verifyRes.json();
if (!valid || score < 60) {
setError(
reason === "disposable_domain"
? "Usa una dirección de email permanente."
: "Escribe una dirección de email válida."
);
setLoading(false);
return;
}
// Paso 2: sigue con el registro en Clerk
try {
const result = await signUp!.create({ emailAddress: email, password });
if (result.status === "complete") {
await setActive!({ session: result.createdSessionId });
window.location.href = "/dashboard";
}
} catch (err: unknown) {
setError((err as { errors?: { message: string }[] })?.errors?.[0]?.message ?? "El registro ha fallado.");
} finally {
setLoading(false);
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4 max-w-sm">
{error && (
<p role="alert" className="rounded-lg bg-destructive/10 p-3 text-sm text-destructive">
{error}
</p>
)}
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Dirección de email"
className="w-full rounded-lg border border-border px-3 py-2 text-sm bg-background"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Contraseña"
className="w-full rounded-lg border border-border px-3 py-2 text-sm bg-background"
required
/>
<button
type="submit"
disabled={loading}
className="w-full rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground disabled:opacity-60"
>
{loading ? "Creando la cuenta…" : "Crear cuenta"}
</button>
</form>
);
}Buenas prácticas
- Verifica antes de llamar a Clerk, no después: evita usuarios huérfanos sin verificar
- Mantén ligero el endpoint de verificación: solo debe llamar a Mailbeam y devolver
- Usa
reasonpara dar mensajes de error concretos