¿Qué es Koa?
Koa es un framework web minimalista para Node.js escrito por el mismo equipo detrás de Express. Mientras que Express desarrolló un ecosistema de middleware extenso y la familiar API req/res, Koa comenzó de cero con dos objetivos: un núcleo diminuto y soporte de primera clase para async/await. Más que un competidor de Express, es un replanteamiento deliberado del mismo.
El autor original de Express, TJ Holowaychuk, creó Koa para solucionar lo que resultaba incómodo de Express en aquel momento: los callbacks anidados, el manejo de errores improvisado y un núcleo que había acumulado más funcionalidades de las que sus autores deseaban. Koa 1 utilizaba funciones generadoras. Koa 2 llegó después de que Node 7.6 introdujera async/await nativos, y esa es la versión que utilizarás hoy en día.
Si Express te enseña la cadena de middleware, Koa te enseña qué sucede cuando se permite que esa cadena envuelva una solicitud en lugar de simplemente pasar a través de ella.
El objeto de contexto
Koa fusiona la solicitud y la respuesta en un único objeto llamado contexto, convencionalmente nombrado ctx. En lugar de leer de req y escribir en res, lees y escribes propiedades en un solo objeto.
app.use(async (ctx) => {
ctx.status = 200; // response status
ctx.type = "application/json"; // response content type
ctx.body = { ok: true }; // response body
});
La solicitud y la respuesta siguen estando disponibles por completo cuando las necesites:
ctx.request— el mensaje entrante envuelto (ctx.request.body,ctx.request.header).ctx.response— el mensaje saliente envuelto (ctx.response.status).ctx.params,ctx.queryyctx.request.body— enrutamiento y entrada procesada.ctx.state— un objeto plano para pasar valores entre middleware, como el usuario autenticado.ctx.throw(status, message)— lanza un error HTTP que el middleware de errores puede capturar.
ctx.state es el lugar idiomático para adjuntar datos compartidos. El middleware de autenticación establece ctx.state.user, y posteriormente el middleware o la ruta lo lee, sin contaminar el objeto de solicitud en sí.
La cebolla: middleware que envuelve
El middleware de Koa es una única función async con la firma (ctx, next). Llamar a await next() transfiere el control hacia el interior; cualquier cosa que escribas después de esa línea se ejecutará una vez que las capas internas hayan terminado.
request ──▶ mw1 before ──▶ mw2 before ──▶ route
│
response ◀── mw1 after ◀── mw2 after ◀────────┘
Este es el modelo de cebolla, y es la idea fundamental que hace que Koa sea distintivo. Una cadena plana solo puede ejecutar código antes de la respuesta; la cebolla puede ejecutar código en ambos lados.
app.use(async (ctx, next) => {
const start = Date.now();
await next(); // everything downstream runs here
ctx.set("X-Response-Time", `${Date.now() - start}ms`);
});
Ese único middleware mide el tiempo de toda la solicitud, incluyendo cada ruta y cualquier otro middleware registrado debajo de él. El mismo patrón se utiliza para el logging, las transacciones de base de datos y la limpieza.
Un middleware también puede interrumpir la cadena: si establece ctx.body y nunca llama a next(), la solicitud termina ahí. Así es como la autenticación rechaza una solicitud antes de que llegue a una ruta.
Enrutamiento con @koa/router
Koa no incluye un router en su núcleo, por lo que el enrutamiento se proporciona a través de @koa/router, el paquete mantenido por la comunidad.
import Koa from "koa";
import Router from "@koa/router";
const app = new Koa();
const router = new Router();
router.get("/posts", async (ctx) => {
ctx.body = await Post.find().limit(20);
});
router.get("/posts/:id", async (ctx) => {
const post = await Post.findById(ctx.params.id);
if (!post) ctx.throw(404, "post not found");
ctx.body = post;
});
app.use(router.routes());
app.use(router.allowedMethods());
router.routes() monta los handlers correspondientes, y router.allowedMethods() responde con el 405 correcto cuando la ruta existe pero el método no. Los routers pueden anidarse y llevar prefijos, lo que permite mantener una API extensa de forma modular, de la misma manera que lo hace express.Router().
Manejo de errores y app.on(“error”)
Dado que los middleware de Koa son funciones async ordinarias, los errores son excepciones comunes. Puedes capturarlos en un solo lugar envolviendo la llamada downstream.
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status ?? 500;
ctx.body = { error: err.expose ? err.message : "internal_error" };
ctx.app.emit("error", err, ctx);
}
});
Dentro de un handler, ctx.throw(404, "not found") crea un error con un status, un mensaje y un flag expose que indica si es seguro mostrarlo a los clientes. Los errores sin un status se convierten en 500, y su mensaje se oculta de la respuesta.
No todos los errores pueden convertirse en una respuesta. Si ocurre un fallo después de que se hayan enviado los headers, Koa emitirá un evento error en la app:
app.on("error", (err, ctx) => {
console.error(`${ctx.method} ${ctx.url}`, err);
});
Suscríbete a ese evento sin falta, para que los fallos inesperados siempre queden registrados en los logs.
Por qué Koa no tiene un router o body parser integrados
El núcleo de Koa está intencionalmente casi vacío. Te proporciona el contexto, el pipeline de middleware y la infraestructura HTTP; el routing, el body parsing, las cookies, las sesiones, los archivos estáticos y los encabezados de seguridad residen en paquetes separados.
Este es un intercambio deliberado. Un núcleo pequeño es fácil de auditar, tiene pocas dependencias y evoluciona lentamente, por lo que es difícil que Koa en sí mismo rompa tu aplicación. El costo es que tú eres el responsable de la composición: tú decides qué body parser añadir, cómo parsearlo y dónde registrarlo.
import bodyParser from "koa-bodyparser";
app.use(bodyParser());
app.use(router.routes());
app.use(router.allowedMethods());
Aquí el orden es fundamental, al igual que sucede en Express. Un parser registrado después del router no estará disponible para las rutas.
¿Koa o Express?
Ambos frameworks comparten las mismas ideas de solicitud/respuesta, por lo que la elección depende principalmente de la filosofía.
Elige Express cuando la familiaridad sea lo más importante: es el framework de Node.js más utilizado, incluye routing y posee la colección de middleware más amplia. Es la opción más segura por defecto para un equipo que necesite avanzar rápidamente.
Elige Koa cuando busques un núcleo más ligero y el modelo de cebolla (onion model). El manejo de errores asíncronos es más limpio, el objeto de contexto elimina gran parte del malabarismo entre req/res y solo pagas por el middleware que añadas. El intercambio es un ecosistema más reducido y que te corresponda hacer más ensamblaje.
Probando una app de Koa
Debido a que una app de Koa es un pipeline de middleware, es sencillo probarla con supertest. Exporta la app y pasa app.callback() al helper de peticiones para que no se abra ningún puerto.
import request from "supertest";
import app from "../app.js";
test("GET /posts returns a list", async () => {
const res = await request(app.callback()).get("/posts").expect(200);
expect(Array.isArray(res.body)).toBe(true);
});
Al igual que con Express, mantén la definición de la app separada de app.listen() para que los tests puedan importarla sin iniciar un servidor.
Mejores prácticas
- Mantén el middleware de manejo de errores al principio para que envuelva a todas las demás capas.
- Usa
ctx.statepara valores con alcance de solicitud (request-scoped), como el usuario autenticado. - Establece
ctx.bodyyctx.statusen lugar de manipular la respuesta raw. - Registra
bodyParserantes del router para quectx.request.bodyse complete. - Usa
ctx.throwpara errores HTTP previstos y un único manejador para el resto. - Suscríbete siempre a
app.on("error")para registrar fallos que ocurran después de enviar los headers. - Exporta la app independientemente del servidor para que los tests sigan siendo rápidos.
Errores comunes
- Llamar a
next()dos veces en un mismo middleware y ejecutar el código downstream nuevamente. - Olvidar
awaitantes denext(), lo que omite la mitad de “salida” de la cebolla. - Registrar el router antes que el body parser y descubrir que
ctx.request.bodyestá vacío. - Asumir que Koa tiene un router o un body parser integrado e importar el paquete incorrecto.
- Silenciar errores sin emitirlos, dejando los logs vacíos.
- Mutar
ctx.resdirectamente y saltarse el manejo de estados y cabeceras de Koa.
Próximos pasos
Koa es la demostración más limpia de middleware async en Node, y su modelo de cebolla (onion model) está presente en frameworks de prácticamente todos los lenguajes. Si buscas un conjunto de funcionalidades integradas más amplio y mayor velocidad, lee la guía de Fastify. Si prefieres el framework del cual surgió Koa, vuelve a consultar Express. Para comprender la capa HTTP que hay debajo de ctx, comienza con la guía de HTTP y ten a mano los conceptos básicos de Node.js.