12k
All articles

Escribiendo tu propio middleware de Hono

Crea middleware de Hono con variables de contexto tipadas, tiempo de solicitud, salidas anticipadas y alcance por ruta. Entiende el orden de next().

OpenReplay Team
OpenReplay Team
Escribiendo tu propio middleware de Hono

Un middleware de Hono es una función asíncrona que recibe (c, next). Puede terminar de dos maneras: espera a next() y no devuelve nada, lo que hace que la petición siga su curso, o devuelve una Response y la petición se detiene ahí.

logger() y cors() se invocan como factories, de modo que esa firma permanece fuera de la vista hasta que escribes la tuya propia. El primer middleware que escribes suele funcionar. Los problemas empiezan un paso más adelante, cuando un handler lee un valor que estableció el middleware y TypeScript no puede decirte de qué tipo es.

Este artículo construye middleware desde la firma hacia arriba: las dos fases y el orden en que se ejecutan, un temporizador de peticiones que necesita ambas fases, variables de contexto tipadas que conservan sus tipos cuando el middleware se traslada a su propio archivo, salida anticipada y delimitación por ruta. Se asume que ya tienes rutas funcionando, como se explica en getting started with Hono. Si vienes de Express, la comparativa de frameworks y las notas de migración cubren el terreno desde el que parte este artículo. Los ejemplos están dirigidos a la línea 4.13.x de Hono publicada en npm.

Puntos clave

  • Un middleware de Hono es (c, next): el código anterior a await next() se ejecuta en el camino de entrada, y el código posterior se ejecuta en el camino de salida.
  • El middleware se ejecuta en orden de registro en la entrada y en orden inverso de registro en la salida, así que el primero en registrarse es el primero en ver la petición y el último en ver la respuesta.
  • createMiddleware() de hono/factory con un genérico Variables mantiene c y next tipados cuando el middleware se traslada a su propio archivo.
  • Devolver una Response sin llamar a next() termina la petición: nada de lo registrado después se ejecuta.
  • Hono captura los errores de handlers y middleware y los dirige a onError() o a un 500, de modo que next() nunca lanza excepciones.

La firma y las dos formas en que termina un middleware

El middleware recibe el contexto y la función next, y la guía de middleware de Hono solo le permite dos finales. Puede pasar el control más abajo en la cadena esperando a next() y no devolviendo nada en absoluto, o puede detener la petición ahí mismo devolviendo una Response propia. La otra primitiva es el handler, que siempre produce una Response, y una misma petición solo llega a uno de ellos.

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

Si olvidas el await, la función conserva su significado pero pierde su garantía de orden, así que escribe siempre await next().

La cebolla: ¿en qué orden se ejecuta el middleware de Hono?

El código colocado antes de await next() se ejecuta en el camino de entrada, y el colocado después se ejecuta en el camino de salida, razón por la cual una sola función puede iniciar un temporizador antes del handler y escribir el tiempo transcurrido en una cabecera de respuesta después de él. La documentación de conceptos de Hono representa esta disposición como una cebolla, en la que cada capa envuelve al handler y toma su turno a cada lado de él. El middleware se ejecuta en orden de registro en el camino de entrada y en orden inverso de registro en el de salida, de modo que el primer middleware que registras es el primero en ver la petición y el último en ver la respuesta.

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

Una petición produce:

timing start
  auth start
    flags start
      handler
    flags end
  auth end
timing end

La indentación está incorporada en las propias cadenas del log, no la genera console.log. Está ahí porque el anidamiento es justamente lo importante: la mitad de salida de cada middleware queda envuelta por la mitad de salida de todo lo registrado antes que él.

Un temporizador de peticiones, escrito a lo largo de ambas fases

Hono incluye un middleware Server-Timing que emite cabeceras Server-Timing desde hono/timing, y es la opción correcta cuando ese formato te sirve. Aun así, un temporizador hecho a mano sigue siendo la demostración más clara de la ejecución en dos fases, porque el valor que reporta no existe en ninguna de las dos fases por separado.

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`)
})

Después de await next() ya existe una respuesta, así que c.header() escribe directamente en las cabeceras de esa respuesta en lugar de en cabeceras preparadas. La misma página de middleware incluye una advertencia para Cloudflare Workers: allí un temporizador queda ligado a la última operación de E/S y no al tiempo real transcurrido, por lo que las cifras pueden inducir a error.

¿Cómo se pasan valores tipados por la cadena?

Todo lo que se escribe con c.set() pertenece a una única petición y desaparece con ella, así que un valor no puede trasladarse a la siguiente petición ni compartirse entre ellas. El código posterior lo recupera con c.get('key') o c.var.key, ambos documentados en la página de la Context API. Si se establecen en línea, los valores llegan sin tipar al código posterior:

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 mantiene intactos los tipos de c y next cuando un middleware se traslada a su propio archivo, y pasarle un genérico Variables hace que cada valor que el middleware establece sea type-safe para los handlers que vienen después.

// 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

Existen tres mecanismos para tipar variables de contexto. Por defecto, recurre a la fila central.

MecanismoDónde es visible el tipoCoste
new Hono<{ Variables }>()Todas las rutas de esa instancia de appTipo combinado mantenido a mano, duplicado en cada punto de declaración
createMiddleware<{ Variables }>()Rutas posteriores a ese middlewareUn genérico por middleware
Ampliación de ContextVariableMapTodos los contextos de la aplicaciónTipa rutas en las que el middleware nunca se ejecutó

Si amplías ContextVariableMap, el tipo aterriza en todos los contextos de la aplicación, se haya ejecutado o no en esa ruta el middleware que establece el valor. Donde no se ejecutó, c.get() sigue pareciendo tipado mientras que el valor falta en tiempo de ejecución. La advertencia en la documentación de la Context API ilustra el punto con un par de rutas, una que usa el middleware y otra que no. Limita el alcance del tipo al middleware en su lugar.

¿Cómo se corta la ejecución sin llamar a next()?

Devuelve una Response y omite next(), y la petición termina ahí: ni middleware posterior, ni 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()
})

Nada de lo registrado después de este middleware llega a ejecutarse, incluida la mitad de salida de cualquier middleware que lo habría envuelto. Las mitades de salida del middleware registrado antes que él sí se ejecutan, así que un temporizador registrado en primer lugar sigue sellando el 401.

¿Cómo se delimita un middleware de Hono a una ruta?

Pasa un patrón de ruta como primer argumento de app.use() para restringir un middleware a las rutas coincidentes, y recuerda que el orden es el orden de registro.

app.use(responseTime)          // every route
app.use('/api/*', apiKeyAuth)  // only /api/**
app.get('/api/orders', (c) => c.json([]))

responseTime se registra primero, así que envuelve a apiKeyAuth y mide también las peticiones rechazadas. Intercambia las dos líneas y el temporizador dejará de ver los 401.

Dos comportamientos que pillan a la gente desprevenida

Un error lanzado en cualquier punto de la cadena, ya sea por un handler o por cualquier middleware, lo captura el propio Hono: va a parar a app.onError() si lo tienes definido, y en caso contrario vuelve como un 500. Por eso next() nunca lanza excepciones, y por eso envolverlo en try/catch no te aporta nada. El error sigue siendo accesible en c.error después de await next() si quieres registrarlo.

Los tipos se van acumulando a lo largo de una cadena de .use(), de modo que un handler situado después de dos middleware tipados ve las variables de ambos. Cada .use() devuelve una instancia cuyo tipo ya incluye lo que vino antes, razón por la cual la guía de middleware afirma que la mayoría de las aplicaciones nunca necesitan escribir de antemano un tipo Env combinado.

Cómo continuar a partir de aquí

El middleware personalizado es una única forma de función con dos mitades y una sola regla sobre cómo termina. Ponle el genérico Variables desde la primera línea que escribas, mantén cada uno en su propio módulo, y el orden de registro hace el resto. Toma el temporizador de más arriba, muévelo a middleware/ y añádele un argumento de configuración envolviendo createMiddleware() en una función normal que lo devuelva: ese es todo el patrón para un middleware parametrizado.

Preguntas frecuentes

¿Cómo ejecuto un middleware en una sola ruta en lugar de en todas?

Regístralo con el método de ruta en lugar de con un app.use() genérico. Hono te permite adjuntar middleware a través de app.HTTP_METHOD, de modo que app.post('/posts/*', basicAuth()) se aplica solo a las peticiones POST en esa ruta, y pasar el middleware como argumento antes del handler, como en app.get('/echo', echoMiddleware, handler), lo limita a esa única ruta sin tocar el resto de la aplicación.

¿Se aplica el middleware a las rutas registradas antes que él?

Registra el middleware por encima de los handlers que debe envolver. Hono ejecuta handlers y middleware en orden de registro, y la documentación de routing expresa la regla con claridad: todo lo que quieras que se ejecute antes de un handler tiene que registrarse antes que él. Una ruta declarada antes de la llamada a app.use() se empareja primero, así que el middleware añadido después no forma parte de manera fiable de la cadena de esa ruta.

¿Cuál es la diferencia entre Bindings y Variables en el genérico Env?

Bindings describe los recursos de plataforma y los valores de entorno que inyecta el runtime, leídos a través de c.env, como una base de datos D1 o un secreto en Cloudflare Workers. Variables describe los valores por petición que tu propio código escribe con c.set() y lee con c.get() o c.var. Ambos residen en el mismo objeto Env pasado como new Hono<{ Bindings: ...; Variables: ... }>().

¿Puedo leer una variable de contexto fuera de un handler o de un middleware?

Sí, con el middleware integrado Context Storage. Registra app.use(contextStorage()) desde 'hono/context-storage' y luego llama a getContext() dentro de cualquier función para acceder al contexto de la petición actual, incluidos c.var y, en Cloudflare Workers, los bindings en c.env. tryGetContext() se comporta igual pero devuelve undefined donde getContext() lanzaría una excepción, lo que resulta adecuado para código que puede ejecutarse fuera de una petición.

DevTools for the frontend

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

We use cookies to improve your experience. By using our site, you accept cookies.