HTTP responses
Infer JSON success and error responses, select a success status, and understand explicit response-contract limits.
A function-backed route turns the function’s validated result or declared error into an HTTP response. Start with inference; override the transport only when the public HTTP contract needs it. Keep reusable errors in the domain’s error declarations.
Understand inferred responses
| Outcome | Default response |
|---|---|
| Non-void function output | JSON 200 |
void or undefined output |
Empty 204 |
| Request mapping or input validation failure | Validation JSON 422 |
| Declared application error | Its http.status, or 500 when omitted |
| Route rate-limit rejection | JSON 429 |
| Invalid function output or an internal defect | Safe server error 500 |
Declared application errors use the public error envelope, including their stable identity and validated data. Do not assume the envelope is identical to the error’s data schema alone. Undeclared exceptions are not a way to create a public error contract.
Runtime failures can also produce responses such as provider failure 502 or timeout 504.
The inferred OpenAPI list is not an exhaustive list of infrastructure or middleware failures.
Choose one successful status
POST does not imply 201. Set successStatus when one successful operation needs a different
2xx status while retaining inferred output and errors:
/**
* Maps order HTTP operations to public service members with the existing request
* and rate-limit policies. The rate-limit reference addresses the declared cache
* by its stable identity without importing a private service implementation.
*/
import { defineServiceRoutes, http } from "@relkit/app/routes";
import { z } from "@relkit/app/schema";
import orders from "../../services/orders/service.js";
export const { GET, POST } = defineServiceRoutes(orders, {
GET: "searchOrders",
POST: {
member: "createOrder",
request: http.input({
orderId: http.header("idempotency-key"),
customerEmail: http.header("x-customer-email"),
sku: http.body("sku"),
quantity: http.body("quantity"),
}),
rateLimit: {
limit: 20,
windowMs: 60_000,
key: http.header("x-customer-email"),
store: {
ref: { kind: "cache", id: "orders.rate-limits" },
key: z.string(),
value: z.number().int().nonnegative(),
},
},
successStatus: 201,
},
});
The Orders POST returns JSON 201; its output schema and declared errors still come from
createOrder. The route above does not use an explicit responses array.
Use successStatus for this case instead of copying all inferred response declarations.
HEAD invokes the target but removes the response body. 204 responses are empty as well;
do not choose 204 when callers need the returned JSON.
Replace the complete response contract only when needed
An explicit responses array replaces inference. The Routes API provides:
| Helper | Declaration |
|---|---|
http.success(status, schema) |
Successful output and status |
http.error(errorId, status, schema) |
Status for a declared application error and its data schema |
http.validationError(status, schema) |
Validation response; status defaults to 422 |
http.response(id, status, schema) |
A named generic response declaration |
Include success, validation, declared application errors, and any other outcomes the endpoint promises. Omission does not prevent a runtime failure; it can leave the generated contract incomplete. An omitted application-error mapping falls back to an internal error rather than a declared status.
Declarations are not branch handlers. Ordinary successful invocation uses the first success
declaration (or the first generic response when there is no success declaration). Listing several
success statuses does not select one based on returned data. These helpers do not define response
headers or content negotiation, and adding a 3xx declaration does not implement a redirect.
Use middleware for response headers and transport short-circuits. For an adapter that owns redirects, binary bodies, or another HTTP protocol, consider a raw handler and document its separate contract.
Distinguish request failures from server failures
Input schema and mapping failures return 422 by default. That includes malformed JSON and
unsupported body content types in the request mapper. The framework’s global Content-Length
check can reject an oversized request with 413; a byte-limit failure encountered later while
mapping the body is a validation response. Test the layer your client actually reaches.
Function output is validated by the invocation engine. Invalid output is a server defect, not
a caller’s 422. Additional explicit response-schema checks run in development and test mode;
do not use a response-only schema as the sole production validation boundary.
Verify status, headers, and body together
With the Echo endpoint running on port 3000:
curl -i -X POST http://localhost:3000/echo \
-H 'content-type: application/json' \
-d '{"message":"hello"}'
curl -i -X POST http://localhost:3000/echo \
-H 'content-type: application/json' \
-d '{"message":""}'
Expect JSON 200 for the first request and validation 422 for the second. Add tests for every
declared application error and middleware rejection in your own endpoint. A correct body with the
wrong status is still a broken HTTP contract.
Middleware-only responses are not automatically added to OpenAPI or the generated client’s error types. Describe them explicitly, and test the RPC client separately: it does not consume the REST response envelope.