CodeGym /Cursos /Spring REST & MVC /Mapa de recursos de Task Tracker API

Mapa de recursos de Task Tracker API

Spring REST & MVC
Nivel 3, Lección 4
Disponible

1. El mapa de recursos antes de codificar

Cuando empiezas a escribir la API directamente «en el controller», el cerebro fácilmente cambia al modo: «ahora hago el método completeTask()... luego uploadFileToTask()... luego algo más». Y ya no tienes una API, sino una colección de conjuros. El mapa de recursos sirve para mantener el proyecto en forma humana: fija los sustantivos y las relaciones entre ellos antes de que empecemos a discutir los detalles de implementación.

A estas alturas ya tenemos tres apoyos: task como objeto principal del dominio, colección vs recurso individual como forma básica de la superficie, y la regla de que formulaciones como complete, assign, upload hay que traducirlas en cambio de estado o en subrecurso. Ahora podemos ensamblar el mapa del proyecto completo y verificar que el modelo no se ha desmoronado.

El mapa de recursos no es una «especificación completa de todos los endpoints». Es un esquema breve del tipo: «qué recursos hay», «cuál de ellos es el principal», «cuáles viven en el contexto de otros», «qué cosas no convertimos en entidades separadas». Puedes pensarlo como el mapa del metro: no dice cuántos minutos hay entre paradas, pero muestra perfectamente dónde están los transbordos y qué ruta existe en absoluto.

Es importante pronunciar el límite por separado: las reglas precisas de buenas URI (plural, estabilidad, profundidad de anidamiento, etc.) y la versión de la API en la ruta no las necesitamos aquí todavía. Primero hace falta el modelo limpio de recursos; de lo contrario la discusión sobre la forma de la ruta empieza antes de que quede claro qué entidades viven en la superficie de la API.

Para sentir la diferencia entre el mapa de recursos y la lista de endpoints, aquí hay una mini-ilustración (por ahora sin métodos ni detalles):

import java.util.List;

// Esto no son «endpoints con todas las operaciones», sino simplemente la superficie de recursos,
// es decir, la lista de URI que existen en el dominio.
List<String> resourceSurface = List.of(
    "/tasks",
    "/tasks/{taskId}",
    "/tasks/{taskId}/comments",
    "/tasks/{taskId}/attachments",
    "/tags"
);

Esta lista no es una «API lista», sino más bien una «promesa»: esto es de lo que hablamos hacia fuera. Luego precisaremos: qué operaciones son adecuadas, qué respuestas devolvemos, cómo se ven los datos. Pero la base ya está aquí.

2. El recurso central: tasks

En cualquier API sensata — incluso la educativa — es útil tener un «eje» alrededor del cual gira todo. De lo contrario el proyecto se convierte en un conjunto de entidades iguales, donde cada una requiere un CRUD completo, filtros, ordenación, documentación, tests... y el estudiante de repente estudia no el diseño REST, sino «cómo cansarse sin motivo».

En Task Tracker API ese recurso central son las tareas. Empezamos con la colección de tareas (/tasks) y la tarea individual (/tasks/{taskId}), porque eso es lo que el cliente más frecuentemente quiere: la lista de tareas y los detalles de una tarea. Lo importante es que la tarea no es una «tabla en la base de datos» (de momento no la tenemos), sino una unidad del dominio con la que trabaja el usuario del sistema. Nuestra tarea como desarrolladores backend es hacer que ese recurso se lea de forma natural.

El modelo interno mínimo (aún no es DTO ni la forma final, simplemente para «ver el recurso con los ojos») puede verse así:

import java.util.UUID;

// Forma mínima de Task: sin fechas, descripciones y otros campos «densos» —
// nos importa fijar el hecho del recurso central y su estado.
public record Task(
    UUID id,                // identificador del recurso de la tarea
    String title,           // nombre legible por humanos
    TaskStatus status,      // estado actual en el workflow
    TaskPriority priority   // prioridad, para que haya «de qué agarrarse» en el modelo
) {}

Este modelo es deliberadamente corto, porque ahora no nos interesa el conjunto completo de campos, sino el hecho en sí: «Task existe como entidad separada, tiene un identificador y un estado». En el proyecto real (según el enunciado) la tarea tendrá más campos: descripción, fecha límite, responsable, etiquetas, fechas de creación y actualización. Pero lo importante es otra cosa: todo eso son atributos de un recurso central.

Y aquí aparece una disciplina importante del mapa de recursos: si Task es la entidad principal, entonces todas las partes adicionales del dominio o permanecen como campos de Task, o se convierten en subrecursos junto a la tarea (como los comentarios y los adjuntos), o se convierten en un pequeño recurso auxiliar que no infla el modelo (como el lookup por etiquetas).

3. comments como subrecurso de tasks

Los comentarios son el ejemplo clásico de lo que conviene modelar como subrecurso. Dentro de la aplicación los comentarios pueden perfectamente ser objetos separados, tablas separadas y lo que sea. Pero el contrato externo debe responder a la pregunta del cliente: «¿qué es esta cosa y dónde vive?» Y el comentario en Task Tracker tiene una respuesta bastante honesta: el comentario pertenece a una tarea concreta y sin ella normalmente no tiene sentido.

Por eso en el mapa de recursos los comentarios viven bajo la tarea: /tasks/{taskId}/comments. El cliente no simplemente «toma comentarios en general», sino los comentarios de la tarea. Esto hace la API más comprensible, y además ayuda a evitar la tentación de inventar rutas «de comando» como /addCommentToTask (que suena como métodos del servicio, no como contrato externo).

El modelo interno (simplificado) del comentario puede ser así:

import java.util.UUID;

// El comentario existe «dentro de la tarea»: aunque tenga su propio id,
// su sentido en el dominio es inseparable del taskId.
public record Comment(
    UUID id,            // identificador del comentario concreto
    UUID taskId,        // referencia a la tarea propietaria (contexto del subrecurso)
    String authorName,  // quién dejó el comentario (simplificado)
    String text         // texto del comentario
) {}

Observa el taskId: aunque el comentario tenga su propio identificador, está vinculado a la tarea. Desde el punto de vista del contrato esto significa algo simple: leemos el comentario no como un planeta independiente, sino como un satélite del «planeta Task».

Aquí frecuentemente se hace la pregunta: «¿Se puede hacer /comments/{commentId}?» Se puede. A veces incluso hace falta (por ejemplo, si los comentarios tienen un ciclo de vida independiente, se buscan globalmente, se moderan, etc.). Pero en el marco de nuestro proyecto eso sería una complicación innecesaria. Conscientemente hacemos los comentarios un subrecurso auxiliar: nos basta con la lista y la creación/eliminación en el contexto de la tarea. Y esa simplicidad no es «simplificación por pereza», sino una elección metodológica para que el modelo REST permanezca compacto.

4. attachments como subrecurso

Los adjuntos (attachments) muy fácilmente convierten el proyecto en un abismo de infraestructura: archivos, tipos MIME, multipart, descargas, almacenamiento, límites... Si empiezas a excavar todo a la vez, de repente te encuentras en un curso sobre «almacenamiento en la nube y seguridad de archivos», cuando en realidad estamos estudiando el contrato REST y el web-layer.

Por eso ya a nivel del mapa de recursos es importante tomar una decisión simple y sensata: los adjuntos son un subrecurso de la tarea. El cliente piensa así: «la tarea tiene adjuntos», y no «existe un almacén de archivos mundial donde en algún lugar hay archivos que pueden estar vinculados a tareas».

De ahí la forma: /tasks/{taskId}/attachments. Eso mantiene el sentido: los attachments viven junto a la tarea, y su ciclo de vida normalmente depende de ella (si se eliminó la tarea — los adjuntos también dejan de ser necesarios).

Si quieres simplemente ver un attachment con los ojos, puedes representarlo como la combinación de «identificador + metadatos». Sin la implementación de la carga y descarga (eso vendrá después) el modelo mínimo de metadatos puede verse así:

import java.util.UUID;

// Los adjuntos en el mapa de recursos son un subrecurso de Task, por eso taskId es la clave aquí.
// Los campos de storage internos no se muestran deliberadamente: el contrato ahora es sobre el dominio, no sobre la infraestructura.
public record AttachmentMetadata(
    UUID id,                 // identificador del adjunto
    UUID taskId,             // referencia a la tarea propietaria
    String originalFileName, // nombre original del archivo (para la UI/el humano)
    String contentType       // MIME-type (conjunto mínimo de metadatos)
) {}

Aquí deliberadamente no hay «ruta en el disco» ni «storage key». Dentro del sistema puede haberlos, pero hacia fuera no los arrastramos a nivel del contrato — y con razón. El mapa de recursos ahora fija lo principal: el adjunto está vinculado a la tarea y se dirige en su contexto.

Otra ventaja de esta decisión — ya de entrada combate el caos RPC. En lugar de la ruta /uploadFileToTask decimos: «hay attachments, se pueden añadir a la tarea». Aunque dentro del servicio sea realmente una «carga de archivo», el contrato permanece orientado a recursos.

5. tags como recurso de lookup

Las etiquetas son el «inflador» más traicionero de la API. De boca suena todo de forma inocente: «bueno, hay etiquetas, hagamos /tags». Luego aparecen «crear etiqueta», «renombrar etiqueta», «eliminar etiqueta», «vincular etiqueta a la tarea», «desvincular etiqueta», «obtener tareas por etiqueta»... y ya estás construyendo un pequeño sistema de gestión de catálogos, aunque en Task Tracker las etiquetas en realidad son simplemente etiquetas convenientes en las tareas.

En nuestro proyecto las etiquetas son ante todo valores dentro de Task. Es decir, las etiquetas pertenecen a la tarea como lista de cadenas, y para el dominio educativo eso es suficiente. Sin embargo, un endpoint separado /tags sigue siendo útil — pero en un sentido muy concreto: como lookup de valores únicos. Responde a la pregunta del cliente: «¿qué etiquetas existen en el sistema?», para, por ejemplo, mostrar sugerencias en la UI.

Así se puede pensar el rol de la etiqueta en el código al nivel más básico:

import java.util.Set;

// Las etiquetas no son una «entidad», sino un conjunto de valores dentro de las tareas.
// Esto es importante para no inflar la API con un CRUD separado para /tags.
public record TaskTags(
    Set<String> tags // etiquetas únicas en cadena
) {}

Esto subraya una vez más la idea: «tag» en nuestro proyecto no es una entidad separada con su propio ciclo de vida, sino simplemente un valor de cadena que vive dentro de las tareas. Y el endpoint /tags no es la «gestión de etiquetas», sino «obtener la lista de etiquetas existentes».

Para fijar la distinción «recurso vs rol del recurso», una pequeña tabla es útil — a menudo salva del desmoronamiento del modelo:

Parte del dominio Rol en la API Por qué es así
tasks recurso principal es el núcleo del dominio
comments subrecurso de la tarea el comentario sin la tarea normalmente no sirve
attachments subrecurso de la tarea el adjunto tiene sentido en el contexto de la tarea
tags recurso de lookup los valores se toman de las tareas, no hace falta un CRUD separado

El sentido aquí no es que «así es correcto según el libro». El sentido es que así el proyecto permanece compacto y educativo: mostramos los patrones REST importantes, pero no convertimos el curso en el análisis de todos los posibles variantes del dominio.

6. status como parte de Task

El estado de la tarea es un excelente test del pensamiento «de recursos». Cuando se piensa en comandos, quieres hacer algo como: /completeTask, /blockTask, /archiveTask. Suena «comprensible», mientras hay pocas acciones. Pero en cuanto aparece una nueva operación de negocio (o simplemente un nuevo estado), empieza a crecer el número de rutas, y el cliente tiene que aprender el «vocabulario de comandos» que no está ordenado de ninguna manera.

En el pensamiento de recursos el estado es una parte del estado de la tarea. Por lo tanto, vive dentro de la representación de Task — en los datos sobre la tarea —, y los cambios de estado son un tipo de actualización del recurso. En el mapa de recursos esto significa una disciplina simple: tenemos /tasks/{taskId}, y el estado no se «extrae a una ruta separada», sino que permanece como campo de la tarea.

Se puede mostrar esto con un ejemplo muy pequeño, sin discutir los métodos ni la mecánica:

// El estado es parte del estado de Task, no un «recurso» separado en el URI.
public enum TaskStatus {
    TODO,        // tarea creada, pero el trabajo no ha empezado
    IN_PROGRESS, // en proceso
    BLOCKED,     // bloqueada por causas externas
    DONE,        // completada
    ARCHIVED     // archivada (condicionalmente: no activa)
}

Aquí lo importante no es cómo exactamente cambiará el estado (eso es un tema aparte), sino a qué está «vinculado» conceptualmente. Pertenece a la tarea, y no vive como un objeto independiente del contrato.

Si quieres tener un rápido «filtro» para la deriva RPC, puedes hacerte la pregunta: «cuando el cliente mira la ruta, ¿ve la tarea o ve el comando?» Compara:

// Enfoque RPC: el URI parece la llamada a un método.
String rpcLike = "/completeTask";

// Enfoque de recursos: el URI señala la entidad, y la acción se expresa mediante el método HTTP.
String resourceLike = "/tasks/{taskId}";

En la segunda variante el cliente entiende inmediatamente dónde está en el dominio. En la primera — necesita recordar que «completeTask» en realidad está relacionado con la tarea, aunque por el URI eso no sea obvio.

7. Mapa de recursos final

Ahora podemos ensamblar todo con cuidado en un único dibujo. Y es precisamente en este momento cuando normalmente se hace evidente cuánto la API se sostiene realmente alrededor del recurso central. Si el mapa se ve de forma que en el centro están las tasks, y al lado dos subrecursos y un lookup — es buena señal: el modelo es comprensible y no está inflado.

Aquí está la superficie básica (sin versión de la API y sin reglas exactas de URI, aún las precisaremos):

import java.util.List;

// Lista final de recursos que «declaramos al mundo».
// Esto sigue sin ser un endpoint map: aquí no hay métodos, códigos de respuesta ni DTO.
List<String> surface = List.of(
    "/tasks",                         // colección
    "/tasks/{taskId}",                // recurso individual
    "/tasks/{taskId}/comments",       // subrecurso
    "/tasks/{taskId}/attachments",    // subrecurso
    "/tags"                           // lookup
);

Y aquí está el esquema visual de las relaciones (muy útil tenerlo en el README del proyecto o al menos en las notas). En mermaid se puede anotar así:

flowchart TD
    A["/tasks (colección de tareas)"] --> B["/tasks/{taskId} (una tarea)"]
    B --> C["/tasks/{taskId}/comments (comentarios)"]
    B --> D["/tasks/{taskId}/attachments (adjuntos)"]
    A -. "valores únicos" .-> E["/tags (lookup)"]

    B --> F["status (campo de la tarea, no recurso separado)"]

Observa: incluso en el esquema subrayamos que status no es «otra ruta», sino un campo de la tarea. Es un pequeño detalle, pero disciplina al equipo: menos tentación de hacer la API «por comandos».

Y otro punto importante: el mapa de recursos debe ser corto. Si dibujas un mapa de dos páginas, pero tienes un Task Tracker educativo sin usuarios, sin equipos, sin proyectos, sin motor de workflow — significa que empezaste a diseñar un producto, no una API educativa. En la vida real eso ocurre, pero en el marco del curso es un riesgo extra: gastarás energía en la escala, y no en la calidad del contrato.

8. Errores típicos al elaborar el mapa de recursos

Error n.° 1: perder el recurso central y hacer «todas las entidades iguales».
A veces se quiere hacer tasks, comments, attachments, tags como cuatro bloques grandes iguales, cada uno con su propio CRUD. Parece «simétrico», pero funciona mal tanto metódicamente como arquitectónicamente: el proyecto se infla y la conexión del dominio se difumina. Para nuestro curso el eje central son las tasks, el resto son elementos auxiliares.

Error n.° 2: convertir un subrecurso en un universo independiente sin motivo.
Crear /comments/{commentId} y /attachments/{attachmentId} se desea por el deseo de «hacer como en los grandes sistemas». Pero en los grandes sistemas eso está justificado por el ciclo de vida de la entidad y los escenarios de acceso. En nuestro caso el acceso a los comentarios y adjuntos casi siempre empieza desde la tarea, por lo que la ruta de subrecurso hace el contrato más simple, no más complejo.

Error n.° 3: derivar hacia el estilo RPC-like por «nombres convenientes».
Rutas como /addCommentToTask, /uploadFileToTask, /completeTask parecen comprensibles mientras son tres. Luego se vuelven diez, luego veinte, y el cliente tiene que aprender la lista de comandos en lugar de orientarse por el dominio. Si en la ruta lo primero que aparece es un verbo, casi siempre es señal de detenerse y volver al modelo de recursos.

Error n.° 4: hacer de las etiquetas un módulo CRUD completo «porque así se hace».
Las etiquetas fácilmente se convierten en un catálogo con sus propias reglas, unicidad, conflictos, panel de administración... Pero en nuestro proyecto las etiquetas son parte de la tarea, y /tags es simplemente un lookup conveniente. En cuanto haces «crear/eliminar etiqueta», estás obligado a responder decenas de nuevas preguntas (¿y las tareas donde estaba esa etiqueta? ¿quién tiene derecho a eliminarla? ¿qué significa renombrar?), y el curso se va por otro camino.

Error n.° 5: mezclar «mapa de recursos» y «endpoint map» prematuramente.
Los principiantes a menudo añaden de inmediato al mapa métodos, estados, content-type, detalles de errores e incluso formatos de DTO. Al final obtienes no un mapa de recursos, sino una semi-especificación, semi-código, semi-sueño. Es más correcto ir por capas: hoy fijamos «qué entidades y relaciones hay», mañana lo convertimos en URI canónicos y la lista completa de rutas, y solo después empezamos a discutir la mecánica de implementación.

1
Tarea
Spring REST & MVC, nivel 3, lección 4
Bloqueada
Superficie de recursos de Task Tracker API
Superficie de recursos de Task Tracker API
1
Tarea
Spring REST & MVC, nivel 3, lección 4
Bloqueada
Generación del archivo resource-map.md
Generación del archivo resource-map.md
1
Cuestionario/control
REST Contratos, nivel 3, lección 4
No disponible
REST Contratos
Recursos, URI y métodos
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION