Skip to content

feat(core): expose rawResponse, a status-preserving normalized response union#114

Open
thetutlage wants to merge 1 commit into
Julien-R44:mainfrom
thetutlage:feat/raw-response
Open

feat(core): expose rawResponse, a status-preserving normalized response union#114
thetutlage wants to merge 1 commit into
Julien-R44:mainfrom
thetutlage:feat/raw-response

Conversation

@thetutlage

@thetutlage thetutlage commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

@tuyau/core — Expose rawResponse: a status-preserving normalized response union

Problem

The registry currently exposes two response types per route under types:

  • response: ExtractResponse<…> — for a 2xx return, extracts only the body and discards the status code. Every success (200, 201, 204, …) collapses to the same shape.
  • errorResponse: ExtractErrorResponse<…> — keeps only non-2xx members, as { status; response }.

So the status is preserved for errors but thrown away for successes. There is no exposed type that says "this endpoint returns 201" or "…returns 204", and no single type that enumerates all responses. Downstream consumers — notably OpenAPI generators — therefore cannot emit correct success status codes, and have to read response (always 200) + errorResponse from two places.

Concretely, a controller action return response.created(await serialize(dto)) (typed { __response: Dto; __status: 201 }) surfaces in the registry only as response: Dto — the 201 is unrecoverable.

Change

Purely additive. Introduces one type and one new registry field.

  1. New type ExtractRawResponse<T> (in the types module, next to ExtractResponse / ExtractErrorResponse):

    /**
     * Normalizes a controller return type into a discriminated union of
     * `{ status; response }` across ALL status codes (success and error alike),
     * preserving the status that ExtractResponse/ExtractErrorResponse drop or split.
     * Members carrying no status brand (a plain returned body) default to 200.
     */
    type ExtractRawResponse<T> = T extends { __response: infer R; __status: infer S }
      ? { status: S; response: R }
      : { status: 200; response: T }

    It's distributive over unions, so a mixed return (Dto | { __response; __status: 401 }) becomes { status: 200; response: Dto } | { status: 401; response: … }. Exported from @tuyau/core/types.

  2. Registry generator (generate_registry) — emit a rawResponse entry in each route's types, wrapping the raw awaited return type with ExtractRawResponse, and appending the same validation-error ({ status: 422; response: … }) clause it already appends to errorResponse. Add ExtractRawResponse to the generated import list.

    Generated output per route becomes:

    types: {
      
      response:      ExtractResponse<Awaited<ReturnType<>>>
      errorResponse: ExtractErrorResponse<Awaited<ReturnType<>>> | { status: 422; response:  }
      rawResponse:   ExtractRawResponse<Awaited<ReturnType<>>>  | { status: 422; response:  }
    }

Backward compatibility

Non-breaking: response and errorResponse are unchanged; rawResponse is a new additive field on types. Existing client typings and consumers are unaffected. (ExtractResponse/ExtractErrorResponse could later be re-expressed in terms of ExtractRawResponse, but this PR leaves them untouched to minimize surface.)

Why the success status matters (discriminated unions)

When a single action has multiple success states — e.g. login returning { status: 'authenticated'; … } vs { status: 'mfaRequired'; … }rawResponse preserves each member as { status: 200; response: <that shape> }, so consumers can render a faithful oneOf. This is the piece that lets an OpenAPI generator (see companion PR to @outloud/adonis-openapi-generator) document 201/204/4xx and multi-state 200s correctly.

Files touched (source equivalents of the compiled build)

  • types module — add + export ExtractRawResponse
  • generate_registry#wrapRawResponseType, emit rawResponse, extend the import list

(The local reproduction patches the compiled build/ outputs: build/index-BPATPJFD.d.ts, build/client/types/index.d.ts, build/backend/generate_registry.js.)


Enables thetutlage/adonis-openapi#1, which consumes rawResponse to emit correct status codes and error responses in the generated OpenAPI spec.

`ExtractResponse` flattens every 2xx return to just its body (dropping the
status), and `ExtractErrorResponse` keeps only non-2xx members. So a route's
success status (201/204) is never surfaced, and there's no single type that
enumerates all responses — which blocks OpenAPI generators from emitting
correct success codes and error responses.

Add `ExtractRawResponse<T>`, which normalizes a controller return type into a
discriminated union of `{ status; response }` across ALL status codes
(unbranded plain bodies default to 200), and emit a new `rawResponse` member
per route in the generated registry (with the same 422 validation clause
already applied to `errorResponse`).

Purely additive: `response`/`errorResponse` are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P3ZYnmGMEZwTuBuZviLDSq
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