Olasunkanmi John Ajiboye TypeScript and Rust enthusiast. Writes code for humans. From the land of Promise.

Type-safe fetching with gretchen

3 min read 1060

Type-safe Fetching With Gretchen

Almost every real-world application relies on data fetching and some sort of asynchronous interaction with an external API. Just like any programming problem, there are quite a number of ways to solve this; some are simple, others are complex. One common theme, however, is that the solutions usually have one or more features missing, like interpolation, re-fetch, etc.

In the browser environment, we are provided with the fetch API, which is simple and relatively flexible. However, it comes with a problem: it assumes all non-success (≥4xx) exceptions and throws them. Hence we are limited to catching these exceptions with a Promise.catch or the try/catch in an async/await pattern.

The issue with this is that those thrown exceptions are notoriously difficult to type since they could be of any shape. In retrospect, we know that we usually have an idea of the shape of our errors if we are allowed to deal with them. Here comes gretchen.

gretchen offers a sound parity with fetch out of the box, which makes it easily adaptable in legacy codebases. It offers resilience to common request/response issues like errors and inconsistent responses returned from APIs. This is handled via built-in timeout handling, configurable retry logic, and error normalization. It also offers good browser support as far back as Internet Explorer 11.

Getting started

Installation

You can install via either yarn or npm

npm install gretchen

OR

yarn add gretchen

Usage

With fetch, you would have something like:

const request = await fetch("/api/dogs/1");
const dog = await request.json();

Similarly, with gretchen, you’d have:

import { gretch } from "gretchen";
const { data: dog1} = await gretch("/api/dog/1").json();

You will notice this provides just enough abstraction, baking in the ease of use of fetch without sacrificing flexibility. One other important difference to note is how you can resolve to JSON within one statement.

Fetching with gretchen

gretchen really begins to shine and differentiate itself from native fetch in error handling. The most common option is to return a typed entity upon which you can then make assertions. This will give TypeScript an opportunity to understand what we are dealing with.

try {
  const cats = await getAllCats();
 return  cats
} catch (e) {
  if (error instanceof ErrorType) {
    // handle Error
  }
}

The problem with this is that you have to manually check whether it satisfies the condition. Admittedly, you could write a middleware to perform this check, but that is an unnecessary additional step and adds boilerplate. The ideal solution would be to not have to perform this check at all, and to do that elegantly, with less code.

Another option would be to not throw any exceptions. fetch works this way by default. So we are able to configure our response based on whether we get the required data — that is, for success and error responses, we can abstract away the assertion and handle the response elegantly. This is similar to the RESULT type in Rust.

enum Result<T, E> {
   Ok(T), // success data
   Err(E), // error
}

We could then have something like this for handling responses:

if (response.status < 300) {return Result.Ok(response);
} else {
return Result.Err(response);
}

While this pattern might be appealing, it introduces a layer of complexity that might not be intuitive for newcomers. gretchen tries to simplify this by providing a much clearer abstraction in the absence of exceptions.

Instead of returning an object with methods for checking the type of the discriminated union, gretchen has employed a pattern similar to the error handling found in Go. Both halves of the union are returned as separate entities:

const { error, data } = await getEndpoint();

if (error) {
  // handle error
} else {
  // handle data
}

GET requests

const response = await fetch("/user/12345");

const { ok, status } = response;

You can then handle the response if error is not ok or handle a success response.

Non-GET requests

Non-GET requests are quite similar to GET requests, but just like fetch, you will need to pass in the method option to fetch, like so:

const { status, error, data: user } = await gretch("/note/create", {
  method: "POST",
  json: {
    title: "Take over",
 text: "We are the world"
  }
}).json();

if (error) {
  // handle error
} else {
  // handle user
}

Internally, gretchen already resolved most of the boilerplate you’d encounter with fetch, like awaiting response and then converting to JSON like arrayBuffer, blob, formData, json, and text.



Other niceties

Automatically retrying some failed requests

By default, gretchen will retry GET requests that return 408, 413, or 429 two times. Failure can occur due to network issues or server load. This gives you the flexibility to retry these requests, and you can, of course, override the default number of retries like so:

await gretch("/notes", {
  method: "POST",
  retry: {
    attempts: 9, // number of retry
    methods: ["POST"]
  }, 
  json: {
        title: "Take over",
       text: "We are the world"
  }
}).json();

Handling request timeouts

This is similar to retrying a request. A request is timed out by default after 10 seconds. You can optionally configure the timeout time.

gretchen with TypeScript

You can specify the type of both your error and response. This is straightforward with the generic interface that gretchen allows.

The typing in the gretchen source code looks something like this:

gretch<T = DefaultGretchResponse, A = DefaultGretchError>

You will notice that a default response and error type are included, hence, you can pass your error and data response type in the same manner. For instance:

interface Note {
id: number
author: string
title: string
text: string
}
interface CustomError {
  message: string
code: number
};

const { error, data } = await gretch<Note, CustomeError>(
  "/note/1"
).json();

Tying this together, you should have a robust response-error handling that looks like this:

interface Note {
id: number
author: string
title: string
text: string
}
interface CustomError {
  message: string
code: number
};

const { error, data } = await gretch<Note, CustomeError>(
  "/note/1"
).json();

error ? handleError() : handleSuccess()

In conclusion

But why gretchen, I hear you ask? We already have similar abstractions that do similar jobs. That is not entirely true. gretchen goes a few steps further by allowing type-safe fetching by providing a very subtle abstraction over the already popular fetch syntax that you already know and love.

Additionally, gretchen makes it easy to elegantly type both the expected data and any errors that might occur. This might not seem important at first, but as your application grows in complexity, the ability to safely type your API response goes a long way in providing clarity to any client consuming them.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

Are you adding new JS libraries to improve performance or build new features? What if they’re doing the opposite?

There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.

LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.

https://logrocket.com/signup/

LogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Build confidently — .

.
Olasunkanmi John Ajiboye TypeScript and Rust enthusiast. Writes code for humans. From the land of Promise.

Leave a Reply