Skip to content

feat: add http_server protocol for generating typed Express handler stubs - #449

Merged
jonaslagoni merged 17 commits into
mainfrom
add_http_server_stubs
Aug 1, 2026
Merged

feat: add http_server protocol for generating typed Express handler stubs#449
jonaslagoni merged 17 commits into
mainfrom
add_http_server_stubs

Conversation

@jonaslagoni

@jonaslagoni jonaslagoni commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Adds http_server as a channels protocol for OpenAPI input: for every operation in the document, generate a register<OperationId>(context) function that mounts a typed handler on a caller-supplied Express Router.

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.

registerGetPetById({
  router,
  callback: ({parameters}) => {
    const pet = petStore.get(parameters.petId);
    if (!pet) {
      return {status: 404};       // declared by the document
    }
    return {status: 200, body: pet};
  }
});

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>ServerResponse union, a Register<Op>Context interface, and the register<Op> function. Generated code handles routing, body reading, parameter extraction, header deserialization, request validation, response marshalling and error mapping — you write the callback body and nothing else.

Shared per file: HttpError, HttpServerHooks (beforeHandler / afterHandler / onError), HttpServerContext (hooks, additionalHeaders, skipRequestValidation), and the readJsonBody / resolveErrorResponse / sendResponse / handleHandlerError helpers.

Changes

Generator

  • channels/protocols/http/server.ts — the per-operation renderer
  • channels/protocols/http/server-common-types.ts — the shared block, emitted once per file
  • channels/protocols/http/shared.ts — source fragments both HTTP protocols emit (HTTP_REASON_PHRASES, the HttpGlobalError alias, the HttpError class). 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 the http_client-only gate with a per-protocol dispatcher, mirroring the AsyncAPI switch. Added collectServerResponseVariants and extracted correlateOperationModels so the client and server paths cannot drift on which model belongs to which operation.
  • inputs/openapi/generators/headers.ts — new deserialize<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 & confighttp_server added to ChannelFunctionTypes, receivingFunctionTypes, the protocols Zod enum and SupportedProtocols; schemas/ regenerated.

Notable details

Declared-but-bodyless status codes. The petstore's addPet declares 200 (a Pet) and 405 (no body). 405 exists only in the raw document — never in the payload models — so the response union is assembled from the raw responses keys unioned with the model-derived body types. Neither source alone is complete.

request.url, not request.originalUrl. Express strips the mount prefix from url but not from originalUrl, and the generated extractPathParameters anchors its regex against the whole path. Using originalUrl silently breaks app.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 HttpError maps 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_server was 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-coding http_client.
  • MCP tool schemas — the data registry knew about http_server, but 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. protocolValues in lib/data/generators.ts is now the single source and every z.enum derives from it; clientImports is keyed Record<Protocol, string> so the compiler requires an entry per protocol.
  • Playground — the protocol picker, plus PROTOCOL_INPUT_COMPATIBILITY as OpenAPI-only. The picker filters on the latter, so an option missing there never renders.
  • Homepage — the protocol marquee, and an http_server variant 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-written index.ts pane compiles against it.

test/protocol-surfaces.spec.ts now 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 from protocolValues rather than re-inlining a list. Verified by reverting a fix and watching it fail.

Testing

  • Unit — response-variant collection across every payload shape, both header functions, the common-types block, per-operation rendering, and end-to-end channels generation (protocol coexistence, organization: tag/path, importExtension), plus the new surfaces gate. 887 tests / 89 snapshots pass.
  • Blackbox — new openapi-http-server.config.js generating both protocols, type-checked against every OpenAPI schema in the set.
  • Runtime — new test:http-server suite 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 in README.md, docs/README.md, docs/getting-started/protocols.md, docs/generators/channels.md and docs/generators/README.md
  • examples/openapi-http-server/ — the same document as examples/openapi-http-client/, generating both sides, with a runnable npm run demo that boots the server and calls it with the generated client
  • MCP registry: protocol union, enum values, descriptions and a usage example. Also corrected the per-input support map, which was missing channels/client for 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. A write-docs skill 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 client preset integration, no listener/server construction, no response-header models, no content-type negotiation, and no basePath option (Express makes request.url mount-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.stringify rather than the model's marshal() — no wire-name mapping for that variant. Documented in the protocol page.

BREAKING CHANGE: OpenAPI header models now unconditionally export a deserialize<Model>Headers function, and that name is registered in headerFunctions — so it also joins the header import line in the existing generated http_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

jonaslagoni and others added 11 commits August 1, 2026 19:21
…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>
@jonaslagoni
jonaslagoni requested a review from ALagoni97 as a code owner August 1, 2026 18:34
@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
the-codegen-project Ready Ready Preview Aug 1, 2026 7:35pm
the-codegen-project-mcp Ready Ready Preview Aug 1, 2026 7:35pm

@jonaslagoni jonaslagoni changed the title feat!: add http_server protocol for generating typed Express handler stubs from OpenAPI feat: add http_server protocol for generating typed Express handler stubs from OpenAPI Aug 1, 2026
@jonaslagoni jonaslagoni changed the title feat: add http_server protocol for generating typed Express handler stubs from OpenAPI feat: add http_server protocol for generating typed Express handler stubs Aug 1, 2026
jonaslagoni and others added 5 commits August 1, 2026 21:13
`--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>
@jonaslagoni
jonaslagoni merged commit ce8772e into main Aug 1, 2026
27 checks passed
@jonaslagoni
jonaslagoni deleted the add_http_server_stubs branch August 1, 2026 19:40
@jonaslagoni

Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in version 0.82.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant