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/client

Initialize the client with your composite AppManifest from src/.taserjs/routes.gen.ts (or ${serverDir}/.taserjs/routes.gen.ts):

src/lib/api.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 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 (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

  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/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


Next Steps