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

fetcher-ts

v1.1.1

Published

REST HTTP client built in functional style

Downloads

344

Readme

Type-Safe Fetcher

npm Build Status

Motivation

Aim of this project is to provide a thin type-safe wrapper around fetch API, useful for working with JSON REST APIs.

Installation

npm install --save fetcher-ts

As this project is a part of fp-ts ecosystem, you'll also need fp-ts and io-ts as a peer dependencies. And don't forget to install a cross-fetch as a peer dependency as well – it provides an isomorphic implementation of fetch using whatwg-fetch and node-fetch:

npm install --save fp-ts io-ts cross-fetch

Usage example

Let's dive into an example right away!

// This is main business model – basically, any interface serializable to JSON you can imagine
type User = { name: string };
// And this is a model for HTTP 422 response code – it contains some internal code plus correlation ID from logging system
type FourTwoTwo = { code: number; correlationId: string };

// Type of possible server responses. It should extend `Result<Code, T>` from `fetcher`:
type GetUserResult =
  | { code: 200, payload: User[] } // 200 OK – we got the result
  | { code: 400, payload: Error } // 400 Bad Request – we did something wrong
  | { code: 401, payload: [Error, string] } // 401 Unauthorized – we tried requesting a resource we don't have access to
  | { code: 422, payload: FourTwoTwo }; // 422 Unprocessable entity – business logic error from some internal system

// `io-ts` validators for 200 and 422 responses.
// Please note that they are optional – if they are not passed to `.handle()`, the validation stage will be skipped.
const TUsers = io.array(io.type({ name: io.string }));
const TFourTwoTwo = io.type({ code: io.number, correlationId: io.string });

const [n, errors] = 
  // We create an instance of `Fetcher` class and parameterize it with our response type and final transformation result we want:
  await new Fetcher<GetUserResult, string>('https://example.com')
    // In 200 handler we need to pass a function from `User[]` to `string`, as specified in `Fetcher` parameters:
    .handle(200, (users) => users.map((u) => u.name).join(', '), TUsers)
    // In 400 handler we need to handle plain `Error`:
    .handle(400, (err) => err.message)
    // In 422 we need to deal with internal error code and correlation ID:
    .handle(
      422,
      ({ correlationId }) => correlationId,
      TFourTwoTwo,
      // For the sake of brewity I use non-null assertion here; in real code you should check for presence:
      async (res) => ({ code: +res.headers.get('x-code')!, correlationId: res.headers.get('x-correlation-id')! }),
    )
    // In 401 handler we get as a response name of permission we lack:
    .handle(401, ([err, permission]) => `You lack ${permission}. Also, ${err.message}`)
    // We CANNOT specify explicit handlers for codes we didn't describe in the `GetUserResult` type:
    // .handle(500, () => `Argument of type '500' is not assignable to parameter of type 'never'`)
    // However, we can use `discardRest` to specify a "fallback" thunk which will be executed for any codes which are not explicitly handled:
    .discardRest(() => '42')
    // `Fetcher<T, A>` is a functor in `A`, i.e. could be transformed into `Fetcher<T, B>`:
    .map((s) => s.length)
    // Finally, we can use `run` to get a `Promise<[Result, Option<io.Errors>]>`:
    .run();

// Here `n` will be a `number`, and `errors` will either be undefined, or an instance of `io.Errors`:
console.log(n, errors);

Public API

import { Fetcher } from 'fetcher-ts';

A Fetcher class is a wrapper around window.fetch with additional type safety. Its public API consists of:

Type parameters: TResult and To

TResult

Sum type of possible API endpoint responses. Should consist of a { code: number, payload: T } entries:

type MyMethodResults = 
  | { code: 200, payload: string[] } 
  | { code: 500, payload: Error };

To

A type into which the response will be transformed. Could easily be the same type as in 200 response – given that you can construct a fallback instance for all other reponse codes.

constructor(input: RequestInfo, init?: RequestInit)

Creates a new instance of a Fetcher class. Parameters are exactly the same you would normally use for window.fetch.

Please note that you'll need to pass type parameters to the constructor as well in order to ensure type inference works correctly:

type MyMethodResults = 
  | { code: 200, payload: string[] } 
  | { code: 500, payload: Error };
const fetcher = new Fetcher<MyMethodResults, string>('https://example.com');

.handle(code: number, handler: (data: From) => To, codec?: io.Type, extractor: (response: Response) => Promise): Fetcher<...>

Register a handler for given code, using optional extractor to conver the raw Response into target type From. Please note that code should be present in the passed to the constructor type parameter:

type MyMethodResults = 
  | { code: 200, payload: string[] } 
  | { code: 500, payload: Error };
const fetcher = new Fetcher<MyMethodResults, string>('https://example.com')
  .handle(400, () => 'no way'); // compilation error: Argument of type '400' is not assignable to parameter of type 'never'

Also an io-ts codec could be passed for each handler, providing validation capability for each handler:

type MyOtherMethod = { code: 400, payload: string }; // this enpoint can only fail with a text of an error :(
const [result, errors] = await new Fetcher<MyOtherMethod, string>('https://example.com/other')
  .handle(400, (msg) => `Oh noes, error: ${msg}`, io.string)
  .run();
// If the server responds not with string, an `io-ts` validation error will be present in `errors` (`Some<Errors>`).

.discardRest(restHandler: () => To): Fetcher<...>

Register a fallback handler for all HTTP status codes not registered explicitly using .handle():

type MyMethodResults = 
  | { code: 200, payload: string[] } 
  | { code: 500, payload: Error };
const fetcher = new Fetcher<MyMethodResults, string>('https://example.com')
  .handle(200, (strings) => string.join(', '))
  .discardRest(() => 'no way'); // code 500 and any other will be handled by this thunk

run(): Promise<[To, Option<io.Errors>]>

The main method to actually consume the built fetch handling chain and execute the request:

type MyMethodResults = 
  | { code: 200, payload: string[] } 
  | { code: 500, payload: Error };
const [result, validationErrors] = await new Fetcher<MyMethodResults, string>('https://example.com')
  .handle(200, (strings) => string.join(', '))
  .discardRest(() => 'no way')
  .run(); // => result: string, validationErrors: Option<io.Errors>

toTaskEither(): TaskEither<Error, [To, Option<io.Errors>]>

A convenience method to transform built fetcher chain into a TaskEither.

Use cases for this project

Such fetcher design will be beneficial for autogenerated APIs – i.e. if your result sum type is generated from something akin to OpenAPI specification. In this case the developer who uses fetcher with such sum type will always be sure that he/she handled all possible codes, as the type system will serve as a guide.