gretchen
Making fetch
happen in TypeScript.
Looking for more info? Check out our blog post.
Features
- safe: will not throw on non-200 responses
- precise: allows for typing of both success & error responses
- resilient: configurable retries & timeout
- smart: respects
Retry-After
header - small: won't break your bundle
Install
npm i gretchen --save
Browser support
gretchen
targets all modern browsers. For IE11 support, you'll need to polyfill
fetch
, Promise
, and Object.assign
. For Node.js, you'll need fetch
and
AbortController
.
Quick links
Usage
With fetch
, you might do something like this:
const request = await fetch('/api/user/12')
const user = await request.json()
With gretchen
, it's very similar:
import { gretch } from 'gretchen'
const { data: user } = await gretch('/api/user/12').json()
👉 gretchen
aims to provide just enough abstraction to provide ease of use
without sacrificing flexibility.
Making a request
Using gretchen
is very similar to using fetch
. It too defaults to GET
, and
sets the credentials
header to same-origin
.
const request = gretch('/api/user/12')
To parse a response body, simply call any of the standard fetch
body interface
methods:
const response = await request.json()
The slight diversion from native fetch
here is to allow users to do this in
one shot:
const response = await gretch('/api/user/12').json()
In addition to the body interface methods you're familiar with, there's also a
flush()
method. This resolves the request without parsing the body (or
errors), which results in slight performance gains. This method returns a
slightly different response object, see below for more details.
const response = await gretch('/api/user/authenticated').flush()
Options
To make different types of requests or edit headers and other request config,
pass a options object:
const response = await gretch('/api/user/12', {
credentials: 'include',
headers: {
'Tracking-ID': 'abcde12345'
}
}).json()
Configuring requests bodies should look familiar as well:
const response = await gretch('/api/user/12', {
method: 'PATCH',
body: JSON.stringify({
name: 'Megan Rapinoe',
occupation: 'President of the United States'
})
}).json()
For convenience, there’s also a json
shorthand. We’ll take care of
stringifying the body and applying the Content-Type
header:
const response = await gretch('/api/user/12', {
method: 'PATCH',
json: {
email: 'm.rapinoe@gmail.com'
}
}).json()
Retrying requests
gretchen
will automatically attempt to retry some types of requests if they
return certain error codes. Below are the configurable options and their
defaults:
attempts
- a number
of retries to attempt before failing. Defaults to 2
.codes
- an array
of number
status codes that indicate a retry-able
request. Defaults to [ 408, 413, 429 ]
.methods
- an array
of string
s indicating which request methods should be
retry-able. Defaults to [ "GET" ]
.delay
- a number
in milliseconds used to exponentially back-off the delay
time between requests. Defaults to 6
. Example: first delay is 6ms, second
36ms, third 216ms, and so on.
These options can be set using the configuration object:
const response = await gretch('/api/user/12', {
retry: {
attempts: 3
}
}).json()
Timeouts
By default, gretchen
will time out requests after 10 seconds and retry them, unless otherwise configured. To configure timeout, pass a value in milliseconds:
const response = await gretch('/api/user/12', {
timeout: 20000
}).json()
Response handling
gretchen
's thin abstraction layer returns a specialized structure from a
request. In TypeScript terms, it employs a discriminated union for ease of
typing. More on that later.
const { url, status, error, data, response } = await gretch(
'/api/user/12'
).json()
url
and status
here are what they say they are: properties of the Response
returned from the request.
data
If the response returns a body
and you elect to parse it i.e. .json()
, it
will be populated here.
error
And instead of throwing errors gretchen
will populate the error
prop with
any errors that occur or body
ies returned from non-success (4xx
)
responses.
Examples of error
usage:
- a
/login
endpoint returns 401
and includes a message for the user - an endpoint times out and an
HTTPTimeout
error is returned - an unknown network error occurs during the request
response
gretchen
also provides the full response
object in case you need it.
Usage with flush
As mentioned above, gretchen
also provides a flush()
method to resolve a
request without parsing the body or errors. This results in a slightly different
response object.
const { url, status, response } = await gretch(
'/api/user/authenticated'
).flush()
Hooks
gretchen
uses the concept of "hooks" to tap into the request lifecycle. Hooks
are good for code that needs to run on every request, like adding tracking
headers and logging errors.
Hooks should be defined as an array. That way you can compose multiple hooks
per-request, and define and merge default hooks when creating
instances.
before
The before
hook runs just prior to the request being made. You can even modify
the request directly, like to add headers. The before
hook is passed the Request
object, and the full options object.
const response = await gretch('/api/user/12', {
hooks: {
before: [
(request, options) => {
request.headers.set('Tracking-ID', 'abcde')
}
]
}
}).json()
after
The after
runs after the request has resolved and any body interface methods
have been called. It has the opportunity to read the gretchen
response. It
cannot modify it. This is mostly useful for logging.
const response = await gretch('/api/user/12', {
hooks: {
after: [
({ url, status, data, error }, options) => {
sentry.captureMessage(`${url} returned ${status}`)
}
]
}
}).json()
Creating instances
gretchen
also exports a create
method that allows you to configure default
options. This is useful if you want to attach something like logging to every
request made with the returned instance.
import { create } from 'gretchen'
const gretch = create({
headers: {
'X-Powered-By': 'gretchen'
},
hooks: {
after ({ error }) {
if (error) sentry.captureException(error)
}
}
})
await gretch('/api/user/12').json()
Base URLs
Another common use case for creating a separate instance is to specify a
baseURL
for all requests. The baseURL
will then be resolved against the base
URL of the page, allowing support for both absolute and relative baseURL
values.
In the example below, assume requests are being made from a page located at
https://www.mysite.com
.
Functionally, this:
const gretch = create({
baseURL: 'https://www.mysite.com/api'
})
Is equivalent to this:
const gretch = create({
baseURL: '/api'
})
So this request:
await gretch('/user/12').json()
Will resolve to https://www.mysite.com/api/user/12
.
Note: if a baseURL
is specified, URLs will be normalized in order to
concatenate them i.e. a leading slash – /user/12
vs user/12
– will not
impact how the request is resolved.
Usage with TypeScript
gretchen
is written in TypeScript and employs a discriminated union to allow
you to type and consume both the success and error responses returned by your
API.
To do so, pass your data types directly to the gretch
call:
type Success = {
name: string
occupation: string
}
type Error = {
code: number
errors: string[]
}
const response = await gretch<Success, Error>('/api/user/12').json()
Then, you can safely use the responses:
if (response.error) {
const {
code,
errors
} = response.error
} else if (response.data) {
const {
name,
occupation
} = response.data
}
Why?
There are a lot of options out there for requesting data. But most modern
fetch
implementations rely on throwing errors. For type-safety, we wanted
something that would allow us to type the response, no matter what. We also
wanted to bake in a few opinions of our own, although the API is flexible enough
for most other applications.
Credits
This library was inspired by ky, fetch-retry, and others.
License
MIT License © Truework