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
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;
}
}
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;
}
}
// 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);
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)
);
You can find fetch-prime
here. Please give it a star.
Top comments (11)
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.Interceptors for what? That's all that's needed. You're adding
fp-ts
andfetch-prime
packages. I see zero advantage to this.I'm not sure you understood my post. Do you use
axios
? This gives you almost the same feature set asaxios
but with the familiarfetch
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 aEither<Error, Response>
. The either is made up of two channelsLeft<Error>
(if the request never reaches the server) andRight<Response>
.Given the above,
fetch-prime
adds extra functionality like interceptors and also reflects errors that happen inside your interceptors asEither<Error | AnyInterceptorError, Response>
.Things that your "simple" solution doesn't take into account.
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 anif
or aswitch
. I also don't need interceptors because the custom function pretty much does pre-work before callingfetch()
and post-work before returning the response. So really, what's the point offp-ts
orfetch-prime
, other than overcomplicating a simple thing to do? It seems to merely add extra syntax and no real value.If you only take happy paths into account, sure.
response.ok
will not help you there.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.
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:
This is terrible.
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!
I had to work with what you gave me.
I'm not sure which one you think is terrible, this
or this
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.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.
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
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.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.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.
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 anEither
: { _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 levelSo 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.
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.