Zum Inhalt springen

Middleware ​

Middleware wird vor oder nach dem Handler eines Endpunkts ausgeführt. Wir können vor der Weiterleitung die Request abrufen oder nach der Weiterleitung die Response verändern.

Definition von Middleware ​

  • Handler – Sollte ein Response-Objekt zurückgeben. Es wird nur ein Handler aufgerufen.
  • Middleware – Sollte await next() ausführen und nichts zurückgeben, um die nächste Middleware aufzurufen, oder eine Response zurückgeben, um die Verarbeitung vorzeitig zu beenden.

Du kannst Middleware wie Handler mit app.use oder app.HTTP_METHOD registrieren. Damit lassen sich Pfad und Methode einfach festlegen.

ts
// match any method, all routes
app.use(logger())

// specify path
app.use('/posts/*', cors())

// specify method and path
app.post('/posts/*', basicAuth())

Wenn der Handler eine Response zurückgibt, wird diese an den Benutzer gesendet und die Verarbeitung beendet.

ts
app.post('/posts', (c) => c.text('Created!', 201))

In diesem Fall werden vor der Weiterleitung vier Middlewares wie folgt verarbeitet:

ts
logger() -> cors() -> basicAuth() -> *handler*

Ausführungsreihenfolge ​

Die Ausführungsreihenfolge der Middleware wird durch die Reihenfolge ihrer Registrierung bestimmt. Der Verarbeitungsschritt vor next der zuerst registrierten Middleware wird zuerst ausgeführt, während der Schritt nach next zuletzt ausgeführt wird. Siehe das folgende Beispiel.

ts
app.use(async (_, next) => {
  console.log('middleware 1 start')
  await next()
  console.log('middleware 1 end')
})
app.use(async (_, next) => {
  console.log('middleware 2 start')
  await next()
  console.log('middleware 2 end')
})
app.use(async (_, next) => {
  console.log('middleware 3 start')
  await next()
  console.log('middleware 3 end')
})

app.get('/', (c) => {
  console.log('handler')
  return c.text('Hello!')
})

Das Ergebnis lautet wie folgt.

middleware 1 start
  middleware 2 start
    middleware 3 start
      handler
    middleware 3 end
  middleware 2 end
middleware 1 end

Beachte: Wenn der Handler oder eine Middleware einen Fehler auslöst, fängt Hono ihn ab und übergibt ihn entweder an deinen app.onError()-Callback oder wandelt ihn automatisch in eine 500-Antwort um, bevor diese durch die Middleware-Kette zurückgegeben wird. Das bedeutet, dass next() niemals einen Fehler auslöst. Du musst es daher nicht in try/catch/finally einschließen.

Integrierte Middleware ​

Hono enthält integrierte Middleware.

ts
import { Hono } from 'hono'
import { poweredBy } from 'hono/powered-by'
import { logger } from 'hono/logger'
import { basicAuth } from 'hono/basic-auth'

const app = new Hono()

app.use(poweredBy())
app.use(logger())

app.use(
  '/auth/*',
  basicAuth({
    username: 'hono',
    password: 'acoolproject',
  })
)

Achtung

In Deno kannst du eine Middleware-Version verwenden, die von der Hono-Version abweicht. Dies kann jedoch zu Fehlern führen. Zum Beispiel funktioniert dieser Code nicht, weil die Versionen unterschiedlich sind.

ts
import { Hono } from 'jsr:@hono/hono@4.4.0'
import { upgradeWebSocket } from 'jsr:@hono/hono@4.4.5/deno'

const app = new Hono()

app.get(
  '/ws',
  upgradeWebSocket(() => ({
    // ...
  }))
)

Eigene Middleware ​

Du kannst deine eigene Middleware direkt in app.use() schreiben:

ts
// Custom logger
app.use(async (c, next) => {
  console.log(`[${c.req.method}] ${c.req.url}`)
  await next()
})

// Add a custom header
app.use('/message/*', async (c, next) => {
  await next()
  c.header('x-message', 'This is middleware!')
})

app.get('/message/hello', (c) => c.text('Hello Middleware!'))

Allerdings kann direkt in app.use() eingebettete Middleware ihre Wiederverwendbarkeit einschränken. Deshalb können wir Middleware in separate Dateien auslagern.

Um beim Auslagern der Middleware die Typdefinitionen für context und next zu erhalten, können wir createMiddleware() aus Honos Factory verwenden. Damit können nachfolgende Handler auch typsicher auf Daten zugreifen, die wir mit set im Context gespeichert haben.

ts
import { createMiddleware } from 'hono/factory'

const logger = createMiddleware(async (c, next) => {
  console.log(`[${c.req.method}] ${c.req.url}`)
  await next()
})

Info

Bei createMiddleware können Generics verwendet werden:

ts
createMiddleware<{Bindings: Bindings}>(async (c, next) =>

Die Antwort nach Next verändern ​

Außerdem kann Middleware bei Bedarf so gestaltet werden, dass sie Antworten verändert:

ts
const stripRes = createMiddleware(async (c, next) => {
  await next()
  c.res = undefined
  c.res = new Response('New Response')
})

Zugriff auf den Kontext in Middleware-Argumenten ​

Um innerhalb von Middleware-Argumenten auf den Kontext zuzugreifen, verwende direkt den von app.use bereitgestellten Kontextparameter. Das folgende Beispiel verdeutlicht dies.

ts
import { cors } from 'hono/cors'

app.use('*', async (c, next) => {
  const middleware = cors({
    origin: c.env.CORS_ORIGIN,
  })
  return middleware(c, next)
})

Den Kontext in Middleware erweitern ​

Verwende c.set, um den Kontext innerhalb einer Middleware zu erweitern. Typsicherheit erhältst du, indem du der Funktion createMiddleware das generische Argument { Variables: { yourVariable: YourVariableType } } übergibst.

ts
import { createMiddleware } from 'hono/factory'

const echoMiddleware = createMiddleware<{
  Variables: {
    echo: (str: string) => string
  }
}>(async (c, next) => {
  c.set('echo', (str) => str)
  await next()
})

app.get('/echo', echoMiddleware, (c) => {
  return c.text(c.var.echo('Hello!'))
})

Typinferenz über verkettete Middleware hinweg ​

Wenn du mehrere Middlewares mit .use() verkettst, sammelt Hono automatisch die Typen von Variables. Routen-Handler, die auf die Middleware-Kette folgen, können typsicher auf alle Variablen jeder vorherigen Middleware zugreifen:

ts
import { createMiddleware } from 'hono/factory'

const authMiddleware = createMiddleware<{
  Variables: { user: { id: string; name: string } }
}>(async (c, next) => {
  c.set('user', { id: '123', name: 'Alice' })
  await next()
})

const dbMiddleware = createMiddleware<{
  Variables: { db: { query: (sql: string) => Promise<unknown> } }
}>(async (c, next) => {
  c.set('db', {
    query: async (sql) => {
      /* ... */
    },
  })
  await next()
})

const app = new Hono()
  .use(authMiddleware)
  .use(dbMiddleware)
  .get('/', (c) => {
    // Both `user` and `db` are available and type-safe
    const user = c.var.user // { id: string; name: string }
    const db = c.var.db // { query: (sql: string) => Promise<unknown> }
    return c.json({ user })
  })

Das funktioniert, weil jeder Aufruf von .use() eine neue Hono-Instanz mit dem zusammengeführten Typ zurückgibt. Der Typ wächst also mit der Middleware-Kette. In den meisten Fällen entfällt dadurch die Notwendigkeit, vorher manuell einen kombinierten Typ Env zu deklarieren.

Middleware von Drittanbietern ​

Integrierte Middleware hängt nicht von externen Modulen ab. Middleware von Drittanbietern kann hingegen andere Bibliotheken verwenden. Damit können wir komplexere Anwendungen erstellen.

Es steht eine Vielzahl von Middlewares von Drittanbietern zur Verfügung. Dazu gehören beispielsweise GraphQL-Server-Middleware, Sentry-Middleware, Firebase-Auth-Middleware und weitere.

Veröffentlicht unter der MIT-Lizenz.