Envío de formularios HTML7 min de lectura
Validación de formularios HTML en el navegador y en el servidor
Valida un formulario con required, type, pattern y min, da estilo a los errores con :user-invalid y aplica las mismas reglas en el servidor con definiciones de campos.
Usa las restricciones integradas de HTML, como required, type="email", min, max y pattern, para que el navegador bloquee la entrada incorrecta antes de enviar nada. Después aplica las mismas reglas en el servidor, porque los bots y los scripts se saltan el navegador por completo. Con Ikiform, define los campos del formulario y se comprueba cada envío: uno incorrecto recibe 422 validation_failed con un mensaje por campo, que puedes mostrar con setCustomValidity.
El ejemplo es un formulario de reserva de un taller. Tiene el tipo de campos donde la validación se gana el sueldo: un tamaño de equipo con límites, una fecha que no puede estar en el pasado, un teléfono en un formato y un código de reserva con una forma fija.
Qué cambia aquí
Hay dos validadores, y no ven las mismas peticiones. El navegador solo comprueba a las personas que usan tu página, con scripts o sin ellos. El servidor lo comprueba todo, incluidos los bots que envían directamente al endpoint y tu propio código llamando a form.submit(), que se salta las comprobaciones del navegador. El trabajo consiste en que los dos digan lo mismo.
- Una persona envía el formulario y el navegador comprueba las restricciones del HTML. Un campo no válido recibe un mensaje y no se envía nada.
- Un envío válido llega a Ikiform, que lo comprueba con la definición de campos del formulario y guarda valores tipados.
- Un bot o un script envía directamente al endpoint, así que no se ejecuta ninguna comprobación del navegador.
- Ikiform lo comprueba con la misma definición y lo rechaza con 422 antes de guardar nada.
La capa del navegador
Vista previa
Ejecuta el validador de Ikiform en tu navegador. No se envía nada.
ws-12.<form id="workshop" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
<label for="name">Your name</label>
<input id="name" name="name" autocomplete="name" maxlength="120" required />
<label for="email">Work email</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<label for="team_size">People attending</label>
<input id="team_size" name="team_size" type="number" min="2" max="20" step="1" required />
<label for="date">Preferred date</label>
<input id="date" name="date" type="date" min="2026-11-02" required />
<label for="phone">Phone</label>
<input id="phone" name="phone" type="tel" autocomplete="tel" placeholder="+14155550123"
pattern="\+[1-9][0-9]{6,14}" title="International format, like +14155550123" />
<label for="code">Booking code</label>
<input id="code" name="code" pattern="WS-[0-9]{4}" title="Codes look like WS-1234" />
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<button>Request the workshop</button>
</form>Cada atributo corresponde a un estado de validez, que puedes leer desde input.validity en la consola:
| Atributo | Estado de validez | Notas |
|---|---|---|
required | valueMissing | En un grupo de radios, un radio con required cubre todo el grupo |
type="email", type="url" | typeMismatch | Permisivo: a@b pasa como email en los navegadores |
pattern | patternMismatch | Debe coincidir con el valor completo. Se ignora en un campo opcional vacío |
minlength, maxlength | tooShort, tooLong | Solo se comprueba después de que la persona edite el valor |
min, max | rangeUnderflow, rangeOverflow | Números, fechas y horas |
step | stepMismatch | step="1" rechaza decimales |
| Entrada que no se puede interpretar | badInput | Letras escritas en un campo numérico en algunos navegadores |
setCustomValidity() | customError | Cualquier mensaje que pongas, hasta que lo vuelvas a dejar vacío |
Patrones en dos motores
Los navegadores compilan pattern con el flag v, que es más estricto que las expresiones regulares que escribe la mayoría: un - literal dentro de una clase de caracteres debe escaparse, como en [a-z\-]. Ikiform compila el pattern de un campo con el flag u y también lo compara con el valor completo. Escribe patrones válidos en ambos, como el del código de reserva de arriba, y prueba cada uno en la vista previa del formulario antes de publicarlo.
Dar estilo a los errores
:invalid coincide con un campo obligatorio vacío en cuanto carga la página, así que todos los campos se ven en rojo antes de que nadie escriba. :user-invalid solo coincide después de que la persona haya interactuado con el campo o intentado enviar, que es casi siempre lo que quieres:
<style>
#workshop :user-invalid {
border-color: #b42318;
}
</style>La capa del servidor
Pega la definición de arriba en la pestaña Fields del formulario. A partir de entonces, Ikiform valida cada envío con ella, venga de donde venga, y guarda valores convertidos: team_size como número, phone sin separadores, date como YYYY-MM-DD. Un envío que incumple una regla se rechaza, no se guarda nada y la respuesta dice qué campo falló:
| Campo | Enviado | Mensaje |
|---|---|---|
team_size | 25 | Must be at most 20 |
team_size | 2.5 | Enter a whole number |
date | 2026-10-30 | Must be on or after 2026-11-02 |
phone | 415 555 0123 | Use international format, e.g. +14155550123 |
code | ws-12 | Codes look like WS-1234 |
website | Cualquier valor | Unknown field "website" |
La última fila importa. Con campos definidos, Ikiform rechaza cualquier campo que no esté en la definición, así que un input renombrado falla de forma visible en lugar de guardarse con el nombre equivocado. Los campos que empiezan por _ y el honeypot no cuentan. Todos los tipos y reglas están en Campos y validación.
Con un envío normal
No hace falta código. Un envío normal rechazado muestra la página de error alojada de Ikiform con el estado 422, cada problema bajo la etiqueta de tu campo y un enlace a la página desde la que se hizo el envío. El botón Atrás del navegador suele conservar lo que escribió la persona; el enlace vuelve a cargar el formulario vacío.
Mostrar los errores del servidor en los campos
Con un script, pasa cada mensaje del servidor a la propia interfaz de errores del navegador. setCustomValidity marca el campo como no válido con ese texto, y reportValidity lo muestra como el mensaje habitual y da el foco al primero:
const form = document.querySelector("#workshop");
for (const control of form.elements) {
control.addEventListener("input", () => control.setCustomValidity?.(""));
}
form.addEventListener("submit", async (event) => {
event.preventDefault();
const response = await fetch(form.action, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
});
const result = await response.json();
if (result.ok) {
form.replaceWith("Thanks. We'll confirm the date by email.");
return;
}
const fields = result.error.fields ?? {};
for (const [name, messages] of Object.entries(fields)) {
form.elements.namedItem(name)?.setCustomValidity?.(messages[0]);
}
if (!form.reportValidity()) return;
alert(result.error.message);
});- Borra el mensaje personalizado en
input. Un campo con un error personalizado sigue siendo no válido hasta que lo vuelvas a dejar como cadena vacía, y mientras tanto la persona no puede enviar en absoluto. - Las comprobaciones del navegador siguen ejecutándose antes del evento
submit, así que el servidor solo ve envíos que las pasaron. - Si el error no tiene
fields, como429 rate_limited,reportValidity()devuelvetruey el script recurre al mensaje general.
Comprobar las reglas del servidor antes de enviar
Para detectar reglas que solo existen en el servidor, como una lista de dominios de email, antes de la petición, carga la definición pública del formulario y ejecuta validate del SDK, que aplica las mismas reglas y mensajes de campo en el navegador:
import { getSchema, validate } from "@ikiform/sdk";
const form = document.querySelector("#workshop");
const result = await getSchema("frm_xxxxxxxxxxxxxxxx");
const fields = result.ok ? result.schema.fields : [];
form.addEventListener("submit", (event) => {
const errors = validate(fields, new FormData(form));
for (const control of form.elements) control.setCustomValidity?.(errors[control.name] ?? "");
if (!form.reportValidity()) event.preventDefault();
});getSchema solo expone definiciones de campos, que de todos modos son públicas: cualquiera puede leerlas en /f/<formId>/schema.Comprueba que funciona
- En Allowed domains del formulario, añade el dominio de tu sitio, y
localhostmientras desarrollas. Déjalo vacío para aceptar envíos desde cualquier lugar. - Envía el formulario vacío. El navegador lo detiene en Tu nombre y no aparece nada en el panel Network.
- Quita
maxdel tamaño de equipo en las herramientas de desarrollo, escribe25y envía. El envío normal muestra la página de error de Ikiform; el envío con script muestra Must be at most 20 en el campo. - Envía con curl, saltándote el navegador:
curl -H "Accept: application/json" -H "Origin: https://example.com" -d team_size=abc https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx, con tu propio dominio permitido como origen. La respuesta es422con un mensaje parateam_sizey para cada campo obligatorio que falte. - Abre Submissions en el panel. El nuevo envío está ahí con todos los campos que enviaste.
Errores comunes
| Problema | Causa y solución |
|---|---|
| El formulario nunca se envía tras un error del servidor | Nunca se borró un mensaje de validez personalizado. Restablécelo con setCustomValidity("") en input |
minlength no salta con valores rellenados de antemano | tooShort solo se aplica después de que la persona edite el campo. La regla del servidor lo detecta |
Un pattern se ignora por completo | No es válido con el flag v, y los navegadores se saltan por completo un patrón no válido. Escapa - y otros caracteres especiales dentro de las clases |
| Los errores se muestran antes de que nadie escriba | El CSS usa :invalid. Cambia a :user-invalid |
422 validation_failed | El envío no coincide con la definición de campos del formulario. error.fields indica cada campo y el motivo. Haz que el name del input coincida con el id del campo. |
El flujo completo de un envío está en el resumen, y Errores lista todos los códigos que puede recibir un envío.
Preguntas
No. Solo se ejecuta en el navegador, en tu página, y los bots o scripts envían directamente al endpoint. Valida también en el servidor. Con Ikiform, define los campos del formulario y se comprueba cada envío.
Llama a input.setCustomValidity(message) y luego a form.reportValidity(), que muestra el mensaje en el aviso habitual del navegador. Bórralo con setCustomValidity("") cuando la persona edite el campo.
:invalid coincide con cualquier campo que no cumpla sus restricciones, incluidos los campos obligatorios vacíos al cargar la página. :user-invalid solo coincide después de que la persona haya interactuado con el campo o intentado enviar.
422 validation_failed y no se guarda nada. Las peticiones JSON reciben error.fields, que asocia cada id de campo con sus mensajes; los envíos de formulario normales reciben una página de error alojada que los lista por etiqueta de campo.
¿Te atascaste en un paso o quieres una guía para tu caso? Escribe a hello@ikiform.com.