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 🙏

© 2024 – Pkg Stats / Ryan Hefner

@sergiodxa/api-client

v1.0.2

Published

A strongly typed API client using Zod

Downloads

149

Readme

createAPIClient

Features

  • Completely type-safe
  • Automatic route params
  • Typed search and body params with Zod
  • Typed responses with Zod
  • Automatic error handling
  • Automatic JSON parsing
  • AbortController/AbortSignal built-in support
  • Customizable fetch options
  • Customizable performance measuring
  • Customizable credentials handling

Install

npm add @sergiodxa/api-client zod

Usage

Import createAPIClient and z from Zod

import { createAPIClient } from "@sergiodxa/api-client";
import { z } from "zod";

Then create a new API client. There are two required parameters:

  • baseURL, which is a URL object
  • endpoints which is an object with the endpoints the API supports

And the following parameters are optional:

  • fetch, all RequestInit options supported by fetch except body and method. Every request will have these options.
  • measure is a function used to measure the performance of the request. It receives the endpoint and an async function that performs the request and parses the response. The default function will do a simple console.log with the message [API] GET /users took 50ms where GET /users is the endpoint and 50ms is the time it took to fetch the endpoint. You can use it to remove the logs or customize the usage.
  • credentials is a function that receives an object with the URL and Headers instances that the request will use and an optional token. Use this method to attach that token to the URL or headers before sending the request based on the API requirements.
let APIClient = createAPIClient({
  baseURL: "https://api.example.com",
  endpoints: {
    "GET /users": {
      expected: {
        success: z.object({ id: z.string(), name: z.string() }).array(),
      },
    },
    "GET /users/:userId": {
      expected: {
        success: z.object({ id: z.string(), name: z.string() }),
        failure: z.object({ error: z.string() }),
      },
    },
  },
});

Once you have your APIClient class (feel free to use another name), you can create a new instance:

let client = new APIClient();

Note: The constructor accepts a single optional token string parameter. This token will be passed to the credentials function if provided.

Finally, use client.request method to call your different endpoints.

const users = await client.request("GET /users", {}); // second argument always required, working on that

The client.request method receives the endpoint as METHOD /path and an object with extra options, which can be:

  • variables is an object with the variables the endpoint needs. The route params, search params, and the body for non-GET requests are part of the variables.
  • headers, is a HeadersInit object with the headers you want to apply to your request.
  • signal , is an AbortSignal instance you can use to abort the request.

The client.request result will depend on the endpoint configuration. If the endpoint doesn't have a failure schema, the result will be the output of the success schema.

If the endpoint does have a failure schema, then the result will be a Result object with the following properties:

  • status, is a string with the result's status. It can be success or failure.
  • data, is the output of the success schema.
  • code, is the HTTP status code of the response, only on failure.
const userResult = await client.request("GET /users/:userId", {
  variables: { params: { userId: 123 } },
});

if (userResult.status === "failure") {
  console.log(userResult.code);
  console.log(userResult.data);
}

if (userResult.status === "success") {
  console.log(userResult.data);
}

In case the response is a 4xx, and there's no failure schema, then client.request will throw an error with the message Missing "failure" schema for endpoint "GET /users". where GET /users is the endpoint that failed.

In case the response is a 5xx, then client.request will throw an error with the message The endpoint "GET /users" throw a 500 code. where GET /users is the endpoint that failed and 500 is the status code of the response.

If the response is not JSON, then client.request will throw an error with the message The endpoint "GET /users" returned a non-JSON response. where GET /users is the endpoint that failed. The response Content-Type header determines whether the response is considered a JSON. It must include json, which means application/json or application/json; charset=utf-8, and other variants are all valid.

In case the response has the Content-Type header but it's not a JSON one, it will fail when trying to parse it, such case client.request will throw an error with the message The endpoint "GET /users" returned an invalid JSON response. where GET /users is the endpoint that failed.

When defining endpoints, an endpoint can have route params, search params, body params, and the expected response results.

const endpoints = {
  "GET /users": {
    search: z.object({ page: z.number() }),
    expected: {
      success: z.object({ id: z.number() }).array(),
    },
  },
  "POST /users": {
    body: z.object({ name: z.string() }),
    expected: {
      success: z.object({ id: z.number() }),
      failure: z.object({ error: z.string() }),
    },
  },
  "GET /users/:userId": {
    expected: {
      success: z.object({ id: z.number() }),
      failure: z.object({ error: z.string() }),
    },
  },
};

The search schema validates the search parameters. The body schema validates the body of non-GET requests, and the expected schema validates the response.

There's automatic inference of route parameters from the endpoint path. In the example above, :userId is a route param, so you will have to use that endpoint like this:

await client.request("GET /users/:userId", {
  variables: { params: { userId: 123 } },
});

All route params are typed as string | number in this case.

The search params can be almost anything, but since URLSearchParams doesn't support everything there are a few considerations based on the type:

  • strings are used as is
  • booleans and numbers are converted to strings using String(value)
  • arrays are iterated and added with searchParams.append sharing the same key, the values are converted to strings using String(value), e.g. [1, 2, 3] for the key id will be ?id=1&id=2&id=3
  • objects are added like ?param[key]=value, values are converted to strings using String(value), e.g. { color: "red", quantity: 1 } for the key filter will be ?filter[color]=red&filter[quantity]=1.

Any other type of value will be ignored.

The body params must be JSON compatible.

Note: Support for file uploads may come in the future.

If you don't want to use client.request(endpoint) you can extend and provide your methods:

let BaseAPIClient = createAPIClient({
  baseURL: "https://api.example.com",
  endpoints: {
    "GET /users": {
      expected: {
        success: z.object({ id: z.string(), name: z.string() }).array(),
      },
    },
    "GET /users/:userId": {
      expected: {
        success: z.object({ id: z.string(), name: z.string() }),
        failure: z.object({ error: z.string() }),
      },
    },
  },
});

class APIClient extends BaseAPIClient {
  fetchUsers() {
    return this.request("GET /users", {});
  }

  fetchUser(id: number) {
    return this.request("GET /users/:userId", {
      variables: { params: { userId: id } },
    });
  }
}

And then use your custom class as an API client.

let client = new APIClient();

let users = await client.fetchUsers();
let userResult = await client.fetchUser(123);

Author