Skip to content

Commit 0a70007

Browse files
authored
Merge pull request #1 from WJSoftware:JP/Launch
feat: Package release
2 parents 76fe3dc + ed8d49d commit 0a70007

File tree

8 files changed

+2524
-0
lines changed

8 files changed

+2524
-0
lines changed

README.md

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# wj-fetch
2+
3+
This is one more package for fetching. The difference with other packages is that this one does it right.
4+
5+
This package:
6+
7+
+ Uses the modern, standardized `fetch` function.
8+
+ Does **not** throw on non-OK HTTP responses.
9+
+ Allows to fully type all possible HTTP responses depending on the HTTP status code.
10+
11+
## Does a Non-OK Status Code Warrant an Error?
12+
13+
No. Non-OK status codes may communicate things like validation errors, which usually requires that the application
14+
*informs* the user about which piece of data is wrong. Why should this logic be inside a `catch` block? The fact is,
15+
`try..catch` would be there as a replacement of branching (using `if` or `switch`). This is a very smelly code smell.
16+
17+
The second reason is that in most runtimes, unwinding the call stack is costly. Why should we pay a price in
18+
performance just to include the code smell of using `try..catch` as a branching statement? There is no reason to do
19+
such thing.
20+
21+
## Quickstart
22+
23+
1. Install the package.
24+
2. Create your custom fetch function, usually including logic to inject an authorization header/token.
25+
3. Create a fetcher object.
26+
4. Optionally add body parsers.
27+
5. Use the fetcher for every HTTP request needed.
28+
29+
### Installation
30+
31+
```bash
32+
npm i wj-fetch
33+
```
34+
35+
### Create Custom Fetch Function
36+
37+
This is optional and only needed if you need to do something before or after fetching. By far the most common task to
38+
do is to add an authorization header to every call.
39+
40+
```typescript
41+
// myFetch.ts
42+
import { obtainToken } from './magical-auth-stuff.js';
43+
44+
export function myFetch(url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) {
45+
const token = obtainToken();
46+
// Add token to request headers. Not shown because it depends on whether init was given, whether init.headers is
47+
// a POJO or not, etc. TypeScript will guide you through the possibilities.
48+
// Finally, do fetch.
49+
return fetch(url, init);
50+
}
51+
```
52+
53+
Think of this custom function as the place where you do interceptions (if you are familiar with this term from `axios`).
54+
55+
### Create Fetcher Object
56+
57+
```typescript
58+
import { WjFetch } from "wj-fetch";
59+
import { myFetch } from "./myFetch.js";
60+
61+
const fetcher = new WjFetch(myFetch);
62+
// If you don't need a custom fetch function, just do:
63+
const fetcher = new WjFetch();
64+
```
65+
66+
### Adding a Custom Body Parser
67+
68+
One can say that the `WjFetch` class comes with 2 basic body parsers:
69+
70+
1. JSON parser when the the value of the `coontent-type` response header is `application/json` or similar
71+
(`application/problem+json`, for instance).
72+
2. Text parser when the vlaue of the `content-type` response header is `text/<something>`, such as `text/plain` or
73+
`text/csv`.
74+
75+
If your API sends a content type not included in any of the above two cases, use `WjFetch.withParser()` to add a custom
76+
parser for the content type you are expecting. The class allows for fluent syntax, so you can chain calls:
77+
78+
```typescript
79+
const fetcher = new WjFetch(myFetch)
80+
.withParser('custom/contentType', async (response) => {
81+
// Do what you must with the provided response object. In the end, you must return the parsed body.
82+
return finalBody;
83+
});
84+
;
85+
```
86+
87+
> **NOTE**: The content type can also be matched passing a regular expression instead of a string.
88+
89+
Now the fetcher object is ready for use.
90+
91+
### Using the Fetcher Object
92+
93+
This is the fun part where we can enumerate the various shapes of the body depending on the HTTP status code:
94+
95+
```typescript
96+
import type { MyData } from "./my-datatypes.js";
97+
98+
const response = await fetcher
99+
.for<200, MyData[]>()
100+
.for<401, { loginUrl: string; }>()
101+
.fetch('/api/mydata/?active=true')
102+
;
103+
```
104+
105+
The object stored in the `response` variable will contain the following properties:
106+
107+
+ `ok`: Same as `Response.ok`.
108+
+ `status`: Same as `Response.status`.
109+
+ `statusText`: Same as `Response.statusText`.
110+
+ `body`: The HTTP response body, already parsed and typed according to the specification: `MyData[]` if the status
111+
code was `200`, or `{ loginUrl: string; }` if the status code was `401`.
112+
113+
Your editor's Intellisense should be able to properly and accurately tell you all this:
114+
115+
```typescript
116+
if (response.status === 200) { // In this example, doing response.ok in the IF narrows the type just as well.
117+
// Say, display the data somehow/somewhere. In Svelte, we would set a store, perhaps?
118+
myDataStore.set(response.body);
119+
}
120+
else {
121+
// TypeScript/Intellisense will tell you that the only other option is for the status code to be 401:
122+
window.location.href = response.body.loginUrl;
123+
}
124+
```
125+
126+
## Smarter Uses
127+
128+
It is smart to create just one fetcher, configure it, then use it for every fetch call. Because generally speaking,
129+
different URL's will carry a different body type, the fetcher object should be kept free of `for<>()` calls. However,
130+
what if your API is standardized so all status `400` bodies look the same? Then configure that type:
131+
132+
```typescript
133+
// root-fetcher.ts
134+
import { WjFetch } from "wj-fetch";
135+
import { myFetch } from "./my-fetch.js";
136+
import type { BadRequestBody } from "my-datatypes.js";
137+
138+
export default new WjFetch(myFetch)
139+
.withParser(...) // Optional parsers
140+
.withParser(...)
141+
.for<400, BadRequestBody>()
142+
;
143+
```
144+
145+
You can now consume this root fetcher object and it will be pre-typed for the `400` status code.
146+
147+
### Specializing the Root Parser
148+
149+
Ok, nice, but what if we needed a custom parser for just one particular URL? It makes no sense to add it to the root
150+
fetcher, and maybe it is even harmful to do so. In that case, clone the fetcher.
151+
152+
Cloning a fetcher produces a new fetcher with the same data-fetching function, the same body parsers and the same body
153+
typings, **unless** we specify we want something different, like not cloning the body types, or specifying a new
154+
data-fetching function.
155+
156+
```typescript
157+
import rootFecher from "./root-fetcher.js";
158+
159+
function specialFetch(url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) {
160+
...
161+
}
162+
163+
const localFetcher = rootFetcher.clone(true); // Same data-fetching function, body parsers and body typing.
164+
const localFetcher = rootFetcher.clone(false); // Same data-fetching function and body parsers. No body typing.
165+
const localFetcher = rootFetcher.clone(true, { fetchFn: specialFetch }); // Different data-fetching function.
166+
const localFetcher = rootFetcher.clone(true, { includeParsers: false }); // No custom body parsers.
167+
```
168+
169+
> **IMPORTANT**: The first parameter to the `clone` function cannot be a variable. It is just used as a TypeScript
170+
> trick to reset the body typing. The value itself means nothing in runtime because types are not a runtime thing.
171+
172+
## Usage Without TypeScript (JavaScript Projects)
173+
174+
Why are you a weird fellow/gal? Anyway, prejudice aside, body typing will mean nothing to you, so forget about `for()`
175+
and anything else regarding types. Do your custom data-fetching function, add your custom body parsers and that's it.

0 commit comments

Comments
 (0)