Tutorial Compojure IV: Exponiendo un API REST

Después de crear la aplicación web, generar vistas con hiccup y preparar un modelo de datos usando atoms, sólo nos falta desarrollar un API REST que nos permita completar nuestra aplicación web con compojure.

Antes de empezar, recordemos cómo es nuesta única página para ver qué vamos a necesitar:

Mockup de la página de Mega Facts

En esta página usaremos jQuery para, mediante ajax y json, obtener la lista de facts asociados a un héroe y añadir un nuevo fact de un héroe. Esto lo haremos a través de las siguientes rutas:

  • GET /hero/name/facts: nos devolverá un array de strings con los facts asociados al héroe con nombre name.
  • POST /hero/name/facts: recibirá en el cuerpo de la petición un documento json de con el formato { "fact": "text" } para añadir al héroe con nombre name un nuevo fact con texto text.

Usando un middleware para lidiar con json en compojure

Puesto que nuestra API va a estar basada en json, lo primero que necesitamos es una forma de poder generar documentos json para enviarlos a los clientes y procesar las peticiones json que nos envíen. Para ello vamos a utilizar un middleware de ring, pero empecemos por el principio, ¿qué es un middleware?

En la primera parte de este tutorial hablamos de los conceptos en que se basa ring (y, por tanto, compojure), y mencionamos los handlers para definir rutas y los adapters encargados de permitirnos ejecutar la aplicación web en distintos servidores.

A estos conceptos básicos debemos sumarles los middlewares, que se encargan de aumentar la funcionalidad implementada en los handlers actuando como decoradores de la petición y/o la respuesta. Para los que conocen ASP.NET MVC, sería algo similar al concepto de action filters y message handlers.

En el fondo un middleware lo único que hace es construir una nueva función a partir de nuestro handler. Esa nueva función se encarga de pre-procesar la petición y post-procesar la respuesta. Podemos aplicarlos a nivel de aplicación o a nivel de ruta individual, aunque lo más normal es utilizarlos a nivel de aplicación ya que el propio middleware se encarga de detectar cuando tiene que hacer algo o cuando debe pasar la llamada sin más al handler que encapsula.

En nuestro caso usaremos el middleware ring-json para lo que deberemos añadirlo como dependencia en el fichero project.clj.

De este middleware usaremos las funciones wrap-json-response, que nos permite devolver una estructura de datos clojure serializada en json como respuesta, y wrap-json-body, que se encarga de parsear los datos json recibidos en el cuerpo de una petición y añadirlos a la clave :body del map que clojure pasa a nuestro a handler.

Para configurarlos en la aplicación, sólo hace falta añadirlos a la definición del sitio que encontraremos en el fichero ./src/compojure_sample/handler.clj:

(def app
  (handler/site (-> app-routes 
                    wrap-json-response 
                    wrap-json-body )))

Nuestro API

Ahora que tenemos claras las rutas que debemos exponer en nuestro API y cómo generar y pasear json, la implementación es trivial aprovechando las funciones que creamos en nuestro modelo. La definición completa de rutas en el fichero ./src/compojure_sample/handlers.clj queda como sigue:

(defroutes app-routes
  (GET "/" [] 
    (view/index (model/get-heroes)))
 
  (GET "/hero/:name/facts" [name] 
    (response (model/get-facts name)))

  (POST "/hero/:name/facts" {{fact "fact"} :body, 
                             {name :name} :params} 
    (model/add-fact! name fact)
    (response {:status "OK"}))

  (route/files "/public" {:root "public"})
  (route/not-found "Not Found"))

La primera ruta que hemos añadido, GET "/hero/:name/facts" utiliza la función response incluida en el espacio de nombres ring.util.response para convertir el vector con los facts asociados a un héroe en una respuesta json válida.

La otra ruta nueva, POST "/hero/:name/facts" ... aprovecha las capacidades de «desestructurado» (destructuring) para acceder a determinados parámetros de la petición. Aquí se puede ver el efecto del middleware wrap-json-body, que ha dejado en el map :body el contenido de la petición json.

El cliente javascript

La parte javascript para interactuar con este API no tiene mucha complicación y es prácticamente calcada de la que describía en el tutorial de node.js + express + jquery. Los únicos cambios que hay son para ajustar las urls y hacer que todo quede un poco más REST.

Sin entrar en muchos detalles (el código completo lo tenéis en github), la parte de invocación del API sería algo así:

// Obtener los facts asociados a un héroe
$.getJSON('/hero/' + name + '/facts', function(data) {
  for (var i = 0; i < data.length; i++) {
    $('<li>').appendTo('#facts').text(data[i]);
  }
});

// Añadir un nuevo fact
$.ajax({
  type: "POST",
  url: "/hero/" + name + '/facts',
  data: JSON.stringify({fact: fact}),
  contentType: "application/json; charset=utf-8",
  dataType: "json",
  success: function(data) {
    $('<li>').appendTo('#facts').text(fact);
    $('#new-fact').val('');
  },
  error: function(err) {
    var msg = 'Status: ' + err.status + ': ' + err.responseText;
    alert(msg);
  }
});

No es la forma más bonita de escribirlo (seguramente hoy en día lo haría de otra forma), pero he querido dejarlo lo más parecido posible al tutorial de node.js + express para que quedara claro que con compojure podemos hacer el mismo tipo de API que con otras plataformas más extendidas.

Conclusiones

Objetivo cumplido. Si has aguantado hasta aquí, enhorabuena, ya sabes cómo crear una aplicación web usando un lenguaje funcional…. bueno, la verdad es que no. Sólo hemos visto unas ideas muy básicas, pero que cubren bastantes de los casos de uso con los que nos podremos encontrar en la vida real.

A lo largo de este pequeño tutorial hemos visto cómo servir contenido estático y crear páginas dinámicas, cómo tratar con estado mutable de forma segura en un entorno concurrente y cómo exponer un API completamente interoperable hacia el exterior. No está mal para algo tan simple :-)

El código fuente completo de este tutorial lo puedes encontrar en mi cuenta en github: https://github.com/jmhdez/compojure-sample. Siéntete libre de clonarlo, cambiarlo y jugar con él todo lo que quieras, y si te animas a realizar alguna corrección, estaré encantado de recibirla.