3.3.0 • Published 1 year ago

sh-api-client v3.3.0

Weekly downloads
-
License
UNLICENSED
Repository
github
Last release
1 year ago

api-client

Sharpr JavaScript api client for use in React and Node

Tell me more

Similar to our Angular ApiProvider but more intuitive. Target uses include:

  • New React code
  • API v4 endpoints written in JavaScript
  • API integration testing (v2, v3, v4)
  • Single-use scripts for reports or importing data (e.g. queries repo)

Extra features include:

  1. HTTP errors cause a promise rejection
  2. You can specify a timeout
  3. It reads Sharpr's API-* HTTP headers
  4. Supports caching in milliseconds or as an expression
  5. You can abort requests that match a certain pattern
  6. It supports interceptors for request, response, error, timeout, abort
  7. Using node-fetch it supports fetching on Node

Table of Contents

Request API

Use the following key for the origin of each option:

  • [f] From window.fetch - docs
  • [S] Custom Sharpr item
const api = require('api-client');
api.head(endpoint, params, options);
api.get(endpoint, params, options);
api.post(endpoint, payload, options);
api.delete(endpoint, payload, options);
api.put(endpoint, payload, options);
api.patch(endpoint, payload, options);
api.request(method, endpoint, params, options);
api.abort(methodStringOrRegex, endpointStringOrRegex);

Where:

  • {String} method GET, POST, DELETE, etc. [f]
  • {String} endpoint The name of the API endpoint such as /posts/123 [S]
  • {Object} [paramsOrData] For GET requests, the query params; otherwise JSON payload [S]
  • {Object} [options] Additional overrides including { headers }
    • {Object} headers Any request headers [f]
    • {Number} timeout The number of milliseconds after which to time out (default is 5 minutes) [S]
    • {Boolean} avoidLoadingBar If true, do not show loading bar (React only) [S]

Returns:

A promise that resolves with ApiResponse or rejects with ApiError.

How endpoints and URLs work

On sharpr.com:

  • /posts/123 Is changed to https://sharpr.com/api/v2/posts/123
  • /v3/posts/search Is changed to https://sharpr.com/api/v3/posts/search
  • https://sharprua.com/status/tests.php Is left alone

When using api-client in Node or fetching from external domains, URLs need to start with the full domain or else you can set a default using setBaseURL():

api.setBaseURL('https://example.com');

Response API

ApiResponse is a custom Sharpr class to make it easier to work with responses.

  • {Object} request The original ApiRequest object that generated this response
    • {String} url A raw URL to use instead of the endpoint [S]
    • {String} method GET, POST, DELETE, etc. [f]
    • {Object} searchParams params that were serialized into the GET string [k]
    • {Object} headers Any request headers [f]
    • {*} json The JSON payload [f]
    • {Number} timeout The number of milliseconds after which to time out [S]
    • {Boolean} avoidLoadingBar If true, do not show loading bar [S]
  • {String} type Either "json", "text" or null depending on response type [S]
  • {*} data The decoded response or text [S]
  • {String} endpoint The name of the API endpoint such as /posts/123 [S]
  • {Response} rawResponse The raw fetch response [S]
  • {Boolean} ok True if response is 2xx [f]
  • {Number} status The status code [f]
  • {String} statusText Status name such as "OK" or "No Content" [f]
  • {Headers} headers Headers object with get(), has(), etc.
  • {Number} total The value of API-Total-Records response header [S]
  • {Number} size The number of items returned in Request.data [S]
  • {Number} limit The limit param that was passed in [S]
  • {Number} page The page param that was passed in [S]
  • {Number} numPages The number of pages based on total and limit [S]
  • {Boolean} isEmpty True if no records were returned [S]
  • {String} location The value of the Location response header [S]
  • {String} contentType The value of the Content-Type response header [S]
  • {String} contentLength The value of the Content-Length response header [S]
  • {Array} notices The parsed value of the API-Response-Notices response header [S]
  • {Array} errors The parsed value of the API-Response-Errors response header [S]
  • {String} responseId The value of the API-Response-Id response header [S]
  • {Number} newId The value of the API-New-Record-Id response header [S]
  • {Number} time The value of the API-Response-Time response header [S]
  • {Boolean} wasAborted True if request was aborted [S]

Handling Errors

ApiError is a custom Sharpr class to make it easier to work with response errors.

api.get('/hello').then(onSuccess, response => {
	// response is instance of ApiError
	// response.error is instance of Error
});

ApiError has the same properties of ApiResponse plus

  • {Error} error The HTTPError or TimeoutError object [S]

Special methods

The following methods are added for convenience:

  • patchDifference(endpoint, oldValues, newValues, options)
  • submitJob(endpoint, payload, options)

Examples

patchDifference example:

const currState = {
	fname: 'John',
	lname: 'Doe',
};
const newState = {
	fname: 'Johnny',
	lname: 'Doe',
};
api
	.patchDifference('/v2/users/123', currState, newState)
	.then(onSuccess, onError);

submitJob example:

const jobInfo = {
	post_ids: [1, 2, 3, 4],
	action: 'destroy',
};
const onSuccess = response => {
	const recheckIntervalMs = 5000;
	response.onJobComplete(() => {
		alert('Posts were all deleted!');
	}, recheckIntervalMs);
};
api.submitJob('/v2/posts/massdelete', jobInfo).then(onSuccess, onError);

Aborting requests

const { abort, get } = require('api-client');

// abort all pending requests
abort();
// abort all pending GET requests
abort('get');
// abort all pending GET /posts/123 requests
abort('get', '/posts/123');
// abort a specific request
const promise = get('/posts/123');
abort(promise);

Hooks

There are some api-related hooks that can be used with React components:

  1. useApiGet(endpoint, params, options)
  2. useApiGetAll(endpointArgSets)
  3. useApiEndpoint(verb, endpoint)

useApiGet

Example:

export function MyComponent() {
	const { isLoading, hasError, response } = useApiGet(
		'https://httpbin.org/get?a=1'
	);
	return (
		<div>
			{isLoading && <Loader size={16} />}
			{hasError && <div>Error fetching data</div>}
			{response && <div>a equals {response.data.args.a}</div>}
		</div>
	);
}

Consolidating multiple useApiGet calls using withResponses

Example:

import useApiGet from '../../libs/api/hooks/useApiGet/useApiGet.js';
import withResponses from '../../libs/api/middleware/withResponses/withResponses.js';
export function MyComponent() {
	const { isLoading, hasError, responses } = withResponses([
		useApiGet('https://httpbin.org/get', { a: 1 }),
		useApiGet('https://httpbin.org/get', { b: 2 }),
	]);
	return (
		<div>
			{isLoading && <Loader size={16} />}
			{hasError && <div>Error fetching data</div>}
			{responses &&
				responses.map((response, i) => (
					<div key={i}>{JSON.stringify(response.data.args)}</div>
				))}
		</div>
	);
}

useApiEndpoint

Example:

export function MyComponent() {
	const verb = 'get';
	const endpoint = '/v3/posts/search';
	const { isLoading, hasError, response, request, abort } = useApiEndpoint(
		verb,
		endpoint
	);
	const [term, setTerm] = useState('');
	const doSearch = () => {
		abort(); // abort any incomplete request
		request({ term }); // make a request
	};
	return (
		<div>
			<form onSubmit={doSearch}>
				Search Posts:
				<input value={term} onChange={e => setTerm(e.target.value)} />
				<button>Go</button>
			</form>
			{isLoading && <Loader size={16} />}
			{hasError && <div>Error fetching data</div>}
			{response &&
				response.data.map(post => (
					<div key={post.id}>
						{post.headline} - {post.summary}
					</div>
				))}
		</div>
	);
}

Caching

To cache a response for later, use the cacheFor option.

cacheFor may be a number of milliseconds or a time expression such as the following:

  • 1d => 1 day
  • 8h => 8 hours
  • 20m => 20 minutes
  • 30s => 30 seconds
  • 300ms => 300 milliseconds
  • 3h 20m => 3 hours, 20 minutes

See parse-duration on npm for more supported expressions.

Example:

const result1 = api.get('/abc', { d: 4 }, { cacheFor: '2h' });
// ... 1 hour later ...
const result2 = api.get('/abc', { d: 4 }, { cacheFor: '2h' });
// result1 === result2

Mocking with fetch-mock

For unit tests, you may want to mock responses. The fetch-mock package works well for that.

Example:

// first require or import api-client
const api = require('api-client');
// fetchMock must be required aftwarwords
const fetchMock = require('fetch-mock');

fetchMock.get(/posts\/search/, {
	status: 200,
	body: range(10).map(id => ({ id, title: `Post ${id}` })),
	headers: {
		'API-Total-Records': '200',
	},
});

api.get('/api/v2/posts/search?limit=10').then(response => {
	response.data; // equal to body specified above
	response.total; // 200
	response.numPages; // 20
});

Interceptors

  • request Called before every request is sent
  • response Called after every response is receoved (before resolving promise)
  • error Called when HTTP status is between 400 and 599 inclusive (before rejecting promise)
  • timeout Called when an API request hits the configured timeout (before rejecting promise)
  • abort Called when the promise creator aborts the request (before rejecting promise)
// NOTE:
// request instanceof ApiRequest
// response instanceof ApiResponse
// api instanceof ApiService
// error instanceof ApiError
api.addInterceptor({
	request: (request, api) => {},
	response: (request, response, api) => {},
	error: (request, error, api) => {},
	timeout: (request, error, api) => {},
	abort: (request, error, api) => {},
});

ApiRequest

PropertyTypeDescriptionExamples
methodStringThe HTTP verb uppercaseGET, POST
endpointStringThe Sharpr endpoint/v3/users/me
paramsObjectThe params to send in the URL{a: '1'}
dataObjectThe payload to be sent{ a: 1 }
optionsObjectThe options passed to fetch{ cacheFor: '15m' }
headersHeadersHeaders to be sentnew Headers({ a: '1'})
queryStringStringThe URL query string based on paramsa=1&b=2
urlStringThe full URL to be senthttps://example.com/abc?a=1&b=2
pendingBooleanTrue when request started but not completedfalse
completedBooleanTrue when request has completedtrue
MethodReturnsDescription
abort()voidAbort a pending request
send()PromiseA promise from fetch