feat: add http_server protocol for generating typed Express handler stubs - #449
Merged
Conversation
…parameters Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phases 1-4 of the http_server plan: register the protocol/function type and render parameters, add the runtime behavioral spec (still RED), and emit a deserialize<Model>Headers counterpart to the existing serializer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ponses Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Factor HTTP_REASON_PHRASES, the HttpGlobalError alias block and the HttpError class source into protocols/http/shared.ts. Both protocol files still emit their own copy — they are independent modules — but the generator now has one source for each. Generated output is byte-identical (all snapshots unchanged). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
`--channels-protocols=http_server` was rejected outright, so `codegen init` could not produce an http_server configuration at all. Split the protocol options per input type: the messaging protocols stay AsyncAPI-only and http_server is offered for OpenAPI, so the interactive prompt (previously AsyncAPI-only) now runs for OpenAPI too without offering protocols the input cannot use. The OpenAPI branch honours an explicit selection instead of hard-coding http_client. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The protocol registry knew about http_server but the tool schemas did not: config-tools, integration-tools and the MCP route each inlined their own seven-protocol z.enum, so create_config, add_generator, get_usage_example and get_imports all rejected it. Make `protocolValues` in lib/data/generators.ts the single source and derive both the `Protocol` type and every z.enum from it, so the lists cannot drift again. Key `clientImports` to `Record<Protocol, string>` so the compiler requires an entry per protocol, and give http_server its Express import. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add the protocol to the playground picker and to PROTOCOL_INPUT_COMPATIBILITY as OpenAPI-only — the picker filters on the latter, so without it the option never renders. Add an HTTP Server chip to the homepage marquee (the existing HTTP chip becomes HTTP Client) and an http_server variant to the spec-to-code showcase. Per that file's header, the snippet is real CLI output for the demo's own embedded spec, and the hand-written index.ts pane compiles against it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adding a protocol touches the Zod enum and the renderer, and it is easy to stop there — the CLI wizard, the website and the MCP server each keep their own copy, none generated from the schema and none read by CI. That is how http_server shipped missing from all of them. protocol-surfaces.spec.ts reads the protocol list off the Zod schema and fails with the exact file and literal to add, covering init.ts, the playground, the homepage marquee and showcase, the MCP protocolValues constant and the docs protocol lists. It also asserts the MCP tool files still derive their z.enum from protocolValues rather than re-inlining one. Document the same surfaces as Phase 6 of the add-protocol skill and in CLAUDE.md, including which copies are release-generated and must not be hand-edited. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The page carried an "Explicit non-goals" section listing things the PR chose
not to build ("no framework-agnostic core layer"), which duplicated the ❌
rows in the support table, gave the reader nothing to act on, and claimed
permanent intent about features that are merely unbuilt. No other protocol
page has such a section.
Replace it with task-shaped sections — "Multiple response bodies" and
"Security requirements" — that say what happens and what to do about it, and
drop the design rationale scattered through the rest of the page.
Rephrase the two capability rows written in negative space:
"Frameworks other than Express ❌" becomes "Frameworks | Express".
Add a `write-docs` skill capturing the rule, since this came from writing
docs at the end of an implementation session, and point CLAUDE.md and the
add-protocol skill at it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
Author
|
🎉 This PR is included in version 0.82.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
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.
Adds
http_serveras achannelsprotocol for OpenAPI input: for every operation in the document, generate aregister<OperationId>(context)function that mounts a typed handler on a caller-supplied ExpressRouter.It is the structural inverse of the existing HTTP client, and shares its vocabulary (
HttpError, hooks, context interfaces) so the two sides read as one feature. Both protocols can be generated from the same document in one run.The handler receives typed path/query parameters, a typed request body and typed request headers, and returns a status-code-discriminated union that generated code marshals and sends. Returning a status the document does not declare is a compile error.
What's generated
Per operation: a
<Op>ServerResponseunion, aRegister<Op>Contextinterface, and theregister<Op>function. Generated code handles routing, body reading, parameter extraction, header deserialization, request validation, response marshalling and error mapping — you write thecallbackbody and nothing else.Shared per file:
HttpError,HttpServerHooks(beforeHandler/afterHandler/onError),HttpServerContext(hooks,additionalHeaders,skipRequestValidation), and thereadJsonBody/resolveErrorResponse/sendResponse/handleHandlerErrorhelpers.Changes
Generator
channels/protocols/http/server.ts— the per-operation rendererchannels/protocols/http/server-common-types.ts— the shared block, emitted once per filechannels/protocols/http/shared.ts— source fragments both HTTP protocols emit (HTTP_REASON_PHRASES, theHttpGlobalErroralias, theHttpErrorclass). Two copies in generated output are correct — the protocol files are independent modules — but the generator now has one source for each.channels/openapi.ts— replaced thehttp_client-only gate with a per-protocol dispatcher, mirroring the AsyncAPIswitch. AddedcollectServerResponseVariantsand extractedcorrelateOperationModelsso the client and server paths cannot drift on which model belongs to which operation.inputs/openapi/generators/headers.ts— newdeserialize<Model>Headers, the inbound counterpart to the existing serializer. Matches on lower-cased wire names (Express lower-cases inbound headers) and coerces per the constrained property type.Registration & config —
http_serveradded toChannelFunctionTypes,receivingFunctionTypes, theprotocolsZod enum andSupportedProtocols;schemas/regenerated.Notable details
Declared-but-bodyless status codes. The petstore's
addPetdeclares200(a Pet) and405(no body).405exists only in the raw document — never in the payload models — so the response union is assembled from the rawresponseskeys unioned with the model-derived body types. Neither source alone is complete.request.url, notrequest.originalUrl. Express strips the mount prefix fromurlbut not fromoriginalUrl, and the generatedextractPathParametersanchors its regex against the whole path. UsingoriginalUrlsilently breaksapp.use('/prefix', router); there is a runtime test guarding this.Validation is compiled once. The Ajv validator is created outside the route handler, not per request.
Internal errors never leak. A thrown
HttpErrormaps to its own status and body; anything else maps to a generic 500 with no message from the original error.Reaching the protocol from every surface
A protocol is not shipped when the generator emits it — it is shipped when a user can select it. The protocol list is duplicated across surfaces that are hand-maintained and were not generated from the Zod schema, and the first pass of this PR missed all of them:
codegen init—--channels-protocols=http_serverwas rejected outright, so the wizard could not produce an http_server config at all. The options are now split per input type, so the interactive prompt (previously AsyncAPI-only) also runs for OpenAPI without offering protocols that input cannot use, and the OpenAPI branch honours an explicit selection instead of hard-codinghttp_client.http_server, butconfig-tools,integration-toolsand the MCP route each inlined their own seven-protocolz.enum, socreate_config,add_generator,get_usage_exampleandget_importsall rejected it.protocolValuesinlib/data/generators.tsis now the single source and everyz.enumderives from it;clientImportsis keyedRecord<Protocol, string>so the compiler requires an entry per protocol.PROTOCOL_INPUT_COMPATIBILITYas OpenAPI-only. The picker filters on the latter, so an option missing there never renders.http_servervariant in the spec-to-code showcase. Per that file's header rule, the snippet is real CLI output generated from the demo's own embedded spec, and the hand-writtenindex.tspane compiles against it.test/protocol-surfaces.spec.tsnow gates this. It reads the protocol list off the Zod schema and fails with the exact file and literal to add, covering all of the above plus the docs protocol lists — and asserts the MCP tool files still derive fromprotocolValuesrather than re-inlining a list. Verified by reverting a fix and watching it fail.Testing
organization: tag/path,importExtension), plus the new surfaces gate. 887 tests / 89 snapshots pass.openapi-http-server.config.jsgenerating both protocols, type-checked against every OpenAPI schema in the set.test:http-serversuite boots a real Express app from the generated stubs: parameters, headers, bodies, validation, every response variant,HttpError, plain-error 500s, hooks,additionalHeaders, a mounted router, a body-parser-less app, and a round trip driving the generated server with the generated client from the same document. Added to the CI matrix.Docs & example
docs/protocols/http_server.md, plus the hand-maintained protocol lists inREADME.md,docs/README.md,docs/getting-started/protocols.md,docs/generators/channels.mdanddocs/generators/README.mdexamples/openapi-http-server/— the same document asexamples/openapi-http-client/, generating both sides, with a runnablenpm run demothat boots the server and calls it with the generated clientchannels/clientfor OpenAPI.The protocol page was first written as a summary of this PR rather than for a reader — an "Explicit non-goals" section duplicating the
❌rows in its own support table, capability rows phrased in negative space (Frameworks other than Express ❌), and design rationale throughout. It has been rewritten into task-shaped sections that say what happens and what to do about it. Awrite-docsskill captures the rule so docs written at the end of an implementation session stop reading like the session.Deliberate non-goals
No inbound auth/security verification, no frameworks besides Express, no AsyncAPI support, no
clientpreset integration, no listener/server construction, no response-header models, no content-type negotiation, and nobasePathoption (Express makesrequest.urlmount-relative, so none is needed).Known limitation
When an operation declares several body-carrying responses, a non-object member of the resulting payload union (an array or primitive) has no importable module of its own, so its body is marshalled with
JSON.stringifyrather than the model'smarshal()— no wire-name mapping for that variant. Documented in the protocol page.BREAKING CHANGE: OpenAPI header models now unconditionally export a
deserialize<Model>Headersfunction, and that name is registered inheaderFunctions— so it also joins the header import line in the existing generatedhttp_client.ts. The import is unused on the client side and nothing is removed or renamed, so no consumer code breaks, but it is a visible diff in regenerated output for every OpenAPI document that declares header parameters.🤖 Generated with Claude Code