Typed Client SDK
Connect to Taser.js backends with end-to-end type safety using @taserjs/client. Enjoy autocomplete for routes, query schemas, and return types.
The @taserjs/client package provides a lightweight, zero-codegen proxy client that infers end-to-end type safety from your generated AppManifest in src/.taserjs/routes.gen.ts.
1. Initialize the Client
Install the client package in your project:
pnpm add @taserjs/clientInitialize the client with your composite AppManifest from src/.taserjs/routes.gen.ts (or ${serverDir}/.taserjs/routes.gen.ts):
import { createClient } from "@taserjs/client";
import type { AppManifest } from "./.taserjs/routes.gen.js";
// Next.js / serverDir layouts:
// import type { AppManifest } from "@/server/.taserjs/routes.gen";
export const api = createClient<AppManifest>({
baseUrl: process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000/api",
headers: async () => {
const token = localStorage.getItem("auth_token");
return token ? { Authorization: `Bearer ${token}` } : {};
},
});Explicit End-to-End Type Safety
Pass AppManifest from routes.gen.ts as the generic argument to createClient. The client
infers routes, methods ($get, $post, $put, $patch, $delete, $options, $query), input
schemas, and return contracts without generating static client source files.
Path Segment Conventions
The client uses nested proxy property access mirroring your URL paths. Certain URL conventions are translated into valid JavaScript property names:
| URL Pattern | Client Property Path | Parameter Passing |
|---|---|---|
/ | api.$get() | api.$get() |
/products | api.products.$get() | api.products.$get() |
/users/:id | api.users._id.$get() | api.users._id.$get({ param: { id: "123" } }) |
/files/* | api.files._splat.$get() | api.files._splat.$get({ param: { _splat: "docs/readme.txt" } }) |
/.well-known/jwks | api.$well_known.jwks.$get() | api.$well_known.jwks.$get() |
/user-profiles | api.user_profiles.$get() | api.user_profiles.$get() |
Calling API Routes
GET Request with Query Parameters
// Calls GET /api/products?category=electronics&page=1
const response = await api.products.$get({
query: {
category: "electronics",
page: 1,
},
});
if (response.ok) {
const data = await response.json();
// Auto-inferred from handler reply helpers or returns[200] schema
console.log(data.products);
}Dynamic Path Parameters (_param)
For parameterized routes like src/routes/users/$id.get.ts:
// Calls GET /api/users/user_456
const response = await api.users._id.$get({
param: { id: "user_456" },
});
if (response.ok) {
const user = await response.json();
console.log(user.name);
}POST Request with JSON Body
For routes like src/routes/posts.post.ts:
// Calls POST /api/posts with Content-Type: application/json
const response = await api.posts.$post({
body: {
title: "Building type-safe APIs with Taser.js",
content: "Deterministic file routing with cascading context.",
},
});
const createdPost = await response.json();Per-Request Options
Pass a second argument to any HTTP method to configure request-level options like one-off headers, a custom fetch implementation, or standard RequestInit settings:
const response = await api.posts.$post(
{
body: { title: "Draft Post" },
},
{
headers: { "X-Idempotency-Key": "req_abc123" },
init: { cache: "no-store" },
},
);Response Typing & Handling
Every client method returns a Promise<ClientResponse<TJson>>, which wraps the standard Web Response object:
res.statusandres.okreflect runtime HTTP status codes.res.json()returnsPromise<TJson>with success payload types inferred automatically —.returns()is optional.
.returns() Is Optional for Client Typing
By default, TJson is inferred from handler reply helpers (json(), ok(), created(), etc.) as the union of successful ReplyOf payload types (200–226). Add .returns({ 200: schema }) only when you want server-side contract enforcement, runtime response validation, or to override client inference with an explicit schema type.
Type Resolution Precedence
- Handler inference (default): When
.returns()is omitted,TJsonunions successfulReplyOf<Status, Body>types from the route handler. returns[200]override: When.returns({ 200: schema })is defined,TJsonuses that schema's output type.- Fallback:
unknownif neither source is available.
const response = await api.users._id.$get({
param: { id: "user_123" },
});
if (response.ok) {
const data = await response.json(); // Typed from handler or returns[200]
console.log(data.email);
} else {
console.error("Request failed with status:", response.status);
}Multipart File Uploads (formBody)
Use formBody() to upload binary files and structured form fields with multipart/form-data:
import { formBody } from "@taserjs/client";
const avatarFile = new File([blob], "avatar.png", { type: "image/png" });
// Calls POST /api/users/user_123/avatar with multipart form payload:
const response = await api.users._id.avatar.$post({
param: { id: "user_123" },
body: formBody({
label: "Profile Picture",
file: avatarFile,
}),
});Server-Side File Validation
On the backend, your body schema validates incoming File objects with native file schemas like
zod's z.file(), checking max size and allowed MIME types. Learn more in the Multipart & File
Validation Guide.
Inferring Request & Response Types
Extract TypeScript types from client endpoint methods using helper generics:
import type { InferRequestType, InferResponseType } from "@taserjs/client";
import type { api } from "@/lib/api";
// Infer input arguments for an endpoint:
type UserGetInput = InferRequestType<typeof api.users._id.$get>;
// { param: { id: string }; query?: ... }
// Infer successful JSON payload:
type UserData = InferResponseType<typeof api.users._id.$get>;Configuration Reference
Client Initialization Options (CreateClientOptions)
Prop
Type
Per-Request Options (ClientRequestOptions)
Prop
Type