CodeGym /Cursos /Spring REST & MVC /Diseñamos code, title y detail

Diseñamos code, title y detail

Spring REST & MVC
Nivel 21, Lección 1
Disponible

1. Limitaciones de status y el rol de code

Cuando haces una API REST por primera vez, parece que todo ya ha sido inventado: el código de estado ya existe, entonces el cliente entenderá. Pero es como decir: «Tenemos la dirección de la casa, ¿para qué necesitamos el piso y el número de apartamento?». En una aplicación real, un solo 404 es demasiado genérico: el cliente necesita entender cuál exactamente recurso no se encontró y qué exactamente debe hacer a continuación — mostrar una pantalla "No encontrado", sugerir actualizar la lista, o indicar al usuario que ha abierto un enlace antiguo.

El estado HTTP es el nivel del protocolo, dice «clase de situación»: éxito, error del cliente, error del servidor. Casi nunca dice «qué hacer en la UI». Por lo tanto, agregamos sobre ProblemDetail nuestro propio campo code — un identificador corto y estable del escenario. Esto es lo que el cliente puede manejar como una enumeración, sin depender del idioma de los mensajes y sin adivinar qué quería decir el servidor en detail.

Para que sea más fácil mantener en mente los roles de los campos, es útil verlos como un «contrato entre humanos y máquinas»:

Campo ¿Para quién principalmente? ¿Qué tan estable debe ser? Ejemplo
status para el cliente a nivel de protocolo estable (obvio) 404
code para el código cliente (frontend/mobile) máximamente estable TASK_NOT_FOUND
title para el humano (usuario/desarrollador) estable Task not found
detail para el humano y depuración de la situación puede cambiar Task with id 42 was not found

Si simplificamos: status responde a la pregunta «¿es esto éxito o error y de qué familia?», code — «¿cuál es el escenario del error?», title — «¿cómo llamarlo brevemente?», detail — «¿qué sucedió exactamente ahora?». A continuación nos interesa no repetir el análisis de los campos estándar, sino cómo no mezclar code con ellos.

2. title y detail: diferentes promesas

Aquí es importante mantener una simple división. title es una etiqueta corta y estable de la clase del problema: Task not found, Invalid input, Unsupported media type. detail es el detalle del caso actual: qué taskId no se encontró, qué valor resultó inválido, qué operación no funcionó.

En resumen: title debe ser reconocible, detail debe ser útil. No conviertas title en una cadena de log dinámica, ni detail en un segundo code. Entonces será fácil de leer para los humanos y el código cliente no comenzará a aferrarse a texto aleatorio.

3. Diseñando code

code es ese «tornillo de fijación» que hace que la API sea amigable para el código cliente. Y aquí es importante acordar un estilo de inmediato, de lo contrario en una semana tendrás códigos task_not_found, TaskNotFound, TASK-NOT-FOUND, taskNotFound y uno misterioso task404 (que será «bueno, lo improvisé rápido»).

En Task Tracker API seguiremos un estilo simple y muy común: UPPER_SNAKE_CASE, solo latín, sin espacios, sin dinámica. No porque sea «canónico», sino porque es lo más conveniente para cualquier cliente y para los logs. code no es texto para el usuario. Si deseas localización al ruso, español y klingon, eso se resolverá en el cliente usando code, no por el code mismo.

Aún más importante que el estilo es la estabilidad de significado. Un mismo code no debe significar cosas diferentes en diferentes manejadores. Si hoy INVALID_INPUT es «la validación del DTO falló», y mañana con el mismo código comienzas a devolver «tipo de medio no soportado», el cliente se divertirá mucho (especialmente si ya se ha lanzado a las tiendas). Desde el punto de vista del cliente, sería el mismo error, aunque en realidad son escenarios diferentes.

En el proyecto fijamos de antemano los códigos de error básicos. Son como el «diccionario de errores» de tu API. El mínimo para el curso ya está definido, y eso es bueno: menos motivos para creatividad donde no es necesaria.

Por ejemplo, se puede crear un enum en el paquete api.dto.error:

package com.example.tasktracker.api.dto.error;

// Códigos de error básicos de la API. Los clientes (frontend/mobile) deben tratarlos como constantes.
public enum ApiErrorCode {
    // Los nombres están en UPPER_SNAKE_CASE: estables y convenientes para logs/clientes.
    TASK_NOT_FOUND,
    INVALID_INPUT,
    MALFORMED_JSON,
    UNSUPPORTED_MEDIA_TYPE,
    INVALID_STATUS_TRANSITION
}

Observa: no estamos tratando de hacer una «enciclopedia perfecta de todos los errores del mundo». Necesitamos un conjunto pequeño y comprensible. Si sientes el deseo de agregar 73 códigos «para el futuro» — es un deseo normal, solo es mejor soportarlo sin hacer commit.

Este es el diccionario público de code para el proyecto. De ahora en adelante, cadenas como "INVALID_INPUT" o "TASK_NOT_FOUND" significarán exactamente los valores de este diccionario, no un nuevo sistema paralelo de códigos. Y sí: type y code no se duplican entre sí — type sigue siendo parte del estándar Problem Details, y code resuelve la tarea aplicada de la lógica del cliente.

4. Diseñando title

title es una breve etiqueta del problema que una persona debe entender de un vistazo. Para nuestro proyecto es más conveniente un estilo orientado a recursos: Task not found, Invalid input, Invalid status transition. Inmediatamente dice el significado del error y al mismo tiempo se mantiene estable.

No ponemos dinámica en title. Task 42 not found suena tentador, pero eso ya es trabajo de detail. Si mantienes title estable, es conveniente verlo en Swagger UI, Postman, logs y pruebas: una categoría de problema — un título.

5. Diseñando detail

detail es el lugar para los detalles del caso actual. Aquí es normal poner taskId, el valor problemático del parámetro o una breve explicación de qué exactamente prohíbe la operación. Por ejemplo: Task with id 42 was not found o Request body is not a valid JSON.

Pero detail no debe convertirse en un volcado interno del servidor. Nombres de clases Java, stack trace, rutas del sistema de archivos y mensajes crudos del framework aquí solo causan daño. El cliente necesita un texto a nivel de contrato de API: suficientemente concreto para entender el problema, y suficientemente externo para no depender de la implementación interna.

6. Implementación en Task Tracker API

Cuando empiezas a escribir @ExceptionHandler, al principio todo se ve ordenado. Luego agregas un segundo manejador, tercero, cuarto... y de repente resulta que en un lugar code se llama "errorCode", en otro "code", y en un tercero directamente "Code" (porque alguien accidentalmente presionó Shift). Por lo tanto, es mejor fijar un solo nombre de campo de inmediato. En el proyecto será code.

Crearemos un pequeño método helper dentro de nuestro GlobalExceptionHandler (o una fábrica separada, pero en esta etapa un helper es más simple y comprensible). Recopilará el esqueleto público base {type, title, status, detail, instance, code}, y los detalles específicos del escenario se agregarán por encima de él.

import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail;

import java.net.URI;

// Recopilamos el esqueleto público básico del error en un solo lugar:
// campos estándar de ProblemDetail + nuestro código específico de la aplicación.
private ProblemDetail appProblem(HttpStatusCode status,
                                 URI type,
                                 String title,
                                 String detail,
                                 ApiErrorCode code,
                                 URI instance) {
    ProblemDetail pd = ProblemDetail.forStatusAndDetail(status, detail);

    pd.setType(type); // type — identificador estable de la clase del problema
    pd.setTitle(title); // title — etiqueta corta para humanos
    pd.setInstance(instance); // instance — URI de la solicitud específica
    pd.setProperty("code", code.name()); // code — escenario estable para el código cliente

    return pd;
}

Este es nuestro builder base. fieldErrors, los extension fields conflictivos, timestamp y requestId solo complementarán este conjunto, no lo reemplazarán.

Ahora usamos este helper en el manejador de TaskNotFoundException. Sí, en el manejador se puede aceptar HttpServletRequest — es normal, porque @ControllerAdvice vive en la capa web, y no pretendemos que sea «domain layer».

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.net.URI;

@ExceptionHandler(TaskNotFoundException.class)
public ProblemDetail handleTaskNotFound(TaskNotFoundException ex,
                                        HttpServletRequest request) {
    // instance lo tomamos del URI de la solicitud para poder vincular el error con una llamada específica
    // detail lo formulamos nosotros mismos (no usamos ex.getMessage() como parte del contrato externo)
    return appProblem(
            HttpStatus.NOT_FOUND,
            URI.create("/problems/task-not-found"),
            "Task not found",
            "Task with id " + ex.getTaskId() + " was not found",
            ApiErrorCode.TASK_NOT_FOUND,
            URI.create(request.getRequestURI())
    );
}

Observa la «separación de responsabilidades»: title es estable, detail es concreto y depende del id, code es estable y adecuado para el código cliente. Y no usamos ex.getMessage() directamente — esa es una disciplina aparte: los textos de excepciones generalmente no están destinados al contrato externo.

7. Ejemplo JSON y lógica del cliente

Para sentir por qué todo esto no es «burocracia» sino un beneficio real, es útil ver el JSON final desde la perspectiva del cliente. Por ejemplo, GET /api/v1/tasks/42 donde la tarea no existe, podría devolver:

{
  "type": "/problems/task-not-found",
  "title": "Task not found",
  "status": 404,
  "detail": "Task with id 42 was not found",
  "instance": "/api/v1/tasks/42",
  "code": "TASK_NOT_FOUND"
}

Mientras type/title/detail ayudan al humano, el cliente puede bifurcarse según code. Esto significa que la UI puede hacer, por ejemplo, la siguiente lógica: si TASK_NOT_FOUND, mostrar «Tarea no encontrada» y un botón «Volver a la lista». Si MALFORMED_JSON, mostrar «Verifique el formato JSON». Y lo mejor es que la UI no tiene que adivinar esto a partir de detail.

Es útil visualizar cómo la aplicación cliente procesa esto:

flowchart LR
    A[HTTP response] --> B["status 404/400/415"]
    B --> C["parse JSON ProblemDetail"]
    C --> D[read code]
    D --> E["UI logic: switch(code)"]
    C --> F[show title/detail to human]

Si code está bien diseñado, la lógica del cliente se convierte en una simple ramificación según una constante, no en «búsqueda de subcadena en el mensaje».

Y otro ejemplo: cuando llega un input inválido. Mientras no agregamos fieldErrors (esa es la siguiente capa del contrato), la respuesta base podría ser así:

{
  "type": "/problems/invalid-input",
  "title": "Invalid input",
  "status": 400,
  "detail": "Request validation failed",
  "instance": "/api/v1/tasks",
  "code": "INVALID_INPUT"
}

Esto ya es mejor que «una cadena message cualquiera». El esqueleto es el mismo, el código cliente lee code de la misma manera, y no creamos un «formato de error de validación» separado. Y la extensión con detalles por campo la agregaremos más adelante sobre este mismo esqueleto, sin romper la base.

8. Errores típicos: code, title, detail

Error n.º1: hacer que code sea igual al texto del error.
A veces el desarrollador escribe pd.setProperty("code", "Task with id 42 was not found") porque «bueno, es un código, es una cadena». Así code deja de ser un código y se convierte en un segundo detail. Al final el cliente no puede confiar en él: hoy la cadena tiene “was not found”, mañana “not found”, pasado mañana decides traducirla al español. code debe ser una constante corta que se pueda comparar estrictamente.

Error n.º2: poner detalles dinámicos en title.
Task 42 not found parece conveniente hasta que aparece el siguiente error Task 43 not found, luego Task 44 not found… y te das cuenta de que title no es estable y no se puede usar como «etiqueta de la clase del problema». Es mucho mejor mantener title constante (Task not found) y mover el id a detail.

Error n.º3: el mismo code se usa para diferentes significados.
Hoy decides que INVALID_INPUT es «validación de DTO», y mañana con el mismo código comienzas a devolver «error de conversión de enum en parámetro query» y «cuerpo de solicitud ausente». Formalmente todas estas situaciones son «bad request», pero para el cliente son ramas de comportamiento completamente diferentes. Si los códigos están mezclados, al cliente solo le queda mirar detail y jugar a «adivina la melodía». Los códigos deben diferenciar escenarios estables que el cliente necesita manejar de manera diferente.

Error n.º4: usar ex.getMessage() como detail «directamente».
Los mensajes de excepción a menudo son inestables, a veces demasiado técnicos, a veces incluyen detalles internos. Hoy ex.getMessage() se ve decente, pero mañana después de una actualización de la biblioteca aparecerá una redacción diferente o un fragmento que no se debe mostrar externamente. Es mejor formular detail por separado, basándose en el contrato externo, incluso si parece un poco más verboso.

Error n.º5: olvidar que code es para todo el error, no para un campo individual.
A veces se quiere hacer code=NotBlank o code=Size y pensar que resolviste el problema. Estos son buenos códigos, pero se refieren a violaciones específicas de validación de campos, no al error general de la solicitud. En el nivel superior, code debe describir la clase del problema en su totalidad (por ejemplo, INVALID_INPUT), y los detalles por campo deben vivir por separado, en las extensiones. Si se mezclan los niveles, la respuesta se vuelve no estructurada: el code superior comienza a significar a veces un campo, a veces otro — y el cliente tiene que adivinar nuevamente.

1
Tarea
Spring REST & MVC, nivel 21, lección 1
Bloqueada
Código estable para una película faltante
Código estable para una película faltante
1
Tarea
Spring REST & MVC, nivel 21, lección 1
Bloqueada
Formato de exportación desconocido con código de error
Formato de exportación desconocido con código de error
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION