Routing and middleware

Use built-in middleware selectively

Apply CORS, secure headers, logging, compression, caching, and other middleware only where their assumptions fit.

Built-in middleware saves boilerplate, but each piece encodes a policy. Mount it where that policy applies, not on every route by default.

Our public read API allows one frontend origin. The internal health route does not need browser CORS at all:

import { cors } from 'hono/cors'
import { secureHeaders } from 'hono/secure-headers'

const publicApi = new Hono()
publicApi.use('*', cors({
  origin: 'https://bookmarks.flaviocopes.com',
  allowMethods: ['GET', 'HEAD']
}))
publicApi.use('*', secureHeaders())
publicApi.get('/bookmarks', (c) => c.json([]))

const app = new Hono()
app.route('/api', publicApi)
app.get('/health', (c) => c.json({ ok: true }))

Preflight from the allowed origin should succeed:

curl -s -D - -X OPTIONS http://localhost:3000/api/bookmarks \
  -H 'Origin: https://bookmarks.flaviocopes.com' \
  -H 'Access-Control-Request-Method: GET' -o /dev/null

Look for access-control-allow-origin: https://bookmarks.flaviocopes.com. A GET with the same Origin header on /api/bookmarks should include that header too.

Hit health without CORS mounted:

curl -s -D - http://localhost:3000/health \
  -H 'Origin: https://bookmarks.flaviocopes.com' -o /dev/null

You should get {"ok":true} with no access-control-allow-origin, because /health never mounted cors().

A realistic failure: you mount cors({ origin: '*' }) globally while cookies authenticate users. Browsers reject credentialed requests with a wildcard origin. The API looks fine in curl but fails in the browser with a CORS error. Fix by scoping CORS to the public sub-app and listing the real frontend origin.

The same rule applies to compression, caching, and logger middleware. Cache headers on POST responses confuse clients. Verbose loggers on /health flood deploy logs.

Before you mount anything, write down what the middleware assumes. Scope with app.use('/api/*', ...) or a sub-app instead of app.use('*', ...).

Vitest can assert headers: expect(res.headers.get('access-control-allow-origin')).toBe('https://bookmarks.flaviocopes.com') on /api/bookmarks and toBeNull() on /health.

Disallowed origins should not receive CORS headers:

curl -s -D - http://localhost:3000/api/bookmarks \
  -H 'Origin: https://evil.example' -o /dev/null

The GET may still return 200 JSON for non-browser clients, but browsers will block the response without a matching access-control-allow-origin.

Logger middleware on the health route adds noise during deploy probes. Keep /health bare unless you need request ids there too.

Compression middleware on tiny JSON responses sometimes costs more CPU than it saves. Measure before enabling globally.

Try this on your own project: move CORS off the root app and onto the one router that serves browser clients.

Lesson completed