refactor!: move route() into @zodapi/core so contracts carry no HTTP deps - #21
Merged
christensena merged 1 commit intoSep 1, 2026
Merged
Conversation
This was referenced Aug 31, 2026
…deps
route() came from @zodapi/hono, whose createRoute import pulled the
@hono/zod-openapi barrel — hono, zod-to-openapi, openapi3-ts, yaml — into
every package holding a contract. Bundlers that do not tree-shake within
modules shipped all of it to clients that never call it, and sideEffects:
false could not help: the barrel runs extendZodWithOpenApi(z) at import
time. The eight lines of createRoute that route() actually used are
inlined, keeping getRoutingPath non-enumerable so it stays out of spreads
and the generated document.
ZodapiRouteConfig is now expressed in zod types rather than re-exporting
hono's RouteConfig. Request and response shapes are exact — they drive
every inference; the OpenAPI documentation fields are typed loosely, and
anything they let through app.openapi() still catches. method drops OAS
3.2's 'query' to match RouteDef['method'], which drives the client.
BREAKING CHANGE: @zodapi/hono is now server-only, exporting createApp()
plus OpenAPIHono and createRoute. Import route, validationErrorResponse,
ZodapiRoute, ZodapiRouteConfig, queryArray, ValidationError,
PROBLEM_JSON_CONTENT_TYPE and ZODAPI_VALIDATION_TYPE from @zodapi/core,
and z from zod. The dropped z re-export also carried the .openapi()
prototype method — use zod's .meta({ id: 'User' }), which produces the
same component and $ref.
The generated OpenAPI document is byte-identical before and after.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P4oNyS84DzZeNFzLGcw7rQ
christensena
force-pushed
the
refactor/core-owns-route
branch
from
September 1, 2026 22:08
df702a8 to
a807164
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #19. Review that first; this PR's diff is against it.
Why
route()lived in@zodapi/hono, and itscreateRouteimport pulled the@hono/zod-openapibarrel —hono,@asteasolutions/zod-to-openapi,openapi3-ts,yaml(~4.8MB installed) — into every package holding a contract. Bundlers that don't tree-shake within modules ship all of it to clients that never call it, andsideEffects: falsecan't help: the barrel runsextendZodWithOpenApi(z)at import time.The eight lines of
createRoutethatroute()actually used are inlined instead.getRoutingPathstays non-enumerable — an enumerable one leaks the function into{...route}spreads and the generated document.The second half of the coupling was
.openapi('User'): a prototype method that only exists because of that same import side effect. Zod's own.meta({ id: 'User' })produces an identical component and$ref, so contracts use it now.What
route(),validationErrorResponse,ZodapiRoute,ZodapiRouteConfig→@zodapi/coreZodapiRouteConfigis expressed in zod types rather than re-exporting hono'sRouteConfig. Request and response shapes are exact — they drive every inference; OpenAPI documentation fields are typed loosely, and anything they let throughapp.openapi()still catches.methoddrops OAS 3.2's'query'to matchRouteDef['method'], which drives the client.examples/apinow depends on@zodapi/core+zodand nothing elseBreaking
@zodapi/honois server-only:createApp(), plusOpenAPIHonoandcreateRoute. Everything else it re-exported comes from@zodapi/core, andzcomes fromzod.honopeer narrows>=4.10.0→^4.10.0: core now modelsRouteConfigstructurally, and an unbounded range let a future major widen it with no signal.Verification
pnpm --filter @zodapi/example-app spec, 393 lines, empty diff) — the inlinedcreateRouteand.meta({ id })are exact substitutes.pnpm why hono --filter @zodapi/example-apiand the same for@hono/zod-openapi: no matches.examples/app/src/server.tstypechecks unchanged, soapp.openapi()still infersc.req.valid('param')and response-status narrowing from a core-built route.-- Claude
🤖 Generated with Claude Code
https://claude.ai/code/session_01P4oNyS84DzZeNFzLGcw7rQ