Response Contracts
Enforce compile-time return shape safety and runtime response validation with .returns(). Eliminate response drift between backend and frontend.
In standard backend frameworks, res.json(data) is unchecked. If a database query changes or a field name is renamed, backend responses drift silently, breaking frontend consumers in production.
Taser.js solves this by letting you define Response Contracts with .returns().
Declaring Response Contracts
Attach a .returns() map containing schemas keyed by HTTP status codes:
import { json, notFound } from "@taserjs/router/reply";
import { z } from "zod";
import { t } from "@taserjs/router";
const UserProfileSchema = z.object({
id: z.string(),
username: z.string(),
email: z.string().email(),
avatarUrl: z.string().url().nullable(),
});
const ErrorSchema = z.object({
message: z.string(),
});
export default t
.get("/profile")
.returns({
200: UserProfileSchema,
401: ErrorSchema,
404: ErrorSchema,
})
.handler(async (ctx) => {
const user = await ctx.db.getCurrentUser(ctx.state.userId);
if (!user) {
// Validated against 404 schema:
return notFound({ message: "Profile not found" });
}
// Validated against 200 schema:
return json({
id: user.id,
username: user.username,
email: user.email,
avatarUrl: user.avatar,
});
});Compile-Time Type Checking
The TypeScript compiler checks the value passed into json() against the schema corresponding to that status code.
If you return an incorrect property name or omit a required field, TypeScript generates an immediate build error:
// ❌ TypeScript Error: Property 'email' is missing in type '{ id: string; username: string }'
return json({
id: user.id,
username: user.username,
});This ensures you can refactor database models and backend services with total confidence that response shapes remain compliant.
Runtime Response Validation
In addition to compile-time verification, Taser.js can validate outgoing response payloads at runtime.
You can configure response validation behavior in src/taser.ts:
import { createTaserApp } from "@taserjs/router";
export default createTaserApp({
response: {
// Enable/disable runtime response validation (default: true)
validate: process.env.NODE_ENV !== "production",
// Custom hook when outgoing response does not match .returns() schema
onValidationFailure: ({ status, issues, data }) => {
console.error(
`[Response Validation Error] Status ${status} payload did not match contract:`,
issues,
);
},
},
});Client SDK Success Response Typing
@taserjs/router-client types await res.json() automatically from your route handlers — you do not need .returns() for client type safety. By default, the client unions successful ReplyOf payload types (200–226) from reply helpers like json(), ok(), and created().
Use .returns() when you want compile-time server-side contract enforcement, runtime response validation in dev/staging, or to override client inference with an explicit 200 schema type.
| Server setup | Client await res.json() type |
|---|---|
Handler only (no .returns()) | Auto-inferred from handler ReplyOf success returns |
.returns({ 200: Schema }) | Schema output type for 200 (takes precedence) |
See the Typed Client Guide for path conventions (_id, param) and per-request options.
Status Handling with Typed Success Payloads
Call parameterized endpoints using property chains like _id and the param argument:
import { api } from "@/lib/api";
export async function loadUserProfile(userId: string) {
const res = await api.users._id.$get({
param: { id: userId },
});
if (res.status === 200) {
// res.json() typed from handler or returns[200] schema
const profile = await res.json();
console.log("Welcome back,", profile.username);
return profile;
}
if (res.status === 404) {
console.error("User missing");
return null;
}
if (res.status === 401) {
window.location.href = "/login";
return null;
}
return null;
}Explicit Status Branching
You can branch on standard HTTP status codes or res.ok:
import { api } from "@/lib/api";
export async function fetchProfile(userId: string) {
const res = await api.users._id.$get({
param: { id: userId },
});
if (!res.ok) {
throw new Error(`Request failed with status ${res.status}`);
}
// Typed success payload (handler inference or returns[200])
const data = await res.json();
return data;
}TanStack Query & React Integration
Typed contracts pair seamlessly with data fetching libraries like @tanstack/react-query:
import { useQuery } from "@tanstack/react-query";
import { api } from "@/lib/api";
export function useProfile(userId: string) {
return useQuery({
queryKey: ["profile", userId],
queryFn: async () => {
const res = await api.users._id.$get({
param: { id: userId },
});
if (res.status === 200) {
return await res.json();
}
if (res.status === 404) {
throw new Error("Profile not found");
}
throw new Error(`Failed to fetch profile: status ${res.status}`);
},
});
}