A type-safe, fetch based, functional HTTP toolkit for building API clients quickly

funfetch

A type-safe, fetch based, functional HTTP toolkit for building API clients quickly.

Features

  • 🌍 Universal: works on browsers and node.js
  • 🍰 Functional: use the whole thing, or pieces of it β€” build your own mix
  • πŸ¦Έβ€β™€οΈ Developer first: better developer experience for 3rd party APIs β€” draw your routes, get your client
  • 🦺 Type safe: Built type safe with Typescript
  • 🍭 Use case driven: and not REST driven β€” funfetch is designed to support many use cases and not just one paradigm
  • πŸ‘Ÿ Make simple things easy and run fast automatic JSON parsing, good defaults, great debugging story
  • πŸ”Œ Extensible: use your own response handler and error handler
  • πŸ‘ Lightweight: funfetch is actually a microframework, with a minimal set of dependencies
  • πŸ”‘ BYOF: bring your own fetch. Use the fetch abstraction you prefer β€” we use a universal fetch implementation by default

Quick Start

$ pnpm add @hyperstackjs/funfetch

A quick network action:

import { funfetch } from '@hyperstackjs/funfetch'
const { get } = funfetch()

// this function now represents the network call, can be reused, passed around
const callHome = get('/home')

// make the request
await callHome()

You can pass your network actions to a higher abstraction (a class) for better architecture and/or testing posture:

import { funfetch } from '@hyperstackjs/funfetch'
const { get } = funfetch()
const httpResultFetcher = get("/results")

class Superclient{
    cache: null
    constructor(private fetchResults)
    async getResults(){
        if(!cache){
            const res = await this.fetchResults()
            this.cache = res
        }
    }
}
const client = new Superclient(httpResultFetcher)

Or just quickly shape your client using a dict like you want it:

const createClient = ()=>{
    const {get, post} = funfetch()
    return {
        users: {
            create: post(({userId})=>`/users/${userId}`),
            list: get('/users')
        } 
    }
}

const client = createClient()
const res = await client.users.create({
    params: {userId: 'foobar'}, 
    data: {firstName: "Foo", lastName: "Bar"}
})
const res = await client.users.list()

Running Examples in examples/

All of our examples are runnable, and you can go to /examples to try them out.

Some examples on the CLI (demos universal usage on node):

$ pnpm ts-node examples/jwt.ts
fetching...
ok {
  body: {
    args: {},
    headers: {
...
$ pnpm ts-node examples/parameters-and-data.ts
$ pnpm ts-node examples/headers.ts
$ pnpm ts-node examples/basic-auth.ts

For a React example (demo universal usage on the browser):

Full vs Simple Response

By default, responses are just the body, JSON or otherwise are detected automatically. If you want a full response (status, body, headers), you can turn on a flag:

const { get } = funfetch({
  fullResponse: true,
})

Parameters, Data and Query

  • params – URL / route parameters. Pass a function to any funfetch request creator and you’ll get those as the function parameter (see { id } below)
  • data – will be POSTed
  • query – a dictionary with key/value pairs that will turn into a properly escaped query string

const ok = get(({ id }) => `https://postman-echo.com/get/?id=${id}`)
const send = post(`https://postman-echo.com/post`)

console.log(
'ok',
await ok({ params: { id: 1 }, query: { search: 'foobar' } })
)

// nicely ignores params because 'send' is not a function
console.log('ok', await send({ params: { id: 1 }, data: { hello: 'world' } }))

Cache, Throttling, Retry, and More

We are by-design deferring these to other libraries, to be use in composition with funfetch. A great example is react-query:

const repoData = get('https://api.github.com/repos/tannerlinsley/react-query')
const { isLoading, error, data } = useQuery('repoData', repoData)

Bearer Token / JWT Auth

Bearer and JWT auth are first-class and have a dedicated flag (bearer), set it to use:

const { get } = funfetch({
  fullResponse: true,
  bearer: 'any-token',
})

Basic Auth

const { get } = funfetch({
  fullResponse: true,
  basic: { user: 'postman', password: 'password' },
})

const ok = get('https://postman-echo.com/basic-auth')

Custom Headers

You can set anything that a proper node Request takes (RequestInit type) as a baseline for all requests, between those, headers:

const { get } = funfetch({
  baseOpts: {
    headers: {
      'User-Agent': 'foobar'
    }
  },
})

Error Handling

You can check for status code, and content:

const { get } = funfetch({
  fullResponse: true,
  throwIfBadStatus: (res) => {
    if (res.status === 404) {
      throw new Error('Hello from custom error')
    }
  },
  throwIfError: (res) => {
    if (res.body.match && res.body.match(/iana/)) {
      throw new Error('Hello from body matching error')
    }
  },
})

Debugging

For node:

$ DEBUG=funfetch yarn ts-node examples/debugging.ts

On browsers:

(console) localStorage.debug='funfetch'

And then refresh

const { get } = funfetch({
  fullResponse: true,
  throwIfBadStatus: (res) => {
    if (res.status === 404) {
      throw new Error('Hello from custom error')
    }
  },
  throwIfError: (res) => {
    if (res.body.match && res.body.match(/iana/)) {
      throw new Error('Hello from body matching error')
    }
  },
})

const ok = get('http://echo.jsontest.com/key/value/one/two')
const bad = get('http://github.com/404')
const lookForIana = get('http://example.com')

License

MIT

GitHub

View Github