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

@wutwind/ezzy-api

v0.2.0

Published

Type-safe HTTP client built from runtime schema contracts

Readme

@wutwind/ezzy-api

A TypeScript library for declaring HTTP API contracts from runtime schemas and creating a type-safe fetch client from those contracts.

The schema is the single source of truth for request and response types. The core uses Standard Schema, so compatible validators such as Valibot can provide runtime validation and type inference.

For a complete application-oriented guide, see Using @wutwind/ezzy-api.

Installation

npm install @wutwind/ezzy-api valibot @praha/byethrow

The current package requires Node.js 24. Valibot may be replaced with another Standard Schema-compatible validator.

Repository setup

The recommended development environment is Docker Compose through the repository Makefile. Install the locked dependencies and run the complete project check from any terminal:

make setup
make check

The Makefile forwards the current user and user ID to the container. Other available commands are listed by make help. To open an interactive shell in the same environment, run:

make run

Docker Compose mounts the host ~/.ssh directory read-only into the container user's home. This allows release checks such as git ls-remote origin to use the same SSH keys and known_hosts without copying credentials into the image.

The underlying Docker Compose commands remain available directly when needed.

To work without Docker, use Node.js 24 and install the locked dependencies:

npm ci
npm run check

The check runs TypeScript, type-aware linting, formatting validation, and runtime tests. TypeScript also verifies the public example and colocated type specs with intentional failures marked by @ts-expect-error.

Define an API

Create runtime schemas and pass the endpoint map to defineApi():

import * as v from 'valibot';
import { defineApi } from '@wutwind/ezzy-api';

const CourseSchema = v.object({
    id: v.string(),
    name: v.string(),
});

const CourseParamsSchema = v.object({
    id: v.pipe(v.string(), v.uuid()),
});

const CreateCourseSchema = v.object({
    name: v.pipe(v.string(), v.minLength(3), v.maxLength(360)),
});

const apiDefinition = defineApi({
    getCourse: {
        method: 'GET',
        path: '/courses/:id',
        params: CourseParamsSchema,
        response: CourseSchema,
    },
    createCourse: {
        method: 'POST',
        path: '/courses',
        body: CreateCourseSchema,
        response: CourseSchema,
    },
});

Endpoint fields:

| Field | Required | Description | | ---------- | -------- | ------------------------------------------------------------- | | method | yes | GET, POST, PUT, PATCH, DELETE, HEAD, or OPTIONS | | path | yes | URL path; :name segments declare path parameters | | params | no | Schema for path parameters | | query | no | Schema for query parameters | | body | no | Schema for the request body | | response | yes | Schema whose output becomes the endpoint result type |

defineApi() preserves endpoint names and path literals while validating the definition at compile time. It is an identity function at runtime and does not create a transport.

createApi() applies the same compile-time validation, so a definition written inline cannot skip these checks.

Runtime client

createApi() maps every endpoint to a typed method and uses fetch at runtime:

import { Result } from '@praha/byethrow';
import { createApi } from '@wutwind/ezzy-api';

const api = createApi(apiDefinition, {
    baseUrl: '/api',
});

const result = await api.getCourse({
    params: {
        id: '550e8400-e29b-41d4-a716-446655440000',
    },
});

if (Result.isFailure(result)) {
    console.error(result.error);
} else {
    console.log(result.value.name);
}

Pass an AbortSignal to cancel an individual call. Cancellation is returned as a typed AbortError failure:

const controller = new AbortController();
const resultPromise = api.getCourse({
    params: { id: '550e8400-e29b-41d4-a716-446655440000' },
    signal: controller.signal,
});

controller.abort();
const result = await resultPromise;

The success value is inferred from CourseSchema as { id: string; name: string }. Expected failures are returned as a typed ApiError instead of being thrown.

Request typing rules

  • Request data is grouped into params, query, and body; fields are not flattened.
  • A schema with required properties makes its request section required.
  • A schema whose properties are all optional makes its request section optional.
  • An endpoint with no required request sections can be called without an argument.
  • Path parameters are extracted from :name segments.
  • A params schema is required for paths with parameters and must exactly match their names.
  • A params schema is rejected when the path has no parameters.
  • Every endpoint must declare a response schema.

An optional query may be omitted:

const coursesResult = await api.getCourses();
const nextPageResult = await api.getCourses({ query: { page: 2 } });

A body schema with a required name makes the body required:

const createdResult = await api.createCourse({
    body: { name: 'TypeScript' },
});

Shared client, headers, query format, and interceptors

Use createApiClient() when several API definitions share transport configuration. Query array format defaults and transport interceptors apply to every API created by that client:

const client = createApiClient({
    baseUrl: '/api',
    queryOptions: { arrayFormat: 'brackets' },
    interceptors: [authInterceptor],
});

const userApi = client.create(userApiDefinition);
const teamApi = client.create(teamApiDefinition);

Every request starts with accept: application/json. Client headers apply next, call headers can override them for per-request credentials, and interceptors receive the resolved set:

await userApi.getUser({
    params: { id },
    headers: { authorization: `Bearer ${token}` },
});

Header precedence is built-in defaults, client, call, then interceptor. Names are normalized to lowercase, and an undefined value removes an earlier header. JSON content-type is added only for requests with a body and may also be overridden or removed.

An endpoint can override the shared query array format with queryOptions. Supported formats are repeat, brackets, and comma; the built-in default is repeat.

Transport interceptors wrap the serialized request and raw HTTP response. Request handlers run in declaration order and response handlers run in reverse order. They can add headers, observe status codes such as 401, and invoke next again for retry policies. Application-specific side effects, such as navigating to a login page, belong in an interceptor supplied by the application.

Client creation is intentionally fail-fast: invalid configuration or API definitions throw one of the exported ClientConfigError or ApiDefinitionError exception types. This keeps the created API convenient to use without an initialization Result to unwrap. Endpoint execution is different: expected request, transport, HTTP, and response failures remain typed ApiError Results. createApi(definition, options) remains the shorthand for creating a single API without retaining a reusable client.

Public API

The package exposes three runtime functions:

  • defineApi() declares and type-checks an endpoint contract;
  • createApi() creates the runtime client;
  • createApiClient() creates reusable transport configuration for multiple API definitions.

It also exposes the supporting API, query, call-option, transport, and interceptor types required to configure clients and custom transports. Path building, query serialization, and schema validation functions remain internal details of the client pipeline.

Project files

  • src/core/types.ts contains the type-level API model.
  • src/core/defineApi.ts contains the API definition helper.
  • src/path/buildPath.ts contains URL path interpolation.
  • src/query/serializeQuery.ts contains query string serialization.
  • *.spec.ts files under src contain colocated Node tests.
  • type-tests/ contains compile-time public-contract tests.
  • src/index.ts is the public entry point.
  • examples/basic.ts demonstrates valid usage and response inference.
  • docs/todo.md records open design questions for the next milestone.
  • docs/releasing.md defines the versioning, tagging, and npm publication process.

Current limitations

The current client supports fetch with JSON request and response bodies, headers, cancellation, and transport interceptors. It does not yet include convenience timeouts, built-in retry or authentication policies, multipart bodies, transfer progress, streaming, optional path parameters, wildcards, or catch-all paths. Empty successful responses are passed to the response schema as undefined, so the contract must explicitly accept them.

Open design questions for these are tracked in docs/todo.md.