Escrevendo Seu Próprio Middleware Hono
Crie middleware Hono com variáveis de contexto tipadas, tempo de requisição, saída antecipada e escopo por caminho. Entenda a ordem de next().
Um middleware Hono é uma função async que recebe (c, next). Ele pode terminar de duas formas: aguarda next() e não retorna nada, o que repassa a requisição adiante, ou retorna uma Response e a requisição para ali.
logger() e cors() são chamados como factories, de modo que essa assinatura permanece fora de vista até você escrever a sua própria. A primeira que você escreve normalmente funciona. O problema começa um passo depois, quando um handler lê um valor definido pelo middleware e o TypeScript não consegue dizer qual é o seu tipo.
Este artigo constrói middleware a partir da assinatura: as duas fases e a ordem em que são executadas, um cronômetro de requisição que precisa de ambas as fases, variáveis de contexto tipadas que mantêm seus tipos quando o middleware é movido para seu próprio arquivo, saída antecipada e escopo por caminho. Ele pressupõe que você já tenha rotas em funcionamento, conforme abordado em getting started with Hono. Se você vem do Express, a comparação de frameworks e as notas de portabilidade cobrem o terreno a partir do qual este artigo começa. Os exemplos têm como alvo a linha Hono 4.13.x publicada no npm.
Principais Conclusões
- Um middleware Hono é
(c, next): o código antes deawait next()é executado na entrada, o código depois dele é executado na saída. - O middleware é executado na ordem de registro no fluxo de entrada e na ordem inversa de registro no fluxo de saída, então o primeiro registrado é o primeiro a ver a requisição e o último a ver a resposta.
createMiddleware()dehono/factorycom um genericVariablesmantémcenexttipados quando o middleware é movido para seu próprio arquivo.- Retornar uma
Responsesem chamarnext()encerra a requisição: nada registrado depois dele é executado. - O Hono captura erros de handlers e middlewares e os encaminha para
onError()ou para um 500, de modo quenext()nunca lança exceção.
A Assinatura e as Duas Formas de o Middleware Terminar
O middleware recebe o contexto e a função next, e o guia de middleware do Hono permite apenas dois desfechos. Ele pode passar o controle adiante na cadeia aguardando next() e não retornando nada, ou pode interromper a requisição ali mesmo devolvendo uma Response própria. A outra primitiva é o handler, que sempre produz uma Response, e uma única requisição só chega a um deles.
import { Hono } from 'hono'
const app = new Hono()
app.use(async (c, next) => {
console.log(`[${c.req.method}] ${c.req.url}`)
await next()
})
app.get('/', (c) => c.text('Hello!'))
export default app
Esqueça o await e a função mantém seu significado, mas perde a garantia de ordenação, então sempre escreva await next().
A Cebola: Em Que Ordem o Middleware Hono é Executado?
O código colocado antes de await next() é executado na entrada, e o código colocado depois dele é executado na saída, razão pela qual uma única função pode iniciar um cronômetro antes do handler e escrever o tempo decorrido em um header de resposta depois dele. A documentação de conceitos do Hono retrata essa organização como uma cebola, com cada camada envolvendo o handler e tendo sua vez de cada lado dele. O middleware é executado na ordem de registro no caminho de entrada e na ordem inversa de registro no caminho de saída, de modo que o primeiro middleware que você registra é o primeiro a ver a requisição e o último a ver a resposta.
import { Hono } from 'hono'
const app = new Hono()
app.use(async (_, next) => {
console.log('timing start')
await next()
console.log('timing end')
})
app.use(async (_, next) => {
console.log(' auth start')
await next()
console.log(' auth end')
})
app.use(async (_, next) => {
console.log(' flags start')
await next()
console.log(' flags end')
})
app.get('/', (c) => {
console.log(' handler')
return c.text('Hello!')
})
export default app
Uma requisição produz:
timing start
auth start
flags start
handler
flags end
auth end
timing end
A indentação está embutida nas strings de log, não é produzida pelo console.log. Ela está ali porque o aninhamento é justamente o ponto: a metade de saída de cada middleware é envolvida pela metade de saída de tudo o que foi registrado antes dele.
Um Cronômetro de Requisição, Escrito Através de Ambas as Fases
O Hono inclui um middleware de Server-Timing que emite headers Server-Timing a partir de hono/timing, e ele é a escolha correta quando esse formato lhe convém. Um cronômetro feito à mão ainda é a demonstração mais clara de execução em duas fases, porque o valor que ele reporta não existe em nenhuma das fases isoladamente.
import { createMiddleware } from 'hono/factory'
export const responseTime = createMiddleware(async (c, next) => {
const start = Date.now()
await next()
c.header('X-Response-Time', `${Date.now() - start}ms`)
})
Depois de await next() já existe uma resposta, então c.header() escreve diretamente nos headers dessa resposta, em vez de em headers preparados. A mesma página de middleware traz uma ressalva para Cloudflare Workers: um cronômetro ali fica atrelado à última operação de I/O em vez do tempo real decorrido, de modo que os números podem enganar.
Como Passar Valores Tipados Adiante na Cadeia?
Tudo o que é escrito com c.set() pertence a uma única requisição e desaparece com ela, então um valor não pode ser levado para a próxima requisição nem compartilhado entre elas. O código a jusante o lê de volta com c.get('key') ou c.var.key, ambos abordados na página da API de Context. Definidos inline, os valores chegam a jusante sem tipagem:
app.use(async (c, next) => {
c.set('requestId', crypto.randomUUID())
await next()
})
app.get('/', (c) => c.json({ id: c.var.requestId })) // no type information
createMiddleware() de hono/factory mantém intactos os tipos de c e next quando um middleware é movido para seu próprio arquivo, e passar a ele um generic Variables torna cada valor definido pelo middleware type-safe para os handlers que vêm em seguida.
// middleware/request-id.ts
import { createMiddleware } from 'hono/factory'
export const requestId = createMiddleware<{
Variables: { requestId: string }
}>(async (c, next) => {
c.set('requestId', crypto.randomUUID())
await next()
})
// index.ts
import { Hono } from 'hono'
import { requestId } from './middleware/request-id'
const app = new Hono().use(requestId).get('/', (c) => {
return c.json({ id: c.var.requestId }) // string
})
export default app
Existem três mecanismos para tipar variáveis de contexto. Opte pela linha do meio por padrão.
| Mecanismo | Onde o tipo é visível | Custo |
|---|---|---|
new Hono<{ Variables }>() | Todas as rotas daquela instância do app | Tipo combinado mantido à mão, duplicado em cada local de declaração |
createMiddleware<{ Variables }>() | Rotas a jusante daquele middleware | Um generic por middleware |
Augmentation de ContextVariableMap | Todos os contextos da aplicação | Tipa rotas nas quais o middleware nunca foi executado |
Faça o augmentation de ContextVariableMap e o tipo aparecerá em todos os contextos da aplicação, tenha ou não o middleware que define o valor sido executado naquela rota. Onde ele não foi executado, c.get() ainda parece tipado enquanto o valor está ausente em tempo de execução. O aviso na documentação da API de Context ilustra o ponto com um par de rotas, uma que usa o middleware e outra que não usa. Restrinja o escopo do tipo ao middleware em vez disso.
Como Interromper a Execução Sem Chamar next()?
Retorne uma Response e pule o next(), e a requisição termina ali: nenhum middleware posterior, nenhum handler.
import { createMiddleware } from 'hono/factory'
export const apiKeyAuth = createMiddleware(async (c, next) => {
if (c.req.header('X-API-Key') !== 'expected-key') {
return c.text('Unauthorized', 401)
}
await next()
})
Tudo o que for registrado depois desse middleware nunca é executado, incluindo a metade de saída de qualquer middleware que o teria envolvido. As metades de saída dos middlewares registrados antes dele ainda são executadas, então um cronômetro registrado primeiro ainda carimba o 401.
Como Restringir o Escopo de um Middleware Hono a um Caminho?
Passe um padrão de caminho como primeiro argumento para app.use() para restringir um middleware às rotas correspondentes, e lembre-se de que a ordem é a ordem de registro.
app.use(responseTime) // every route
app.use('/api/*', apiKeyAuth) // only /api/**
app.get('/api/orders', (c) => c.json([]))
responseTime é registrado primeiro, então ele envolve apiKeyAuth e mede também as requisições rejeitadas. Troque as duas linhas de lugar e o cronômetro deixa de enxergar os 401s.
Dois Comportamentos Que Pegam as Pessoas de Surpresa
Um erro lançado em qualquer ponto da cadeia, por um handler ou por qualquer middleware, é capturado pelo próprio Hono: ele vai para app.onError() quando você tem um, e caso contrário retorna como um 500. É por isso que next() nunca lança exceção, e por que envolvê-lo em try/catch não traz nenhum benefício. O erro ainda pode ser acessado em c.error depois de await next() se você quiser registrá-lo em log.
Os tipos se somam ao longo de uma cadeia de .use(), então um handler posicionado depois de dois middlewares tipados enxerga as variáveis de ambos. Cada .use() devolve uma instância cujo tipo já inclui o que veio antes dele, razão pela qual o guia de middleware afirma que a maioria das aplicações nunca precisa declarar antecipadamente um tipo Env combinado.
Para Onde Ir a Partir Daqui
Middleware customizado é um único formato de função com duas metades e uma regra sobre como termina. Coloque o generic Variables nele desde a primeira linha que você escrever, mantenha cada um em seu próprio módulo, e a ordem de registro cuida do resto. Pegue o cronômetro acima, mova-o para middleware/ e adicione um argumento de configuração envolvendo createMiddleware() em uma função comum que o retorne: esse é todo o padrão para um middleware parametrizado.
Perguntas Frequentes
Como executo um middleware em apenas uma rota em vez de em todas as rotas?
Registre-o com o método da rota em vez de um app.use() genérico. O Hono permite anexar middleware através de app.HTTP_METHOD, então app.post('/posts/*', basicAuth()) se aplica apenas a requisições POST naquele caminho, e passar o middleware como argumento antes do handler, como em app.get('/echo', echoMiddleware, handler), restringe seu escopo àquela única rota sem afetar o restante da aplicação.
O middleware se aplica a rotas que foram registradas antes dele?
Registre o middleware acima dos handlers que ele deve envolver. O Hono executa handlers e middlewares na ordem de registro, e a documentação de roteamento coloca a regra de forma clara: qualquer coisa que você queira executar antes de um handler precisa ser registrada antes dele. Uma rota declarada antes da chamada de app.use() é correspondida primeiro, então o middleware adicionado depois não faz parte de forma confiável da cadeia daquela rota.
Qual é a diferença entre Bindings e Variables no generic Env?
Bindings descrevem recursos de plataforma e valores de ambiente que o runtime injeta, lidos através de c.env, como um banco de dados D1 ou um secret no Cloudflare Workers. Variables descrevem os valores por requisição que o seu próprio código escreve com c.set() e lê com c.get() ou c.var. Ambos residem no mesmo objeto Env passado como new Hono<{ Bindings: ...; Variables: ... }>().
Posso ler uma variável de contexto fora de um handler ou middleware?
Sim, com o middleware embutido Context Storage. Registre app.use(contextStorage()) de 'hono/context-storage' e depois chame getContext() dentro de qualquer função para acessar o contexto da requisição atual, incluindo c.var e, no Cloudflare Workers, bindings em c.env. tryGetContext() se comporta da mesma forma, mas devolve undefined onde getContext() lançaria uma exceção, o que é adequado para código que pode ser executado fora de uma requisição.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k