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

aspi

v2.0.1

Published

Rest API client for typescript projects with chain of responsibility design pattern.

Readme

Aspi

A tiny, type‑safe wrapper around the native fetch API that gives you a clean, monadic interface for HTTP requests. It ships with zero runtime dependencies, a tiny bundle size, and full TypeScript support out of the box.

Why use Aspi? • End‑to‑end TypeScript typings (request + response) • No extra weight – only a thin wrapper around fetch • Chain‑of‑responsibility middleware support via use • Result‑based error handling (values as errors) • Built‑in retry, header helpers, query‑string handling, and schema validation (Zod, Arktype, Valibot) • Flexible error mapping with error and convenience shortcuts


Installation

npm bash npm install aspi

yarn bash yarn add aspi

pnpm bash pnpm add aspi


Quick start

import { Aspi, Result } from 'aspi';

// Create a client with a base URL and default headers
const api = new Aspi({
  baseUrl: 'https://api.example.com',
  headers: {
    'Content-Type': 'application/json',
  },
});

// Simple GET request – returns a tuple [value, error]
async function getTodo(id: number) {
  const [value, error] = await api
    .get(`/todos/${id}`)
    .setQueryParams({ include: 'details' }) // optional query string
    .notFound(() => ({ message: 'Todo not found' }))
    .json<{ id: number; title: string; completed: boolean }>();

  if (value) console.log('Todo:', value);
  if (error) {
    if (error.tag === 'aspiError') console.error(error.response.status);
    if (error.tag === 'notFoundError') console.warn(error.data.message);
    if (error.tag === 'jsonParseError') console.error(error.data.message);
  }
}

getTodo(1);

Using the Result monad

If you prefer a single Result value instead of a tuple, call .withResult() before a body‑parser method.

async function getTodoResult(id: number) {
  const response = await api
    .get(`/todos/${id}`)
    .notFound(() => ({ message: 'Todo not found' }))
    .withResult() // enable Result mode
    .json<{ id: number; title: string; completed: boolean }>();

  Result.match(response, {
    onOk: (data) => console.log('✅', data),
    onErr: (err) => {
      if (err.tag === 'aspiError') console.error(err.response.status);
      if (err.tag === 'notFoundError') console.warn(err.data.message);
    },
  });
}

Throwable

The throwable() toggle makes a request throw on any non‑2xx HTTP response, allowing you to use the familiar try / catch pattern instead of dealing with tuples or Result objects.

When a request is in throwable mode, the body‑parser methods (json(), text(), blob()) resolve with the parsed value directly. If the response status indicates an error, the promise is rejected with a typed Aspi error (e.g., aspiError, unauthorisedError, jsonParseError, …).

Basic usage

// Using throwable with async/await + try/catch
try {
  const todo = await api
    .get('/todos/1')
    .throwable() // <─ enable throwable mode
    .json<{ id: number; title: string; completed: boolean }>(); // returns the parsed JSON

  console.log('✅ Todo:', todo);
} catch (err) {
  // `err` is a typed Aspi error
  if (err.tag === 'aspiError') {
    console.error('HTTP error:', err.response.status);
  } else if (err.tag === 'jsonParseError') {
    console.error('Invalid JSON:', err.data.message);
  } else {
    console.error('Unexpected error:', err);
  }
}

Interaction with withResult()

throwable() and withResult() are mutually exclusive – the last toggle applied wins.

// Result mode wins (throwable is ignored)
const result = await api
  .post('/login')
  .withResult() // enables Result mode
  .throwable() // ignored because withResult was called later
  .json<{ token: string }>();

// Throwable mode wins (Result is ignored)
const data = await api
  .get('/profile')
  .throwable() // enables throwable mode
  .withResult() // ignored because throwable was called later
  .json();

When to use throwable()

  • You prefer native try / catch flow over tuple/result handling.
  • You want the request to reject automatically on HTTP errors, keeping the success path clean.
  • You are integrating Aspi into existing codebases that already rely on exception handling.

throwable() gives you the flexibility to choose the error‑handling style that best fits your project.

Schema validation (Zod example)

import { z } from 'zod';
import { Aspi, Result } from 'aspi';

const api = new Aspi({
  baseUrl: 'https://jsonplaceholder.typicode.com',
  headers: { 'Content-Type': 'application/json' },
});

async function getValidatedTodo(id: number) {
  const response = await api
    .get(`/todos/${id}`)
    .withResult()
    .schema(
      z.object({
        id: z.number(),
        title: z.string(),
        completed: z.boolean(),
      }),
    )
    .json(); // type inferred from the schema

  Result.match(response, {
    onOk: (data) => console.log('Todo ✅', data),
    onErr: (err) => {
      if (err.tag === 'parseError') {
        const parseErr = err.data as z.ZodError;
        console.error('Validation failed:', parseErr.errors);
      } else {
        console.error('Other error', err);
      }
    },
  });
}

Retry & back‑off

const api = new Aspi({
  baseUrl: 'https://example.com',
  headers: { 'Content-Type': 'application/json' },
}).setRetry({
  retries: 3,
  retryDelay: 1000, // simple fixed delay
  retryOn: [404, 500], // retry on specific status codes
});

// Override retry options for a single request
api
  .get('/todos/1')
  .setHeader('Accept', 'application/json')
  .setRetry({
    // exponential back‑off for this call only
    retryDelay: (attempt) => Math.pow(2, attempt) * 1000,
  })
  .withResult()
  .json()
  .then((res) =>
    Result.match(res, {
      onOk: (data) => console.log('Got data', data),
      onErr: (err) => console.error('Failed', err),
    }),
  );

Global configuration helpers

| Method | Description | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | setBaseUrl(url) | Change the base URL for all subsequent requests. | | setHeaders(headers) | Merge an object of headers with any existing ones. | | setHeader(key, value) | Set a single header. | | setBearer(token) | Shortcut for Authorization: Bearer <token>. | | setRetry(retryConfig) | Define a global retry strategy (overridable per request). | | setQueryParams(params) | Replace the request’s query string – accepts object, URLSearchParams, array of tuples, or raw string. | | schema(schema) | Attach a StandardSchemaV1 validator for the response body. | | use(fn) | Register a request‑transformer middleware that receives the current RequestInit and returns a new one. Returns a new Aspi instance typed with the transformed config. | | withResult() | Switch the request into Result mode (returns a Result instead of a tuple). | | throwable() | Make the request throw on non‑2xx responses (useful for try / catch patterns). | | url() | Get the fully‑qualified URL that will be used for the request. |


Custom error handling

Aspi lets you map any HTTP status to a typed error object that can be pattern‑matched later.

api
  .error('badRequestError', 'BAD_REQUEST', (req, res) => ({
    message: 'The request payload is invalid',
    payload: res.body,
  }))
  .error('unauthorisedError', 'UNAUTHORIZED', () => ({
    message: 'You must log in first',
  }));

Convenient shortcuts are provided for the most common statuses (each forwards to error internally and augments the generic Opts['error'] type):

api.notFound(cb); // 404
api.tooManyRequests(cb); // 429
api.conflict(cb); // 409
api.badRequest(cb); // 400
api.unauthorised(cb); // 401 (British spelling, matches the Request API)
api.forbidden(cb); // 403
api.notImplemented(cb); // 501
api.internalServerError(cb); // 500

These helpers allow you to write:

api
  .get('/secret')
  .unauthorised(() => ({ message: 'You need a token' }))
  .withResult()
  .json()
  .then((res) =>
    Result.match(res, {
      onOk: (data) => console.log(data),
      onErr: (err) => {
        if (err.tag === 'unauthorisedError') {
          console.warn(err.data.message);
        }
      },
    }),
  );

API reference (selected)

class Request<
  Method extends HttpMethods,
  TRequest extends AspiRequestInitWithBody = AspiRequestInit,
  Opts extends Record<any, any> = { error: {} },
> {
  // core request factories
  get(path: string): Request<'GET', TRequest, Opts>;
  post(path: string): Request<'POST', TRequest, Opts>;
  put(path: string): Request<'PUT', TRequest, Opts>;
  patch(path: string): Request<'PATCH', TRequest, Opts>;
  delete(path: string): Request<'DELETE', TRequest, Opts>;
  head(path: string): Request<'HEAD', TRequest, Opts>;
  options(path: string): Request<'OPTIONS', TRequest, Opts>;

  // configuration
  setBaseUrl(url: BaseURL): this;
  setHeaders(headers: HeadersInit): this;
  setHeader(key: string, value: string): this;
  setBearer(token: string): this;
  setRetry(cfg: AspiRetryConfig<TRequest>): this;
  setQueryParams(
    params: Record<string, string> | string[][] | string | URLSearchParams,
  ): this;
  use<T extends TRequest, U extends TRequest>(
    fn: RequestTransformer<T, U>,
  ): Request<U>;

  // schema validation
  schema<TSchema extends StandardSchemaV1>(
    schema: TSchema,
  ): Request<
    Method,
    TRequest,
    Merge<
      Omit<Opts, 'schema'>,
      {
        schema: TSchema;
        error: Merge<
          Opts['error'],
          {
            parseError: CustomError<
              'parseError',
              StandardSchemaV1.FailureResult['issues']
            >;
          }
        >;
      }
    >
  >;

  // result / throwable toggles
  withResult(): Request<
    Method,
    TRequest,
    Merge<
      Omit<Opts, 'withResult' | 'throwable'>,
      {
        withResult: true;
        throwable: false;
      }
    >
  >;
  throwable(): Request<
    Method,
    TRequest,
    Merge<
      Omit<Opts, 'withResult' | 'throwable'>,
      {
        withResult: false;
        throwable: true;
      }
    >
  >;

  // custom error handling
  error<Tag extends string, A extends {}>(
    tag: Tag,
    status: HttpErrorStatus,
    cb: CustomErrorCb<TRequest, A>,
  ): Request<
    Method,
    TRequest,
    Merge<
      Omit<Opts, 'error'>,
      {
        error: {
          [K in Tag | keyof Opts['error']]: K extends Tag
            ? CustomError<Tag, A>
            : Opts['error'][K];
        };
      }
    >
  >;
  notFound<A>(cb: CustomErrorCb<TRequest, A>): this;
  tooManyRequests<A>(cb: CustomErrorCb<TRequest, A>): this;
  conflict<A>(cb: CustomErrorCb<TRequest, A>): this;
  badRequest<A>(cb: CustomErrorCb<TRequest, A>): this;
  unauthorised<A>(cb: CustomErrorCb<TRequest, A>): this;
  forbidden<A>(cb: CustomErrorCb<TRequest, A>): this;
  notImplemented<A>(cb: CustomErrorCb<TRequest, A>): this;
  internalServerError<A>(cb: CustomErrorCb<TRequest, A>): this;

  // helpers
  url(): string;

  // response parsers
  json<T extends StandardSchemaV1.InferOutput<Opts['schema']>>(): Promise<
    Opts['withResult'] extends true
      ? Result.Result<
          AspiResultOk<TRequest, T>,
          | AspiError<TRequest>
          | (Opts extends { error: any }
              ? Opts['error'][keyof Opts['error']]
              : never)
          | JSONParseError
        >
      : Opts['throwable'] extends true
        ? AspiPlainResponse<TRequest, T>
        : [
            AspiResultOk<TRequest, T> | null,
            (
              | (
                  | AspiError<TRequest>
                  | (Opts extends { error: any }
                      ? Opts['error'][keyof Opts['error']]
                      : never)
                  | JSONParseError
                )
              | null
            ),
          ]
  >;
  text(): Promise<
    Opts['withResult'] extends true
      ? Result.Result<
          AspiResultOk<TRequest, string>,
          | AspiError<TRequest>
          | (Opts extends { error: any }
              ? Opts['error'][keyof Opts['error']]
              : never)
        >
      : Opts['throwable'] extends true
        ? AspiPlainResponse<TRequest, string>
        : [
            AspiResultOk<TRequest, string> | null,
            (
              | (
                  | AspiError<TRequest>
                  | (Opts extends { error: any }
                      ? Opts['error'][keyof Opts['error']]
                      : never)
                )
              | null
            ),
          ]
  >;
  blob(): Promise<
    Opts['withResult'] extends true
      ? Result.Result<
          AspiResultOk<TRequest, Blob>,
          | AspiError<TRequest>
          | (Opts extends { error: any }
              ? Opts['error'][keyof Opts['error']]
              : never)
        >
      : Opts['throwable'] extends true
        ? AspiPlainResponse<TRequest, Blob>
        : [
            AspiResultOk<TRequest, Blob> | null,
            (
              | (
                  | AspiError<TRequest>
                  | (Opts extends { error: any }
                      ? Opts['error'][keyof Opts['error']]
                      : never)
                )
              | null
            ),
          ]
  >;
}

License

MIT © Aspi contributors