El ¿qué?, ¿cómo?, ¿cuándo? y ¿dónde? de KoaJS

nodeJS ene. 19, 2020

En el ambiente de nodeJS, hay diversos frameworks que facilitan la creación de servidores, podemos nombrar a adonisJS, que ofrece un paquete completo de herramientas, FeatherJS, que permite crear API en tiempo real, el muy conocido express que es uno de los más populares para crear APIs rápidamente, y otros más que seguro tienes en mente, sin embargo hoy vamos enfocarnos en KoaJS, un framework increíblemente pequeño pero potente, veremos sus ventajas, características y secretos que lo hacen genial y rápido, acompáñame en las siguientes líneas, vamos allá!

¿Qué es koa?

Podemos decir que Koa es el hermano mejorado de express, pues es mucho más ligero, más rápido y por supuesto mucho más sólido, lo más sorprendente es que el mismo equipo de express es quien está detrás del proyecto, sin embargo esta vez tomaron un enfoque diferente aprovechandose de las nuevas caracteristicas de javascript, con forme vayas avanzando en está publicación te vas a dar cuenta de las optimizaciones que hacen de koa una muy buena alternativa para tus aplicaciones web.

¿Por qué elegir Koa?

Para responder esta pregunta debemos considerar sus características, como dicen en españa ¡con esto vas a flipar!

Libre de middlewares: Tal vez parece una desventaja, pero es todo lo contrario, pues muchas veces no necesitamos el paquete entero de características, esto nos permite tener un servidor rápido y podemos ir añadiendo características a medida que las necesitemos.

Adiós callbacks: Koa, funciona completamente bien con las nuevas características desde es6, lo que permite usar promesas, async/await, que mejoran la lectura del código, y nos permite olvidarnos de los callbacks.

Middlewares en cascada: La aplicación de middlewares ahora es mucho más legible y koa los aplica de manera descendente, es decir los aplica a medida que los encuentra, y cuando devuelve respuestas vuelve a retornar por cada uno de los middlewares, más adelante lo entenderás perfectamente.

Manejo de errores: Con koajs podemos tener un control mas fino cuando suceden excepciones, un solo middleware es suficiente para ayudar a solventar esto, aunque no lo creas es muy útil para enviar respuestas personalizadas cuando un error 500 se dispara.

Gran rendimiento: Comparado con otros frameworks podemos notar que koa se ubica entre los mas rápidos, sin un core simple y ligero no podría ser posible.

Comparación de rendimiento entre expressjs y koajs

El modelo cebolla (Onion model)

Onion model (Model cebolla)

Lineas arriba mencionamos una de las características estrella de koa, los middlewares en cascada, que fueron pensados para seguir el modelo Onion, ¿por qué? pues sencillamente porque la solicitud cruza por cada middleware como si fuera una capa de la cebolla hasta llegar a la última, y luego continua su trayecto pasando nuevamente por cada capa desde el interior hacia el exterior, y esa es la simple razón por la que esa característica tiene este curioso nombre. Continuemos.

Tengo una aplicación en express ¿Puedo migrar a koa?

Si tienes una aplicación por ejemplo en express, lastimosamente no podrás beneficiarte de las propiedades de koa a menos que reescribas completamente tu aplicación, una de las razones es porque el núcleo de express esta fuertemente acoplado con la lógica del enrutador, otra razón es porque le añade características propias del framework lo que hace básicamente hace complicada una sencilla transición.

Un ejemplo más que básico

Mucho palabreo... "Cállate y dame el maldito código", si, estoy seguro que tu mente lo dijo, bueno tranquilo viejo, esto es lo más simple que vamos a hacer hoy, el clásico hola mundo.

Lo primero que debes hacer es ubicarte en tu ruta de trabajo e instalar a koa con el siguiente comando:

npm install koa // instalación con npm
yarn add koa // instalación con yarn

Luego de eso crea un archivo llamado index.js, y agrega el siguiente fragmento de código:

const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.body = 'Hello World from eldevsin.site';
});

app.listen(3000);
Ejemplo básico en koajs

Este ejemplo sencillo de entender, primero importamos koa e instanciamos una aplicación, lo que ves en app.use(), es la forma de agregar middlewares, por último iniciamos el server en el puerto 3000. Muy fácil, pero esto no es todo lo de hoy, así que avancemos.

Lo mejor ¿Cómo funciona Koa por dentro?

Importante: Para que puedas comprender sin problemas este punto, considera revisar las promesas en javascript antes de continuar leyendo, si no tienes problemas en asumir el reto... continua :D.

Importante: Para que puedas comprender sin problemas este punto, considera revisar las promesas en javascript antes de continuar leyendo, si no tienes problemas en asumir el reto... continua :D.

Durante mucho tiempo se habló de las +-500 lineas de código de koa, esa cantidad actualmente no es la misma, es obvio, el tiempo no pasa en vano y siempre hay cambios que son necesarios, sin embargo su base continúa conformada por 4 archivos:

La chicha se encuentra en el archivo application.js, vamos a revisar las partes más valiosas, una vez que entiendas esta información, estarás un paso más adelante que otros desarrolladores:

Primera parte: Se definen las opciones por defecto

Aquí muchas cosas esenciales de koa se agregan por defecto, por ejemplo el entorno en que se esta trabajando, se construye el objeto context, el request, el response, las claves de cookie firmadas y por supuesto una lista de middlewares vacía, estas últimas a medida que se vayan agregando (hola app.use()?) se irán apilando.

  constructor(options) {
      super();
      options = options || {};
      this.proxy = options.proxy || false;
      this.subdomainOffset = options.subdomainOffset || 2;
      this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
      this.maxIpsCount = options.maxIpsCount || 0;
      this.env = options.env || process.env.NODE_ENV || 'development';
      if (options.keys) this.keys = options.keys;
      this.middleware = []; // se definen los middlewares vacios
      this.context = Object.create(context);
      this.request = Object.create(request);
      this.response = Object.create(response);
      if (util.inspect.custom) {
          this[util.inspect.custom] = this.inspect;
      }
  }
Opciones por defecto de koaJS

Segunda Parte: Ejecutar listen

Para crear un servidor todos parten desde el mismo punto, utilizar la librería http o https, koa no es ajeno a esto y utiliza una función para definir con que configuración desea crear su instancia de servidor, como se muestra abajo this.callback() es el encargado de esta tarea.

  listen(...args) {
      debug('listen');
      const server = http.createServer(this.callback()); // crea un server basado en lo que devuelva callback()
      return server.listen(...args);
  }
Listen de koaJS

Tercera parte: Construir el manejador de solicitudes

Una parte importante para el funcionamiento es la forma en que se ejecutan los middlewares en este caso cuando se ejecutan las solicitudes van de forma descendente y en las respuestas de forma ascendente, como lo explicamos anteriormente nos referimos a Onion model.

 callback() {
     const fn = compose(this.middleware); // (3.1) agrupación y organización de middlewares

     if (!this.listenerCount('error')) this.on('error', this.onerror);

     const handleRequest = (req, res) => {
         const ctx = this.createContext(req, res); // (3.2) construccion objeto context
         return this.handleRequest(ctx, fn); // (3.3) ejecución del manejador de llamadas
     };

     return handleRequest; //devolución de closure
 }
Función callback de koajs

Vamos desenvolviendo con más detalle cada paso de esta función:

3.1- La agrupación de middlewares

koa-compose que es la librería que usa koa en su core es el encargado del trabajo de agrupación de middlewares, y siempre va a devolver una promesa.

Para entender el proceso primero quiero que consideres que vamos a tener, de ejemplo 3 middlewares agregados a koa:

...
app.use(async (ctx, next) => {
    console.log('1');
    await next();
    console.log('6');
});

app.use(async (ctx, next) => {
    console.log('2');
    await next();
    console.log('5');
})

app.use(async (ctx, next) => {
    console.log('3');
    await next();
    console.log('4');
})
Ejemplos de middlewares en koa

Dentro de koa-compose

function compose (middleware) {
    // compose necesita un array, si no lo es no te dejará pasar
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) { // se asegura que cada elemento sea una función
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
Código de koa-compose

Primero vamos a comentar de forma general el funcionamiento de este código, bien, puedes notar que compose retorna el valor que devuelve una función, nos referimos a dispatch(0), es decir, esta ejecutando inmediatamente la función dispatch y a su vez esta retorna una promesa, hasta allí eso es sencillo de entender ¿no lo crees?

Ahora vamos por el detalle, tenemos lista con el orden de ejecución que toma compose:

  • Al ejecutar dispatch(0), se ejecuta Promise.resolve(fn(context, dispatch.bind (null, 0 + 1))).
  • El primer middleware es ejecutado, se ejecuta su contenido, hasta que encuentre la linea await next();.
  • Cuando nos referimos a next estamos hablando del segundo middleware: Promise.resolve(fn(context, dispatch.bind (null, 0 + 1))).
  • Se ejecuta el segundo middleware, hasta encontrar la linea await next();
  • En este caso next, es el middleware Promise.resolve(fn(context, dispatch.bind (null, 1 + 1))).
  • Nótese que cada vez que se ejecuta un middleware se pasa como parámetro el siguiente en forma de promesa.
  • Ejecuta el tercer middleware y espera await next();
  • Como ya no hay otro middleware compose devuelve return Promise.resolve(), ojo también es una promesa, y partir de aquí empieza a "desenvolverse" el flujo.
  • El await del tercer middleware es resuelto, y se ejecuta lo restante del código.
  • El await del segundo middleware es resuelto, y ejecuta lo restante de código.
  • Finalmente el await del primer middleware se resuelve, y se ejecuta lo que queda del código.

3.2 Creación de context

El context es un objeto que es parte de koa y es pasado en todos los middlewares, tiene asociado a otros manejadores de solicitud y respuesta de este framework, además tiene como propiedades a los objetos nativos de node como request y response, y al state, donde básicamente es el objeto en donde puedes almacenar "cosas" y pasarlas entre middlewares, esa información va a estar presente mientras dure el flujo de la petición, es decir desde que se produce la solicitud hasta que se devuelve la respuesta al cliente.

createContext(req, res) {
    const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    context.app = request.app = response.app = this;
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    request.ctx = response.ctx = context;
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    context.state = {};

    return context;
}
Función que crea al objeto context.

Sin más complejidad de este punto, una vez que se termina de ingresar los valores por defecto, el objeto context es devuelto.

3.3 Manejador de solicitudes

Como su nombre lo indica es una función que se va a encargar de dar el inicio a la ejecución de middlewares, nótese que al inicio de la función se le pasa el objeto context(ctx) y el grupo de middlewares(fnMiddleware):

handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
Manejador de solicitudes de koa

Por defecto el status de respuesta es 404 en caso no se encuentre ninguna ruta, además como mencionamos anteriormente compose devuelve una promesa(fnMiddlware), la ejecutamos pasándole el contexto como parámetro para que pueda ser pasado a todos los middlewares que se hayan definido(app.use()), cuando se resuelve esta promesa se envía una respuesta al cliente y cuando se produce un error éste es atrapado (catch) para luego ser ejecutada la función onerror.

Conclusiones para cenar

Hemos visto como koa es una buena alternativa para tus proyectos en nodejs, todas las ventajas y como funciona por debajo, lo de los middlewares es básicamente belleza pura, sin duda debe estar presente entre tu bajara de herramientas de desarrollo, ojo, recuerda siempre que la tecnología que utilices debe ir de acuerdo a las necesidades del problema que vas a resolver, no optes por ser un absolutista y elige con cabeza. Este es el inicio de nuestra historia con koa js y te prometo que más adelante realizaremos tutoriales muy interesantes.

Gif by GIPHY

Si has llegado hasta acá, entonces eres uno de los míos, si consideras que este post te ha aportado valor compártelo en tus redes sociales, y no olvides dejar tu comentario, amamos saber lo que piensas, nos vemos en un próximo post.

Fernando Palacios

¡Hoy es un gran día para hacer grandes cosas!

¡Genial! Te has suscrito con éxito.
¡Genial! Ahora, completa el checkout para tener acceso completo.
¡Bienvenido de nuevo! Has iniciado sesión con éxito.
Éxito! Su cuenta está totalmente activada, ahora tienes acceso a todo el contenido.