Recurso colección — /books
Representa todos los libros de la tienda. GET /books retorna la colección completa; POST /books agrega un libro nuevo a la colección.
Diseñar un API REST significa tomar cuatro decisiones, en este orden:
El primer paso es identificar los recursos que la aplicación maneja o va a manejar. En el API REST de Spotify encontramos recursos como tracks, albums, artists o playlists: son los sustantivos que describen los conceptos que maneja la aplicación. Para identificarlos usamos el listado de requisitos funcionales y el diagrama de clases con los conceptos principales de la aplicación.
Supongamos el siguiente modelo conceptual, parcial, de una aplicación para vender libros: un libro puede tener varios autores, un autor puede tener varios libros, un libro tiene una única editorial, y un libro tiene su propio conjunto de reseñas (reviews) de revistas o periódicos.
classDiagram
class Book {
+Long id
+String name
+String isbn
}
class Author {
+Long id
+String name
}
class Editorial {
+Long id
+String name
}
class Review {
+Long id
+String source
}
Book "0..*" -- "0..*" Author
Book "0..*" --> "1" Editorial
Book "1" *-- "0..*" Review
De este modelo identificamos cuatro recursos, uno por cada clase: Book, Author, Editorial y Review. Para nombrar el conjunto de recursos de un mismo tipo usamos el nombre de la clase en minúscula y en plural: para Book usamos books. El servicio que retorna todos los libros de la tienda es:
GET /books
También podemos decidir que un subconjunto de esa colección tenga su propio nombre. Por ejemplo, losmasvendidosestemes, como parte de la colección books, para representar los libros más vendidos de este mes:
GET /books/losmasvendidosestemes
Que el recurso se llame así es una decisión del diseñador del API. Pero el nombre, por sí solo, no garantiza nada: es la implementación la que tiene que corresponder con esa decisión — el código detrás de ese path debe efectivamente calcular y devolver la lista de los libros más vendidos de este mes, ni más ni menos que eso.
La URL completa debe identificar exactamente el servidor y la aplicación que contiene el recurso, por ejemplo uniandes-disc:8080/frontbooks/api/books. Para simplificar los ejemplos de aquí en adelante solo indicamos el nombre del recurso raíz, en este caso books.
Recurso colección — /books
Representa todos los libros de la tienda. GET /books retorna la colección completa; POST /books agrega un libro nuevo a la colección.
Recurso individual — /books/23
Representa un libro específico: el que tiene id 23. GET /books/23 lo retorna; PUT lo actualiza; DELETE lo borra.
Servicio sobre books |
Descripción |
|---|---|
GET /books |
Retorna la colección de libros de la tienda. |
POST /books |
Crea un libro nuevo y lo agrega a la colección. |
GET /books/id |
Retorna el libro con identificador id. |
PUT /books/id |
Actualiza el libro con identificador id. |
DELETE /books/id |
Borra el libro con identificador id. |
El identificador de un recurso individual no tiene que ser numérico: se puede diseñar con nombre propio, por ejemplo GET /authors/ultimopremionobel. Que el recurso se llame ultimopremionobel es una decisión de diseño del API; la implementación deberá encargarse de que, ante ese pedido, efectivamente se retorne el autor que ganó el premio Nobel más reciente.
Antes de seguir con la identificación de recursos, vale la pena precisar qué es lo que se retorna: una representación del recurso, de acuerdo con nuestra decisión de diseño. Nosotros decidimos en qué formato y con qué información se representa.
Un objeto de la clase Book se representaría en JSON así:
{ "id": 1, "name": "Cien años de soledad", "isbn": "0307474720", "image": "http://goo.gl/IWNdCX", "publishingDate": "01071967"}Esta representación, sin embargo, deja por fuera las asociaciones: no dice nada sobre la editorial del libro. ¿Cómo representamos las asociaciones entre clases?
Para los ejemplos del curso tomamos la decisión de diseño de tener, para cada recurso, hasta dos representaciones JSON distintas:
Para expresar esta decisión conviene transformar el modelo conceptual en un diagrama de DTO (Data Transfer Object): las clases cuyas instancias se convierten en objetos JSON. Por convención, cada DTO se llama RecursoDTO, donde Recurso es el nombre del recurso; su versión detallada se llama RecursoDetailDTO.
Por ejemplo, en una aplicación de viajes donde un Viajero vive en una Ciudad y tiene muchos Viaje:
classDiagram
class CiudadDTO {
+Long id
+String nombre
}
class ViajeDTO {
+Long id
+String destino
}
class ViajeroDTO {
+Long id
+String nombre
+CiudadDTO ciudad
}
class ViajeroDetailDTO {
+List~ViajeDTO~ viajes
}
ViajeroDetailDTO --|> ViajeroDTO : incluye los atributos de
ViajeroDTO --> CiudadDTO
ViajeroDetailDTO --> ViajeDTO
Ciudad y Viaje solo tienen representación básica (CiudadDTO, ViajeDTO) porque de esas clases no sale ninguna asociación hacia otras. Viajero sí tiene las dos representaciones: la básica, ViajeroDTO, incluye la representación básica de Ciudad porque esa asociación es de cardinalidad 1; la detallada, ViajeroDetailDTO, agrega la colección de ViajeDTO, porque esa asociación es de cardinalidad múltiple.
bookAplicando la misma decisión al modelo de libros:
classDiagram
class EditorialDTO {
+Long id
+String name
}
class ReviewDTO {
+Long id
+String source
+String description
}
class AuthorDTO {
+Long id
+String name
}
class AuthorDetailDTO {
+List~BookDTO~ books
}
class BookDTO {
+Long id
+String name
+String isbn
+EditorialDTO editorial
}
class BookDetailDTO {
+List~AuthorDTO~ authors
+List~ReviewDTO~ reviews
}
AuthorDetailDTO --|> AuthorDTO : incluye los atributos de
BookDetailDTO --|> BookDTO : incluye los atributos de
BookDTO --> EditorialDTO
BookDetailDTO --> AuthorDTO
BookDetailDTO --> ReviewDTO
editorials y reviews solo tienen representación básica, porque no tienen relaciones con las demás clases. books y authors tienen representación básica (BookDTO, AuthorDTO) y detallada (BookDetailDTO, AuthorDetailDTO).
La representación básica de book incluye los atributos propios de Book y la representación básica de Editorial:
{ "id": 1, "name": "Cien años de soledad", "isbn": "0307474720", "image": "http://goo.gl/IWNdCX", "publishDate": "01071967", "editorial": { "id": 1, "name": "Plaza y Janés" }}La representación detallada agrega las colecciones de representaciones básicas de Author y de Review, porque esas dos asociaciones son de cardinalidad múltiple:
{ "id": 1, "name": "Cien años de soledad", "isbn": "0307474720", "editorial": { "id": 1, "name": "Plaza y Janés" }, "authors": [ { "id": 200, "name": "Gabriel García Márquez" } ], "reviews": [ { "id": 123, "source": "El Tiempo", "description": "Magnífico, inigualable." }, { "id": 456, "source": "Arcadia", "description": "Realismo mágico al extremo." } ]}Llamamos recurso raíz a aquel para el que ofrecemos servicios de creación y acceso directos: en el ejemplo son books, authors y editorials. No todos los recursos son raíz: algunos dependen de otro y solo tiene sentido acceder a ellos a través de él.
reviews no es un recurso raíz porque depende del libro al que pertenece: en el modelo de clases, la asociación entre Book y Review es una composición (rombo negro). Eso implica que para crear un review primero hay que identificar el libro al que va a pertenecer. En el diseño del API, la URL del recurso reviews es:
/books/23/reviews
Decimos que reviews es un subrecurso de books.
Servicio sobre el subrecurso books/id/reviews |
Descripción |
|---|---|
GET /books/id/reviews |
Retorna los reviews del libro con identificador id. |
POST /books/id/reviews |
Crea un review nuevo para el libro con identificador id. |
PUT /books/id/reviews/id2 |
Actualiza el review id2 del libro con identificador id. |
DELETE /books/id/reviews/id2 |
Borra del libro id el review con identificador id2. |
Book y Author sí son ambos recursos raíz: cada uno tiene sus propios servicios de creación (POST /books, POST /authors). Lo que necesitamos es un servicio para asociar un autor existente a un libro existente. Esa asociación también se modela como subrecurso, con URL:
/books/23/authors
Servicio sobre el subrecurso books/id/authors |
Descripción |
|---|---|
GET /books/id/authors |
Retorna los authors del libro con identificador id. |
POST /books/id/authors/id2 |
Crea la asociación entre el libro id y el author id2. |
DELETE /books/id/authors/id2 |
Elimina la asociación entre el libro id y el author id2. |