Skip to content

refactor!: move route() into @zodapi/core so contracts carry no HTTP deps - #21

Merged
christensena merged 1 commit into
feat/wire-string-type-checkfrom
refactor/core-owns-route
Sep 1, 2026
Merged

refactor!: move route() into @zodapi/core so contracts carry no HTTP deps#21
christensena merged 1 commit into
feat/wire-string-type-checkfrom
refactor/core-owns-route

Conversation

@christensena

Copy link
Copy Markdown
Owner

Stacked on #19. Review that first; this PR's diff is against it.

Why

route() lived in @zodapi/hono, and its createRoute import pulled the @hono/zod-openapi barrel — 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, and sideEffects: false can't help: the barrel runs extendZodWithOpenApi(z) at import time.

The eight lines of createRoute that route() actually used are inlined instead. getRoutingPath stays 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/core
  • ZodapiRouteConfig is expressed in zod types rather than re-exporting hono's RouteConfig. Request and response shapes are exact — they drive every inference; 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.
  • examples/api now depends on @zodapi/core + zod and nothing else

Breaking

@zodapi/hono is server-only: createApp(), plus OpenAPIHono and createRoute. Everything else it re-exported comes from @zodapi/core, and z comes from zod.

- import { queryArray, route, z } from '@zodapi/hono'
+ import { queryArray, route } from '@zodapi/core'
+ import { z } from 'zod'

hono peer narrows >=4.10.0^4.10.0: core now models RouteConfig structurally, and an unbounded range let a future major widen it with no signal.

Verification

  • The generated OpenAPI 3.1 document is byte-identical before and after (pnpm --filter @zodapi/example-app spec, 393 lines, empty diff) — the inlined createRoute and .meta({ id }) are exact substitutes.
  • pnpm why hono --filter @zodapi/example-api and the same for @hono/zod-openapi: no matches.
  • examples/app/src/server.ts typechecks unchanged, so app.openapi() still infers c.req.valid('param') and response-status narrowing from a core-built route.
  • Build, typecheck, lint, format clean; 150 tests pass. The live client demo exercises 200/409/400/404.

-- Claude

🤖 Generated with Claude Code

https://claude.ai/code/session_01P4oNyS84DzZeNFzLGcw7rQ

…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
christensena force-pushed the refactor/core-owns-route branch from df702a8 to a807164 Compare September 1, 2026 22:08
@christensena
christensena merged commit 60550c5 into main Sep 1, 2026
1 check passed
@christensena
christensena deleted the refactor/core-owns-route branch September 1, 2026 22:21
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.

1 participant