feat(zod-openapi): add defineOpenAPIRoute and openapiRoutes for batch route registration#1752
Conversation
…inition and type safety
…inition and type safety
…agement and type safety
🦋 Changeset detectedLatest commit: ef67047 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
|
This pull request is the implementation for the proposed feature 1751. |
Codecov Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…th addRoute set to false
|
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! |
There was a problem hiding this comment.
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/OpenAPIRouteand supporting types for explicitly-typed route definitions. - Adds
OpenAPIHono#openapiRoutes()for registering many OpenAPI routes in one call (with optionaladdRoutegating). - 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.
| // Helper: Calculate the expected Handler type for a specific RouteConfig | ||
| type HandlerFromRoute<R extends RouteConfig, E extends Env> = Handler< | ||
| E, |
There was a problem hiding this comment.
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.
| // 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>, |
| // 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 | ||
|
|
There was a problem hiding this comment.
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.
| // 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> |
| 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 |
There was a problem hiding this comment.
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.
| 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 |
|
|
||
| /** | ||
| * Register a list of routes with full Type Safety and RPC support. | ||
| * * @param inputs - An array of objects containing { route, handler, hook }. |
There was a problem hiding this comment.
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.
| * * @param inputs - An array of objects containing { route, handler, hook }. | |
| * @param inputs - An array of objects containing { route, handler, hook, addRoute }. |
| // 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. |
There was a problem hiding this comment.
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.
| // 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. |
| const app = new OpenAPIHono() | ||
|
|
||
| ```ts |
There was a problem hiding this comment.
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).
| const app = new OpenAPIHono() | |
| ```ts | |
| ```ts | |
| const app = new OpenAPIHono() |
Description
This PR adds two new utilities to improve route definition and registration in
@hono/zod-openapi:defineOpenAPIRoute: Provides explicit type safety for route definitionsopenapiRoutes: Enables batch registration of multiple routes with full type safetyProblem
Solution
defineOpenAPIRoute: Wraps route definitions with explicit types for better IDE support and type checkingopenapiRoutes: Accepts an array of route definitions and registers them all at onceaddRouteflag for conditional registrationBenefits
Examples
See the updated README for usage examples.
Testing
Documentation