fch 2.0.1 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fetch.js +129 -100
- package/package.json +9 -7
- package/readme.md +252 -103
package/fetch.js
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
|
-
import swear from "swear";
|
|
2
|
-
|
|
3
|
-
// To avoid making parallel requests to the same url if one is ongoing
|
|
4
|
-
const ongoing = new Map();
|
|
5
|
-
|
|
6
1
|
// Plain-ish object
|
|
7
2
|
const hasPlainBody = (options) => {
|
|
8
3
|
if (options.headers["content-type"]) return;
|
|
9
|
-
if (typeof options.body
|
|
4
|
+
if (!["object", "array"].includes(typeof options.body)) return;
|
|
10
5
|
if (options.body instanceof FormData) return;
|
|
11
6
|
return true;
|
|
12
7
|
};
|
|
13
8
|
|
|
14
|
-
const createUrl = (
|
|
9
|
+
const createUrl = (url, query, base) => {
|
|
10
|
+
let [path, urlQuery = {}] = url.split("?");
|
|
11
|
+
|
|
12
|
+
// Merge global params with passed params with url params
|
|
13
|
+
const entries = new URLSearchParams({
|
|
14
|
+
...Object.fromEntries(new URLSearchParams(query)),
|
|
15
|
+
...Object.fromEntries(new URLSearchParams(urlQuery)),
|
|
16
|
+
}).toString();
|
|
17
|
+
if (entries) {
|
|
18
|
+
path = path + "?" + entries;
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
if (!base) return path;
|
|
16
|
-
const
|
|
17
|
-
return
|
|
22
|
+
const fullUrl = new URL(path, base);
|
|
23
|
+
return fullUrl.href;
|
|
18
24
|
};
|
|
19
25
|
|
|
20
26
|
const createHeaders = (user, base) => {
|
|
@@ -31,6 +37,15 @@ const createHeaders = (user, base) => {
|
|
|
31
37
|
return headers;
|
|
32
38
|
};
|
|
33
39
|
|
|
40
|
+
const createDedupe = (ongoing, url) => ({
|
|
41
|
+
save: (prom) => {
|
|
42
|
+
ongoing.set(url, prom);
|
|
43
|
+
return prom;
|
|
44
|
+
},
|
|
45
|
+
get: () => ongoing.get(url),
|
|
46
|
+
clear: () => ongoing.delete(url),
|
|
47
|
+
});
|
|
48
|
+
|
|
34
49
|
const createFetch = (request, { after, dedupe, error, output }) => {
|
|
35
50
|
return fetch(request.url, request).then(async (res) => {
|
|
36
51
|
// No longer ongoing at this point
|
|
@@ -70,102 +85,116 @@ const createFetch = (request, { after, dedupe, error, output }) => {
|
|
|
70
85
|
});
|
|
71
86
|
};
|
|
72
87
|
|
|
73
|
-
const
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
88
|
+
const create = (defaults = {}) => {
|
|
89
|
+
// DEDUPLICATION is created on a per-instance basis
|
|
90
|
+
// To avoid making parallel requests to the same url if one is ongoing
|
|
91
|
+
const ongoing = new Map();
|
|
92
|
+
|
|
93
|
+
const fch = async (url, options = {}) => {
|
|
94
|
+
// Second parameter always has to be an object, even when it defaults
|
|
95
|
+
if (typeof options !== "object") options = {};
|
|
96
|
+
|
|
97
|
+
// Accept either fch(options) or fch(url, options)
|
|
98
|
+
options = typeof url === "string" ? { url, ...options } : url || {};
|
|
99
|
+
|
|
100
|
+
// Exctract the options
|
|
101
|
+
let {
|
|
102
|
+
dedupe = fch.dedupe,
|
|
103
|
+
output = fch.output,
|
|
104
|
+
baseURL = fch.baseURL, // DO NOT USE; it's here only for user friendliness
|
|
105
|
+
baseUrl = baseURL || fch.baseUrl,
|
|
106
|
+
|
|
107
|
+
// Extract it since it should not be part of fetch()
|
|
108
|
+
query = {},
|
|
109
|
+
|
|
110
|
+
// Interceptors can also be passed as parameters
|
|
111
|
+
before = fch.before,
|
|
112
|
+
after = fch.after,
|
|
113
|
+
error = fch.error,
|
|
114
|
+
|
|
115
|
+
...request
|
|
116
|
+
} = options; // Local option OR global value (including defaults)
|
|
117
|
+
|
|
118
|
+
// Merge it, first the global and then the local
|
|
119
|
+
query = { ...fch.query, ...query };
|
|
120
|
+
// Absolute URL if possible; Default method; merge the default headers
|
|
121
|
+
request.url = createUrl(request.url ?? fch.url, query, baseUrl);
|
|
122
|
+
request.method = (request.method ?? fch.method).toLowerCase();
|
|
123
|
+
request.headers = createHeaders(request.headers, fch.headers);
|
|
124
|
+
|
|
125
|
+
if (request.method !== "get") {
|
|
126
|
+
dedupe = false;
|
|
127
|
+
}
|
|
128
|
+
if (dedupe) {
|
|
129
|
+
dedupe = createDedupe(ongoing, request.url);
|
|
130
|
+
}
|
|
109
131
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
132
|
+
if (!["body", "response"].includes(output)) {
|
|
133
|
+
const msg = `options.output needs to be either "body" (default) or "response", not "${output}"`;
|
|
134
|
+
throw new Error(msg);
|
|
135
|
+
}
|
|
114
136
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
137
|
+
// JSON-encode plain objects
|
|
138
|
+
if (hasPlainBody(request)) {
|
|
139
|
+
request.body = JSON.stringify(request.body);
|
|
140
|
+
request.headers["content-type"] = "application/json; charset=utf-8";
|
|
141
|
+
}
|
|
120
142
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
143
|
+
// Hijack the requeset and modify it
|
|
144
|
+
if (before) {
|
|
145
|
+
request = before(request);
|
|
146
|
+
}
|
|
125
147
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
148
|
+
// It should be cached and it's not being manually manipulated
|
|
149
|
+
if (dedupe && !request.signal) {
|
|
150
|
+
// It's already cached! Just return it
|
|
151
|
+
if (dedupe.get()) return dedupe.get();
|
|
130
152
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
});
|
|
153
|
+
// Otherwise, save it in the cache and return the promise
|
|
154
|
+
return dedupe.save(
|
|
155
|
+
createFetch(request, { dedupe, output, error, after })
|
|
156
|
+
);
|
|
157
|
+
} else {
|
|
158
|
+
// PUT, POST, etc should never dedupe and just return the plain request
|
|
159
|
+
return createFetch(request, { output, error, after });
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Default values
|
|
164
|
+
fch.url = defaults.url ?? "/";
|
|
165
|
+
fch.method = defaults.method ?? "get";
|
|
166
|
+
fch.query = defaults.query ?? {};
|
|
167
|
+
fch.headers = defaults.headers ?? {};
|
|
168
|
+
|
|
169
|
+
// Default options
|
|
170
|
+
fch.dedupe = defaults.dedupe ?? true;
|
|
171
|
+
fch.output = defaults.output ?? "body";
|
|
172
|
+
fch.credentials = defaults.credentials ?? "include";
|
|
173
|
+
|
|
174
|
+
// Interceptors
|
|
175
|
+
fch.before = defaults.before ?? ((request) => request);
|
|
176
|
+
fch.after = defaults.after ?? ((response) => response);
|
|
177
|
+
fch.error = defaults.error ?? ((error) => Promise.reject(error));
|
|
178
|
+
|
|
179
|
+
const get = (url, opts = {}) => fch(url, { ...opts });
|
|
180
|
+
const head = (url, opts = {}) => fch(url, { ...opts, method: "head" });
|
|
181
|
+
const post = (url, opts = {}) => fch(url, { ...opts, method: "post" });
|
|
182
|
+
const patch = (url, opts = {}) => fch(url, { ...opts, method: "patch" });
|
|
183
|
+
const put = (url, opts = {}) => fch(url, { ...opts, method: "put" });
|
|
184
|
+
const del = (url, opts = {}) => fch(url, { ...opts, method: "delete" });
|
|
185
|
+
|
|
186
|
+
fch.get = get;
|
|
187
|
+
fch.head = head;
|
|
188
|
+
fch.post = post;
|
|
189
|
+
fch.patch = patch;
|
|
190
|
+
fch.put = put;
|
|
191
|
+
fch.del = del;
|
|
192
|
+
|
|
193
|
+
fch.create = create;
|
|
194
|
+
|
|
195
|
+
return fch;
|
|
196
|
+
};
|
|
138
197
|
|
|
139
|
-
|
|
140
|
-
fch.method = "get";
|
|
141
|
-
fch.headers = {};
|
|
142
|
-
|
|
143
|
-
// Default options
|
|
144
|
-
fch.dedupe = true;
|
|
145
|
-
fch.output = "body";
|
|
146
|
-
|
|
147
|
-
// Interceptors
|
|
148
|
-
fch.before = (request) => request;
|
|
149
|
-
fch.after = (response) => response;
|
|
150
|
-
fch.error = (error) => Promise.reject(error);
|
|
151
|
-
|
|
152
|
-
const request = (url, opts = {}) => fch(url, { ...opts });
|
|
153
|
-
const get = (url, opts = {}) => fch(url, { ...opts });
|
|
154
|
-
const head = (url, opts = {}) => fch(url, { ...opts, method: "head" });
|
|
155
|
-
const post = (url, opts = {}) => fch(url, { ...opts, method: "post" });
|
|
156
|
-
const patch = (url, opts = {}) => fch(url, { ...opts, method: "patch" });
|
|
157
|
-
const put = (url, opts = {}) => fch(url, { ...opts, method: "put" });
|
|
158
|
-
const del = (url, opts = {}) => fch(url, { ...opts, method: "delete" });
|
|
159
|
-
|
|
160
|
-
fch.request = request;
|
|
161
|
-
fch.get = get;
|
|
162
|
-
fch.head = head;
|
|
163
|
-
fch.post = post;
|
|
164
|
-
fch.patch = patch;
|
|
165
|
-
fch.put = put;
|
|
166
|
-
fch.del = del;
|
|
167
|
-
|
|
168
|
-
fch.swear = swear;
|
|
198
|
+
const fch = create();
|
|
169
199
|
|
|
170
200
|
export default fch;
|
|
171
|
-
export { request, get, head, post, patch, put, del, swear };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fch",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "Fetch interface with better promises, deduplication, defaults, etc.",
|
|
5
5
|
"homepage": "https://github.com/franciscop/fetch",
|
|
6
6
|
"repository": "https://github.com/franciscop/fetch.git",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"scripts": {
|
|
12
|
+
"build": "terser --compress --mangle -- ./fetch.js | sed 's/export default fch;/window.fch=fch;/g' > ./index.js",
|
|
12
13
|
"start": "npm run watch # Start ~= Start dev",
|
|
13
14
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --detectOpenHandles",
|
|
14
15
|
"watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --coverage --detectOpenHandles"
|
|
@@ -21,18 +22,19 @@
|
|
|
21
22
|
"async",
|
|
22
23
|
"ajax"
|
|
23
24
|
],
|
|
24
|
-
"main": "fetch.js",
|
|
25
|
-
"
|
|
25
|
+
"main": "./fetch.js",
|
|
26
|
+
"browser": "./index.js",
|
|
27
|
+
"files": [
|
|
28
|
+
"fetch.js"
|
|
29
|
+
],
|
|
26
30
|
"type": "module",
|
|
27
31
|
"engines": {
|
|
28
32
|
"node": ">=18.0.0"
|
|
29
33
|
},
|
|
30
|
-
"dependencies": {
|
|
31
|
-
"swear": "^1.1.0"
|
|
32
|
-
},
|
|
33
34
|
"devDependencies": {
|
|
34
35
|
"jest": "^28.0.1",
|
|
35
|
-
"jest-fetch-mock": "^3.0.3"
|
|
36
|
+
"jest-fetch-mock": "^3.0.3",
|
|
37
|
+
"terser": "^5.13.1"
|
|
36
38
|
},
|
|
37
39
|
"jest": {
|
|
38
40
|
"testEnvironment": "jest-environment-node",
|
package/readme.md
CHANGED
|
@@ -1,45 +1,49 @@
|
|
|
1
|
-
# Fch [](https://www.npmjs.com/package/fch) [](https://www.npmjs.com/package/fch) [](https://github.com/franciscop/fetch/blob/master/index.js)
|
|
2
2
|
|
|
3
|
-
A library to make API calls easier. Similar to Axios, but tiny size and simpler API:
|
|
3
|
+
A tiny library to make API calls easier. Similar to Axios, but tiny size and simpler API:
|
|
4
4
|
|
|
5
5
|
```js
|
|
6
6
|
import api from "fch";
|
|
7
|
-
|
|
7
|
+
api.baseUrl = "https://pokeapi.co/";
|
|
8
|
+
const mew = await api.get("/pokemon/150");
|
|
8
9
|
console.log(mew);
|
|
9
10
|
```
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- Automatically
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
- Await/Async Promise interface works as you know and love.
|
|
17
|
-
- Better error handling. `>= 400 and <= 100` will _reject_ the promise with an error instance.
|
|
18
|
-
- Advanced [Promise interface](https://www.npmjs.com/swear) for better scripting.
|
|
19
|
-
- Easily define shared options straight on the root `fetch.baseUrl = "https://...";`.
|
|
20
|
-
- Interceptors: `before` (the request), `after` (the response) and `error` (it fails).
|
|
12
|
+
- Create instances with shared options across requests.
|
|
13
|
+
- Automatically encode object and array bodies as JSON.
|
|
14
|
+
- Automatically decode JSON responses based on the headers.
|
|
15
|
+
- Await/Async Promises; `>= 400 and <= 100` will _reject_ with an error.
|
|
16
|
+
- Interceptors: `before` the request, `after` the response and catch with `error`.
|
|
21
17
|
- Deduplicates parallel GET requests.
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
18
|
+
- Works the same way in Node.js and the browser.
|
|
19
|
+
- No dependencies; include it with a simple `<script>` on the browser.
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
import api from 'fch';
|
|
23
|
+
|
|
24
|
+
api.get(url, { headers, ...options })
|
|
25
|
+
api.head(url, { headers, ...options })
|
|
26
|
+
api.post(url, { body, headers, ...options })
|
|
27
|
+
api.patch(url, { body, headers, ...options })
|
|
28
|
+
api.put(url, { body, headers, ...options })
|
|
29
|
+
api.del(url, { body, headers, ...options })
|
|
30
|
+
|
|
31
|
+
api.create({ url, body, headers, ...options})
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
| Options | Default | Description |
|
|
35
|
+
| ----------------------| ----------| -----------------------------------|
|
|
36
|
+
| [`method`](#method) | `"get"` | Default method to use for the call |
|
|
37
|
+
| [`url`](#url) | `null` | The path or url for the request |
|
|
38
|
+
| [`baseUrl`](#url) | `null` | The shared base of the API |
|
|
39
|
+
| [`body`](#body) | `null` | The body to send with the request |
|
|
40
|
+
| [`query`](#query) | `{}` | Add query parameters to the URL |
|
|
41
|
+
| [`headers`](#headers) | `{}` | Shared headers across all requests |
|
|
42
|
+
| [`output`](#output) | `"body"` | The return value of the API call |
|
|
43
|
+
| [`dedupe`](#dedupe) | `true` | Reuse concurrently GET requests |
|
|
44
|
+
| [`before`](#interceptors) |`req => req` |Process the request before sending it |
|
|
45
|
+
| [`after`](#interceptors) |`res => res` |Process the response before returning it |
|
|
46
|
+
| [`error`](#interceptors) |`err => throw err` |Process errors before returning them |
|
|
43
47
|
|
|
44
48
|
## Getting Started
|
|
45
49
|
|
|
@@ -52,58 +56,94 @@ npm install fch
|
|
|
52
56
|
Then import it to be able to use it in your code:
|
|
53
57
|
|
|
54
58
|
```js
|
|
55
|
-
import
|
|
59
|
+
import fch from 'fch';
|
|
60
|
+
const body = await fch.get('/');
|
|
61
|
+
```
|
|
56
62
|
|
|
57
|
-
|
|
63
|
+
On the browser you can add it with a script and it will be available as `fch`:
|
|
64
|
+
|
|
65
|
+
```html
|
|
66
|
+
<!-- Import it as usual -->
|
|
67
|
+
<script src="https://cdn.jsdelivr.net/npm/fch"></script>
|
|
68
|
+
<script>
|
|
69
|
+
fch('/hello');
|
|
70
|
+
</script>
|
|
58
71
|
```
|
|
59
72
|
|
|
60
73
|
## Options
|
|
61
74
|
|
|
75
|
+
These are all available options and their defaults:
|
|
76
|
+
|
|
62
77
|
```js
|
|
63
|
-
import api
|
|
78
|
+
import api from 'fch';
|
|
64
79
|
|
|
65
|
-
// General options with their defaults;
|
|
66
|
-
api.baseUrl = null; // Set an API endpoint
|
|
80
|
+
// General options with their defaults; all of these are also parameters:
|
|
67
81
|
api.method = 'get'; // Default method to use for api()
|
|
68
|
-
api.
|
|
82
|
+
api.url = '/'; // Relative or absolute url where the request is sent
|
|
83
|
+
api.baseUrl = null; // Set an API base URL reused all across requests
|
|
84
|
+
api.query = {}; // Merged with the query parameters passed manually
|
|
85
|
+
api.headers = {}; // Merged with the headers on a per-request basis
|
|
69
86
|
|
|
70
87
|
// Control simple variables
|
|
71
|
-
api.dedupe = true; // Avoid parallel GET requests to the same path
|
|
72
88
|
api.output = 'body'; // Return the body; use 'response' for the full response
|
|
89
|
+
api.dedupe = true; // Avoid sending concurrent GET requests to the same path
|
|
73
90
|
|
|
74
91
|
// Interceptors
|
|
75
92
|
api.before = req => req;
|
|
76
93
|
api.after = res => res;
|
|
77
94
|
api.error = err => Promise.reject(err);
|
|
95
|
+
```
|
|
78
96
|
|
|
79
|
-
|
|
80
|
-
api(url, { method, body, headers, ... });
|
|
97
|
+
They can all be defined globally as shown above, passed manually as the options argument, or be used when [creating a new instance](#create-an-instance).
|
|
81
98
|
|
|
82
|
-
|
|
83
|
-
api.get(url, { headers, ... });
|
|
84
|
-
api.post(url, { body, headers, ... });
|
|
85
|
-
api.put(url, { body, headers, ... });
|
|
86
|
-
// ...
|
|
99
|
+
### Method
|
|
87
100
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
101
|
+
The HTTP method to make the request. When using the shorthand, it defaults to `GET`. We recommend using the method syntax:
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
import api from 'fch';
|
|
105
|
+
|
|
106
|
+
api.get('/cats');
|
|
107
|
+
api.post('/cats', { body: { name: 'snowball' } });
|
|
108
|
+
api.put(`/cats/3`, { body: { name: 'snowball' }});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
You can use it with the plain function as an option parameter. The methods are all lowercase but the option as a parameter is case insensitive; it can be either uppercase or lowercase:
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
// Recommended way of dealing with methods:
|
|
115
|
+
api.get(...);
|
|
116
|
+
|
|
117
|
+
// INVALID; won't work
|
|
118
|
+
api.GET(...);
|
|
119
|
+
|
|
120
|
+
// Both of these are valid:
|
|
121
|
+
api({ method; 'GET' })
|
|
122
|
+
api({ method; 'get'})
|
|
93
123
|
```
|
|
94
124
|
|
|
95
|
-
|
|
125
|
+
Example: adding a new cat and fixing a typo:
|
|
96
126
|
|
|
97
|
-
|
|
127
|
+
```js
|
|
128
|
+
import api from 'fch';
|
|
129
|
+
|
|
130
|
+
const cats = await api.get('/cats');
|
|
131
|
+
console.log(cats);
|
|
132
|
+
const { id } = await api.post('/cats', { body: { name: 'snowbll' } });
|
|
133
|
+
await api.put(`/cats/${id}`, { body: { name: 'snowball' }})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Url
|
|
137
|
+
|
|
138
|
+
Specify where to send the request to. It's normally the first argument, though technically you can use both styles:
|
|
98
139
|
|
|
99
140
|
```js
|
|
100
|
-
// All of these methods are valid
|
|
101
141
|
import api from 'fch';
|
|
102
142
|
|
|
103
|
-
//
|
|
143
|
+
// Recommended way of specifying the Url
|
|
104
144
|
await api.post('/hello', { body: '...', headers: {} })
|
|
105
145
|
|
|
106
|
-
//
|
|
146
|
+
// These are also valid if you prefer their style; we won't judge
|
|
107
147
|
await api('/hello', { method: 'post', body: '...', headers: {} });
|
|
108
148
|
await api({ url: '/hello', method: 'post', headers: {}, body: '...' });
|
|
109
149
|
await api.post({ url: '/hello', headers: {}, body: '...' });
|
|
@@ -118,9 +158,11 @@ api.get('/hello');
|
|
|
118
158
|
// Called https//api.filemon.io/hello
|
|
119
159
|
```
|
|
120
160
|
|
|
161
|
+
> Note: with Node.js you need to either set an absolute baseUrl or make the URL absolute
|
|
162
|
+
|
|
121
163
|
### Body
|
|
122
164
|
|
|
123
|
-
The `body` can be a string, a
|
|
165
|
+
The `body` can be a string, a plain object|array or a FormData instance. If it's an array or object, it'll be stringified and the header `application/json` will be added. Otherwise it'll be sent as plain text:
|
|
124
166
|
|
|
125
167
|
```js
|
|
126
168
|
import api from 'api';
|
|
@@ -137,6 +179,46 @@ form.onsubmit = e => {
|
|
|
137
179
|
};
|
|
138
180
|
```
|
|
139
181
|
|
|
182
|
+
The methods `GET` and `HEAD` do not accept a body and it'll be ignored.
|
|
183
|
+
|
|
184
|
+
The **response body** will be returned by default as the output of the call:
|
|
185
|
+
|
|
186
|
+
```js
|
|
187
|
+
const body = await api.get('/cats');
|
|
188
|
+
console.log(body);
|
|
189
|
+
// [{ id: 1, }, ...]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
When the server specifies the header `Content-Type` as `application/json`, then we'll attempt to parse the response body and return that as the variable. Otherwise, the plain text will be returned.
|
|
193
|
+
|
|
194
|
+
When the function returns the response (if you set `output: "response"` as an option), then the body can be accessed as `response.body`:
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
const response = await api.get('/cats', { output: 'response' });
|
|
198
|
+
console.log(response.body);
|
|
199
|
+
// [{ id: 1, }, ...]
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
### Query
|
|
204
|
+
|
|
205
|
+
You can easily pass GET query parameters by using the option `query`:
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
api.get('/cats', { query: { limit: 3 } });
|
|
209
|
+
// /cats?limit=3
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
While rare, some times you might want to persist a query parameter across requests and always include it; in that case, you can define it globally and it'll be added to every request:
|
|
213
|
+
|
|
214
|
+
```js
|
|
215
|
+
import api from 'fch';
|
|
216
|
+
api.query.myparam = 'abc';
|
|
217
|
+
|
|
218
|
+
api.get('/cats', { query: { limit: 3 } });
|
|
219
|
+
// /cats?limit=3&myparam=abc
|
|
220
|
+
```
|
|
221
|
+
|
|
140
222
|
|
|
141
223
|
### Headers
|
|
142
224
|
|
|
@@ -145,13 +227,28 @@ You can define headers globally, in which case they'll be added to every request
|
|
|
145
227
|
|
|
146
228
|
```js
|
|
147
229
|
import api from 'fch';
|
|
148
|
-
api.headers.abc = 'def';
|
|
149
230
|
|
|
150
|
-
|
|
231
|
+
// Globally, so they are reused across all requests
|
|
232
|
+
api.headers.a = 'b';
|
|
233
|
+
|
|
234
|
+
// With an interceptor, in case you need dynamic headers per-request
|
|
235
|
+
api.before = req => {
|
|
236
|
+
req.headers.c = 'd';
|
|
237
|
+
return req;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// Set them for this single request:
|
|
241
|
+
api.get('/hello', { headers: { e: 'f' } });
|
|
151
242
|
// Total headers on the request:
|
|
152
|
-
// {
|
|
243
|
+
// { a: 'b', c: 'd', e: 'f' }
|
|
153
244
|
```
|
|
154
245
|
|
|
246
|
+
When to use each?
|
|
247
|
+
|
|
248
|
+
- If you need headers shared across all requests, like an API key, then the global one is the best place.
|
|
249
|
+
- When you need to extract them dynamically from somewhere it's better to use the .before() interceptor. An example would be the user Authorization token.
|
|
250
|
+
- When it changes on each request, it's not consistent or it's an one-off, use the option argument.
|
|
251
|
+
|
|
155
252
|
|
|
156
253
|
### Output
|
|
157
254
|
|
|
@@ -198,7 +295,7 @@ fch('/a', { dedupe: true }); // [DEFAULT] Dedupes GET requests
|
|
|
198
295
|
fch('/a', { dedupe: false }) // All fetch() calls trigger a network call
|
|
199
296
|
```
|
|
200
297
|
|
|
201
|
-
> We do not support deduping other methods
|
|
298
|
+
> We do not support deduping other methods besides `GET` right now
|
|
202
299
|
|
|
203
300
|
Note that opting out of deduping a request will _also_ make that request not be reusable, see this test for details:
|
|
204
301
|
|
|
@@ -250,45 +347,6 @@ fch.error = async err => {
|
|
|
250
347
|
```
|
|
251
348
|
|
|
252
349
|
|
|
253
|
-
|
|
254
|
-
### Advanced promise interface
|
|
255
|
-
|
|
256
|
-
This library uses [the `swear` promise interface](https://www.npmjs.com/swear), which allows you to query operations seamlessly on top of your promise:
|
|
257
|
-
|
|
258
|
-
```js
|
|
259
|
-
import api from "fch";
|
|
260
|
-
|
|
261
|
-
// You can keep performing actions like it was sync
|
|
262
|
-
const firstFriendName = await api.get("/friends")[0].name;
|
|
263
|
-
console.log(firstFriendName);
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
### Define shared options
|
|
267
|
-
|
|
268
|
-
You can also define values straight away:
|
|
269
|
-
|
|
270
|
-
```js
|
|
271
|
-
import api from "fch";
|
|
272
|
-
|
|
273
|
-
api.baseUrl = "https://pokeapi.co/";
|
|
274
|
-
|
|
275
|
-
const mew = await api.get("/pokemon/150");
|
|
276
|
-
console.log(mew);
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
If you prefer Axios' style of outputting the whole response, you can do:
|
|
280
|
-
|
|
281
|
-
```js
|
|
282
|
-
// Default, already only returns the data on a successful call
|
|
283
|
-
api.output = "data";
|
|
284
|
-
const name = await api.get("/users/1").name;
|
|
285
|
-
|
|
286
|
-
// Axios-like
|
|
287
|
-
api.output = "response";
|
|
288
|
-
const name = await api.get("/users/1").data.name;
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
|
|
292
350
|
## How to
|
|
293
351
|
|
|
294
352
|
### Stop errors from throwing
|
|
@@ -340,7 +398,7 @@ const body = await fch.get('/blabla');
|
|
|
340
398
|
```
|
|
341
399
|
|
|
342
400
|
|
|
343
|
-
### Set
|
|
401
|
+
### Set authorization headers
|
|
344
402
|
|
|
345
403
|
You can set that globally as a header:
|
|
346
404
|
|
|
@@ -375,3 +433,94 @@ import fch from 'fch';
|
|
|
375
433
|
|
|
376
434
|
const me = await fch('/users/me', { headers: { Authorization: 'bearer abc' } });
|
|
377
435
|
```
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
### Create an instance
|
|
439
|
+
|
|
440
|
+
You can create an instance with its own defaults and global options easily. It's common when writing an API that you want to encapsulate away:
|
|
441
|
+
|
|
442
|
+
```js
|
|
443
|
+
import fch from 'fch';
|
|
444
|
+
|
|
445
|
+
const api = fch.create({
|
|
446
|
+
baseUrl: 'https://api.filemon.io/',
|
|
447
|
+
...
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
api.get('/hello'); // Gets https://api.filemon.io/hello
|
|
451
|
+
fch.get('/hello'); // Gets http://localhost:3000/hello (or wherever you are)
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Note: for server-side (Node.js) usage, you always want to set `baseUrl`.
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
### Cancel ongoing requests
|
|
459
|
+
|
|
460
|
+
You can cancel ongoing requests [similarly to native fetch()](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort#examples), by passing it a signal:
|
|
461
|
+
|
|
462
|
+
```js
|
|
463
|
+
import api from 'fch';
|
|
464
|
+
|
|
465
|
+
const controller = new AbortController();
|
|
466
|
+
const signal = controller.signal;
|
|
467
|
+
|
|
468
|
+
abortButton.addEventListener('click', () => {
|
|
469
|
+
controller.abort();
|
|
470
|
+
console.log('Download aborted');
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
api.get(url, { signal });
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
### Define shared options
|
|
479
|
+
|
|
480
|
+
You can also define values straight away:
|
|
481
|
+
|
|
482
|
+
```js
|
|
483
|
+
import api from "fch";
|
|
484
|
+
|
|
485
|
+
api.baseUrl = "https://pokeapi.co/";
|
|
486
|
+
|
|
487
|
+
const mew = await api.get("/pokemon/150");
|
|
488
|
+
console.log(mew);
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
You can also [create an instance](#create-an-instance) that will have the same options for all requests made with that instance.
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
### Node.js vs Browser
|
|
496
|
+
|
|
497
|
+
First, we use the native Node.js' fetch() and the browser's native fetch(), so any difference between those also applies to this library. For example, if you were to call `"/"` in the browser it'd refer to the current URL, while in Node.js it'd fail since you need to specify the full URL. Some other places where you might find differences: CORS, cache, etc.
|
|
498
|
+
|
|
499
|
+
In the library itself there's nothing different between the browser and Node.js, but it might be interesting to note that (if/when implemented) things like cache, etc. in Node.js are normally long-lived and shared, while in a browser request it'd bound to the request itself.
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
### Differences with Axios
|
|
504
|
+
|
|
505
|
+
The main difference is that things are simplified with fch:
|
|
506
|
+
|
|
507
|
+
```js
|
|
508
|
+
// Modify headers
|
|
509
|
+
axios.defaults.headers.Authorization = '...';
|
|
510
|
+
fch.headers.Authorization = '...';
|
|
511
|
+
|
|
512
|
+
// Set a base URL
|
|
513
|
+
axios.defaults.baseURL = '...';
|
|
514
|
+
fch.baseUrl = '...';
|
|
515
|
+
|
|
516
|
+
// Add an interceptor
|
|
517
|
+
axios.interceptors.request.use(fn);
|
|
518
|
+
fch.before = fn;
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
API size is also strikingly different, with **7.8kb** for Axios and **1.1kb** for fch.
|
|
522
|
+
|
|
523
|
+
As disadvantages, I can think of two major ones for `fch`:
|
|
524
|
+
|
|
525
|
+
- Requires Node.js 18+, which is the version that includes `fetch()` by default.
|
|
526
|
+
- Does not support many of the more advanced options, like `onUploadProgress` nor `onDownloadProgress`.
|