fp-ts and Beautiful API Calls
Tags: typescript, functional, fpts, iots
Published: 2020-03-05 16:53:17

---
title: fp-ts and Beautiful API Calls
published: true
description: How to make beautiful API calls with fp-ts
tags: typescript, functional, fp-ts, io-ts
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/77l54jal3bebnlkgawup.jpg
---

Last time we visted fp-ts, [we made concurrent API calls](https://dev.to/gnomff_65/fp-ts-sequencet-and-sweet-sweet-async-typed-fp-5aop) but didn't spend any time on error handling or keeping it DRY ([Don't Repeat Yourself](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself#DRY_vs_WET_solutions)). Well we're a bit older and wiser now, and it's time to re-visit. Let's add some elegant error handling and tighten things up. Here's what we had last time:

```javascript
const getUser = pipe(
  httpGet('https://reqres.in/api/users?page=1'),
  TE.map(x => x.data),
  TE.chain((str) => pipe(
    users.decode(str), 
    E.mapLeft(err => new Error(String(err))), 
    TE.fromEither)
  )
);

const getAnswer = pipe(
  TE.right("tim"),
  TE.chain(ans => pipe(
    answer.decode({ans}), 
    E.mapLeft(err => new Error(String(err))), 
    TE.fromEither)
  )
)
```

Bleh, there is a lot of duplication. Also our errors are going to be useless. If we run that the code above we get `Error: [object Object]`. What the heck is that? Completely useless, that's what. We can do better. First thing, let's make our error messages actually readable.

```javascript
import { failure } from 'io-ts/lib/PathReporter'

const getAnswer = pipe(
  TE.right("tim"),
  TE.chain(ans => pipe(
    answer.decode({ans}), 
    E.mapLeft(err => new Error(failure(err).join('\n'))), 
    TE.fromEither)
  )
)
```
The [failure](https://github.com/gcanti/io-ts/blob/c3d53fdf768b774f1ad49d1f1cb8a408e4d79e87/src/PathReporter.ts#L34) method from the io-ts PathReporter takes an array of `ValidationError`s and gives back a string. If we run this we get `Error: Invalid value "tim" supplied to : { ans: number }/ans: number` which is definitely a lot more helpful. Nice.

Ok, next up let's see what we can do to get rid of that gross duplication.
```javascript
const decodeWith = <A>(decoder: t.Decoder<unknown, A>) =>
  flow(
    decoder.decode,
    E.mapLeft(errors => new Error(failure(errors).join('\n'))),
    TE.fromEither
  )

const getUser = pipe(
  httpGet('https://reqres.in/api/users?page=1'),
  TE.map(x => x.data),
  TE.chain(decodeWith(users))
);

const getAnswer = pipe(
  TE.right({ans: 42}),
  TE.chain(decodeWith(answer))
)
```
Well that looks _way_ better. `decoder.decode` takes an `unknown` and gives back an `Either<Errors, A>` which is perfect. But `getUser` is still pretty specific to that url and to that type which is uncomfortable. One more time:
```javascript
const getFromUrl = <A>(url:string, codec:t.Decoder<unknown, A>) => pipe(
  httpGet(url),
  TE.map(x => x.data),
  TE.chain(decodeWith(codec))
);
```
Aw yis. Now we can make any API call that we want and the response will be validated against our codec. We can even throw in a `TE.mapLeft` after `httpGet` if we want to do something fancy with errors thrown by axios.

Let's put it all together.
```javascript
import axios, { AxiosResponse } from 'axios'
import { flatten, map } from 'fp-ts/lib/Array'
import * as TE from 'fp-ts/lib/TaskEither'
import * as E from 'fp-ts/lib/Either'
import * as T from 'fp-ts/lib/Task'
import { sequenceT } from 'fp-ts/lib/Apply'
import { pipe } from 'fp-ts/lib/pipeable'
import { flow } from 'fp-ts/lib/function'
import { failure } from 'io-ts/lib/PathReporter'
import * as t from 'io-ts'

//create a schema to load our user data into
const users = t.type({
  data: t.array(t.type({
    first_name: t.string
  }))
});
type Users = t.TypeOf<typeof users>

//schema to hold the deepest of answers
const answer = t.type({
  ans: t.number
});

//Convert our api call to a TaskEither
const httpGet = (url:string) => TE.tryCatch<Error, AxiosResponse>(
  () => axios.get(url),
  reason => new Error(String(reason))
)

//function to decode an unknown into an A
const decodeWith = <A>(decoder: t.Decoder<unknown, A>) =>
  flow(
    decoder.decode,
    E.mapLeft(errors => new Error(failure(errors).join('\n'))),
    TE.fromEither
  )

//takes a url and a decoder and gives you back an Either<Error, A>
const getFromUrl = <A>(url:string, codec:t.Decoder<unknown, A>) => pipe(
  httpGet(url),
  TE.map(x => x.data),
  TE.chain(decodeWith(codec))
);

const getAnswer = pipe(
  TE.right({ans: 42}),
  TE.chain(decodeWith(answer))
)

const apiUrl = (page:number) => `https://reqres.in/api/users?page=${page}`

const smashUsersTogether = (users1:Users, users2:Users) =>
  pipe(flatten([users1.data, users2.data]), map(item => item.first_name))

const runProgram = pipe(
  sequenceT(TE.taskEither)(
    getAnswer, 
    getFromUrl(apiUrl(1), users), 
    getFromUrl(apiUrl(2), users)
  ),
  TE.fold(
    (errors) => T.of(errors.message),
    ([ans, users1, users2]) => T.of(
      smashUsersTogether(users1, users2).join(",") 
      + `\nThe answer was ${ans.ans} for all of you`),
  )
)();

runProgram.then(console.log)
```
```
George,Janet,Emma,Eve,Charles,Tracey,Michael,Lindsay,Tobias,Byron,George,Rachel
The answer was 42 for all of you
```
And if we return erroneous data like:
```javascript
const getAnswer = pipe(
  TE.right({ans: "tim"}),
  TE.chain(decodeWith(answer))
)
```
we get
```
Invalid value "tim" supplied to : { ans: number }/ans: number
```

Damn that's pretty. With this pattern we can handle _any_ API calls that:
* Can error
* Return something that needs validation
* Run in sequence, in parallel or by themselves

with complete confidence that all of the edge cases are covered. Stay (type)safe out there!