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

@gwakko/fetch-x

v1.1.1

Published

A sleek interface wrapped over fetch, offering both an intuitive syntax and robust response body validation for data assurance.

Downloads

10

Readme

Features

fetchx is a small wrapper around fetch designed to simplify the way to perform network requests and handle responses.

  • Intuitive - lean API, handles errors, headers and (de)serialization
  • Immutable - every call creates a cloned instance that can then be reused safely
  • Modular - intercept requests, responses
  • Compatibility - crafted for any modern browser supporting the "Fetch API" or Node.js version 18 and newer
  • Type safe - strongly typed, written in TypeScript
  • Validation Scheme Integration - Seamlessly validate response bodies using the power of Zod, or with the implemented Schema type.

Table of Contents

Motivation

Because Interface or Type Alone Doesn't Guarantee Safety

In the vast realm of web development, simply providing a type or interface to a fetch operation is no silver bullet for ensuring type safety in responses. This creates an illusion of security, while hidden discrepancies might lurk beneath. That's where FetchX steps in. We recognize the crucial importance of true type safety, and we've architected our library to guarantee it. With FetchX, you're not just promised safety — it's delivered. Dive in, and experience the certainty of genuinely type-safe responses.

function api<T>(url: string): Promise<T> {
    return fetch(url)
        .then(response => {
            if (!response.ok) {
                throw new Error(response.statusText)
            }
            return response.json() as Promise<T>
        })
}

FetchX Secures it with Robust Validation.

import { z } from 'zod';
import { UserSchema } from '@/user';

export const SignInSchema = z
    .object({
        username: z.string().nonempty(),
        password: z.string().nonempty(),
    })
    .required();

export type SignInRequest = z.infer<typeof SignInSchema>;

export const SignInResponseSchema = z
    .object({
        accessToken: z.string().nonempty('Invalid Access Token'),
        refreshToken: z.string().nonempty('Invalid Refresh Token'),
        user: UserSchema,
    })
    .required();

export type SignInResponse = z.infer<typeof SignInResponseSchema>;
const API_BASE_URL = 'http://localhost:8080';
const API_LOGIN_URL = '/api/auth/sign-in';
const signIn = async ({ email, password }): Promise<User | null> => {
    try {
        const response = await fetchx<SignInResponse, SignInRequest>(
            API_BASE_URL,
        )
            .resolver(SignInResponseSchema)
            .options({ cache: 'no-cache' })
            .url(API_LOGIN_URL)
            .post({
                email,
                password,
            })
            .validated();

        return UserMapper.toAuthUser(response.user);
    } catch (error) {
        console.error(error);
        return null;
    }
};

Because manually checking and throwing every request error code is tedious.

Fetch won’t reject on HTTP error status.

fetch('dashboard')
    .then(response => {
        if (!response.ok) {
            if (response.status === 404) throw new Error('ot found')
            else if (response.status === 401) throw new Error('Unauthorized')
            else if (response.status === 418) throw new Error("I'm a teapot !")
            else throw new Error('Other error')
        } else {// ... }
        }
    })
    .then(data => {/* ... */
    })
    .catch(error => {/* ... */
    })

FetchX throws when the response is not successful and contains helper methods to handle common HTTP status codes.

fetchx('dashboard')
    .searchParams({ page: String(1) })
    .notFound(error => {/* ... */
    })
    .unauthorized(error => {/* ... */
    })
    .onUnauthorized((self/*type of fetchx() instance*/) => {/* ... */
    })
    .error(HttpStatusCode.IM_A_TEAPOT, error => {/* ... */
    })
    .error(425, error => {/* ... */
    })
    .get()
    .response(response => {/* ... */
    })
    .catch(error => {/* uncaught errors */
    })

Because sending a json object should be easy.

With fetch you have to set the header, the method and the body manually.

fetch('http://api/posts', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ hello: 'world' })
}).then(response => {/* handle */
})

With fetchx, you have shorthands at your disposal.

fetchx('http://api')
    .url('/posts')
    .post({ hello: 'world' })
    .response(response => {/* handle */
    })

fetchx('http://api')
    .url('/posts')
    .post(new FormData())
    .then(response => {/* handle */
    })

Because configuration should not rhyme with repetition.

A fetchx object is immutable which means that you can reuse previous instances safely.

// Cross origin authenticated requests on an external API
const apiInstance = fetchx('http://api.localhost') // Base url
    // Authorization header
    .authorization(`Bearer ${accessToken}`)
    // Cors fetch options
    .options({
        credentials: "include",
        mode: "cors"
    }) // or .options((prevOpts) => ({ ...prevOpts, credentials: "include", mode: "cors" }))
    // Handle Any Error
    .fetchError((exception) => {/* handle */
    });

// Fetch a resource
const resource = await apiInstance
    // Add a custom header for this request
    .headers({ 'If-Unmodified-Since': 'Wed, 21 Oct 2015 07:28:00 GMT' })
    .url('/posts/1')
    .get()
    .json((jsonData) => {/* handle */
    });

// Post a resource
apiInstance
    .url('/resource')
    .post({ title: 'new title' })
    .json((jsonData) => {/* handle */
    });

Installation

Package Manager

npm i @gwakko/fetch-x # or yarn/pnpm add @gwakko/fetch-x

Usage

Import

// ECMAScript modules
import fetchx from '@gwakko/fetch-x';
import { fetchx } from '@gwakko/fetch-x';

Minimal Example

import { z } from 'zod';
import { fetchx } from '@gwakko/fetch-x';

// Instantiate and configure fetchx
const api = fetchx('https://jsonplaceholder.typicode.com', { mode: 'cors' });
// or options with callback
// const api = fetchx('https://jsonplaceholder.typicode.com', (prevOptions) => ({ ...prevOptions, mode: 'cors' }));

const TodoSchema = z.object({
    userId: z.number(),
    id: z.number(),
    title: z.string(),
    completed: z.boolean()
});

const TodosSchema = z.array(TodoSchema);

const GeoSchema = z.object({
    lat: z.string(),
    lng: z.string()
});

const AddressSchema = z.object({
    street: z.string(),
    suite: z.string(),
    city: z.string(),
    zipcode: z.string(),
    geo: GeoSchema
});

const CompanySchema = z.object({
    name: z.string(),
    catchPhrase: z.string(),
    bs: z.string()
});

const UserSchema = z.object({
    id: z.number(),
    name: z.string(),
    username: z.string(),
    email: z.string(),
    address: AddressSchema,
    phone: z.string(),
    website: z.string(),
    company: CompanySchema
});

const UsersArraySchema = z.array(UserSchema);

try {
    // Fetch todos
    const todos = await api.resolver(TodosSchema).url('/todos').get().validated();
    // const todos = await api.url('/todos').get().json(); // without validation
    const todo = todos.at(0);
    const todoUser = await api.resolver(UserSchema).url(`/users?id=${todo.userId}`).get().validated();

    // Create a new todo
    const newTodo = await api.url('/todos').post({
        userId: todoUser.id,
        title: "New Todo",
        completed: false
    });

    // Patch it
    await api.url(`/todos/${newTodo.id}`).patch({
        completed: true
    });

    // Fetch it
    const todo2 = await api.resolver(TodosSchema).url(`/todos/${newTodo.id}`).get().validated();
} catch (error) {
    // The API could return an empty object - in which case the status text is logged instead.
    const message =
        typeof error.message === "object" && Object.keys(error.message).length > 0
            ? JSON.stringify(error.message)
            : error.response.statusText
    console.error(`${error.status}: ${message}`)
}

API

FetchX defaults

These methods are available from the main default export and can be used to instantiate fetchx and configure it globally.

import { fetchx } from '@gwakko/fetch-x';

fetchx.defaults.baseUrl = 'http://base.api';
fetchx.defaults.interceptors.request.push((request) => {
});
fetchx.defaults.interceptors.response.push((response) => {
})
fetchx.defaults.headers = {
    'Some-header': 'test',
};
fetchx.defaults.requestOptions = {
    cache: 'no-cache',
    credentials: 'include',
};
fetchx.defaults.catches.set(404, (exception) => {
});

Helper Methods

Helper Methods are used to configure the request and program actions.

fetchx()
    .url('/posts/1')
    .headers({ 'Cache-Control': 'no-cache' })
    // or
    .headers((headers) => ({ ...headers, 'New-header': 'hello' }))
    .contentType('text/html')

HTTP Methods

Sets the HTTP method and sends the request.

Calling an HTTP method ends the request chain and returns a response chain. You can pass optional body arguments to these methods.

fetchx().url('/url').get();
fetchx().url('/url').post({ propertyName: 'body' });

NOTE: The Content-Type header will be automatically set based on the datatype of the body in a fetch request:

Object/JSON:
    DataType: Object
    Header: application/json

FormData:
    DataType: FormData
    Header: multipart/form-data

Text:
    DataType: string
    Header: text/plain

Ensure that your server is correctly configured to handle the respective Content-Type headers for the data you're sending. Adjust fetch headers manually if a different Content-Type is required for your specific use case.

Catchers

Catchers are optional, but if none are provided an error will still be thrown for http error codes and it will be up to you to catch it.

import { fetchx } from '@gwakko/fetch-x';

fetchx('...')
    .badRequest((err) => console.log(err.status))
    .unauthorized((err) => console.log(err.status))
    .forbidden((err) => console.log(err.status))
    .notFound((err) => console.log(err.status))
    .timeout((err) => console.log(err.status))
    .internalError((err) => console.log(err.status))
    .error(418, (err) => console.log(err.status))
    .fetchError((err) => console.log(err))
    .get()
    .response();

The error passed to catchers is enhanced with additional properties.

interface ApiResponseException {
    status: number;
    response: Response;
    url: string;
    text?: string;
    json?: unknown;
}

Response Types

Setting the final response body type ends the chain and returns a regular promise.

All these methods accept an optional callback, and will return a Promise resolved with either the return value of the provided callback or the expected type.

// Without a callback
fetchx('...').get().json().then(json => /* json is the parsed json of the response body */)
// Without a callback using await
const json = await fetchx("...").get().json()
// With a callback the value returned is passed to the Promise
fetchx('...').get().json(json => "Hello world!").then(console.log) // => Hello world!

If an error is caught by catchers, the response type handler will not be called.

QueryString

Used to construct and append the query string part of the URL from an object.

fetchx('http://example.com').searchParams({ page: String(1), hello: 'world' }); // url is now http://example.com?page=1&hello=world
fetchx('http://example.com?some=prop').searchParams({ page: String(1), hello: 'world' }); // url is now http://example.com?some=prop&page=1&hello=world
fetchx('http://example.com?some=prop').searchParams({ page: String(1), hello: 'world' }, true); // url is now http://example.com?page=1&hello=world

Abort

const abortController = new AbortController();

fetchx('...')
    .abortController(abortController)
    .onAbort((_) => console.log('Aborted!'))
    .get()
    .text((_) => console.log('should never be called'));

abortController.abort();

TODO

  • Add Progress Upload & Download

License

MIT