Contexto
La mayoria de backends retornan 400 Bad Request para cualquier error de formulario: campo faltante, telefono duplicado, token expirado, demasiados intentos. El frontend termina parseando el body del error para saber que paso. Hay una forma mejor.
Lo que aprendi
Los codigos HTTP del rango 4xx tienen semanticas especificas que el frontend puede usar para routing de errores sin parsear el body. En un survey multi-paso con OTP, use cuatro codigos distintos:
409 Conflict -- Telefono duplicado
is_duplicate, existing_data = check_duplicate_phone(clean_phone)
if is_duplicate:
logger.info(f"[DUPLICATE] Telefono duplicado detectado: {clean_phone[-4:]}")
return jsonify({
"error": "Este numero de telefono ya fue registrado previamente.",
"duplicate": True,
"existing_name": existing_data.get('name', '')
}), 409
El 409 dice "el recurso que intentas crear conflicta con uno existente". El frontend puede mostrar un mensaje especifico con el nombre del registro previo.
410 Gone -- Token expirado
elapsed = int(time.time()) - otp_generated_at
if elapsed > CONFIRMATION_TOKEN_TTL:
return jsonify({
"error": "El codigo ha expirado. Solicita uno nuevo.",
"expired": True
}), 410
El 410 dice "este recurso existio pero ya no esta disponible". Distinto de 404 (nunca existio) -- aqui el token fue valido pero su TTL expiro.
429 Too Many Requests -- Rate limit
if remaining_attempts <= 0:
return jsonify({
"error": "Has agotado los intentos de verificacion.",
"max_attempts_reached": True,
"redirect": url_for('inicio')
}), 429
El 429 es estandar para rate limiting. El frontend sabe que debe frenar al usuario, no pedirle que corrija datos.
400 Bad Request -- Solo para validacion de input
if not form_data['name'] or not form_data['phone']:
return jsonify({"error": "Nombre y telefono son obligatorios."}), 400
clean_phone = ''.join(filter(str.isdigit, form_data['phone']))
if len(clean_phone) != 10:
return jsonify({"error": "El telefono debe tener 10 digitos."}), 400
El 400 queda reservado para lo que realmente es: datos malformados o faltantes.
El beneficio en frontend
// En vez de parsear el body para cada caso:
if (response.status === 409) showDuplicateWarning(data);
else if (response.status === 410) promptResendOTP();
else if (response.status === 429) showRateLimitMessage(data);
else if (response.status === 400) showValidationError(data);
El switch por status code es mas robusto que comparar strings del body. Si cambias el texto del error, el frontend sigue funcionando.