Diseñando tus API’s REST con RAML

Hablando del diseño de API’s existe un principio que nos dice que todo software que se comunica con otro está acoplado de alguna manera, este acoplamiento puede ser débil o fuerte; El principio de Acoplamiento Débil del Servicio (Service Loose Coupling) descrito por Thomas Erl en su libro SOA Principles of Service Design nos dice que existen dos tipos de acoplamientos: positivos y acoplamientos negativos. Un tipo de acoplamiento positivo es aquel en el que el diseño del contrato del servicio se realiza antes de implementar la lógica interna al servicio ( aproach conocido como API First). Esto significa que yo como diseñador de API voy a poner atención principalmente en la definición del contrato sin voltear a ver en estos momentos al Backend para saber como lo vamos a implementar, esos detalles se describirán en etapas posteriores al diseño del contrato.

Entre otras ventajas, este enfoque también nos brinda el beneficio de permitirnos reemplazar la lógica del Backend de manera transparente y con un impacto prácticamente nulo de cara a aquellos que consumen el servicio, existen varios lenguajes en la industria que nos permiten definir el contrato de nuestra API REST, el lenguaje RAML es uno de ellos, nos permite enfocarnos en el diseño de nuestra API antes de implementarla.

¿Qué es RAML?

RAML es un lenguaje de modelado para APIs RESTful, el cual básicamente nos permite escribir el contrato del API y todos sus aspectos como definir sus recursos, métodos, parámetros, respuestas, tipos de medios y otros componentes HTTP básicos. Finalmente, puede usarse también para generar documentación más amigable de cara a los consumidores del API.

Pero RAML no es el único lenguaje usado en la industria para describir API’s RESTful, también existen otros productos como Open API Swagger ambos con mucha popularidad, sin embargo existen algunas diferencias entre ambos:

  • La gran característica para Swagger es que está diseñado como una especificación con aproach bottom-up (pone atención principalmente en la implementación y de ahí parte para exponer el contrato) , lo contrario a RAML que es una especificación del tipo top-down.
  • Swagger cuenta con una gran comunidad y algunas herramientas disponibles cosa que con RAML aún no es tanto, existen pocas herramientas para RAML por lo que algunas empresas que lo usan han optado por desarrollar sus propias herramientas customizadas a las necesidades propias.

Nomenclatura de RAML

RAML se basa en otro lenguaje llamado YAML el cual es un formato de datos legible por humanos (human friendly) que se alinea bien con los objetivos de diseño de la especificación RAML. Al igual que en YAML, todos los nodos son como claves, valores y etiquetas lo cual ayuda a comprender mejor la lectura de un fichero RAML.

RAML 1.0 (versión que se describe en este artículo) es la versión más actual del lenguaje, de tal manera que son documentos compatibles con YAML 1.2 que comienzan con una línea de comentarios YAML requerida que indica la versión RAML, de la siguiente manera:

#%RAML 1.0
title: My API

Types

RAML usa estructuras llamadas Types para especificar el modelo de entidades a utilizar dentro del API, es un modelo sencillo de representar a las entidades y a mi parecer más fácil de utilizar que otros modelos como por ejemplo JSON Schema.

Con la palabra reservada ‘Library’ en el encabezado especificamos que estamos creando una librería en este caso que contiene el type llamado user. Aunque podemos declarar varios types en una misma librería se suele crear una librería por cada Type.

#%RAML 1.0 Library

types:
  user:
    type: object
    properties:
      firstname: string
      lastname:  string
      age:       number

Un Type puede tener atributos sencillos como parte de su estructura o tener atributos más complejos (anidados) como en el siguiente ejemplo donde el atributo ‘status’ a su vez tiene mas atributos:

types:
  card:
    type: object
    properties:
      alias:
        type: string
        description: |
          Card alias. This attribute allows customers to assign a custom name to their cards.
        required: false
      status:
        type: object
        description: |
          Card current status.
        required: false
        properties:
          id:
            type: string
            enum: [INOPERATIVE, BLOCKED, PENDING_EMBOSSING, PENDING_DELIVERY, PRE_ACTIVATED]
           description: |
              Card status identifier.
          reason:
            type: string
            description: |
              Reason of the state of the card.
            required: false

Tipos de datos

RAML define diversos tipos de datos, parecidos a los que se definen en lenguajes de programación como Java:

  • object
  • array
  • union
  • tipos escalares: number, boolean, string, date-only, time-only, datetime-only, datetime, o integer

Los tipos se pueden clasificar respecto a los siguientes puntos:

  • Los tipos se clasifican dentro de 4 familias: external, object, array, y scalar.
  • Los ‘facets’ son configuraciones especiales, algunos de los facets permitidos son: properties, minPoperties, maxPoperties, additionalProperties, discriminator, y discriminatorValue.
  • Solo los objetos pueden declarar el ‘properties’ facelet.

Herencia

En RAML 1.0 se permite la Herencia, por ejemplo se puede tener un Type que represente a una Persona y aparte tener a un Type Teacher que herede algunas de sus atributos de Person:

types:
  person:
    type: object
    properties:
      name: string
  teacher:
    type: Person
properties:
      level: string

Resource Definitions

¿Cómo se modelan los recursos en RAML?

En el siguiente ejemplo vamos a modelar en REST un listado de películas y queremos definir un filtro opcional (queryParam) por el año en que salió la película, a continuación esperamos que el servidor nos conteste con un arreglo de películas que cumplen con ese criterio de búsqueda.

Técnicamente hablando definimos un recurso que se llamará /films el cual podríamos invocar de esta manera:

GET /films?year=

La declaración de ese endPoint en lenguaje RAML quedaría como:

/films:
  description: |
    Manage Films

  get:
    description: |
      Service for get films.

    queryParameters:
      year:
        description: Filters the film by release year.
        type: string
        example: «2009»
        required: false
    responses:
      200:
        body:
          application/json:
            type: object
            properties:
              data:
                type: films.films
                required: false

Métodos

Los métodos http cuelgan de cada recurso, subrecurso o URI Parameter como se ejemplifica a continuación:

/books:
    get:
    post:
    /{id}:
      get:
      put:
      delete:
      /chapters:
        get:

Responses

La definición de las respuestas deben venir acompañados del código http y del formato en el que vendrán los datos, generalmente las respuestas vienen en formato ‘application/json’. Se deben poner los códigos http de éxito el cual generalmente es uno 2XX así como los códigos de error, por ejemplo la invocación al endPoint puede regresar un error de tipo 404 ‘Recurso no encontrado’.

/books:
  …
  /{id}:
    get:
      description: Get a Book by id
      responses:
        200:
          body:
            application/json:
              type: book.book
404:
            description: Not found             

Ejemplo de API para gestión de películas

Para este blog a manera de ejemplo práctico vamos a modelar con RAML un API de películas que permita:

  • Ver un listado de películas 
    • Filtrar por año de estreno.
    • Ver información detallada de la película como la duración, el genero, el año de estreno o el ranking de calificación de los usuarios.
    • Listar  los directores que intervienen.
    • Listar los actores que intervienen.
    • Listar los personajes dentro de la película.
    • Listar las imágenes asociadas a un película (por ejemplo una imagen pequeña para mostrarse en un listado y una imagen grande para mostrase en un detalle).
    • Poder ver el detalle de un personaje de la película.
  • Dar de alta una nueva película.

De tal forma que se van a generar los siguientes endPoints:

Modelo de Datos

El modelo de datos detrás de un API puede especificarse mediante cualquier lenguaje de modelado de datos conceptual como por ejemplo un modelo de entidad-relación simple o con los clásicos diagramas de clase UML.

En este ejemplo utilizaremos un modelo entidad-relación para representar de manera gráfica la relación entre las entidades identificadas así como la definición de sus atributos.

La imagen tiene un atributo ALT vacío; su nombre de archivo es modelo_peliculas.png

Modelo de types de películas en RAML

En este caso lo primero que vamos a definir en RAML son los Types que representan al modelo de lo cuales obtendremos los siguientes archivos:

actor.raml
actors.raml
character.raml
characters.raml
director.raml
directors.raml
film.raml
films.raml
genre.raml
image.raml
person.raml
persons.raml

Cabe destacar que vamos a generar un Type que usaremos para representar el listado de un recurso y otro Type que represente el detalle del recurso (lo hacemos así porque para un listado solo vamos a mostrar algunos atributos y para el detalle otros), para el listado se nombrará al Type en plural. Por ejemplo tenemos un Type ‘film.raml’ que usaremos para mostrar los atributos de una película en un detalle y el Type ‘films.raml’ para mostrar los atributos de la película en el listado.

A continuación describiremos algunos de los types identificados (la lista completa se puede encontrar el el repositorio de GitHub anexo a este artículo).

person.raml

#%RAML 1.0 Library
uses:
 
types:
  person:
    type: object
    properties:
      name:
        type: string
        required: false
        description: |
          Person name.
      lastName:
        type: string
        required: false
        description: |
          Person last name.
      secondLastName:
        type: string
        required: false
        description: |
          Person second last name.
      birthDate:
        type: string
        required: false
        description: |
          Person birthdate.  

A continuación definiremos la entidad que representa a un actor el cual hereda algunas de sus propiedades de person.

Podemos hacer uso del nodo ‘uses’ para importar librerías externas, en este caso vamos a exportar la entidad ‘person’:

#%RAML 1.0 Library
uses:
  person: person.raml
 
types:
  actor:
    type: person.person
    properties:
      actorId:
        type: string
        required: false
        description: |
          Unique actor identifier.
      actorIntro:
        type: string
        required: false
        description: |
          Actor intro text.

A continuación la definición del Type que representa al genero de la película, aquí podemos ver que hacemos uso de la estructura ‘enum’ el cual nos permite delimitar los posibles valores del genero:

#%RAML 1.0 Library
uses:
  
types:
  genre:
    type: object    
    properties:
      genreId:
        type: string
        required: false
        description: |
          Unique director identifier.   
        enum: [«Sci-Fi»,»Action»,»Adventure»,»Fantasy»,»Thriller»,»Crime», «Drama»]
      description:
        type: string
        required: false
        description: |
          Genre description. 

Y a continuación la representación de un listado de películas:

#%RAML 1.0 Library
uses:
  character: character.raml
  director: director.raml
  genre: genre.raml
  image: image.raml

types:
  films:
    type: array
    items:
      properties:
        filmId:
          type: string
          required: false
          description: |
            Unique film identifier.
        name:
          type: string
          required: false
          description: |
            Film name.
        duration:
          type: string
          required: false
          description: |
            duration in minutes.
        principalCast: 
          type: character.character[]
          required: false
          description: |
            film principal cast.  
        genres:
          type: genre.genre[]
          required: false
          description: |
            film genres.
        images:
          type: image.image[]
          required: false
          description: |
            film genres.
        year:
          type: number
          required: false
          description: |
            film release year.
        rank:
          type: number
          required: false
          description: |
            film users rank.

Definir los recursos del API de películas

Para hacer la definición de los recursos de nuestra API la haremos en un archivo llamado ‘api.raml’ donde estarán definidos todos los recursos del API. Para comenzar describiremos la sección de los encabezados y la importación de los types.

#%RAML 1.0
title: FILMS
version: v0.1.0
baseUri: https://www.mysite.com/filmsapi/v0

uses:
  actors: types/actors.raml
  directors: types/directors.raml
  characters: types/characters.raml
  character: types/character.raml
  films: types/films.raml
  film: types/film.raml
  image: types/image.raml

Listado de Películas

Ahora vamos a modelar la siguiente funcionalidad con RAML:

  • Vamos a definir un recurso REST llamado ‘/films’ que gestione las operaciones a realizar sobre las películas.
  • Vamos a definir un método GET para poder obtener un listado de películas.
  • Vamos a definir un ‘queryParameter’ llamado ‘year’ con el cual vamos a poder listar las películas filtradas con una fecha de lanzamiento dada.
/films:
  description: |
    Manage Films

  get:
    description: |
      Service for get films.

    queryParameters:
      year:
        description: Filters the film by release year.
        type: string
        example: «2009»
        required: false
    responses:
      200:
        body:
          application/json:
            type: object
            properties:
              data:
                type: films.films
                required: false
      204:
        description: Not Content
      400:
        description: Bad Request
      401:
        description: Unauthorized
      403:
        description: Forbidden
      500:
        description: Server Error      

Las partes importantes de la definición del bloque anterior es que en la respuesta vamos a modelar que se espera que el servicio regrese la información en notación JSON dentro de un nodo llamado data el cual es de tipo array y cada elemento del array regresará la información de cada película como se ve en el siguiente ejemplo JSON:

«data»: [
    {
      «filmId»: «67490436a8be5153fe7d8bc54344fa1e»,
      «name»: «The Lord of the Rings: The Return of King»,
      «duration»: «3h 21m»,
      «principalCast»: [
        {
          «name»: «Elijah»,
          «lastName»: «Wood»,
          «actorId»: «60490436a8be5153fe7d8bc54344fa78»
        },
        {
          «name»: «Viggo»,
          «lastName»: «Mortensen»,
          «actorId»: «60390426a8be8153fe7d8bc54344fa7e»
        }
      ],
      «genres»: [
        {
          «genreId»: «1»,
          «description»: «Adventure»
        },
        {
          «genreId»: «2»,
          «description»: «Drama»
        },
        {
          «genreId»: «3»,
          «description»: «Fantasy»
        }
      ],
      «images»: [
        {
          «imageId»: «60390426a8be8153fe7d8bc54344fa7e»,
          «imageName»: «small»,
          «url»: «images/igaj_2x_20.jpg»,
          «size»: «20 x 15»
        }
      ],
      «year»: 2003,
      «rank»: 8.9
    },
    {
      «filmId»: «66490436a8be5153ae7d8bc54344fa17»,
      «name»: «21 Grams»,
      «duration»: «2h 4m»,
      «principalCast»: [
        {
          «name»: «Sean»,
          «lastName»: «Penn»,
          «actorId»: «65490436b8be5153fe7y8bc54344fa78»
        },
        {
          «name»: «Benicio»,
          «lastName»: «Del Toro»,
          «actorId»: «10390426a8be8153fe7d8bc54344fa7f»
        }
      ],
      «genres»: [
        {
          «genreId»: «1»,
          «description»: «Crime»
        },
        {
          «genreId»: «2»,
          «description»: «Drama»
        },
        {
          «genreId»: «3»,
          «description»: «Thriller»
        }
      ],
      «images»: [
        {
          «imageId»: «80390426a8be8153fe7d8bc54344fa8e»,
          «imageName»: «small»,
          «url»: «images/igfrj_2x_20.jpg»,
          «size»: «20 x 15»
        }
      ],
      «year»: 2003,
      «rank»: 7.7
    }
  ]
}

Definiendo ejemplos JSON en el API

Se pueden definir ejemplos JSON para ayudarle al consumidor del API a darse una idea de la información que regresará el endPoint como se ve a continuación. El archivo llamado ‘get_200.json’ contiene el ejemplo de los datos regresados por el endPoint:

/films:
  description: |
    Manage Films

  get:
    description: |
      Service for get films.

    queryParameters:
      year:
        description: Filters the film by release year.
        type: string
        example: «2009»
        required: false
    responses:
      200:
        body:
          application/json:
            example: !include examples/films/get_200.json
            type: object
            properties:
              data:
                type: films.films
                required: false

Obtener el detalle de una película

Ahora vamos a definir el URI parameter para poder obtener el detalle de una película, las llaves {} alrededor del nombre del parámetro son los que nos indican la declaración del URI parameter. La declaración se hace de esta manera:

GET /films/67490436a8be5153fe7d8bc54344fa1e

/{film-id}:
    description: |
      Manages the information of a specific Film.
    uriParameters:
      film-id:
        description: «Unique film identifier»
        type: string
        example: «1234»

    get:
      description: |
          Service for retrieving the information associated to a specific film.

      responses:
        200:
          description: Ok
          body:
            application/json:
example: !include examples/films/film-id/get_200.json
              type: object
              properties:
                data:
                  type: film.film       
        401:
          description: Unauthorized
        403:
          description: Forbidden
        404:
          description: Not found
        500:
          description: Server Error

Poder ver el detalle de un personaje dentro de la película

A continuación veremos el fragmento donde se declara la sección para poder ver el detalle de un personaje de la película:

GET /films/67490436a8be5153fe7d8bc54344fa1e/characters/00490436a8be5153fe7d8bc54344fa4

/{character-id}:
        description: |
          Manages the information of a specific Character.
        uriParameters:
          film-id:
            description: «Unique character identifier»
            type: string
            example: «1234»
        get:
          description: |
              Service for retrieving the information associated to a specific character.

          responses:
            200:
              description: Ok
              body:
                application/json:
                  example: !include examples/films/film-id/characters/character-id/get_200.json
                  type: object
                  properties:
                    data:
                      type: character.character       
            401:
              description: Unauthorized
            403:
              description: Forbidden
            404:
              description: Not found
            500:
              description: Server Error     

Crear una película nueva

El siguiente fragmento muestra como crear una nueva película:

POST /films/

post:
    description: |
      Service to create a new Film.

    body:
      application/json:
        example: !include examples/films/post_request.json
        type: film.film
    responses:
      201:
        description: Created
        headers:
          Location:
            displayName: location
            description: This header must provide the URI to reach the created resource.
            type: string
            required: true
            example: «/films/1»
        body:
          application/json:
            example: !include examples/films/post_201.json
            type: object
            properties:
              data:
                type: film.film
      400:
        body:
          application/json:
            type: object
            properties:
              message:
                type: error.error
                required: false
        description: Bad Request
      401:
        description: Unauthorized
      403:
        description: Forbidden
      500:
        description: Server Error 

En el ejemplo mostrado en el fragmento anterior (a diferencia de los métodos GET) para métodos POST se tiene que enviar un payload de entrada además de que también el endPoint regresará un encabezado http llamado ‘Location’ con la ubicación del recurso recién creado.

Conclusión

Este artículo habla de la introducción al lenguaje para modelar API’s REST llamado RAML (RESTful API Modeling Language), se describieron algunas sintaxis básicas del lenguaje, además se demostró su uso con un ejemplo práctico para un API de gestión de películas. El código completo de este ejemplo se puede descargar desde mi GitHub el cual está disponible aquí.

En una próxima entrega describiré sintaxis más complejas del lenguaje RAML como es el uso de traits, resource-types y custom-anotations, features que nos van a permitir escribir código RAML de manera más eficiente y reutilizable. También en futuras entregas hablaré de una herramienta que nos permitirá diseñar nuestras API’s RAML de manera gráfica. Por el momento les dejo estas dos referencias de herramientas para editar API’s RESTful usando RAML:

Para escribir el código de este ejemplo lo he hecho usando Sublime usando el Plugin para RAML y me ha resultado bastante útil, más adelante veremos que existen herramientas más amigables para la edición de nuestro código de momento nos va bien hacerlo en este editor.

También dejo el link con referencia a la especificación del lenguaje en su versión 1.0 la más actual.