←

HTTP status codes semanticos para formularios en vez de 400 para todo

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.

Referencia