Skip to content

Quick Start

bash npm install expect-status

Requires TypeScript 5.4+ and Node 18+ (or Deno / Bun / Cloudflare Workers).

expect-status works with any client that returns a status-discriminated union:

type CreateOrgResponse =
| { status: 201; body: { id: string; name: string } }
| { status: 409; body: { message: string; orgId: string } }
| { status: 422; body: { message: string; field: string } };

This is the default output of ts-rest, orval, hey-api, and openapi-fetch. For Axios or custom shapes, use the adapter option.


import { expectStatus } from "expect-status";
const org = await expectStatus(201, client.createOrg({ body: data }));
// ^? { id: string; name: string }

If the status isn’t 201, expect-status throws with a message extracted from the response body.

const result = await expectStatus([200, 201], client.upsert({ body: data }));
// ^? body of 200 | body of 201
await expectStatus("success", client.health()); // any 2xx
await expectStatus("!4xx", client.maybeRedirect()); // anything except 400-499
await expectStatus([200, "3xx"], response); // 200 or any 300-399

Strings are messages, functions are handlers:

const org = await expectStatus(201, client.createOrg({ body: data }), {
409: ({ orgId }) => redirect(`/org/${orgId}`), // handler — body is typed
422: "Please check your input.", // message — auto-throws
"5xx": "Service is temporarily unavailable.", // range — catches 500-599
});

Handlers can return values, widening the return type:

const result = await expectStatus(201, client.createOrg({ body: data }), {
409: (body) => ({ conflict: true, orgId: body.orgId }),
});
// result: { id: string; name: string } | { conflict: true; orgId: string }

Two ways to avoid throwing:

// recover — catch-all that returns a fallback
const flags = await expectStatus(200, client.getFlags(), {
recover: () => DEFAULT_FLAGS,
});
// throws: false — typed discriminated union
const result = await expectStatus(200, client.getUser(id), { throws: false });
if (result.ok) {
result.data; // typed body
} else {
result.error; // ExpectStatusError
}

Configure shared behaviour across your app:

import { createExpectStatus } from "expect-status";
export const expectStatus = createExpectStatus({
fallbackMessage: "Something went wrong.",
groups: { auth: [401, 403] },
defaults: {
auth: "Please sign in or check your permissions.",
"5xx": "Service is temporarily unavailable.",
},
onError: (err, res) => {
Sentry.captureException(err, { extra: { status: res.status } });
},
});

Per-call dispatch always shadows instance defaults.

For clients that don’t return { status, body } (e.g. Axios), normalize at the instance level:

const expectStatus = createExpectStatus({
adapter: (res) => ({ status: res.status, body: res.data }),
});
const org = await expectStatus(201, axios.post("/orgs", data));