Ir al contenido

Diseño de un API REST

Diseñar un API REST significa tomar cuatro decisiones, en este orden:

  1. Decidir cuáles serán los recursos a los que el cliente del API tendrá acceso.
  2. Por cada recurso, decidir cuál será la URL que lo identifica.
  3. Por cada recurso, decidir cuál o cuáles serán sus representaciones.
  4. Decidir cuáles serán los servicios (GET, POST, PUT, DELETE) y su significado preciso en cada caso.

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.

Paso 3: las representaciones de cada recurso

Sección titulada «Paso 3: las representaciones de cada recurso»

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?

Representación básica y representación detallada

Sección titulada «Representación básica y representación detallada»

Para los ejemplos del curso tomamos la decisión de diseño de tener, para cada recurso, hasta dos representaciones JSON distintas:

  • Básica: contiene los atributos propios de la clase y los de la clase destino de las asociaciones de cardinalidad 1 (muchos-a-uno o uno-a-uno).
  • Detallada: contiene los atributos de la representación básica, más las representaciones básicas de las relaciones de cardinalidad múltiple.

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.

El ejemplo completo: representaciones del recurso book

Sección titulada «El ejemplo completo: representaciones del recurso book»

Aplicando 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.

Asociación Book–Author: una asociación entre dos recursos raíz

Sección titulada «Asociación Book–Author: una asociación entre dos recursos raíz»

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.
1. ¿Cuál es el primer paso al diseñar un API REST?
2. ¿Qué diferencia a la representación básica de un recurso de su representación detallada?
3. En el modelo Book–Review (composición) y Book–Author (asociación), ¿por qué reviews no es un recurso raíz?
4. ¿Qué hace POST /books/23/authors/7?
5. ¿Por qué el recurso authors/ultimopremionobel es una decisión de diseño válida, aunque no use un id numérico?
6. ¿Qué diferencia a books/losmasvendidosestemes de authors/ultimopremionobel?