Apis que no odiaré

No puedo negar que estoy un poco cansado de oír a las vedettes de la programación. Ultimamente tiendo a conceder mas crédito a blogeros, amigos, comentaristas de reddit y en general programadores anónimos que realizan cada día su trabajo en un entorno real.

Porque seamos serios, para estos tíos solo hay una forma correcta de hacer las cosas: hacerlas bien. Y no es lo que encuentro cada día en el mundo real (odio hablar así pero…).

Mi entorno es la trinchera ya sabes.

 

Dicho esto, valoro mucho a estos personajes, es solo que muchas veces su discurso perfecto emitido desde la torre de marfil no casa bien en un mundo real de deadlines y balances empresariales. Voy a tomar algunas notas rápidas sobre lo que me encuentre en este libro. No pretendo ser pedagógico, mas bien quiero tener algunas notas que me puedan ayudar cuando construya la próxima api.

Lo primero que necesitas para hacer apis que no odies es un programa que genere datos de prueba. Un seeder que llene tu base de datos de bullshit para hacer tests. En el libro recomienda Faker. Lo probaré.

Después hay que planear y crear las URL (endpoints). Para ello vamos a crear una lista de nombres, y para cada nombre, una lista de acciones que contendrán las 4 tipicas de cualquier CRUD mas cualquier otra que necesitemos, incluyendo posibles filtros para los datos (parámetros). Por ejemplo:

Lugares:

  • Create
  • Read
  • Update
  • Delete
  • List (lat, lon)
  • Images

En este caso necesitamos una lista de lugares, que opcionalmente se pueda filtrar por latitud y longitud. también podemos subir imágenes. Si nuestra api solo permite subir una imagen, que sustituye a la anterior, el recurso sera /image.


Básicos de Restful

  • GET /resources
  • GET /resources/x
  • GET /resources/x,y,z
  • GET /users/x/places/y (embebbed data)

Recordar que x, y o z aquí es mejor que sean UUID y no un simple autoincrement. Los autoincrement ofrecen una información a los usuarios de la api que normalmente preferimos ocultar.

  • DELETE /places/x
  • DELETE /places/x,y,z
  • DELETE /places
  • DELETE /places/x/image
  • DELETE /places/x/images

PUT / POST

Usamos PUT cuando conocemos la url completa de antemano y no importa cuantas veces se ejecute, el resultado sera el mismo. En los otros casos, usa POST. Por ejemplo, si quiere subir la imagen de un usuario:

PUT /users/x/image

Conozco la url completa y aunque use el endpoint diez veces el resultado sera siempre el mismo, sustituir la imagen existente.

POST /computers/x/settings

Aqui puedo subir a cada vez una setting diferente, así que el resultado no es necesariamente el mismo.

Algún consejo mas

  • Usa siempre plural en los recursos, es mas consistente.
  • No pongas verbos en la url, solo nombres (recursos)
  • Cada recurso / nombre tiene su controlador. El resto de normas tiene cierta flexibilidad, por ejemplo si un recurso solo atañe a users, tal vez podamos tratarlo en el UsersController.

Input & Output

O lo que es lo mismo, como organizar y estructurar las requests y las responses de nuestra api:

Requests:

POST /authors/4/book HTTP/1.1
Host: api.cool-authors.com
Authorization : Bearer cn389ncoiwuencr
Content-Type: application/json
{"user_id" : 2 }

Responses:

HTTP/1.1 200 OK
Server: nginx
Content-Type: application/json
...
{
    "id":"44556",
    "book": {
        "id":"4562"
    }
}

Como Content-Type usaremos siempre application/json que es mucho mas claro y legible y da lugar a menos errores y mas precisión en el tipado que por ejemplo application/x-www-form-urlencoded, muy usado en PHP. Hay muy pocos casos en los que xml sea necesario. Personalmente me parece odioso XML y SOAP y solo trabajo en ello como mercenario o obligado por circunstancias particulares que espero se repitan lo menos posible en el futuro.

KISS

Sobre la estructura del contenido no hay consenso.

  • JSON api (uno o varios resources, no hay diferencia, devuelve un array de objetos)
  • Twitter (si hay varios recursos devuelve un array, si no devuelve el objeto)
  • Facebook (como twitter pero incluye los recursos dentro de un namespace, lo que facilita añadir metadata potencialmente)

Se recomienda incluir siempre un namespace que agrupe el recurso único o la colección:

{
  "data": {
    "id": "503294",
    "name": "defnx"
  }
}

{
  "data": [
    {
      "id": "503294",
      "name": "defnx"
    },
    {
      "id": "503296",
      "name": "yuale"
    }
  ]
}

Mi experiencia personal me hace preferir devolver siempre una colección, aunque puedo ver ventajas en ambos estilos. Tal vez solo sea una cuestión de gustos personales donde finalemente se impondrá la visión del manager (generalmente arbitraria).


Códigos de estado HTTP, errores y mensajes

Incluso cuando algo va mal preparando la respuesta, devolvemos datos. La información sobre como ha ido en el servidor llega al cliente gracias a los codigos HTTP y los mensajes de error. Algunos códigos HTTP típicos son:

  • 200 – Todo ha ido bien
  • 201 – Algo se ha creado bien
  • 202 – Request aceptada, se procesa asíncronamente
  • 400 – Error en la estructura de la request
  • 401 – No autorizado (no hay usuario)
  • 403 – El usuario no puede acceder esos datos
  • 404 – La ruta no es valida o el recurso no existe
  • 405 – El método no esta permitido
  • 410 – Los datos han sido borrados, desactivados…
  • 500 – Un error inesperado en el servidor
  • 503 – Servicio no disponible en este momento

Los códigos http deben usarse como categorías generales de errores, pero probablemente vamos a necesitar mas granularidad en la respuesta, creando nuestros propios tipos de errores y mensajes explicativos.

{
  "error": {
    "type": "OAuthException",
    "code": "err-1234",
    "message": "sorry bro",
    "href": "http://api.example.com/docs/errors/#err-1234"
  }
}

Si hay varios, se pueden agrupar en un array “errors”. La estructura de un objeto error puede contener algunos elementos que especifica la json-api, como id, href, status, code, title, detail, links o path.


Testing

Como los unit tests en una api con muchos endpoints muchas veces se convierten en una pesadilla, en el libro se propone usar Behat en un entorno de desarrollo PHP. Se trata de una herramienta BDD (Behaviour Driven Development) que viene a ser lo mismo que Cucumber.

Feature: Users

Scenario: Finding a specific user
    When I request "GET /users/1"
    Then I get a "200" response
    Then I scope into de "data" property
        And the properties exist:
            """
            id
            name
           """
        And the "id" property is an integer

Con una herramienta de este tipo debemos definir “Features” que corresponden a nuestros recursos (los nombres). A continuación definimos “Scenarios” que corresponden de alguna forma a los métodos _test de nuestro set de unit tests. Y para cada escenario definimos “Steps” que pueden recordar los asserts de nuestros unit tests. Todo ello expresado casi en lenguaje natural.

Ahora que no hay nada de código escrito, es el mejor momento para escribir todos los tests de esta forma. Así continua la reflexión sobre la api antes de ponerse a escribir líneas.

Para poder ejecutar estos tests, hay que darle a la librería algunos detalles como el host de nuestra api. Mas sobre como usar behat aquí.


Outputting data

En un capitulo anterior el autor recomienda usar un namespace para agrupar los resultados, el usa “data”.

También devuelve un array de objetos si hay varios recursos en la respuesta:

{
  "data": [
    {
      "id": 5,
      "name": "defn.es"
    },
    {
      "id": 6,
      "name": "abrahammesa.com"
    }
  ]
}

O el objeto mismo directamente si solo hay un recurso:

{
  "data": {
    "id": 5,
    "name": "defn.es"
  }
}

Respecto al controlador, no debería hacer uso de un ORM u otro mecanismo de recuperación de datos de la base directamente para devolver los datos. Es decir que entre la recuperación de datos y el return debe haber una selección de los elementos concretos que deseamos devolver y castings adecuados a los tipos de datos que esperamos. Aqui algunas razones:

  • Rendimiento
  • Casting en string de las extensions PHP (boolean se convierte en “1” o “0”)
  • Seguridad (clientes de la api que potencialmente ven datos comprometedores)
  • Estabilidad

Para hacer ese tipo de transformaciones podemos usar la librería Fractal.

Si nuestro modelo de datos cambia debemos ser cuidadosos para dejar esos cambios dentro del scope de nuestra aplicación y que no salgan al exterior si no es indispensable.

Los errores tendrán  cada tipo que usemos su método correspondiente, y tendremos la posibilidad de definir errores customizados si es necesario en un método genérico que responde con un error. Tiendo a pensar que los códigos de error HTTP ya gestionan muchos casos, pero que es posible que en efecto necesitemos mas granularidad.


Relación entre los datos

Nuestro modelo de base de datos no tiene porque ser fiel a la estructura de nuestra api, aunque en la mayoría de los casos se parecerán mucho. Cuando nuestro recurso tiene datos asociados tenemos que encontrar el equilibrio entre enviar todos los datos relacionados, o enviar urls para que sea el cliente quien recupere los datos. Es decir, entre enviar muchos Kb y tener menos trafico http, o enviar pocos Kb y tener mas requests http.

Otras soluciones:

Enviar los identificadores de los objetos relacionados y hacer una segunda requests con todos ellos.

{
  "data": {
    "id": 5,
    "name": "defn.es",
    "_links": {
      "domains": ["3", "4"]
    }
  }
}

Sideloading: Evita la duplicación de datos embebidos.

{
  "data": [
    {
      "id": 5,
      "name": "defn.es",
      "_links": {
        "domains": ["3", "4"]
      }
    },
    {
      "id": 6,
      "name": "defn.com",
      "_links": {
        "domains": ["3", "4"]
      }
    }
  ],
  "_linked": {
    "domains": [
      {
        "id": "3",
        "name": "www.defn.com"
      },
      {
        "id": "4",
        "name": "api.defn.com"
      }
    ]  
  }
}

Embedded Documents (Nesting)

Para mi es la mejor de las que expone el libro, porque el cliente obtiene exactamente lo que necesita y se reduce el numero de requests, optimizando la descarga de datos. Se trata de que nuestra api de lo que se le pide.

/posts?include=comments,author.image

Debugging

Postman! El libro no habla de ello, pero me encanta. Junto con un browser claro.


Authentication

Autenticarse en una api sirve para hacer un seguimiento de los usuarios, dar contexto a los usuarios, dar o quitar acceso a ciertos recursos, activar o desactivar cuentas, etc.

A veces nuestra api no necesita esta funcionalidad, porque sus recursos son públicos y son de lectura únicamente o tal vez se encuentra en una red privada, protegida de otras formas. Para el resto de casos seguramente convenga implementar un login.

Basic

Se trata de enviar un nombre y una contraseña. Es fácil de implementar pero no es segura, especialmente en http. En cada request el nombre y la contraseña son enviados en el header, lo cual es potencialmente un riesgo.

Digest

Mejora la seguridad con respecto a Basic. En lugar de enviar la contraseña envía el hash MD5 calculado de ella. El problema es que sigue siendo una forma no muy segura de logarse en la que hay que enviar en cada request la contraseña. Sobre SSL es bastante seguro.

OAuth 1.0a

Es muy segura y no envía la contraseña constantemente en el header. Pero es difícil trabajar con ello. El token nunca cambia así que la seguridad se compromete si se usa mucho tiempo. Se trata de una tecnología en desuso.

OAuth 2.0

Requiere SSL. El usuario recibe un token de acceso que puede usar en cada request. Estos tokens caducan al cabo de un tiempo determinado. Para controlar ese tiempo, puedes recoger una exception “Not Authorized”, y pedir otro token en ese wrapper. Implementarlo a mano es muy difícil, así que mejor usar alguna librería. El libro recomienda dos (en el caso de PHP):


Pagination

De lo que hablamos aquí es de dividir el resultado potencial de una sola request imaginaria, en varias requests. Esto nos ayuda a presentar los datos de forma mas amable para el cliente así como mejorar la performance de la llamada (http y database).

Para conseguirlo vamos a enviar un parámetro mas en nuestra query string:

/domains?number=10

La response incluiría información sobre la pagination así como la url que debemos llamar para obtener la siguiente pagina:

{
  "data": [
    
  ],
  "pagination": {
    "total": 100,
    "count": 10,
    "per_page": 10,
    "current_page": 1,
    "total_pages": 10,
    "next_url": "/domains?page=2&number=10"
  }
}

Sin duda contar los totales puede ser pesado en grandes bases de datos. En muchos casos hay que cachear el resultado, precalcularlo o filtrarlo a través de otros atributos:

/domains?iso=es&page=2&number=120

Otra forma de obtener una pagination es gracias a los cursores, una respuesta podría quedar así:

{
  "data": [

  ],
  "pagination": {
    "cursors": {
      "after": 10,
      "next_url": "/domains?cursor=10&number=10"
    }
  }
}

Es una forma eficiente de tratar con muchos datos sin contarlos, el problema es que siempre habrá una ultima request que no retornara nada.


Content negotiation

Envía el tipo de documento que deseas en el Accept Header y tratalo en la API recuperando el MimeType. Si el tipo no existe, devuelve un error 415.

GET /domains HTTP/1.1
Host: api.test.loc
Accept: application/json

HATEOAS

Una api no puede ser considerada restfull si no incluye hiperlinks en sus respuestas. Gracias a ellos podemos explorar el resto de la api y trabajar con los recursos cómodamente.

{
  "data": [
      ...
  ],
  "link": [
    {
      "rel": "self",
      "uri": "/domains/1"
    },
    {
      "rel": "domain.subdomains",
      "uri": "/domains/1/subdomains"
    }
  ]
}

Versioning

Por ultimo en este largo post, solo comentar que respecto al versionado (algo a lo que se vera obligada nuestra api de una forma u otra) parece que no hay una solución ideal, ya que todas rompen de una forma u otra RestFull.

A mi la aproximación que mas me gusta (Sturgeon approved) es la que usa el header para indicar la versión que se usa, a través del atributo “Content”, de la manera en que se hace en la api de Github:

Accept: application/vnd.github.v3+json

De esta forma las URL de nuestros recursos serán siempre iguales, respeta las ideas de HATEOAS, es simple de usar y no hay problemas con el cache.