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

@zodapi/client

v0.3.0

Published

Typed fetch/axios client over zodapi route contracts with optional zod validation

Downloads

227

Readme

@zodapi/client

Typed HTTP client over zodapi route contracts: path- or alias-addressed calls, optional zod validation at runtime, and zodios-style error guards. Works over fetch by default, axios via @zodapi/client/axios, or any custom adapter.

import { ValidationApiError, createClient, matchErrorByStatus } from '@zodapi/client'
import { z } from 'zod'
import { createUser, routes } from './contract.js'

const client = createClient(routes, { baseUrl: 'http://localhost:3000' })

const user = await client.get('/users/{id}', { params: { id: '1' } }) // by path
const same = await client.getUser({ params: { id: '1' } }) // by alias

try {
  await client.createUser({ body: newUser })
} catch (err) {
  if (err instanceof ValidationApiError) {
    z.flattenError(err.error).fieldErrors // a real ZodError, revived from the server's issues
  } else if (matchErrorByStatus(createUser, err, 409)) {
    err.data.error.existingId // fully typed, runtime-checked with zod
  }
}

Argument objects (params, query, body, headers, signal) are typed from the route's request schemas; return types are the union of the route's declared 2xx json bodies.

Options

createClient(routes, {
  baseUrl: 'http://localhost:3000',
  validate: 'response', // 'none' | 'request' | 'response' | 'both' (default 'response')
  encodeRequests: false, // body IO (default false): wire-form (z.input) body; true = decoded (z.output), z.encode'd
  fullResponse: false, // resolve with { data, status, headers } instead of the bare body
  headers: () => ({ authorization: `Bearer ${token}` }), // static object or (async) function
  adapter: fetchAdapter(), // transport seam
  decoders: decodersFor(problemFlavor), // error decoders (default: zodapi's own problem+json 400)
  onError: ({ error, attempt }) => {}, // error hook; return 'retry' to re-run the request
})
  • Validation default is 'response' (zodios behaviour): 2xx bodies are parsed with the contract schema; error-response bodies stay raw and are checked by the error guards instead. Override per call with { validate: ... }.
  • Errors throw. Non-2xx responses run through the error decoders first: server-side validation failures throw ValidationApiError (its error is a real z.ZodError revived from the server's issues, so client- and server-side failures share one handling path), other recognised problem+json responses throw ProblemApiError. Otherwise declared statuses throw ApiError (narrow with isErrorFromRoute, isErrorFromAlias, matchErrorByStatus, isValidationError — all re-exported from @zodapi/core); undeclared statuses throw UnexpectedResponseApiError; client-side validation failures throw RequestValidationError / ResponseValidationError.
  • Non-zodapi backends that speak RFC 9457 (ASP.NET, Spring, ...) opt in with decoders: [...decodersFor('problem-details', { keyCasing: 'camel' })] — an ASP.NET ValidationProblemDetails errors map is converted to zod issues (keys camelCased and split into paths; $.items[0].qty becomes ['items', 0, 'qty']). Contracts generated by @zodapi/codegen export a detected problemFlavor to feed decodersFor. Pass decoders: [] to disable decoding entirely.
  • Query arrays are serialised with the [] key suffix (tags[]=a&tags[]=b), matching the normalisation createApp() from @zodapi/hono applies at the edge.
  • Params, query, and headers are always decoded values. The transport turns them into strings regardless, so you pass natural types — a plain number for a coerced param, true for a z.stringbool(), a Date for a codec — typed z.output with keys the input side lets you omit (.default()ed or .optional()) staying optional. Codec-bearing values are always z.encoded to their wire form (per key, so a codec can sit next to a queryArray(), which z.encode alone would reject as a one-way transform).
  • Bodies are wire-form by default; encodeRequests: true flips them to decoded. With it on (client-level or per call) you pass schema-output values — Date objects where the contract uses date codecs — typed z.output, and the client z.encodes them; encoding is a serialization concern independent of validate, though z.encode validates as it encodes, so an invalid value throws RequestValidationError even with request validation off.
  • Codecs (e.g. the date codecs @zodapi/codegen emits with its dates options) only decode when response validation runs, so the client fails fast — before sending — when a 2xx response schema with a codec would be skipped (validate must be 'response' or 'both'). In the default body mode, validated codec-bearing bodies are re-encoded to their wire form (a date-only codec stays YYYY-MM-DD instead of being JSON.stringify'd as a full datetime).
  • Raw response access goes through fullResponse (client-level default, or per call in either direction): the call resolves with { data, status, headers } — data validated/decoded exactly as without the envelope, headers the raw Headers — for pagination headers, tests, and the like. Non-2xx responses already carry status/headers/data on the thrown ApiError.
  • Retries go through onError (client-level, or per call to replace it): the hook receives { error, route, alias, attempt } before an error is thrown and may return 'retry' (sync or async) to re-run the request. The headers function is re-evaluated on every attempt, so an expired-token flow is just: on a 401, refresh the token, return 'retry'. Transport errors, ApiError and subclasses, and ResponseValidationError reach the hook; client-side request validation/encoding failures don't (the same input would fail again). There's no built-in attempt cap — bound retries with attempt.

Axios

import axios from 'axios'
import { axiosAdapter } from '@zodapi/client/axios'

const client = createClient(routes, { baseUrl, adapter: axiosAdapter(axios.create()) })

Status handling stays with the zodapi client (validateStatus is disabled), so declared error responses throw ApiError exactly as with the fetch adapter. axios is an optional peer dependency. A custom transport is just an Adapter: (request: AdapterRequest) => Promise<AdapterResponse>.

Install

pnpm add @zodapi/client zod

Contracts come from @zodapi/hono route definitions shared out of a TypeScript backend, or from @zodapi/codegen for backends that only publish an OpenAPI 3.1 document.