middleware icon indicating copy to clipboard operation
middleware copied to clipboard

Include auth middleware responses in zod-openapi reasponses

Open Soviut opened this issue 5 months ago • 7 comments

Which middleware is the feature for?

@hono/zod-openapi

What is the feature you are proposing?

I originally posted this on StackOverflow and plan to provide an answer if I can find a solution here. https://stackoverflow.com/questions/79715942/how-to-include-hono-jwt-auth-middleware-responses-in-hono-zod-openapi


I have an API built using Hono and the @hono/zod-openapi. I've integrated hono/jwt auth middleware.

My auth middleware is a thin wrapper around hono/jwt in order to supply secrets.

import { jwt } from 'hono/jwt'
import { env } from 'hono/adapter'
import { type ContextType } from '../context'

export default (c: ContextType, next: () => Promise<void>) => {
  const { JWT_SECRET } = env(c)
  const jwtMiddleware = jwt({ secret: JWT_SECRET })
  return jwtMiddleware(c, next)
}

For a protected route I'm applying the auth middleware as follows.

app.use('/protected/*', authMiddleware)
app.route('/protected', protectedRouter)

However, I also need to create an OpenAPI definition for the 401 response that each route in the protectedRouter could return. This gives a type error since zod-openapi is unable to infer the error response from the route because it is not being explicitly returned from the route itself.

router.openapi(
  createRoute({
    method: 'get',
    path: '/',
    responses: {
      200: {
        description: 'List all items',
        content: {
          'application/json': {
            schema: responseSchema(Item),
          },
        },
      },
      401: {
        description: 'Unauthorized',
        content: {
          'application/json': {
            schema: ErrorSchema,
          },
        },
      },
    },
  }),
  async (c) => {  // <-- type error here
    const db = c.get('db')
    const res = await db.select().from(items)
    return c.json({ data: res })
  }
)

How can I include hono/jwt auth middleware responses into my hono/zod-openapi schema?

Soviut avatar Jul 28 '25 07:07 Soviut

@Soviut

Can you create a minimal project to reproduce it?

yusukebe avatar Jul 29 '25 10:07 yusukebe

I believe this should work.

The middleware should return a 401, but it does not get added to the OpenAPI doc automatically, and when I add it manually, there is a type error is VSCode claiming the route does not return a 401.

import { type Context } from 'hono'
import { OpenAPIHono } from '@hono/zod-openapi'
import { z, type ZodError } from 'zod/v4'
import { type JwtVariables, jwt } from 'hono/jwt'
import { env } from 'hono/adapter'
import { createRoute } from '@hono/zod-openapi'

export interface Bindings {
  BASE_URL: string
  CORS_ORIGIN: string
  DATABASE_URL: string
  JWT_SECRET: string
  JWT_COOKIE_SECRET: string
  JWT_ACCESS_TOKEN_EXPIRY: string
  JWT_REFRESH_TOKEN_EXPIRY: string
}

export interface Variables extends JwtVariables {
  // db: DatabaseClient
}

export interface Env {
  Bindings: Bindings
  Variables: Variables
}

export type ContextType = Context<Env>

export const ErrorSchema = z.object({
  code: z.number().openapi({ example: 400 }),
  errors: z.array(z.string()).openapi({ example: ['Bad request'] }),
})

export const formatZodErrors = (error: ZodError) =>
  error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`)

export const ResponseSchema = <T extends z.ZodTypeAny>(item: T) =>
  z.object({
    data: item,
  })

export const ResponseSchemaList = <T extends z.ZodTypeAny>(item: T) =>
  z.object({
    data: z.array(item),
  })

export const organizationSchema = z.object({
  id: z.string(),
  name: z.string(),
})

export const createHono = () =>
  new OpenAPIHono<Env>({
    defaultHook: (result, c) => {
      if (!result.success) {
        return c.json(
          {
            code: 400,
            errors: formatZodErrors(result.error),
          },
          400
        )
      }
    },
  })


const authMiddleware = (c: ContextType, next: () => Promise<void>) => {
  const { JWT_SECRET } = env(c)

  const jwtMiddleware = jwt({
    secret: JWT_SECRET,
  })

  return jwtMiddleware(c, next)
}

const organizationsRouter = createHono()

organizationsRouter.openapi(
  createRoute({
    method: 'get',
    path: '/',
    responses: {
      200: {
        description: 'List all organizations',
        content: {
          'application/json': {
            schema: ResponseSchemaList(organizationSchema),
          },
        },
      },
      401: {
        description: 'Unauthorized',
        content: {
          'application/json': {
            schema: ErrorSchema,
          },
        },
      },      
    },
  }),
  async (c) => {
    return c.json({ data: [ { id: 'org_123', name: 'My Organization' } ] })
  }
)

const app = createHono()

app.use('/organizations/*', authMiddleware)
app.route('/organizations', organizationsRouter)

Soviut avatar Aug 03 '25 03:08 Soviut

@Soviut

The code is not complete. OpenAPIHono is not exported by hono, and there is no ../context. Please provide working code:

Image

And,

The middleware should return a 401, but it does not get added to the OpenAPI doc automatically, and when I add it manually, there is a type error is VSCode claiming the route does not return a 401.

Please describe in more detail, in an understandable way. Write the instructions to reproduce the bug. Write the expected behavior. Write the actual behavior.

yusukebe avatar Aug 04 '25 07:08 yusukebe

@yusukebe I've updated my example with the correct imports.

If I put the 401 response in the createRoute() I get a type error on the route handler saying that it does not return a 401.

I've added comments with arrow to the lines in question.

Soviut avatar Aug 04 '25 08:08 Soviut

@Soviut

If I put the 401 response in the createRoute() I get a type error on the route handler saying that it does not return a 401.

Can you explain it with a screenshot of your editor?

yusukebe avatar Aug 04 '25 09:08 yusukebe

@Soviut

I've added comments with arrow to the lines in question.

Where is the arrow?

yusukebe avatar Aug 04 '25 09:08 yusukebe

@yusukebe Yes, I'll get a screenshot when I have a moment.

The arrows were added as comments to my example code above.

Soviut avatar Aug 04 '25 17:08 Soviut