npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@contract-kit/client

v1.0.0

Published

HTTP client for contract-kit

Downloads

933

Readme

@contract-kit/client

HTTP client for Contract Kit

This package provides type-safe client adapters for making contract-based HTTP requests.

Installation

npm install @contract-kit/client @contract-kit/core

TypeScript requirements

This package requires TypeScript 5.0 or higher for proper type inference.

HTTP client

Creating a client

import { createClient } from "@contract-kit/client";

export const apiClient = createClient({
  baseUrl: "https://api.example.com",
  headers: async () => ({
    "x-app-version": "1.0",
    authorization: `Bearer ${getToken()}`,
  }),
  providedHeaders: ["authorization"] as const,
});

Client-side validation

Enable validate: true to validate request parameters and declared request headers against your contract schemas before sending the request. When validation is enabled, the client serializes the parsed path, query, body, and header values returned by your schema.

const client = createClient({
  baseUrl: "https://api.example.com",
  validate: true,
});

If a contract declares request headers with .headers(...), pass them through the same headers call arg. Header keys are normalized to lowercase before validation and fetch:

await apiClient.endpoint(getSecureTodo).call({
  path: { id: "123" },
  headers: {
    authorization: `Bearer ${token}`,
  },
});

Use providedHeaders when required contract headers are supplied by client-level headers, such as auth/session headers. This makes those keys optional at each call site while validate: true still validates the final merged headers.

If the body schema accepts undefined such as z.object({ ... }).optional(), you can omit body entirely and the client will send no request body.

Request bodies are supported for POST, PUT, and PATCH contracts. Passing body or rawBody to GET, HEAD, DELETE, or OPTIONS contracts throws INVALID_REQUEST_BODY.

Raw request bodies and text responses

Use body for contract-validated JSON. Use rawBody when you intentionally need to send a non-JSON transport body such as FormData, Blob, ArrayBuffer, a stream, or pre-serialized text.

await apiClient.endpoint(uploadAvatar).call({
  rawBody: formData,
  headers: {
    // Let the browser set multipart boundaries when using FormData.
  },
});

rawBody is sent as-is and is not schema-validated or JSON-serialized. The client only adds Content-Type: application/json for regular JSON body requests.

Responses with application/json are parsed as JSON. Text responses are parsed as strings and can be validated with a string response schema.

Making requests

import { apiClient } from "@/lib/api-client";
import { getTodo, createTodo, listTodos } from "@/contracts/todos";

// GET request with path params
const todo = await apiClient
  .endpoint(getTodo)
  .call({ path: { id: "123" } });

// POST request with body
const newTodo = await apiClient
  .endpoint(createTodo)
  .call({ body: { title: "Learn Contract Kit" } });

// GET request with query params
const todos = await apiClient
  .endpoint(listTodos)
  .call({ query: { completed: false, limit: 10 } });

Error handling

call() returns the response body for 2xx responses and throws a ContractError for non-2xx responses, local validation failures, malformed responses, and network failures.

If a contract declares any responses, successful response statuses are treated as exhaustive. For example, a contract with only 401 and 404 responses declared will reject a 200 response as undeclared; use responses: {} when you want to skip response validation.

ContractError.source tells you where the failure came from:

  • "http": the server returned a non-2xx response
  • "client": local request construction or validation failed
  • "network": fetch failed before a response was received
  • "contract": a server response was malformed or did not match the contract

For declared route-owned error responses, error.body is the parsed and validated contract response. Framework-owned errors use Contract Kit's standard { code, message, details?, requestId? } envelope when the response includes x-contract-kit-error-owner: framework. Native transport responses may produce text or no body.

If a server returns a non-2xx status that does not match the declared route error schema and does not include the framework ownership header, the client treats it as a contract failure instead of guessing ownership.

Code-based narrowing such as { code: "TODO_NOT_FOUND" } comes from catalog errors declared on the contract with .errors(...).

const getTodoEndpoint = apiClient.endpoint(getTodo);

try {
  const todo = await getTodoEndpoint.call({ path: { id: "123" } });
} catch (error) {
  if (getTodoEndpoint.isError(error, { code: "TODO_NOT_FOUND" })) {
    console.log(error.status); // 404
    console.log(error.details); // Typed from the catalog details schema
  } else if (getTodoEndpoint.isError(error, { status: 404, source: "http" })) {
    console.log(error.status); // 404
    console.log(error.body); // Parsed error response body
  } else if (getTodoEndpoint.isError(error, { source: "network" })) {
    console.log("Network failure:", error.message);
  }
}

Use safeCall() when you want explicit result handling instead of exceptions:

const getTodoEndpoint = apiClient.endpoint(getTodo);
const result = await getTodoEndpoint.safeCall({ path: { id: "123" } });

if (result.ok) {
  console.log(result.data.title);
} else if (getTodoEndpoint.isError(result.error, { status: 404, source: "http" })) {
  console.log("Not found:", result.error.body);
} else {
  console.error(result.error.message);
}

Creating API wrappers

// features/todos/api.ts
import { apiClient } from "@/lib/api-client";
import { getTodo, createTodo, deleteTodo } from "@/contracts/todos";

export const todosApi = {
  get: (id: string) =>
    apiClient.endpoint(getTodo).call({ path: { id } }),

  create: (data: { title: string; completed?: boolean }) =>
    apiClient.endpoint(createTodo).call({ body: data }),

  delete: (id: string) =>
    apiClient.endpoint(deleteTodo).call({ path: { id } }),
};

API reference

HTTP client

createClient(config)

Creates an HTTP client instance.

const client = createClient({
  baseUrl: string;
  headers?: () => Promise<Record<string, string>> | Record<string, string>;
  providedHeaders?: readonly string[];
  fetch?: typeof fetch;
});

client.endpoint(contract)

Creates a typed endpoint for a contract.

const endpoint = client.endpoint(getTodo);
const result = await endpoint.call({ path: { id: "123" } });
const safeResult = await endpoint.safeCall({ path: { id: "123" } });

Type exports

import { ContractError } from "@contract-kit/client";
import type {
  CallArgs,
  Client,
  ClientConfig,
  Endpoint,
  EndpointCallArgs,
  EndpointErrorResult,
  EndpointResult,
  EndpointSuccessResult,
  InferBody,
  InferEndpointErrorResponse,
  InferErrorResponse,
  InferPathParams,
  InferQuery,
  InferSuccessResponse,
} from "@contract-kit/client";

Related packages

License

MIT