1. Los URI de comandos dificultan la vida
Los URI de comandos rápidamente convierten la API en un catálogo de comandos. Cuando haces una API por primera vez, en la cabeza normalmente no hay recursos, sino tareas del tracker de tareas (perdón por el juego de palabras). En los requisitos pone: «Hay que completar la tarea», «Hay que adjuntar el archivo», «Hay que cambiar el estado». Y la mano sola tiende a hacer algo como POST /api/v1/completeTask. Parece lógico: hay una acción — hay una dirección para la acción.
El problema es que esa ruta realmente convierte la API en un catálogo de comandos. El cliente tiene que memorizar decenas de direcciones «verbales»: /api/v1/completeTask, /api/v1/changeStatus, /api/v1/uploadFileToTask, /api/v1/renameTask, /api/v1/assignUser... Y cada nuevo escenario casi automáticamente genera un nuevo endpoint. Al final la API empieza a parecerse no a un mapa de la ciudad, sino a una lista de conjuros de un juego de fantasía: «pronuncia /archiveTask — y sucederá».
Aún peor: los URI de comandos rompen la idea de predictibilidad. En una API de recursos puedes casi adivinar cómo se verá la dirección: si hay una tarea, entonces hay /tasks/{taskId}. Si la tarea tiene comentarios, entonces hay /tasks/{taskId}/comments. Pero el estilo de comandos no se puede adivinar: tú escribes completeTask, tu colega escribe finish, un tercero — markDone, un cuarto — close. Al cliente no le queda más que sufrir y leer la documentación como una novela de 800 páginas.
Lo más desagradable es que el estilo de comandos normalmente lleva a mezclar el sentido de los métodos HTTP. Si hiciste GET /api/v1/completeTask?taskId=..., ya violaste la «seguridad» de GET (safe method): un inocente reintento automático de la solicitud, un caché en el proxy — y de repente «completar tareas» ocurre solo. Sí, suena como un bug de un universo paralelo, pero esos bugs existen exactamente porque los URI empezaron a expresar una acción en lugar de la dirección de un recurso.
2. El URI es un sustantivo, el método es un verbo
Si de esta lección recuerdas una sola frase, que sea esta: el URI es un sustantivo, el método es un verbo. El URI debe responder a la pregunta «¿con qué trabajamos?» (recurso), y el método HTTP debe responder a la pregunta «¿qué hacemos con este recurso?».
Imagina la dirección normal de una casa. La dirección no cambia según lo que quieras hacer: ver la casa, comprarla, reformarla o derribarla (no lo hagas sin permiso, es mala práctica). La dirección es el «dónde», la acción es el «qué». En la REST API es lo mismo: /api/v1/tasks/{taskId} es el «dónde está la tarea», y GET/PUT/PATCH/DELETE es lo «que quieres hacer con ella».
De ahí se deriva un criterio muy práctico: si en el URI apareció un verbo, pregúntate si no estás intentando meter en la ruta lo que ya debería expresar el método HTTP. Suena a recursión por la recursión, pero en la práctica funciona sorprendentemente bien.
Por ejemplo, tenemos dos registros:
# Mal: el URI describe una acción (estilo de comandos)
POST /api/v1/completeTask
# Bien: el URI describe el recurso, y la acción se expresa con el método HTTP
PATCH /api/v1/tasks/{taskId}
En la segunda variante el URI es estable: es sobre el recurso task. Y la acción (modificación parcial) se expresa con el método PATCH. Precisamente por eso en muchas APIs de recursos «completar una tarea» no es un comando separado, sino una actualización parcial del estado de la tarea.
Y otro punto importante: el cuerpo de la solicitud (request body) es el lugar donde describes exactamente qué cambio quieres. No la ruta debe contar «completa la tarea», sino el cuerpo debe decir «el estado ahora es DONE» (u otro estado necesario). La ruta permanece siendo la dirección.
3. Acción = cambio de estado
La mayoría de las acciones de negocio en una API de recursos son simplemente un cambio de estado. Cuando el negocio dice «completa la tarea», describe la acción con palabras humanas. Y la API de recursos lo traduce al idioma del estado: la tarea tiene el campo status, entonces «completar» es «pasar el status al valor DONE». Ahora no discutimos los detalles de las reglas de transiciones y conflictos, solo aprendemos a ver la forma correcta del URI.
Desde el punto de vista del URI todo es simple: si cambiamos la tarea, trabajamos con /api/v1/tasks/{taskId}. No con /completeTask, no con /tasks/{taskId}/complete, no con /doComplete, sino con la dirección de la tarea. Luego elegimos el método: con más frecuencia para «cambiar uno o dos atributos» se usa PATCH, y para «reemplazar todo el estado modificable» — PUT. Aquí lo que importa es el propio pensamiento: el URI no debe inflarse por las acciones de negocio.
Así puede verse esto como un sencillo escenario .http (de momento como concepto, sin discutir los estados de la respuesta ni los esquemas de DTO — eso vendrá después):
# Actualización parcial del estado de una tarea concreta
PATCH /api/v1/tasks/2b1f8d3a-7c0e-4e21-9a7a-0d3d0c9a7f11
Content-Type: application/json
{
# El comando está oculto en los datos: cambiamos el estado del recurso task
"status": "DONE"
}
Aquí el «comando» no está en el URI, sino en los datos: decimos que en el recurso task cambia el campo status. Si luego aparece la acción «bloquear la tarea», el URI no cambia — cambia el valor del status. Eso reduce drásticamente la probabilidad de que la API crezca hasta tener mil endpoints.
De forma análoga se puede pensar en otras operaciones de negocio que en realidad son cambios de estado: asignación del responsable (assigneeName), añadir/eliminar etiqueta (en nuestro proyecto es parte de la tarea), cambiar dueDate. Si cada vez haces un comando separado, construyes RPC. Si trabajas con el estado del recurso, te quedas en el modelo de recursos.
Pequeño truco técnico que ayuda al cerebro: intenta leer el URI en voz alta como un sustantivo. /api/v1/tasks/{taskId} — «la tarea con ese id» suena normal. Y /api/v1/completeTask — eso ya es «ejecuta la tarea», es decir, un comando puro. Cuando escuchas un comando, casi siempre sientes que no es una dirección.
4. Subrecursos para comentarios y archivos
Si tras la operación aparece un nuevo objeto, normalmente no es un comando, sino la creación de un recurso. Los ejemplos más claros en nuestro dominio son los comentarios y los adjuntos. Cuando «añades un comentario», no solo cambias un campo de la tarea — creas un nuevo comentario que tiene su propio commentId, autor, texto y momento de creación.
El estilo de recursos aquí pide literalmente: los comentarios son un subrecurso de la tarea. Por lo tanto, trabajamos con la colección de comentarios de una tarea concreta: /api/v1/tasks/{taskId}/comments. Y la acción «añadir un comentario» se convierte en la creación ordinaria de un elemento de la colección a través de POST.
Con los adjuntos la historia es similar: «adjuntar un archivo» en realidad significa «crear un nuevo recurso de adjunto vinculado a esta tarea». Por eso en lugar de POST /api/v1/uploadFileToTask preferimos POST /api/v1/tasks/{taskId}/attachments. El URI permanece siendo un sustantivo, y la acción se expresa con el método y el formato de la solicitud (el multipart vendrá después, por ahora es simplemente la idea).
Para no confundirse, es útil tener en mente una pequeña regla: si tras la operación aparece un nuevo identificador (por ejemplo, commentId o attachmentId), casi seguramente estás creando un nuevo recurso, no «ejecutando un comando».
Prácticamente esto significa dos formas de dirección:
- POST /api/v1/tasks/{taskId}/comments
- POST /api/v1/tasks/{taskId}/attachments
Eso ya es suficiente para no caer en addCommentToTask y uploadFileToTask.
5. El sufijo de acción como excepción rara
El sufijo de acción es admisible en contadas ocasiones — y solo cuando realmente especifica la representación del recurso. A veces haces todo bien, pero te enfrentas a una realidad desagradable: quieres obtener el mismo objeto en formas diferentes. Los adjuntos son un caso clásico. Un adjunto tiene metadatos (nombre del archivo, tamaño, tipo de contenido, descripción, fecha de carga) — cómodos de entregar en JSON. Pero también tiene el contenido binario del archivo: entregarlo en JSON sería extraño, y al cliente le es más fácil simplemente descargar el flujo de bytes.
Se podría intentar hacer un solo endpoint y cambiar el formato con la cabecera Accept, pero para el proyecto educativo es demasiado pronto y fácilmente se convierte en un «contrato mágico». Por eso elegimos el modelo honesto y claro: un URI para los metadatos, otro para el contenido.
Y aquí es donde como excepción rara aparece el sufijo de acción: .../download. No convierte la API en un catálogo RPC, porque permanece «anclado» a un recurso de adjunto concreto y vive al final de la ruta.
Compara los dos enfoques:
# Mal: comando de nivel superior y query en lugar de la dirección del recurso
GET /downloadAttachment?attachmentId=...
# Bien: direccionamiento normal del recurso + especificación estrecha de la representación de los datos
GET /api/v1/tasks/{taskId}/attachments/{attachmentId}/download
En la segunda variante sigues dirigiéndote al attachment a través de su ruta normal de recurso, y /download simplemente especifica que quieres no los metadatos, sino el contenido. Esto es muy importante: la excepción no rompe el estilo, porque no se convierte en una «acción de nivel superior» independiente.
El criterio que ayuda a no abusar de esos sufijos suena así: si añadiste /download, pregúntate si es un «comando de negocio» o una «representación alternativa de los datos». Para los attachments es lo segundo. Para /complete, /archive, /changeStatus — es lo primero, y eso ya es una señal de alarma.
Si al cabo de un mes encuentras en el proyecto rutas como /start, /stop, /approve, /reject, /recalculate — enhorabuena, construiste RPC sin darte cuenta. /download es bueno precisamente porque permanece raro y explicable.
6. Mini-algoritmo y tabla de URI
Para no discutir cada nuevo URI, es útil tener un algoritmo corto de elección. Cuando el equipo discute una nueva ruta, la discusión normalmente se ve así: uno quiere «lo que sea más conveniente ahora», otro — «lo correcto según REST», el tercero simplemente quiere irse a casa. Para no convertir cada esa discusión en una serie eterna, es útil pasar mentalmente por un corto algoritmo antes de escribir el nuevo endpoint.
A continuación — un esquema simplificado que encaja en nuestro proyecto educativo. No pretende ser perfecto según el RFC, pero es práctico y ayuda a mantener el estilo de la API uniforme.
flowchart TD
A["Hay que añadir un nuevo endpoint"] --> B{"¿Trabajamos con un recurso?"}
B -->|No| Z["Probablemente el modelo de recursos está mal elegido. Primero encuentra el recurso."]
B -->|Sí| C{"¿El resultado de la operación es un nuevo objeto con nuevo id?"}
C -->|Sí| D["Crea una colección de subrecurso y usa POST ejemplo: /api/v1/tasks/{taskId}/comments"]
C -->|No| E{"¿Es un cambio de estado de un recurso existente?"}
E -->|Sí| F["Nos quedamos en el URI del recurso ejemplo: /api/v1/tasks/{taskId}, y la acción la expresamos con el método y el cuerpo"]
E -->|No| G{"¿Es otra representación del mismo recurso (metadata vs content)?"}
G -->|Sí| H["Se admite action-suffix como excepción estrecha ejemplo: .../download"]
G -->|No| Y["Para. Revisa. Posiblemente necesitas otro recurso o estás mezclando responsabilidades."]
Y ahora — la tabla «directa». A menudo ayuda más rápido que las largas explicaciones.
| Sentido de negocio | Variante mala (de comandos) | Variante buena (de recursos) | Por qué la buena es mejor |
|---|---|---|---|
| Completar la tarea | POST /api/v1/completeTask | con status=DONE |
Direccionamos la tarea, cambiamos su estado, no proliferamos comandos |
| Cambiar el estado | POST /api/v1/changeStatus | con status=... |
El estado es parte del estado del recurso |
| Añadir un comentario | POST /api/v1/addCommentToTask | POST /api/v1/tasks/{taskId}/comments | El comentario es un nuevo recurso (subrecurso) |
| Adjuntar un archivo | POST /api/v1/uploadFileToTask | POST /api/v1/tasks/{taskId}/attachments | El adjunto es un nuevo recurso (subrecurso) |
| Descargar el archivo | GET /api/v1/downloadAttachment | GET /api/v1/tasks/{taskId}/ attachments/{attachmentId}/download | Excepción estrecha para otra representación de los datos (contenido binario vs JSON) |
Observa: en las buenas variantes casi siempre quedan los mismos sustantivos: tasks, comments, attachments. Ese es el principal marcador de una API «sana»: no inventas cada vez un nuevo idioma de comandos, sino que simplemente combinas recursos y métodos.
7. Errores típicos al diseñar URI
Este tema parece simple hasta que haces el tercer o cuarto endpoint «conveniente» de comandos, y luego intentas explicar la API a un principiante (o a ti mismo dos meses después). Los errores aquí normalmente no son sintácticos, sino arquitectónicos: todo compila, todo «funciona», pero el contrato se vuelve cada vez menos predecible. A continuación — los problemas más frecuentes en los que caen incluso los buenos desarrolladores.
Error n.° 1: hacer un endpoint separado para cada acción porque «así es más rápido».
Cuando añades /completeTask, en efecto es más rápido: una nueva ruta, un nuevo método en el servidor. Pero luego aparecen /archiveTask, /unarchiveTask, /assignTask, /unassignTask, /blockTask, /unblockTask, y la API se convierte en una lista de conjuros. En el modelo de recursos muchas de esas acciones son simplemente cambios de estado en un mismo recurso.
Error n.° 2: ocultar los comandos en parámetros de query.
A veces el desarrollador piensa: «Bueno, un verbo en la ruta — mal. Lo haré así: POST /api/v1/tasks/{taskId}?action=complete». Lamentablemente, es el mismo RPC, solo con capa invisible. El URI de todas formas deja de ser una dirección del recurso y se convierte en un comando. En un buen contrato el query normalmente se encarga del filtrado/configuración de la lectura, no de ejecutar operaciones sobre el estado.
Error n.° 3: usar GET para operaciones con efectos secundarios.
Parece cómodo: «abrí en el navegador — y la tarea se completó». Luego alguien pone accidentalmente el enlace en el chat, el bot previsualiza la página, el navegador repite la solicitud al actualizar — y «completar tareas» se convierte en el ruido de fondo casual de tu vida. GET debe ser safe, y cualquier cambio de estado debe ir a través de los métodos de escritura.
Error n.° 4: convertir la excepción rara (como /download) en el estilo principal de la API.
/download es apropiado como sufijo cuando entregas otra representación de los datos (contenido binario en lugar de metadatos JSON). Pero si empiezas a hacer /complete, /archive, /changeStatus como sufijos, vuelves a ensamblar un catálogo de comandos, solo que ahora «anidado» en los recursos. Las excepciones deben ser raras, de lo contrario ya no son una excepción sino tu nuevo estilo.
Error n.° 5: mezclar dos estilos en la misma zona de recursos.
Lo peor es el híbrido: la mitad de las operaciones están hechas de forma de recursos (PATCH /tasks/{taskId}), y la otra mitad de forma de comandos (POST /tasks/{taskId}/complete, POST /tasks/{taskId}/changeStatus). Al cliente le resulta difícil entender «cuál es la costumbre aquí». Si elegiste el modelo de recursos, mantenlo de forma coherente: un recurso, las operaciones se expresan con métodos y cuerpo, y los sufijos especiales déjalos solo donde realmente son sobre la representación de los datos (como download para los attachments).
GO TO FULL VERSION