
I Built a Fetching Library That I Kinda' Like
Ethan Glover
I believe it's a good idea to centralize API calls for a frontend. I don't believe there are a lot of good ways to do this. If you Google the issue, what you end up with is advice to write `get()`, `post()`, `put()` functions that don't do much more than set an HTTP method. Maybe they'll use a BASE_URL constant and, at best, spread HTTP request options into fetch. It always seems like a good idea until you're looking at it a week later wondering what the point is. I could write `fetch()` in just as many keystrokes.
While taking a Udemy course I was watching the instructor type out one of these HTTP method abstractions and started to write my own. What I've done traditionally is create an object that maps a bunch of functions which all return Request objects. Like this:
This makes it very easy to define a Request, call it later with fetch, and keep a level of customization when it's called.
I think this works very well. You can define endpoints, add default request options, and override them when they're called. It works especially well when making use of the browsers Cache API because that cache uses Request objects as keys.
But then I found myself using URL and URLSearchParams objects quite a bit. Typing out raw strings makes me nervous. Both of these objects provide a decent level of validation, and make things just a little bit easier than template strings. An example use would be this:
This is a pretty powerful set of tools. So yesterday, while procrastinating on this Udemy course with it's API I don't like, I started building a library that could take care of a lot of this for me. There's a lot I want to abstract here. searchParams.append() doesn't accept undefined values, it will always stringify. The library needs to ignore those so my code doesn't have to. I still want to make use of Cache API. I need to account for dynamic paths which can be set at any time. Whether on the global api object, or in the fetch itself. This is what I've come up with so far:
Now, I know what it looks like, it looks like I've just abtracted the HTTP method and threw in a Zod schema. But did you notice you don't have to worry about whether there's a slash on the end of the baseUrl? Pretty cool right? But the flexibility comes in the fetch.
Totally type safe too! The returned data object will match to the output type of the the Zod schema given to the request. When you call `api.fetch('`, your IDE will give you hints with available strings matching to your methods.
Just about everything can be overridden when you get to a lower level. For example, what if as a default you want all API requests to have a 60 second cache, but for one in particular you want it to have a 10 second cache, except in one spot where you want it to have no cache? No worries.
Of course, it's worth noting that because this depends on Cache API, it's subject to Cache API rules. There is a limited amount of storage available, it's a lot. And you do need to be careful about garbage collection. Because this is a permanent storage on the users machine that can take up to 80% of their hard disk space. Typically, sticking to in memory data is perfectly fine. I'm far more likely to just not use the caching option and depend on React Query instead.
Regardless, I find this to be the most helpful way to centralize an API. There is a lot to work on still. What if I don't want response.json(). That was one of the reasons my original method returned Request objects to begin with. I may restructure and rewrite this entire thing. But so far, it feels comfortable and hits on every note I wanted it to. And I kinda' like that.
1const BASE_URL = 'https://jsonplaceholder.typicode.com'
2
3const api = {
4 getTodos(id: string) {
5 return new Request(`${BASE_URL}/todos/${id}`);
6 }
7}
1const response = await fetch(api.getTodos(1));
2
3const data = await response.json();
1const BASE_URL = 'https://jsonplaceholder.typicode.com'
2
3const url = new URL('todos/1', BASE_URL);
4url.searchParams.append('orderBy', 'name');
5console.log(url.toString()); // https://jsonplaceholder.typicode.com/todos/1?orderBy=name
6
7const request = new Request(url, { ...fetchOptions });
8await fetch(request)
1import { Api } from "@ethang/fetch/api";
2
3export const api = new Api({
4 baseUrl: 'https://jsonplaceholder.typicode.com',
5 requests: {
6 getTodos: {
7 path: 'todos',
8 zodSchema: todosSchema,
9 },
10 getTodo: {
11 path: 'todos/:id',
12 zodSchema: todoSchema,
13 },
14 createTodo: {
15 path: 'todos',
16 requestOptions: {
17 method: 'POST'
18 },
19 zodSchema: todoSchema,
20 },
21 updateTodo: {
22 path: 'todos/:id',
23 requestOptions: {
24 method: 'PUT'
25 },
26 zodSchema: todoSchema,
27 },
28 deleteTodo: {
29 path: 'todos/:id',
30 requestOptions: {
31 method: 'DELETE'
32 },
33 zodSchema: todoSchema,
34 },
35 },
36});
1const { data, errors, isSuccess } = await api.fetch('getTodos', {
2 searchParams: {
3 filterBy: 'name'
4 orderBy: 'date'
5 }
6}
7// https://jsonplaceholder.typicode.com/todos?filterBy=name&orderBy=date
1const { data, errors, isSuccess } = await api.fetch('getTodo', {
2 pathVariables: {
3 id: 1
4 }
5}
6// https://jsonplaceholder.typicode.com/todos/1
1import { Api } from "@ethang/fetch/api";
2
3export const api = new Api({
4 baseUrl: 'https://jsonplaceholder.typicode.com',
5 cacheInterval: 30
6 requests: {
7 getTodos: {
8 path: 'todos',
9 cacheInterval: 10
10 zodSchema: todosSchema,
11 },
12 },
13});
14
15api.fetch('getTodos', {)
16 cacheInterval: 0
17}