O que é Koa?
Koa é um framework web minimalista para Node.js escrito pela mesma equipe por trás do Express. Enquanto o Express desenvolveu um vasto ecossistema de middleware e a familiar API req/res, o Koa começou do zero com dois objetivos: um núcleo minúsculo e suporte de primeira classe a async/await. Ele é menos um concorrente do Express e mais uma reformulação deliberada dele.
O autor original do Express, TJ Holowaychuk, criou o Koa para corrigir o que parecia problemático no Express na época — callbacks aninhados, tratamento de erros improvisado e um núcleo que havia acumulado mais funcionalidades do que seus autores desejavam. O Koa 1 utilizava generator functions. O Koa 2 surgiu após o Node 7.6 trazer async/await nativos, e essa é a versão que você usará hoje.
Se o Express te ensina a cadeia de middleware, o Koa te ensina o que acontece quando essa cadeia pode envolver (wrap) uma requisição, em vez de apenas passar por ela.
O objeto de contexto
O Koa agrupa a requisição e a resposta em um único objeto chamado contexto, convencionalmente nomeado como ctx. Em vez de ler de req e escrever em res, você lê e escreve propriedades em um único objeto.
app.use(async (ctx) => {
ctx.status = 200; // response status
ctx.type = "application/json"; // response content type
ctx.body = { ok: true }; // response body
});
A requisição e a resposta ainda estão disponíveis integralmente quando você precisar delas:
ctx.request— a mensagem de entrada encapsulada (ctx.request.body,ctx.request.header).ctx.response— a mensagem de saída encapsulada (ctx.response.status).ctx.params,ctx.queryectx.request.body— roteamento e entrada processada.ctx.state— um objeto simples para passar valores entre middleware, como o usuário autenticado.ctx.throw(status, message)— lança um erro HTTP que o middleware de erro pode capturar.
ctx.state é o lugar idiomático para anexar dados compartilhados. O middleware de autenticação define ctx.state.user, e middlewares posteriores ou a rota o leem, sem poluir o próprio objeto de requisição.
A cebola: middleware que envolve
O middleware do Koa é uma única função async com a assinatura (ctx, next). Chamar await next() passa o controle para a camada interna; qualquer coisa que você escrever após essa linha será executada assim que as camadas internas forem concluídas.
request ──▶ mw1 before ──▶ mw2 before ──▶ route
│
response ◀── mw1 after ◀── mw2 after ◀────────┘
Este é o modelo de cebola (onion model), e é a ideia central que torna o Koa distinto. Uma cadeia linear só consegue executar código antes da resposta; a cebola consegue executar código em ambos os 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`);
});
Aquele único middleware cronometra a requisição inteira, incluindo cada rota e cada outro middleware registrado abaixo dele. O mesmo padrão lida com logging, transações de banco de dados e limpeza.
Um middleware também pode interromper a cadeia (short-circuit): se ele definir ctx.body e nunca chamar next(), a requisição termina ali. É assim que a autenticação rejeita uma requisição antes que ela chegue a uma rota.
Roteamento com @koa/router
O Koa não possui um roteador em seu núcleo, portanto, o roteamento é fornecido pelo @koa/router, o pacote mantido pela comunidade.
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());
O router.routes() monta os handlers correspondentes, e o router.allowedMethods() responde com o 405 correto quando o caminho existe, mas o método não. Os roteadores podem ser aninhados e prefixados, o que mantém uma API grande modular da mesma forma que o express.Router() faz.
Tratamento de erros e app.on(“error”)
Como os middleware do Koa são funções async comuns, os erros são exceções comuns. Capture-os em um único lugar envolvendo a chamada 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 um handler, ctx.throw(404, "not found") cria um erro com um status, uma mensagem e uma flag expose que o marca como seguro para ser exibido aos clientes. Erros sem um status tornam-se 500, e sua mensagem é ocultada da resposta.
Nem todo erro pode ser transformado em uma resposta. Se ocorrer uma falha após o envio dos headers, o Koa emitirá um evento error no app:
app.on("error", (err, ctx) => {
console.error(`${ctx.method} ${ctx.url}`, err);
});
Inscreva-se nesse evento independentemente de qualquer coisa, para que falhas inesperadas sejam sempre registradas.
Por que o Koa não possui roteador ou body parser integrados
O núcleo do Koa é intencionalmente quase vazio. Ele fornece o contexto, o pipeline de middleware e a infraestrutura HTTP; roteamento, body parsing, cookies, sessões, arquivos estáticos e headers de segurança ficam todos em pacotes separados.
Essa é uma escolha deliberada. Um núcleo reduzido é fácil de auditar, possui poucas dependências e evolui lentamente, tornando difícil que o próprio Koa quebre sua aplicação. O custo disso é que você é responsável pela composição: você decide qual body parser adicionar, como processá-lo e onde registrá-lo.
import bodyParser from "koa-bodyparser";
app.use(bodyParser());
app.use(router.routes());
app.use(router.allowedMethods());
A ordem aqui é fundamental, assim como no Express. Um parser registrado após o roteador não estará disponível para as rotas.
Koa ou Express?
Ambos os frameworks compartilham as mesmas ideias de request/response, então a escolha é majoritariamente sobre filosofia.
Escolha o Express quando a familiaridade for a prioridade: ele é o framework Node mais utilizado, já vem com roteamento e possui a maior coleção de middleware. É a escolha padrão mais segura para equipes que precisam de agilidade.
Escolha o Koa quando você preferir um núcleo menor e o modelo de cebola (onion model). O tratamento de erros assíncronos é mais limpo, o objeto de contexto elimina grande parte da manipulação de req/res, e você paga apenas pelo middleware que adicionar. A contrapartida é um ecossistema menor e a necessidade de montar mais coisas por conta própria.
Testando um app Koa
Como um app Koa é um pipeline de middleware, é simples testá-lo com supertest. Exporte o app e passe app.callback() para o helper de requisição para que nenhuma porta seja aberta.
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);
});
Assim como no Express, mantenha a definição do app separada de app.listen() para que os testes possam importá-lo sem iniciar um servidor.
Melhores práticas
- Mantenha o middleware de tratamento de erros primeiro, para que ele envolva todas as outras camadas.
- Use
ctx.statepara valores com escopo de requisição, como o usuário autenticado. - Defina
ctx.bodyectx.statusem vez de manipular a resposta bruta. - Registre
bodyParserantes do roteador para que octx.request.bodyseja preenchido. - Use
ctx.throwpara erros HTTP esperados e um único handler para os demais. - Sempre se inscreva no
app.on("error")para registrar falhas que ocorrem após o envio dos headers. - Exporte o app separadamente do servidor para que os testes permaneçam rápidos.
Erros comuns
- Chamar
next()duas vezes em um único middleware e executar o código downstream novamente. - Esquecer o
awaitantes donext(), o que pula a metade de “retorno” da cebola (onion model). - Registrar o router antes do body parser e descobrir que o
ctx.request.bodyestá vazio. - Assumir que o Koa possui um router ou body parser integrado e importar o pacote errado.
- Silenciar erros sem emiti-los, não deixando rastros nos logs.
- Mutar o
ctx.resdiretamente e ignorar o tratamento de status e headers do Koa.
Próximos passos
O Koa é a demonstração mais limpa de middleware assíncrono no Node.js, e seu modelo de cebola (onion model) está presente em frameworks de diversas linguagens. Se você busca um conjunto de funcionalidades nativas mais amplo e maior performance, leia o guia do Fastify. Se prefere o framework do qual o Koa derivou, revisite o Express. Para entender a camada HTTP por trás de ctx, comece pelo guia de HTTP e mantenha os fundamentos de Node.js por perto.