Skip to content

With Next.js

expect-status works in every Next.js context — Server Components, Server Actions, Route Handlers, Middleware, and client-side hooks. No adapter needed.

Fetch data in Server Components and let errors propagate to the nearest error.tsx:

import { expectStatus } from "expect-status";
async function OrganisationPage({ params }: { params: { id: string } }) {
const org = await expectStatus(
200,
client.getOrganisation({ params: { id: params.id } }),
{ 404: "Organisation not found." },
);
return <h1>{org.name}</h1>;
}

Non-success statuses throw → caught by the nearest error.tsx boundary. The error message is the per-status message or the extracted backend message.

app/org/[id]/error.tsx
"use client";
import { ExpectStatusError } from "expect-status";
export default function ErrorPage({ error }: { error: Error }) {
if (error instanceof ExpectStatusError && error.status === 404) {
return <p>Organisation not found.</p>;
}
return <p>{error.message}</p>;
}

Use expectStatus in Server Actions for form submissions:

"use server";
import { expectStatus } from "expect-status";
import { redirect } from "next/navigation";
export async function createOrganisation(formData: FormData) {
const org = await expectStatus(
201,
client.createOrganisation({
body: { name: formData.get("name") as string },
}),
{
409: "An organisation with that name already exists.",
422: "Please check the form and try again.",
},
);
redirect(`/org/${org.id}`);
}

In the client component, catch the error with useActionState:

"use client";
import { useActionState } from "react";
import { createOrganisation } from "./actions";
export function CreateOrgForm() {
const [error, action, isPending] = useActionState(
async (_prev: string | null, formData: FormData) => {
try {
await createOrganisation(formData);
return null;
} catch (err) {
return (err as Error).message;
}
},
null,
);
return (
<form action={action}>
<input name="name" />
<button disabled={isPending}>Create</button>
{error && <p className="text-red-500">{error}</p>}
</form>
);
}

For Server Actions that return structured results (no throw):

"use server";
export async function createOrganisation(formData: FormData) {
const result = await expectStatus(
201,
client.createOrganisation({
body: { name: formData.get("name") as string },
}),
{
409: "An organisation with that name already exists.",
recover: (err) => ({ error: err.message }),
},
);
if ("error" in result) {
return result; // { error: string }
}
redirect(`/org/${result.id}`);
}

Use expectStatus in Route Handlers to proxy or aggregate upstream APIs:

app/api/org/[id]/route.ts
import { expectStatus } from "expect-status";
import { NextResponse } from "next/server";
export async function GET(
_req: Request,
{ params }: { params: { id: string } },
) {
const result = await expectStatus(
200,
client.getOrganisation({ params: { id: params.id } }),
{ throws: false },
);
if (result.ok) {
return NextResponse.json(result.data);
}
return NextResponse.json(
{ message: result.error.message },
{ status: result.status },
);
}

Use expectStatus in Next.js Middleware to validate upstream calls (e.g. auth token verification) at the edge:

middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { fetchExpect } from "expect-status/fetch";
export async function middleware(request: NextRequest) {
const token = request.cookies.get("session")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
try {
await fetchExpect(`${process.env.AUTH_API_URL}/verify`, 200, {
init: {
headers: { Authorization: `Bearer ${token}` },
},
});
return NextResponse.next();
} catch {
return NextResponse.redirect(new URL("/login", request.url));
}
}
export const config = {
matcher: ["/dashboard/:path*", "/org/:path*"],
};

For client-side data fetching, combine expectStatus with TanStack Query:

"use client";
import { useQuery } from "@tanstack/react-query";
import { expectStatus } from "expect-status";
export function useOrganisation(id: string) {
return useQuery({
queryKey: ["org", id],
queryFn: () =>
expectStatus(200, client.getOrganisation({ params: { id } })),
});
}

See With TanStack Query for more patterns.

For production apps, create a shared configured instance:

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. Please try again.",
},
onError: (err, response) => {
console.error("[API Error]", {
status: response.status,
message: err.message,
});
},
});

Then import from lib/expect-status everywhere — default messages, error logging, and error class apply automatically.