Skip to content

With ts-rest

ts-rest returns { status, body } discriminated unions by default — no adapter needed. expect-status reads the types directly from the contract.

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.

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.",
});
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 }

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
});
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.",
}),
});
}
// Fallback value instead of throwing
const flags = await expectStatus(200, client.getFlags(), {
recover: () => DEFAULT_FLAGS,
});
// Discriminated result
const result = await expectStatus(200, client.getOrg({ params: { id } }), {
throws: false,
});
if (!result.ok) showError(result.error.message);
lib/expect-status.ts
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 } });
},
});