Skip to content

feat: typed error responses with $error<T> and status-keyed response maps#2

Merged
aleclarson merged 10 commits into
mainfrom
copilot/add-typed-error-responses
May 24, 2026
Merged

feat: typed error responses with $error<T> and status-keyed response maps#2
aleclarson merged 10 commits into
mainfrom
copilot/add-typed-error-responses

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 23, 2026

Adds declared error responses as part of the route contract. Declared HTTP errors resolve as typed tuples on the client instead of rejecting, while unexpected/undeclared failures still reject.

Changes

  • $error<T>() marker — new compile-time marker analogous to $type<T>(), distinguishes error branches in a response map
  • Status-keyed response mapsresponse field now accepts { [status]: $type<T>() | $error<T>() } in addition to the existing single-marker form
  • ctx.error(status, body) — server handler helper typed to only accept statuses declared with $error<T>(); returns a Response with the given status
  • Client tuple resolution — when a route declares $error<T> entries, the client resolves all declared statuses as [error, result, status] discriminated tuples
  • Type inferenceInferRouteResponse produces the tuple union; InferRouteHandlerResult extracts success types; InferResponseMapErrors types the error helper

Example

const getUser = http.get('users/:id', {
  response: {
    200: $type<User>(),
    401: $error<AuthError>(),
    404: $error<NotFoundError>(),
  },
})

// Server
createRouter().use({ getUser }, {
  async getUser(ctx) {
    if (!authorized) return ctx.error(401, { code: 'UNAUTHORIZED', message: '...' })
    return { id: ctx.path.id, name: 'Ada' }
  },
})

// Client — resolved type is [null, User, 200] | [AuthError, null, 401] | [NotFoundError, null, 404]
const [error, result, status] = await client.getUser({ path: { id: '123' } })

Backward compatibility

Existing response: $type<T>() and response: ndjson.$type<T>() routes are unchanged — no tuple wrapping, no breaking type changes. The response map form is purely additive.

Copilot AI and others added 2 commits May 23, 2026 05:14
Agent-Logs-Url: https://github.com/alloc/rouzer/sessions/ad2874c8-97ca-4ce2-8013-e7bf328459c6

Co-authored-by: aleclarson <1925840+aleclarson@users.noreply.github.com>
- Add $error<T>() marker function for declaring error response types
- Support status-keyed response maps in route schemas
- Add ctx.error(status, body) to server handler context
- Client resolves declared errors as [error, null, status] tuples
- Undeclared statuses still reject the promise
- Add runtime and type-level tests

Agent-Logs-Url: https://github.com/alloc/rouzer/sessions/ad2874c8-97ca-4ce2-8013-e7bf328459c6

Co-authored-by: aleclarson <1925840+aleclarson@users.noreply.github.com>
Honor per-status response plugin markers when decoding client tuples and encoding router responses. Validate plugin requirements nested inside status-keyed response maps and cover the behavior with an NDJSON response-map fixture.
Add ctx.success(status, body) for handlers that need to select among multiple declared success statuses. Route both ctx.success and ctx.error through the shared response-map encoder so undeclared statuses fail at runtime and plugin-backed success responses keep their per-status encoding.
Document the user-visible response map API added on this branch: <T>(), generated client tuple results, ctx.error, ctx.success, and response-plugin success entries inside maps.

Add a runnable typed error response example and keep public TSDoc aligned so declaration docs describe response maps alongside direct JSON and plugin responses.
Avoid implying Rouzer is a poor fit for any runtime response validation. The docs now distinguish server-boundary response validation from the intended model: response markers are type contracts, with runtime integrity checks placed where data enters server or client code.
@aleclarson aleclarson merged commit fef48b2 into main May 24, 2026
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.

2 participants