Zodra

Client Setup

Configure the @zodra/client TypeScript API client

Client Setup

The @zodra/client package provides a fully typed API client that uses your generated contract definitions.

Installation

npm install @zodra/client

Requires zod v4+ as a peer dependency:

npm install zod

Creating a client

src/api.ts
import { createApiClient } from "@zodra/client";
import { contracts } from "./zodra";

const api = createApiClient({
  baseUrl: "/api/v1",
  contracts,
});

The contracts object is auto-generated by rails zodra:export. It contains action definitions with Zod schemas for params and responses.

Configuration options

const api = createApiClient({
  // Required
  baseUrl: "/api/v1",
  contracts,

  // Optional
  headers: {
    Authorization: "Bearer token",
  },
  transport: customTransport,
  validateParams: true,
  validateResponse: true,
});

baseUrl

The base URL for all API requests. Trailing slashes are stripped.

contracts

The generated contract definitions. Each contract maps action names to their HTTP method, path, params schema, and response schema.

headers

Default headers included in every request. Individual requests can override these.

transport

A custom transport function. Defaults to fetchTransport which uses the Fetch API. See Custom Transport.

validateParams

When true, params are validated against the Zod schema before sending the request. Invalid params throw a ZodraValidationError. Default: false.

validateResponse

When true, the response body is validated against the response Zod schema. Invalid responses throw a ZodraValidationError. Default: false.

Type inference

The client infers param and response types from your contract definitions:

// Params are typed — TypeScript will error on missing or wrong fields
const { data } = await api.products.create({
  name: "Widget",
  price: 9.99,
  // published: "yes" — TypeError: string is not assignable to boolean
});

// Response is typed — data has the Product type
console.log(data.id);   // string
console.log(data.name); // string

No manual type definitions needed — everything flows from your Ruby type definitions through generated contracts.

On this page