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-Afterheader - small: won't break your bundle
Install
npm i gretchen --saveBrowser 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- anumberof retries to attempt before failing. Defaults to2.codes- anarrayofnumberstatus codes that indicate a retry-able request. Defaults to[ 408, 413, 429 ].methods- anarrayofstrings indicating which request methods should be retry-able. Defaults to[ "GET" ].delay- anumberin 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();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 bodyies returned from non-success (4xx)
responses.
Examples of error usage:
- a
/loginendpoint returns401and includes a message for the user - an endpoint times out and an
HTTPTimeouterror 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, // number
errors, // array of strings
} = response.error; // typeof Error
} else if (response.data) {
const {
name, // string
occupation, // string
} = response.data; // typeof Success
}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
