With ts-rest
ts-rest returns { status, body } discriminated unions by default — no adapter needed. expect-status reads the types directly from the contract.
Contract → expect-status
Section titled “Contract → expect-status”import { initClient } from "@ts-rest/core";import { expectStatus } from "expect-status";
const contract = c.router({ createOrg: { method: "POST", path: "/orgs", responses: { 201: z.object({ id: z.string(), name: z.string() }), 409: z.object({ message: z.string(), orgId: z.string() }), 422: z.object({ message: z.string(), field: z.string() }), }, },});
const client = initClient(contract, { baseUrl: "https://api.example.com" });
const org = await expectStatus(201, client.createOrg({ body: data }));// ^? { id: string; name: string }Every status branch in responses becomes a union member. expect-status narrows the return type to the matching branch.
Flat dispatch
Section titled “Flat dispatch”Handlers receive the typed body of the matching error branch:
const org = await expectStatus(201, client.createOrg({ body: data }), { 409: ({ orgId }) => redirect(`/org/${orgId}`), // orgId: string 422: "Please check your input.", "5xx": "Service unavailable.",});Multiple success statuses
Section titled “Multiple success statuses”const contract = c.router({ upsertItem: { method: "PUT", path: "/items/:id", responses: { 200: z.object({ id: z.string(), updated: z.literal(true) }), 201: z.object({ id: z.string(), created: z.literal(true) }), 404: z.object({ message: z.string() }), }, },});
const result = await expectStatus( [200, 201], client.upsertItem({ body: data, params: { id: "1" } }),);// ^? { id: string; updated: true } | { id: string; created: true }Exhaustive checking
Section titled “Exhaustive checking”ts-rest contracts define a finite set of statuses. Combine with exhaustive to get compile-time coverage:
await expectStatus(201, client.createOrg({ body: data }), { 409: "Conflict.", 422: "Invalid input.", exhaustive: true, // ✅ compiles — all error statuses covered});With TanStack Query
Section titled “With TanStack Query”function useOrg(id: string) { return useQuery({ queryKey: ["org", id], queryFn: () => expectStatus(200, client.getOrg({ params: { id } })), });}
function useCreateOrg() { return useMutation({ mutationFn: (data: CreateOrgInput) => expectStatus(201, client.createOrg({ body: data }), { 409: ({ orgId }) => redirect(`/org/${orgId}`), 422: "Invalid organisation details.", }), });}Recover & SafeResult
Section titled “Recover & SafeResult”// Fallback value instead of throwingconst flags = await expectStatus(200, client.getFlags(), { recover: () => DEFAULT_FLAGS,});
// Discriminated resultconst result = await expectStatus(200, client.getOrg({ params: { id } }), { throws: false,});if (!result.ok) showError(result.error.message);Configured instance
Section titled “Configured instance”import { createExpectStatus } from "expect-status";
export const expectStatus = createExpectStatus({ fallbackMessage: "Something went wrong.", groups: { auth: [401, 403] }, defaults: { auth: "Please sign in.", "5xx": "Service unavailable.", }, onError: (err, res) => { Sentry.captureException(err, { extra: { status: res.status } }); },});See also
Section titled “See also”- ts-rest documentation
- With TanStack Query — full query and mutation patterns
- Exhaustive Checking — compile-time coverage guide