Skip to content

feat(zod-openapi): add defineOpenAPIRoute and openapiRoutes for batch route registration#1752

Open
destroSunRay wants to merge 9 commits intohonojs:mainfrom
destroSunRay:zod-openapi-with-better-compatability-for-rpc-2
Open

feat(zod-openapi): add defineOpenAPIRoute and openapiRoutes for batch route registration#1752
destroSunRay wants to merge 9 commits intohonojs:mainfrom
destroSunRay:zod-openapi-with-better-compatability-for-rpc-2

Conversation

@destroSunRay
Copy link
Copy Markdown

Note
This is my first time contributing to open-source, if I made mistakes I'm sorry. Any feedback is welcome.

Description

This PR adds two new utilities to improve route definition and registration in @hono/zod-openapi:

  • defineOpenAPIRoute: Provides explicit type safety for route definitions
  • openapiRoutes: Enables batch registration of multiple routes with full type safety

Problem

  • Registering many routes individually was repetitive and verbose
  • Type inference for complex route configurations was challenging
  • Organizing routes across multiple files was difficult
  • No built-in support for conditional route registration
  • RPC type safety was hard to maintain across scattered route registrations

Solution

  • defineOpenAPIRoute: Wraps route definitions with explicit types for better IDE support and type checking
  • openapiRoutes: Accepts an array of route definitions and registers them all at once
  • Supports addRoute flag for conditional registration
  • Maintains full type safety and RPC support through recursive type merging
  • Enables clean modular organization of routes

Benefits

  • ✅ Reduced boilerplate code
  • ✅ Better type inference and IDE autocomplete
  • ✅ Easier code organization and maintainability
  • ✅ Declarative conditional routes
  • ✅ Full backward compatibility

Examples

See the updated README for usage examples.

Testing

  • All existing tests pass (102/102)
  • Added tests for new functionality
  • Verified type inference works correctly

Documentation

  • Updated package README with usage examples

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Feb 16, 2026

🦋 Changeset detected

Latest commit: ef67047

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@hono/zod-openapi Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@destroSunRay
Copy link
Copy Markdown
Author

This pull request is the implementation for the proposed feature 1751.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 2, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.73%. Comparing base (031f805) to head (ef67047).
⚠️ Report is 58 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1752      +/-   ##
==========================================
+ Coverage   92.68%   92.73%   +0.04%     
==========================================
  Files         112      112              
  Lines        3733     3744      +11     
  Branches      946      946              
==========================================
+ Hits         3460     3472      +12     
+ Misses        245      244       -1     
  Partials       28       28              
Flag Coverage Δ
zod-openapi 94.90% <100.00%> (+1.06%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@destroSunRay
Copy link
Copy Markdown
Author

I pushed fixes for the previous CI errors. Could a maintainer please rerun the failing workflow checks (or “Re-run all jobs”) when you have a moment? Thanks!

Copilot AI review requested due to automatic review settings April 12, 2026 06:27
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds new batch route-definition/registration utilities to @hono/zod-openapi to make large sets of createRoute() + openapi() registrations less repetitive while preserving RPC typing.

Changes:

  • Introduces defineOpenAPIRoute/OpenAPIRoute and supporting types for explicitly-typed route definitions.
  • Adds OpenAPIHono#openapiRoutes() for registering many OpenAPI routes in one call (with optional addRoute gating).
  • Updates README, tests, eslint suppressions, and adds a changeset for the new feature.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/zod-openapi/src/index.ts Adds defineOpenAPIRoute types/util and openapiRoutes() batch registration method.
packages/zod-openapi/src/index.test.ts Adds extensive tests for the new utilities and adjusts a few existing tests/types.
packages/zod-openapi/README.md Documents batch registration and conditional/modular route organization.
packages/zod-openapi/eslint-suppressions.json Updates suppression counts following test changes.
.changeset/quiet-snakes-stare.md Declares a minor release and summarizes the new APIs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +408 to +410
// Helper: Calculate the expected Handler type for a specific RouteConfig
type HandlerFromRoute<R extends RouteConfig, E extends Env> = Handler<
E,
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HandlerFromRoute (and consequently OpenAPIRoute.handler) doesn’t mirror the openapi() handler env typing when route.middleware is present. openapi() widens the handler env to RouteMiddlewareParams<R>['env'] & E, but HandlerFromRoute always uses E, so routes with middleware can lose env type info (or become incompatible when used with openapi()). Consider reusing the existing RouteMiddlewareParams logic (or the exported RouteHandler type) so middleware-provided env is reflected consistently.

Suggested change
// Helper: Calculate the expected Handler type for a specific RouteConfig
type HandlerFromRoute<R extends RouteConfig, E extends Env> = Handler<
E,
// Helper: Merge route middleware env into the handler env, matching `openapi()`
type EnvFromRoute<R extends RouteConfig, E extends Env> = RouteMiddlewareParams<R>['env'] & E
// Helper: Calculate the expected Handler type for a specific RouteConfig
type HandlerFromRoute<R extends RouteConfig, E extends Env> = Handler<
EnvFromRoute<R, E>,

Copilot uses AI. Check for mistakes.
Comment on lines +400 to +444
// Helper: Consolidate all Input types (Query, Param, Json, etc.)
type ComputeInput<R extends RouteConfig> = InputTypeParam<R> &
InputTypeQuery<R> &
InputTypeHeader<R> &
InputTypeCookie<R> &
InputTypeForm<R> &
InputTypeJson<R>

// Helper: Calculate the expected Handler type for a specific RouteConfig
type HandlerFromRoute<R extends RouteConfig, E extends Env> = Handler<
E,
ConvertPathType<R['path']>,
ComputeInput<R>,
R extends {
responses: {
[statusCode: number]: {
content: {
[mediaType: string]: ZodMediaTypeObject
}
}
}
}
? MaybePromise<RouteConfigToTypedResponse<R>>
: MaybePromise<RouteConfigToTypedResponse<R>> | MaybePromise<Response>
>

type HookFromRoute<R extends RouteConfig, E extends Env> =
| Hook<
ComputeInput<R>,
E,
ConvertPathType<R['path']>,
R extends {
responses: {
[statusCode: number]: {
content: {
[mediaType: string]: ZodMediaTypeObject
}
}
}
}
? MaybePromise<RouteConfigToTypedResponse<R>> | undefined
: MaybePromise<RouteConfigToTypedResponse<R>> | MaybePromise<Response> | undefined
>
| undefined

Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These helper types (ComputeInput, HandlerFromRoute, HookFromRoute) duplicate logic that already exists earlier in this file (RouteHandler / RouteHook and the repeated InputType* intersections). This duplication increases the risk of the two drifting over time (e.g., middleware env handling is already different). Prefer reusing the existing exported types/aliases instead of redefining parallel versions here.

Suggested change
// Helper: Consolidate all Input types (Query, Param, Json, etc.)
type ComputeInput<R extends RouteConfig> = InputTypeParam<R> &
InputTypeQuery<R> &
InputTypeHeader<R> &
InputTypeCookie<R> &
InputTypeForm<R> &
InputTypeJson<R>
// Helper: Calculate the expected Handler type for a specific RouteConfig
type HandlerFromRoute<R extends RouteConfig, E extends Env> = Handler<
E,
ConvertPathType<R['path']>,
ComputeInput<R>,
R extends {
responses: {
[statusCode: number]: {
content: {
[mediaType: string]: ZodMediaTypeObject
}
}
}
}
? MaybePromise<RouteConfigToTypedResponse<R>>
: MaybePromise<RouteConfigToTypedResponse<R>> | MaybePromise<Response>
>
type HookFromRoute<R extends RouteConfig, E extends Env> =
| Hook<
ComputeInput<R>,
E,
ConvertPathType<R['path']>,
R extends {
responses: {
[statusCode: number]: {
content: {
[mediaType: string]: ZodMediaTypeObject
}
}
}
}
? MaybePromise<RouteConfigToTypedResponse<R>> | undefined
: MaybePromise<RouteConfigToTypedResponse<R>> | MaybePromise<Response> | undefined
>
| undefined
// Reuse the canonical route typing aliases defined earlier in this file
type HandlerFromRoute<R extends RouteConfig, E extends Env> = RouteHandler<R, E>
type HookFromRoute<R extends RouteConfig, E extends Env> = RouteHook<R, E>

Copilot uses AI. Check for mistakes.
Comment on lines +685 to +706
openapiRoutes = <
const Inputs extends readonly {
route: RouteConfig
handler: any
hook?: any
addRoute?: boolean
}[],
>(
inputs: Inputs
): OpenAPIHono<E, S & SchemaFromRoutes<Inputs, BasePath>, BasePath> => {
type Result = {
[K in keyof Inputs]: Inputs[K] extends {
route: infer R extends RouteConfig
addRoute?: infer AR extends boolean | undefined
}
? OpenAPIRoute<R, E, AR>
: never
}

const typedInputs = inputs as unknown as Result

typedInputs
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openapiRoutes currently accepts handler: any / hook?: any in its Inputs constraint, which means callers can pass non-handler values and still typecheck (the later as unknown as Result cast won’t enforce correctness). If this API is intended to provide “full type safety”, consider constraining Inputs to readonly OpenAPIRoute<any, E, any>[] (or similar) and removing the unsafe cast so invalid route definitions are rejected at compile time.

Suggested change
openapiRoutes = <
const Inputs extends readonly {
route: RouteConfig
handler: any
hook?: any
addRoute?: boolean
}[],
>(
inputs: Inputs
): OpenAPIHono<E, S & SchemaFromRoutes<Inputs, BasePath>, BasePath> => {
type Result = {
[K in keyof Inputs]: Inputs[K] extends {
route: infer R extends RouteConfig
addRoute?: infer AR extends boolean | undefined
}
? OpenAPIRoute<R, E, AR>
: never
}
const typedInputs = inputs as unknown as Result
typedInputs
openapiRoutes = <const Inputs extends readonly OpenAPIRoute<any, E, any>[]>(
inputs: Inputs
): OpenAPIHono<E, S & SchemaFromRoutes<Inputs, BasePath>, BasePath> => {
inputs

Copilot uses AI. Check for mistakes.

/**
* Register a list of routes with full Type Safety and RPC support.
* * @param inputs - An array of objects containing { route, handler, hook }.
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSDoc has an extra leading * before @param (* * @param ...), which will render oddly in generated docs. Also, the param description mentions only { route, handler, hook }, but addRoute is also supported and should be documented there.

Suggested change
* * @param inputs - An array of objects containing { route, handler, hook }.
* @param inputs - An array of objects containing { route, handler, hook, addRoute }.

Copilot uses AI. Check for mistakes.
Comment on lines +3017 to +3019
// The route should technically still be in OpenAPI definitions
// if `hide: true` is not set, but the actual Hono router won't have it.
// Let's verify type safety and runtime behaviors.
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is inaccurate with the current implementation: when addRoute is false, openapiRoutes() skips calling this.openapi(...), so the route won’t be registered in the Hono router or in the OpenAPI registry/definitions. Either update the comment to match the behavior, or extend the test to assert the OpenAPI document does/doesn’t include /disabled based on the intended semantics.

Suggested change
// The route should technically still be in OpenAPI definitions
// if `hide: true` is not set, but the actual Hono router won't have it.
// Let's verify type safety and runtime behaviors.
// When `addRoute` is `false`, `openapiRoutes()` skips registering the
// route entirely, so it is not added to the Hono router or the OpenAPI
// definitions. This test verifies the runtime behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +440 to +442
const app = new OpenAPIHono()

```ts
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README formatting: const app = new OpenAPIHono() is outside the fenced ts code block, so it will render as plain text and makes the example harder to copy/paste. Move that line into the same ```ts block as the app.openapiRoutes(...) call (or wrap it in its own code block).

Suggested change
const app = new OpenAPIHono()
```ts
```ts
const app = new OpenAPIHono()

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants