DEV Community

Cover image for fetch + interceptors
Joshua Amaju
Joshua Amaju

Posted on • Edited on

fetch + interceptors

Intro

I often reach for HTTP libraries like axios to achieve features like intercepting request and response for things like attaching request token and refreshing authentication token. But we can still achieve the same result staying true to the native fetch API.

Setup

npm i fetch-prime
Enter fullscreen mode Exit fullscreen mode

Code

// features/users.ts
import {pipe} from "fp-ts/function";
import * as E from "fp-ts/Either";

import {chain as andThen} from "fetch-prime";
import {fetch, andThen, Fetch} from "fetch-prime/Fetch";
import * as Response from "fetch–prime/Response";

export function getUser(user: string) {
  return (adapter: Fetch) => {
    const res = await fetch(`/users/${user}`)(adapter);
    const ok = andThen(res.response, Response.filterStatusOk);
    const users = await andThen(ok, res => res.json());
    return users;
  }
}
Enter fullscreen mode Exit fullscreen mode

or a simple option

export function getUser(user: string) {
  return (adapter: Fetch) => {
    const res = await fetch(`/users/${user}`)(adapter);
    const users = await res.ok(res => res.json());
    return users;
  }
}
Enter fullscreen mode Exit fullscreen mode
// main.ts
import {pipe} from "fp-ts/function";

import Fetch from "fetch-prime/Adapters/Platform";
import * as Interceptor from "fetch-prime/Interceptor";
import BaseURL from "fetch-prime/Interceptors/Url";

import {getUsers} from "./features/users";

// ...

const baseURL = "https://myapi.io/api";

const interceptors = Interceptor.add(Interceptor.empty(), BaseURL(baseURL))

const interceptor = Interceptor.make(interceptors);

const adapter = interceptor(Fetch);

const users = await getUser(user_id)(adapter);
Enter fullscreen mode Exit fullscreen mode

Writing your own interceptor

import {pipe} from "fp-ts/function";
import * as Interceptor from "fetch-prime/Interceptor";
import { HttpRequest } from "fetch-prime/Request";

const TokenInterceptor = function (chain: Interceptor.Chain) {
  const { url, init } = chain.request.clone();
  const headers = new Headers(init?.headers);
  headers.set("Authorization", `Token ${token}`);
  return chain.proceed(new HttpRequest(url, { ...init, headers}));
};

// ...

const interceptors = pipe(
  Interceptor.empty(),
  Interceptor.add(BaseURL("https://myapi.io/api")),
  Interceptors.add(TokenInterceptor)
);
Enter fullscreen mode Exit fullscreen mode

You can find fetch-prime here. Please give it a star.

Top comments (11)

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

I'll never understand why people do this so complicated. I just have a function with the same signature as fetch() and that's it.

export function myFetch(url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]): ReturnType<typeof fetch> {
    // Get/refresh token.
    // Add token to request.
    return fetch(url, init);
}
Enter fullscreen mode Exit fullscreen mode

Interceptors for what? That's all that's needed. You're adding fp-ts and fetch-prime packages. I see zero advantage to this.

Collapse
 
joshuaamaju profile image
Joshua Amaju • Edited

I'm not sure you understood my post. Do you use axios? This gives you almost the same feature set as axios but with the familiar fetch API.

My post is for people that care about things like inversion of control, and handling of the error path and not just the happy path.

fp-ts is simply for error handling. Instead of throwing an error when the request fails, you get back a Either<Error, Response>. The either is made up of two channels Left<Error> (if the request never reaches the server) and Right<Response>.

Given the above, fetch-prime adds extra functionality like interceptors and also reflects errors that happen inside your interceptors as Either<Error | AnyInterceptorError, Response>.

Things that your "simple" solution doesn't take into account.

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

My "simple" solution takes everything into account. fetch() doesn't error out like axios does on non-OK HTTP responses, so there's no need for anything fancier. I just need an if or a switch. I also don't need interceptors because the custom function pretty much does pre-work before calling fetch() and post-work before returning the response. So really, what's the point of fp-ts or fetch-prime, other than overcomplicating a simple thing to do? It seems to merely add extra syntax and no real value.

Thread Thread
 
joshuaamaju profile image
Joshua Amaju • Edited

If you only take happy paths into account, sure.

  • Your request will fail (throw an error) in situations where the user doesn't have internet connection or a timeout. So a simple response.ok will not help you there.
  • Your solution doesn't handle errors that happen before or after the actual request.
  • Your solution is not composable

And finally, this is not for you if you don't care about properly handling recoverable errors that happen in your codebase.

You'll have to get into functional programming to really understand why you'll need a library like fp-ts.

Thread Thread
 
webjose profile image
José Pablo Ramírez Vargas

I haven't written any solution. I added comments in a sample block of code. That's it. I haven't given a complete solution of any kind. You cannot say my solution lacks things because I did not give one. I just gave a sample of how it can be done. You do whatever you need to do in there.

As for exceptions, this package of yours is forces logic-by-exception programming, which is a big No No. Why? Because unwinding callstacks is expensive, and because exceptions (errors in JS) should be that: Out-of-the-norm situations.

If I have a function that returns an error or the response, I am force to write code like this:

const eitherOr = await func();
if (eitherOr instanceof whatever) {
}
else {
    // Error.
}
Enter fullscreen mode Exit fullscreen mode

This is terrible.

try {
    const result = await func();
    // Do stuff with  your result.
catch (error) {
    // Do stuff about the error.
}
Enter fullscreen mode Exit fullscreen mode

The difference is night and day. The second block is far clearer, more straightforward top maintain and clearly differentiates the blocks of code that maintains errors vs the business logic. Generally speaking, catching errors deep is bad practice. Ideally, we only have error traps in the user interface. NPM packages, service layers, etc. should never try..catch.

If you want to go the vertical concern route, set up a global error handler, which could be included in your custom fetch() function, BTW.

If you still think that my solution cannot cope with yours. I'll be happy to put it to the test with a concrete example that we both do. Let me know!

Thread Thread
 
joshuaamaju profile image
Joshua Amaju • Edited

I haven't written any solution. I added comments in a sample block of code. That's it. I haven't given a complete solution of any kind. You cannot say my solution lacks things because I did not give one. I just gave a sample of how it can be done.

I had to work with what you gave me.

I'm not sure which one you think is terrible, this

const eitherOr = await func();
if (eitherOr instanceof whatever) {
}
else {
    // Error.
}
Enter fullscreen mode Exit fullscreen mode

or this

try {
    const result = await func();
    // Do stuff with  your result.
catch (error) {
    // Do stuff about the error.
}
Enter fullscreen mode Exit fullscreen mode

Generally speaking, catching errors deep is bad practice. Ideally, we only have error traps in the user interface. NPM packages, service layers, etc. should never try..catch.

The only reason that it's bad is if the library swallows the error, which isn't the case here. The opposite is true, it surfaces errors to you. So there's no guessing about what might go wrong. Think about it like throws Exception in Java.

The difference is night and day. The second block is far clearer, more straightforward top maintain and clearly differentiates the blocks of code that maintains errors vs the business logic

We can both agree on one thing, writing code that focuses on the happy path is easy.

One has to grow to understand that you have to deal with the worst case scenarios first as a frontend dev or else your users are left confused. Which explains why we have shitty web applications these days, developers focusing on the happy (simple) paths only.

If you still think that my solution cannot cope with yours. I'll be happy to put it to the test with a concrete example that we both do. Let me know!

I know that having to deal with the error path first can be cumbersome at first, I felt the same when I was initially introduced to it. But you get used to it with time. I don't like untyped errors, if you're ok with that, no problem

Thread Thread
 
webjose profile image
José Pablo Ramírez Vargas

The only reason that it's bad is if the library swallows the error, which isn't the case here.

Not true. Adding an unnecessary IF in code to check if what you get is an error or not adds to the CPU load microscopically. Add dozens or hundreds of these and you will have a sensible performance decrease. Catching errors to introduce a seemingly harmless syntax change is at best, a performance hit, and this is what that package seems to do with its Either type. I would understand if you catch the error because the NPM package CAN do something useful about at least some errors, but to change the syntax only? I would never.

Thread Thread
 
joshuaamaju profile image
Joshua Amaju

Adding an unnecessary IF in code to check if what you get is an error or not adds to the CPU load microscopically

But the try/catch wouldn't, you can't be serious about that, right? it's not much of a change. it's not like this is a new idea, checkout rust, ocaml etc.

Thread Thread
 
webjose profile image
José Pablo Ramírez Vargas

I am serious. Every time you use that package, you set up a try..catch + the syntax change, which forces you to write an extra IF statement.

Don't do the syntax change and place a global error handler. 1 try..catch vs 1 try..catch everytime you use this package.

Thread Thread
 
joshuaamaju profile image
Joshua Amaju • Edited

There's a misunderstanding here. Either isn't a try/catch wrapper, it doesn't do that each time you use the Either interface. Here's the structure of an Either: { _tag: "Left", left: your value } or { _tag: "Right", right: your value }. It's just an object, It doesn't do try/catch.

It's up to you to then do the try/catch and then return the error or success value. So fetch-prime does the try/catch for you at the global level

// fetch-prime library

const fetch = async (url, init) => {
  try {
  // the actual fetch call
    const res = await fetch(url, init));
    return right(res);
  } catch (error) {
    return left(new HttpError(error));
  }
};
Enter fullscreen mode Exit fullscreen mode

So this is similar to your solution but instead of throwing error, you get back a data structure that holds the error and response, plus interceptors.

Thread Thread
 
webjose profile image
José Pablo Ramírez Vargas

Ah! Yes! Total misunderstanding. I thought internally the package would try..catch and then return the object built of the response or the caught error. Ok, good to know it isn't like this.