@contactlab/appy
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:
- Typescript >= v3.2.2
fp-ts
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 POST
s or PUT
s):
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.