Typed Client SDK

Connect to Taser.js backends with end-to-end type safety using @taserjs/router-client. Enjoy autocomplete for routes, query schemas, and return types.

The @taserjs/router-client package provides a lightweight, zero-codegen proxy client that infers end-to-end type safety directly from your server's router type definition (RouteManifest or typeof app).


1. Initialize the Client

Install the client package in your project:

pnpm add @taserjs/router-client

Initialize the client with your backend baseUrl and your server app or manifest type:

src/lib/api.ts
import { createClient } from "@taserjs/router-client";
import type { RouteManifest } from "../.taser/types/routes.js";
// Or when using virtual modules / fullstack adapters:
// import type { routeManifest } from "#taserjs/virtual/manifest";
// import type { app } from "@/.taser/entry";

export const api = createClient<RouteManifest>({
  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

By passing your server's RouteManifest or typeof app type as a generic to createClient<TApp>(), the client automatically infers all available routes, methods, 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 PatternClient Property PathParameter Passing
/api.$get()api.$get()
/productsapi.products.$get()api.products.$get()
/users/:idapi.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/jwksapi.$well_known.jwks.$get()api.$well_known.jwks.$get()
/user-profilesapi.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.status and res.ok reflect runtime HTTP status codes.
  • res.json() returns Promise<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 (200226). 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

  1. Handler inference (default): When .returns() is omitted, TJson unions successful ReplyOf<Status, Body> types from the route handler.
  2. returns[200] override: When .returns({ 200: schema }) is defined, TJson uses that schema's output type.
  3. Fallback: unknown if 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/router-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/router-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


Next Steps