Skip to content

Commit b4f0c04

Browse files
committed
docs: readme, adds text functions
1 parent 659aac6 commit b4f0c04

File tree

2 files changed

+171
-118
lines changed

2 files changed

+171
-118
lines changed

README.md

+78-41
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,108 @@
11
# Fetch
2-
Tiny wrapper around DOM fetch for common API wrappings.
2+
Tiny wrapper around DOM fetch for common API wrappings. Isomorphic (supports browsers and Node.js), if `fetch` is available or polyfilled.
33

44
[![](https://shields.servallapps.com/npm/v/@lcdev/fetch.svg?registry_uri=https%3A%2F%2Fnpm.servalldatasystems.com)](https://npm.servalldatasystems.com/#/detail/@lcdev/fetch)
55

66
```bash
77
yarn add @lcdev/fetch@VERSION
88
```
99

10-
Use:
10+
Features:
11+
- Easy to use builder-style API
12+
- Quick JSON, blob and text parsing options
13+
- Shareable builders for common options (authorization headers, onResponse hooks, etc.)
14+
- No magic - call `build()` and pass to fetch if you want
15+
- TypeScript friendly
16+
- Tiny footprint (2kb)
1117

12-
```typescript
13-
import { HttpMethod, api, apiCall } from '@lcdev/fetch';
18+
If you are looking for something not available here, try [ky-universal](https://github.com/sindresorhus/ky-universal) or [axios](https://github.com/axios/axios).
1419

15-
// one-off requests are easy
16-
await apiCall('https://base-url.com/endpoint').json<MyReturnType>();
20+
There are two main functions exported by this package:
1721

18-
// re-use this any time you want to make a call to this api
19-
const myCoreApi = api('https://base-url.com');
22+
1. The `apiCall` function, which is used for creating a one-off fetch request
23+
2. The `api` function, which creates a shared builder for many fetch requests
2024

21-
// calling .then or `await` triggers the request
22-
await myCoreApi.get('/endpoint')
23-
// chainable interface
24-
.withBody({ foo: 'bar' })
25-
.withQuery({ baz: 'bat' })
26-
// chain .json if you know the response is json
27-
.json<MyReturnType>();
28-
```
25+
### `apiCall`
26+
The simplest function is `apiCall`, which sets up a fetch request.
2927

30-
Requests start on await/then. Chain to add data to the request. This is just a thin way to make `fetch` calls.
28+
```typescript
29+
import { HttpMethod, apiCall } from '@lcdev/fetch';
30+
31+
await apiCall('https://base-url.com/endpoint', HttpMethod.GET).json<TheResponseObject>();
32+
```
3133

32-
'Api's can have global 'transforms' which can do things with `withBearerToken`, `onResponse`, etc.
33-
The common use is for authorization tokens.
34+
This can be shortened by using the http method aliases exported by this package.
3435

3536
```typescript
36-
// let's assume that this is something that manages the current token
37-
const authManager = {
38-
token: '...',
39-
};
40-
41-
const myCoreApi = api('https://base-url.com')
42-
// whenever a request is made, this gets `authManager.token` and attachs it to the Authorization header
43-
.withBearerToken(authManager);
37+
import { get } from '@lcdev/fetch';
38+
39+
await get('https://base-url.com/endpoint').json<TheResponseObject>();
4440
```
4541

46-
You can add little callbacks to `myCoreApi` using `onResponse` or `onJsonResponse`. You might
47-
do so to watch for 401 responses, or maybe just for logging.
42+
There are `get`, `post`, `put`, `patch`, and `remove` aliases.
43+
44+
With a `ApiCall` builder (the object returned by `apiCall`), we can chain many options for the request.
4845

49-
Chainable methods for API calls:
50-
- `withBody(object, isJson?: boolean)`: adds json or other type of request body
51-
- `withQuery(object)`: adds query parameters
52-
- `withBearerToken(BearerToken)`: adds Authorization: Bearer {token} header
53-
- `withContentType(string)`: changes default content type header
46+
- `withQuery(object, options?: SerializationOptions)`: adds query parameters, stringifying the object with `query-string`
5447
- `withHeaders(Headers)`: adds headers to request
55-
- `withHeader(key, value)`: adds a header to the request
56-
- `expectStatus(number)`: throw error is response status isn't the expect one
57-
- `build()`: constructs options that can be passed into `fetch`
48+
- `withHeader(key, value)`: adds a single header to the request
49+
- `withContentType(string)`: changes the content-type header
50+
- `withBearerToken(object: { token?: string })`: adds `Authorization: Bearer {token}` header
51+
- `withBody(object, isJson?: boolean, options?: SerializationOptions)`: adds a request body
52+
- `withJsonBody(object, options?: SerializationOptions)`: adds JSON request body
53+
- `withFormDataBody(FormData, options?: SerializationOptions)`: adds form-data request body
54+
- `withURLEncodedBody(object, options?: SerializationOptions)`: adds 'application/x-www-form-urlencoded' request body
55+
- `expectStatus(number)`: throw an error if the response status isn't the expected one
56+
- `expectSuccessStatus()`: throw an error if the response status isn't in 200 range
57+
- `onResponse(callback)`: calls your function whenever responses are received
58+
- `onJsonResponse(callback)`: calls your function whenever JSON responses are received
59+
- `build()`: constructs options that can be passed into `fetch` directly
5860
- `json<T>()`: calls fetch and parses response as JSON
59-
- `jsonAndResponse<T>()`: calls fetch and parses response as JSON, along with full Response
61+
- `jsonAndResponse<T>()`: calls fetch and parses response as JSON, along with the full Response object
6062
- `blob<T>()`: calls fetch and parses response as a blob
61-
- `blobAndResponse<T>()`: calls fetch and parses response as a blob, along with full Response
63+
- `blobAndResponse<T>()`: calls fetch and parses response as a blob, along with the full Response object
64+
- `text<T>()`: calls fetch and parses response as text
65+
- `textAndResponse<T>()`: calls fetch and parses response as text, along with the full Response object
66+
67+
Because we expose `build`, there is always an escape hatch if you need something non-standard.
68+
69+
Note that fetch calls are **lazy** - meaning that nothing will run until you call `.then` or `await` it.
6270

63-
A base API itself can be called with `.get`, `.post`, etc. You can change the base URL if required
64-
with `changeBaseURL(path)`, though be warned that every request from then on will then be based on that.
71+
### `api`
72+
Most of the time, we make web apps that call APIs many times in different ways (endpoints, authorization, etc.).
73+
This package provides a way to share configuration easily between all calls, without being "global".
74+
75+
```typescript
76+
import { api } from '@lcdev/fetch';
77+
78+
const myBackend = api('https://base-url.com')
79+
.withBearerToken({ token: '...' })
80+
.onResponse((res) => {
81+
if (res.status === 401) logout();
82+
});
83+
84+
// we can re-use myBackend where we want to
85+
// you might put myBackend in a React Context, or inject it into state management
86+
await myBackend.get('/endpoint').json<TheResponseObject>();
87+
await myBackend.post('/endpoint').withJsonBody({ foo: 'bar' }).json<TheOtherResponse>();
88+
```
89+
90+
Here, `myBackend` is an `Api` object, which exposes ways to create `ApiCall`s (like above).
91+
You can perform the same builder functions on these as with `apiCall`.
92+
93+
You can add little callbacks to `myBackend` using `onResponse` or `onJsonResponse`. You might
94+
do this for logging, for business logic, etc.
95+
96+
You can change the base URL if required with `changeBaseURL(path)`, though be warned that
97+
every request from then on will then be based on that.
6598

6699
## NodeJS Support
67100
Just polyfill `fetch`, and this package will work. Install `cross-fetch` package and add the following to your main file.
68101

102+
```bash
103+
yarn add cross-fetch@3
104+
```
105+
69106
```typescript
70107
import 'cross-fetch/polyfill';
71108
```

src/index.ts

+93-77
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ export interface ApiCall<Method extends HttpMethod> extends Promise<Response> {
6565
jsonAndResponse<D extends Json = Json>(): Promise<[D, Response]>;
6666
blob(): Promise<Blob>;
6767
blobAndResponse(): Promise<[Blob, Response]>;
68+
text(): Promise<string>;
69+
textAndResponse(): Promise<[string, Response]>;
6870
}
6971

7072
export type ApiCallTransform<M extends HttpMethod> =
@@ -91,6 +93,93 @@ export interface Api {
9193
changeBaseURL(path: string): void;
9294
}
9395

96+
export const buildPath = (...args: string[]) => {
97+
// https://stackoverflow.com/a/46427607/1165996
98+
return args
99+
.map((part, i) => {
100+
if (i === 0) {
101+
return part.trim().replace(/[/]*$/g, '');
102+
}
103+
104+
return part.trim().replace(/(^[/]*|[/]*$)/g, '');
105+
})
106+
.filter(x => x.length)
107+
.join('/');
108+
};
109+
110+
export const api = (baseURL: string, transforms: ApiCallTransform<any>[] = []): Api => {
111+
const call = <M extends HttpMethod>(path: string, method: M) => {
112+
return new ApiCallImpl(buildPath(baseURL, path), method).applyTransforms(transforms);
113+
};
114+
115+
const withTransform = <M extends HttpMethod>(t: ApiCallTransform<M>) => {
116+
return api(baseURL, transforms.concat([t]));
117+
};
118+
119+
const withBearerToken = (token: BearerToken) => {
120+
return withTransform(c => c.withBearerToken(token));
121+
};
122+
123+
const withBaseURL = (path: string) => {
124+
return api(buildPath(baseURL, path), transforms);
125+
};
126+
127+
const onResponse = (cb: OnResponse) => {
128+
return withTransform(c => c.onResponse(cb));
129+
};
130+
131+
const onJsonResponse = (cb: OnJsonResponse) => {
132+
return withTransform(c => c.onJsonResponse(cb));
133+
};
134+
135+
return {
136+
call,
137+
withTransform,
138+
withBearerToken,
139+
withBaseURL,
140+
onResponse,
141+
onJsonResponse,
142+
get: path => call(path, HttpMethod.GET),
143+
post: path => call(path, HttpMethod.POST),
144+
put: path => call(path, HttpMethod.PUT),
145+
patch: path => call(path, HttpMethod.PATCH),
146+
delete: path => call(path, HttpMethod.DELETE),
147+
head: path => call(path, HttpMethod.HEAD),
148+
options: path => call(path, HttpMethod.OPTIONS),
149+
changeBaseURL: path => {
150+
baseURL = path;
151+
},
152+
};
153+
};
154+
155+
export const apiCall = <M extends HttpMethod>(path: string, method: M): ApiCall<M> => {
156+
return new ApiCallImpl(path, method);
157+
};
158+
159+
export const get = (path: string): ApiCall<HttpMethod.GET> => apiCall(path, HttpMethod.GET);
160+
export const post = (path: string): ApiCall<HttpMethod.POST> => apiCall(path, HttpMethod.POST);
161+
export const put = (path: string): ApiCall<HttpMethod.PUT> => apiCall(path, HttpMethod.PUT);
162+
export const patch = (path: string): ApiCall<HttpMethod.PATCH> => apiCall(path, HttpMethod.PATCH);
163+
export const remove = (path: string): ApiCall<HttpMethod.DELETE> => apiCall(path, HttpMethod.DELETE);
164+
165+
function applySerializationOptions(obj: any, options: SerializationOptions = {}) {
166+
if (typeof obj !== 'object' || Array.isArray(obj)) {
167+
return obj;
168+
}
169+
170+
const output = { ...obj };
171+
172+
if (options?.stripEmptyStrings) {
173+
for (const [key, val] of Object.entries(output)) {
174+
if (val === '') {
175+
delete output[key];
176+
}
177+
}
178+
}
179+
180+
return output;
181+
}
182+
94183
class ApiCallImpl<Method extends HttpMethod> implements ApiCall<Method> {
95184
private consumed = false;
96185
private queryOptions?: SerializationOptions;
@@ -296,85 +385,12 @@ class ApiCallImpl<Method extends HttpMethod> implements ApiCall<Method> {
296385
async blobAndResponse() {
297386
return this.then().then(async res => [await res.blob(), res] as [Blob, Response]);
298387
}
299-
}
300-
301-
export const buildPath = (...args: string[]) => {
302-
// https://stackoverflow.com/a/46427607/1165996
303-
return args
304-
.map((part, i) => {
305-
if (i === 0) {
306-
return part.trim().replace(/[/]*$/g, '');
307-
}
308-
309-
return part.trim().replace(/(^[/]*|[/]*$)/g, '');
310-
})
311-
.filter(x => x.length)
312-
.join('/');
313-
};
314-
315-
export const api = (baseURL: string, transforms: ApiCallTransform<any>[] = []): Api => {
316-
const call = <M extends HttpMethod>(path: string, method: M) => {
317-
return new ApiCallImpl(buildPath(baseURL, path), method).applyTransforms(transforms);
318-
};
319-
320-
const withTransform = <M extends HttpMethod>(t: ApiCallTransform<M>) => {
321-
return api(baseURL, transforms.concat([t]));
322-
};
323-
324-
const withBearerToken = (token: BearerToken) => {
325-
return withTransform(c => c.withBearerToken(token));
326-
};
327388

328-
const withBaseURL = (path: string) => {
329-
return api(buildPath(baseURL, path), transforms);
330-
};
331-
332-
const onResponse = (cb: OnResponse) => {
333-
return withTransform(c => c.onResponse(cb));
334-
};
335-
336-
const onJsonResponse = (cb: OnJsonResponse) => {
337-
return withTransform(c => c.onJsonResponse(cb));
338-
};
339-
340-
return {
341-
call,
342-
withTransform,
343-
withBearerToken,
344-
withBaseURL,
345-
onResponse,
346-
onJsonResponse,
347-
get: path => call(path, HttpMethod.GET),
348-
post: path => call(path, HttpMethod.POST),
349-
put: path => call(path, HttpMethod.PUT),
350-
patch: path => call(path, HttpMethod.PATCH),
351-
delete: path => call(path, HttpMethod.DELETE),
352-
head: path => call(path, HttpMethod.HEAD),
353-
options: path => call(path, HttpMethod.OPTIONS),
354-
changeBaseURL: path => {
355-
baseURL = path;
356-
},
357-
};
358-
};
359-
360-
export const apiCall = <M extends HttpMethod>(path: string, method: M): ApiCall<M> => {
361-
return new ApiCallImpl(path, method);
362-
};
363-
364-
function applySerializationOptions(obj: any, options: SerializationOptions = {}) {
365-
if (typeof obj !== 'object' || Array.isArray(obj)) {
366-
return obj;
389+
async text() {
390+
return this.textAndResponse().then(([text]) => text);
367391
}
368392

369-
const output = { ...obj };
370-
371-
if (options?.stripEmptyStrings) {
372-
for (const [key, val] of Object.entries(output)) {
373-
if (val === '') {
374-
delete output[key];
375-
}
376-
}
393+
async textAndResponse() {
394+
return this.then().then(async res => [await res.text(), res] as [string, Response]);
377395
}
378-
379-
return output;
380396
}

0 commit comments

Comments
 (0)