@contactlab/appy

Node CI npm (scoped) node-current (scoped) GitHub package.json dependency version (dev dep on branch) GitHub package.json dependency version (dev dep on branch) GitHub

A functional wrapper around Fetch API.

Install

$ npm install @contactlab/appy fp-ts

# --- or ---

$ yarn add @contactlab/appy fp-ts

Motivation

appy tries to offer a better model for fetching resources, using the standard global fetch() function as a “backbone” and some principles from Functional Programming paradigm.

The model is built around the concepts of:

  • a function with some configurable options (Reader)
  • that runs asynchronous operations (Task)
  • which can fail for some reason (Either)

In order to achieve this, appy intensely uses:

API

appy exposes a simple core API that can be extended with “combinators”.

It encodes through the Req<A> type a resource’s request, or rather, an async operation that can fail or return a Resp<A>.

For better composability, the request is expressed in terms of ReaderTaskEither - a function that takes a ReqInput as parameter and returns a TaskEither: we can act on both side of operation (input and output) with the tools provided by fp-ts.

interface Req<A> extends ReaderTaskEither<ReqInput, Err, Resp<A>> {}

ReqInput encodes the fetch() parameters: a single RequestInfo (simple string or Request object) or a tuple of RequestInfo and RequestInit (the object containing request’s options, that it’s optional in the original fetch() API).

type ReqInput = RequestInfo | RequestInfoInit;

// Just an alias for a tuple of `RequesInfo` and `RequestInit` (namely the `fetch()` parameters)
type RequestInfoInit = [RequestInfo, RequestInit];

Resp<A> is an object that carries the original Response from a fetch() call, the actual retrieved data (of type A) and the request’s input (optional).

interface Resp<A> {
  response: Response;
  data: A;
  input?: RequestInfoInit;
}

Err encodes (as tagged union) the two kind of error that can be generated by Req: a RequestError or a ResponseError.

type Err = RequestError | ResponseError;

RequestError represents a request error. It carries the generated Error and the input of the request (RequestInfoInit tuple).

interface RequestError {
  type: 'RequestError';
  error: Error;
  input: RequestInfoInit;
}

ResponseError represents a response error. It carries the generated Error, the original Response object and the request’s input (optional).

interface ResponseError {
  type: 'ResponseError';
  error: Error;
  response: Response;
  input?: RequestInfoInit;
}

Examples

import {get} from '@contactlab/appy';
import {fold} from 'fp-ts/Either';

const users = get('https://reqres.in/api/users');

users().then(
  fold(
    err => console.error(err),
    resp => console.log(resp.data)
  )
);

You can find other examples here.

Combinators

To make easier extending the library functionalities, any other feature should then be expressed as a simple combinator Req<A> => Req<A>.

So, for example, decoding the response body as JSON:

import {get} from '@contactlab/appy';
import {withDecoder, Decoder} from '@contactlab/appy/combinators/decoder';
import {pipe} from 'fp-ts/function';

interface User {
  id: number;
  email: string;
  first_name: string;
  last_name: string;
  avatar: string;
}

declare const userDec: Decoder<User>;

const getUser = pipe(get, withDecoder(userDec));

const singleUser = getUser('https://reqres.in/api/users/1');

or adding headers to the request:

import {get} from '@contactlab/appy';
import {withHeaders} from '@contactlab/appy/combinators/headers';

const asJson = pipe(get, withHeaders({'Content-Type': 'application/json'}));

const users = asJson('https://reqres.in/api/users');

or setting request’s body (for POSTs or PUTs):

import {post} from '@contactlab/appy';
import {withBody} from '@contactlab/appy/combinators/body';
import {pipe} from 'fp-ts/function';

const send = pipe(
  post,
  withBody({email: 'foo.bar@mail.com', first_name: 'Foo', last_name: 'Bar'})
);

const addUser = send('https://reqres.in/api/users');

io-ts integration

io-ts is recommended but not automatically installed as dependency.

In order to use it with the Decoder combinator you can write a simple helper like:

import * as t from 'io-ts';
import {failure} from 'io-ts/PathReporter';
import {Decoder, toDecoder} from '@contactlab/appy/combinators/decoder';

export const fromIots = <A>(d: t.Decoder<unknown, A>): Decoder<A> =>
  toDecoder(d.decode, e => new Error(failure(e).join('\n')));

Or, with the Decoder module:

import * as D from 'io-ts/Decoder';
import {Decoder, toDecoder} from '@contactlab/appy/combinators/decoder';

export const fromIots = <A>(d: D.Decoder<unknown, A>): Decoder<A> =>
  toDecoder(d.decode, e => new Error(D.draw(e)));

About fetch() compatibility

The Fetch API is available only on “modern” browsers: if you need to support legacy browsers (e.g. Internet Explorer 11 or older) or you want to use it in a Nodejs script we recommend you the excellent cross-fetch package.

Be aware that Nodejs lacks of some classes and directives which have to be exposed to the global scope (check out the tests setup file).

Publish a new version

In order to keep the package’s file structure as flat as possible, the “usual” npm publish command was disabled (via a prepublishOnly script) in favour of a release script:

$ npm run release

This command will execute npm publish directly in the /dist folder, where the postbuild script previously copied the package.json and other usefull files (LICENSE, CHANGELOG.md, etc…).

License

Released under the Apache 2.0 license.