Making fetch
happen in TypeScript.
Looking for more info? Check out our blog post.
- 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
npm i gretchen --save
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
.
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.
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();
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: "[email protected]",
},
}).json();
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
- anumber
of retries to attempt before failing. Defaults to2
.codes
- anarray
ofnumber
status codes that indicate a retry-able request. Defaults to[ 408, 413, 429 ]
.methods
- anarray
ofstring
s indicating which request methods should be retry-able. Defaults to[ "GET" ]
.delay
- anumber
in milliseconds used to exponentially back-off the delay time between requests. Defaults to6
. 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();
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();
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.
If the response returns a body
and you elect to parse it i.e. .json()
, it
will be populated here.
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 returns401
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
gretchen
also provides the full response
object in case you need it.
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();
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.
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();
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();
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();
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.
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, // number
errors, // array of strings
} = response.error; // typeof Error
} else if (response.data) {
const {
name, // string
occupation, // string
} = response.data; // typeof Success
}
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.
This library was inspired by ky, fetch-retry, and others.
MIT License © Truework