|
| 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