Desajustes en el contrato front-end/back-end que rompen el guardado de formularios
Desajustes entre frontend y backend hacen que formularios parezcan exitosos cuando en realidad nada se guarda. Alinea DTOs, errores de validación, códigos de estado y formatos de paginación.

Qué significa cuando un formulario se envía pero nada se guarda
Un contrato front-end/back-end es el acuerdo compartido entre tu formulario y tu API: qué campos se envían, cómo se llaman, qué tipos tienen y qué envía la API en caso de éxito o error.
Cuando un formulario "se envía" pero nada se guarda, normalmente significa que el front-end hizo su parte (envió una petición), pero el back-end no persistió los datos. Lo complicado es que el usuario a menudo no ve un error claro porque la respuesta parece correcta, la interfaz ignora la respuesta o la API devuelve una forma inesperada.
Un ejemplo común: el formulario hace POST con { email, password }, pero la API espera { userEmail, passwordHash }. La petición llega al servidor, la validación falla y la API devuelve un 200 genérico con { ok: false } (o un cuerpo vacío). El front-end trata cualquier 200 como éxito, muestra una notificación y el usuario continúa, aunque la base de datos nunca cambió.
Esto ocurre mucho en prototipos construidos rápido o generados por IA. Las herramientas pueden generar una UI y una API funcionales rápidamente, pero a menudo adivinan nombres de campos, olvidan reglas de validación del servidor o inventan formatos de respuesta. Cuando luego cambias un endpoint simulado por uno real, el "contrato" cambia ligeramente y los guardados empiezan a fallar silenciosamente.
Arreglar esto no es un cambio en un solo archivo. Normalmente necesitas alinear varias piezas a lo largo del stack:
- DTOs de solicitud (nombres de campos, tipos, requerido vs opcional)
- Errores de validación (una forma consistente que la UI pueda mostrar junto a los campos)
- Códigos de estado (para que éxito y fallo sean inequívocos)
- Respuestas de éxito (devuelve lo que la UI necesita para confirmar el guardado)
- Respuestas de listado (formato de paginación que no cambie según el endpoint)
Si heredaste una app generada por IA y los guardados son inestables, este es precisamente el tipo de problema que equipos como FixMyMess ven: el código "funciona", pero el contrato es inconsistente. El resto de esta guía se centra en hacer ese contrato explícito, predecible y difícil de romper por accidente.
Síntomas comunes y a qué suelen apuntar
Un formulario puede parecer que funcionó mientras nada cambió. La pista más común es una notificación de éxito (o un check verde), pero tras un refresco los datos antiguos siguen ahí. Eso suele significar que la UI decidió que fue un éxito basándose en la señal equivocada, no en lo que el servidor realmente hizo.
En el lado del backend, fíjate en respuestas que parecen “exitosas” pero no lo son. Un problema clásico de contrato es un 200 OK que contiene un error dentro del JSON (por ejemplo, { "ok": false, "message": "invalid" }). Otro es 204 No Content aunque el front-end espere el registro guardado y necesite un id o campos actualizados.
En el lado del desarrollador, las pistas suelen ser pequeñas y fáciles de ignorar: un console.log muestra undefined para un campo que estás seguro de haber completado, o la pestaña de Red muestra una forma de respuesta que no programaste (como data vs result, o un array donde esperabas un objeto). Estos son desajustes del contrato front-end/back-end a simple vista.
Síntomas comunes y sus causas probables:
- Mensaje de éxito, pero al refrescar no hay cambio: la petición nunca alcanzó el endpoint de guardado, o lo hizo con campos faltantes/renombrados.
- El guardado funciona solo para algunos usuarios: la validación del backend difiere de las reglas del frontend, o los campos requeridos dependen del rol del usuario.
- El backend devuelve 200, pero la UI se comporta raro: el error está codificado dentro del JSON, no mediante el código de estado.
- La UI muestra “Guardado” pero la lista sigue mostrando el elemento antiguo: caché no invalidado, o la respuesta no incluye el registro actualizado.
- La paginación parece rota (faltan elementos, repeticiones): el frontend espera
page/total, el backend devuelvenextCursor/items(o al revés).
Regla rápida: confía más en la pestaña de Red que en la UI. Si el payload de la petición y la respuesta no coinciden con lo que tu código asume, el formulario puede “enviarse” sin guardar nada.
Este patrón es habitual en FixMyMess cuando un prototipo generado por IA conecta el botón y la notificación, pero nunca confirma que el servidor realmente persistió algo.
El contrato que deben acordar: formas, nombres y tipos
Cuando un guardado “funciona” en la UI pero nada cambia en la base de datos, a menudo no es un bug en un solo lugar. Es un desacuerdo entre el cliente y la API sobre cómo luce una petición y una respuesta válidas. Muchos desajustes del contrato front-end/back-end son detalles mundanos, pero rompen la app silenciosamente.
Empieza por escribir el contrato mínimo para un único guardado. Si alguna parte es vaga, distintas partes del stack rellenarán los huecos de forma diferente.
- Endpoint + método (por ejemplo,
POST /usersvsPUT /users/:id) - Headers requeridos (especialmente
Content-Typey auth) - Forma del cuerpo de la petición (nombres de campos, anidamiento, opcional vs requerido)
- Forma de la respuesta (qué debe leer el cliente para actualizar la UI)
- Forma del error (cómo se devuelven los problemas de validación)
El nombrado es el primer lugar donde los contratos se desvían. Si el frontend envía firstName pero el backend espera first_name, puedes obtener una respuesta “exitosa” mientras el backend ignora el campo desconocido o almacena un valor por defecto.
Los tipos son el segundo. Un caso común: la UI envía age: "32" como string, pero el backend espera un número. Algunos frameworks lo coercean, otros lo rechazan y otros convierten fallos en null. Si null está permitido, terminas guardando un valor vacío sin darte cuenta.
Los campos extra y los faltantes también pueden desaparecer en silencio. Por ejemplo, el formulario incluye marketingOptIn, pero el DTO en el servidor no lo contempla. Dependiendo del stack, ese campo puede perderse durante la deserialización sin error. Lo contrario también duele: el backend requiere companyId, pero el frontend nunca lo envía, así que el servidor crea un registro sin asociación.
Una forma práctica de captarlo temprano es tomar una petición real desde las herramientas de desarrollo del navegador, compararla con el DTO y las reglas de validación del servidor línea por línea, y acordar nombres y tipos exactos antes de tocar la lógica.
Paso a paso: alinear DTOs desde campos del formulario hasta la base de datos
Cuando un formulario “se envía” pero nada se guarda, la causa habitual es simple: el frontend envía una forma, y el backend espera otra. Arreglarlo empieza por decidir quién define la verdad y luego verificar cada salto desde los campos del formulario hasta el almacenamiento.
1) Elige una sola fuente de verdad
Escoge un lugar que defina nombres y tipos. Para la mayoría de equipos, los DTOs de request/response del backend son la fuente más segura, porque están junto a la validación y la persistencia. Si usas un esquema compartido, trátalo como un contrato y versiónalo.
2) Escribe los DTOs con ejemplos reales
No confíes en el “es obvio”. Escribe un ejemplo para crear y otro para actualizar. Las actualizaciones suelen fallar porque requieren un id, permiten campos parciales o usan nombres distintos.
// Create
{ "email": "[email protected]", "displayName": "Sam", "marketingOptIn": true }
// Update
{ "id": "usr_123", "displayName": "Sam Lee", "marketingOptIn": false }
Luego escribe el DTO de respuesta que la UI realmente necesita. Si la UI espera user.id pero la API devuelve userId, los guardados pueden “funcionar” pero la UI no podrá renderizar el estado actualizado.
3) Traza la ruta del formulario a la base de datos
Recorre la cadena completa al menos una vez, de extremo a extremo:
- Nombres y tipos de campos del formulario (strings, números, booleanos)
- Payload enviado por la red (incluyendo headers como content type)
- Parsing y validación del DTO en el backend (campos requeridos, valores por defecto)
- Mapeo a columnas de la base de datos (nombres y conversiones de tipo)
- Cuerpo de la respuesta que la UI lee para actualizar la pantalla
4) Verifica usando el payload exacto que envía la UI
Copia la petición real desde la pestaña de red del navegador y reprodúcela. Esto detecta problemas como "true" (string) vs true (booleano), campos faltantes o anidamiento inesperado.
5) Cambia un lado y vuelve a probar con un ejemplo conocido bueno
Arregla el mapeo del frontend o el DTO del backend, pero no ambos a la vez. Mantén un payload “dorada” y una respuesta esperada para confirmar que no solo moviste el desajuste a otro lugar.
Si heredaste código generado por IA, estos desajustes son comunes porque las UIs y APIs generadas suelen evolucionar por separado. Plataformas como FixMyMess normalmente empiezan por auditar el contrato y los puntos de mapeo antes de tocar la lógica de negocio, porque ahí es donde se esconden los fallos silenciosos de guardado.
Haz los errores de validación consistentes y fáciles de mostrar
Cuando el frontend y el backend no coinciden en cómo se ven los errores, los usuarios reciben la peor experiencia: el formulario “se envía”, pero nada les dice qué arreglar. Una estructura simple y predecible para los errores es una de las victorias más fáciles contra los desajustes de contrato.
Un patrón práctico es devolver siempre la misma estructura para fallos de validación:
{
"error": {
"type": "validation_error",
"fields": [
{ "field": "email", "code": "invalid_format", "message": "Introduce un correo válido." },
{ "field": "password", "code": "too_short", "message": "La contraseña debe tener al menos 12 caracteres." }
],
"non_field": [
{ "code": "state_conflict", "message": "Esta invitación ya ha sido usada." }
]
}
}
Mantén tres piezas para cada problema: el nombre del campo (coincidiendo con tu DTO), un código estable (para que la UI reaccione) y un mensaje humano (para mostrar). Si solo devuelves mensajes, la UI terminará adivinando y se romperá cuando el texto cambie.
Los errores globales importan igual que los de campo. Permisos, conflictos de estado y límites de tasa no están ligados a un único input, así que deberían ir a un lugar separado como non_field (o global). La UI puede mostrarlos cerca del botón enviar o como un pequeño banner.
En el frontend, el mapeo debe ser aburrido y consistente:
- Borra errores previos antes de enviar.
- Para cada elemento en
fields[], adjuntamessageal input cuyo nombre coincida. - Si el campo es desconocido, trátalo como error global (a menudo indica una deriva del DTO).
- Muestra los mensajes en
non_field[]en un lugar visible.
Finalmente, no escondas la validación dentro de respuestas “exitosas”. Si el guardado falló, devuelve una respuesta de error con cuerpo de error. Mezclar advertencias en una respuesta 200 es cómo obtienes fallos silenciosos, especialmente en apps generadas por IA que vemos en FixMyMess.
Códigos de estado y respuestas de éxito que no mientan
Muchos desajustes del contrato empiezan con una mentira simple: el servidor devuelve éxito, pero la UI no puede saber si el guardado realmente ocurrió. Si el frontend trata cualquier 200 como “guardado”, obtendrás el clásico “la notificación dice éxito pero al refrescar no hay nada”.
Usa los códigos de estado como señal clara y mantén la forma de la respuesta honesta.
Un patrón simple y predecible
Elige reglas que puedas seguir siempre:
- 201 Created cuando se crea un nuevo registro, e incluye el recurso nuevo en el cuerpo.
- 200 OK para lecturas y actualizaciones, con un JSON que represente el estado guardado.
- 204 No Content solo cuando realmente no devuelves cuerpo (y el cliente no necesita datos nuevos).
- 422 Unprocessable Entity para problemas de validación (errores de campo que el usuario puede corregir).
- 409 Conflict para duplicados o conflictos de versión (la petición es válida, pero no se puede aplicar tal cual).
Devolver 200 OK con un { error: ... } es una trampa. Muchas UIs solo comprueban response.ok o el código HTTP. La UI mostrará éxito mientras el backend silenciosamente rechazó el guardado.
Idempotencia, duplicados y comportamiento de “intentar de nuevo”
Si los usuarios pueden hacer doble clic en guardar, refrescar a mitad de guardado o reintentar tras un timeout, necesitas una regla clara para duplicados.
Usa 409 cuando el mismo valor único ya exista (por ejemplo, email único) o cuando falle el locking optimista (stale updatedAt o version). Usa 422 cuando el payload en sí esté mal (campos requeridos faltantes, formato inválido).
Qué debe devolver un guardado exitoso
Incluso en actualizaciones, devuelve los datos canónicos que el servidor almacenó, no un eco de lo que envió el cliente. Una buena respuesta de guardado suele incluir:
idupdatedAt(oversion)- los campos normalizados (strings recortados, defaults calculados)
- cualquier valor generado por el servidor (slugs, estado)
Ejemplo: si el frontend envía " Acme ", la respuesta debe devolver "Acme". Así la UI coincide de inmediato con la realidad y detectas problemas de contrato pronto. Los equipos suelen traer APIs generadas por IA a FixMyMess donde una respuesta “exitosa” en realidad ocultaba un rechazo del guardado detrás de un 200.
Formatos de paginación que se mantienen estables en todo el stack
La paginación es un contrato, no un detalle de implementación. Si el frontend y el backend no coinciden en la forma, obtienes tablas vacías, filas repetidas o un “Cargar más” que nunca termina. Estos desajustes son comunes en APIs generadas por IA donde la UI y el servidor se scaffoldearon por separado.
Elige un estilo de paginación y nómbralo claramente
La forma más rápida de evitar confusiones es elegir un estilo y escribir exactamente los parámetros de petición:
- page + pageSize: sencillo para números de página, pero puede ser lento si la BD debe contar y saltar muchas filas.
- offset + limit: fácil de implementar, pero inserciones y borrados pueden causar duplicados o filas faltantes.
- cursor: mejor para "scroll infinito", estable ante cambios, pero necesita un token de cursor y un orden estricto.
Una vez elegido, manténlo consistente entre endpoints. Una UI hecha para page=3&pageSize=20 no se comportará bien si un endpoint espera offset=40&limit=20 silenciosamente.
Congela la forma de respuesta que la UI lee
Decide los campos exactos de los que el frontend pueda fiarse. Un valor por defecto seguro es: items más una forma de saber si hay más datos. Los totales son opcionales y pueden ser costosos.
Un desajuste muy común es que el backend devuelva { data: [...] } mientras la UI espera [...] (o espera items). La petición tiene éxito, la UI no renderiza nada y nadie ve un error.
Para evitar reordenamientos de páginas, congela estas reglas en el contrato:
- Siempre exige un orden determinista (por ejemplo
sort=createdAt:desc). - Aplica filtros antes de paginar y devuelve los filtros aplicados si es posible.
- Para paginación por cursor, basa el cursor en los mismos campos de orden que devuelves.
- Sé consistente con los estados vacíos: devuelve
items: []conhasMore: false.
Cuando FixMyMess audita prototipos rotos, la paginación inestable suele ser la causa oculta de informes de “nada se guarda”, porque el registro existe pero nunca aparece en la vista de lista que el usuario comprueba justo después de guardar.
Trampas comunes que causan fallos silenciosos al guardar
Un formulario puede parecer sano y aun así fallar en persistir porque la UI y la API discrepan en detalles pequeños y fáciles de pasar por alto. Estos desajustes suelen no lanzar un error obvio, por lo que la gente asume que la base de datos es la que falla cuando en realidad es la forma de la petición.
Una trampa común es la coerción silenciosa de JSON. El frontend envía un string donde la API espera un número, o envía una cadena vacía para un campo nullable. Algunos servidores silenciosamente descartan campos que no pueden parsear, y luego el guardado falla más adelante porque falta ese campo requerido.
Otro clásico: campos que parecen opcionales en la UI pero son requeridos para guardar. Las apps multitenant a menudo necesitan tenantId, orgId o userId. Si normalmente se rellenan desde el contexto de autenticación, un pequeño bug de auth puede hacerlos vacíos sin cambiar el formulario.
Las fechas también provocan fallos sutiles. Un selector de fecha puede enviar "01/02/2026" mientras la API espera ISO como "2026-02-01". Las zonas horarias pueden desplazar valores. Guardas "14 Ene" pero el servidor almacena "13 Ene" en UTC, y parece que el guardado no funcionó.
Los desajustes en el contexto de autenticación son sigilosos. La UI te muestra como logueado porque tiene un token, pero la API trata la petición como anónima porque falta el header, la cookie está bloqueada o el token expiró.
Las actualizaciones optimistas pueden ocultar todo esto. La pantalla se actualiza como si el guardado hubiera ocurrido, pero el backend rechazó la petición.
Fíjate en estas señales:
- La pestaña de red muestra 200, pero el cuerpo de la respuesta indica "error" o devuelve un registro sin cambios.
- La API devuelve 204 y la UI no puede confirmar qué se guardó realmente.
- Faltan IDs requeridos en el payload, pero no se muestra un error de campo.
- Una fecha parece correcta en la UI pero es distinta en la base de datos.
- La UI se actualiza antes de que la llamada a la API termine.
Cuando auditamos apps generadas por IA en FixMyMess, a menudo encontramos dos formas de DTO distintas usadas en pantallas distintas, de modo que una página guarda y otra falla en silencio con los mismos campos del formulario.
Lista de comprobación rápida antes de lanzar
Un formulario que "se envía" no es lo mismo que datos guardados. Antes de publicar, haz una comprobación rápida del contrato que cubra todo el camino: campos UI, DTOs de la API, validación y lo que el servidor devuelve.
Empieza recogiendo ejemplos reales, no solo tipos. Pon una petición de ejemplo y una respuesta de ejemplo junto al código y confirma que coinciden con lo que el servidor realmente recibe y devuelve. Fíjate en nombres (camelCase vs snake_case), opcional vs requerido y peculiaridades de tipo como números que llegan como strings.
Aquí tienes una lista corta que detecta la mayoría de fallos silenciosos:
- Confirma que los DTOs de crear y actualizar coinciden exactamente con los campos UI (nombres, tipos y qué campos pueden ser null o ausentes).
- Haz que cada fallo de guardado devuelva un código no 2xx, además de una forma de error consistente (incluyendo un mensaje general y errores por campo).
- Asegúrate de que la UI pueda mapear errores de campo del servidor a los nombres exactos que el usuario ve (no usar
emailAddressen el servidor si el formulario usaemail). - Verifica que todos los endpoints de lista usan el mismo formato de paginación (clave items, total, page/limit y dónde vive la metadata).
- Prueba un create real y un update real de extremo a extremo con un registro real en la base de datos, luego refresca la página y confirma que los valores guardados persisten.
Una prueba práctica rápida: envía intencionadamente un campo inválido (por ejemplo, una contraseña demasiado corta). Si la UI muestra una notificación de éxito, o la pestaña de red muestra 200 mientras nada cambió, tu contrato está mintiendo en algún punto.
Si heredaste una app generada por IA, aquí es donde se concentran los problemas: los DTOs derivan, los formatos de error varían por endpoint y la paginación se reinventa en cada pantalla. Equipos como FixMyMess suelen empezar con una auditoría corta centrada en estos contratos para que los guardados sean predecibles antes de añadir más funciones.
Un ejemplo realista: el guardado parece bien, pero los datos son incorrectos
Una historia común: un formulario de registro muestra una notificación de éxito, pero el usuario no puede iniciar sesión después. Todos asumen que “auth está rota”, pero el bug real es la forma del request/response.
El frontend envía este payload:
{
"email": "[email protected]",
"password": "P@ssw0rd!",
"passwordConfirm": "P@ssw0rd!"
}
La API espera password_confirmation (snake_case) e ignora passwordConfirm. Si la API además devuelve 200 OK con un genérico { "success": true }, la UI celebrará aunque el servidor nunca validó la confirmación y quizá almacene un valor erróneo o lo rechace internamente.
La solución es aburrida pero efectiva: acordar un solo DTO y un solo formato de error. O renombra el campo en la UI, o acepta ambas claves en el servidor y mápales al mismo DTO.
En caso de éxito, devuelve algo que demuestre que el guardado ocurrió:
{
"id": "usr_123",
"email": "[email protected]"
}
Usa 201 Created para un usuario nuevo. En fallo de validación, usa 422 Unprocessable Entity y devuelve errores a nivel de campo que la UI pueda mostrar junto a los inputs:
{
"errors": {
"password_confirmation": ["No coincide con la contraseña"]
}
}
Un segundo caso miniaparece en páginas de lista. El frontend construye controles de paginación basándose en total, pero la API solo devuelve un cursor y items. La UI renderiza “Página 1 de 0” o desactiva Siguiente aun cuando hay más datos.
Elige un estilo de paginación y cíñete a él. Si quieres totales, devuelve items y total. Si prefieres paginación por cursor, devuelve items, nextCursor y hasNext, y que la UI deje de pedir total.
Próximos pasos: fija el contrato y evita regresiones
Los desajustes de contrato suelen repetirse por una razón: el contrato vive en la cabeza de la gente, no en algo que puedas comprobar. La solución es aburrida pero efectiva: escríbelo, pruébalo y trata los cambios como cambios reales.
Empieza con una nota de contrato de una página para los endpoints que más importan (a menudo: crear, actualizar, listar). Mantenla en lenguaje llano e incluye ejemplos concretos.
- Request DTO: nombres de campos, requerido vs opcional, tipos y cómo se envían valores vacíos
- Response DTO: qué devuelve el “éxito” (incluir el registro guardado vs solo un id)
- Formato de error: una sola forma para validación y errores del servidor, con unos pocos ejemplos
- Códigos de estado: qué usar para crear/actualizar/no encontrado/errores de validación
- Paginación: parámetros y forma de respuesta (items, total, page, pageSize)
Luego añade un pequeño conjunto de comprobaciones de contrato para endpoints clave para detectar roturas el mismo día que se introducen. Esto puede ser pruebas de snapshot sencillas en el backend o un script en CI que haga POST con payloads conocidos y verifique la forma y el código de la respuesta.
Elige una lista corta de reglas “que nunca deben cambiar silenciosamente” y hazlas cumplir:
- Los errores de validación siempre se mapean a campos (e incluyen un mensaje legible)
- El éxito nunca devuelve 200 con un mensaje de error oculto en el cuerpo
- La paginación siempre devuelve las mismas claves, incluso cuando
itemsestá vacío - Los DTOs no renombran campos sin un bump de versión o un lanzamiento coordinado
Antes de pulir la UI, estandariza el formato de errores de la API. Cuando el frontend pueda mostrar errores de campo de forma fiable, la mayoría de los informes de “se guardó pero no se guardó” se vuelven mucho más claros.
Si tu base de código fue generada por herramientas de IA y los patrones son inconsistentes, una auditoría y una fase de reparación focalizada pueden ser el camino más rápido hacia la estabilidad. FixMyMess ofrece auditorías de código gratuitas y luego repara contratos de extremo a extremo (DTOs, validación, códigos de estado, paginación) para que la app se comporte igual en producción que en las demos.