@zakkudo/api-tree v1.0.0
@zakkudo/api-tree
Make working with backend api trees enjoyable.
Generate an easy to use api tree that includes format checking using JSON Schema for the body and params with only a single configuration object. Network calls are executed using a thin convenience wrapper around fetch.
If you're using swagger/openapi,
checkout @zakkudo/open-api-tree
which generates this configuration for
you from swagger's metadata.
Why use this?
- Consistancy with simplicity
- Leverages native fetch, adding a thin convenience layer executed in the form of Fetch Functions
- Use json schemas to ensure correct usage of the apis
- Share authorization handling using a single location that can be updated dynamically
- Share a single transform for the responses and request in a location that can be updated dynamically
- Supports overloading the tree methods so that you can use the same method for getting a single item or a collection of items
Install
# Install using npm
npm install @zakkudo/api-tree
# Install using yarn
yarn add @zakkudo/api-tree
Examples
Base example
// Api methods are simply generated whenever an array is found in the object tree.
const api = new ApiTree(baseurl, {get: [pathname, apiDefaultOptions, schema]}, treeDefaultOptions);
// The url and default options are predetermed during construction, but the options are overridable on the final function call
api.get(overrideOptions);
// Or don't override anything
api.get();
//ApiTree merges the different scopes of options to provide many different levels for overriding settings where if it was written with fetch, it would be similar to this
fetch(baseurl + pathname, Object.assign({}, treeDefaultOptions, apiDefaultOptions, overrideOptions));
Overloading a function
//When an api is overloaded by having a nested array, the one with the closest url/params signature will be selected
const first = ['/users/:userId'];
const second = ['/users', {params: {limit: 10}}];
const api = new ApiTree('http://backend', {get: [first, second]}, treeDefaultOptions);
api.get({params: {userId: '1234'}}); // Uses the first config, making a GET http://backend/users/1234
api.get(); // Uses the second config, making a GET http://backend/users?limit=10
Adding a convenience function
//Sometimes you'll want a function in the api tree that you made yourself
const api = new ApiTree('http://backend', {
delete: ['/users/:id', {method: 'DELETE'}],
deleteAll(ids) {
console.log(this.base, this.options); // You have direct access to the configuration of the api tree
return Promise.all(ids.map((i) => api.delete({params: {userId: i}})));
}
}, treeDefaultOptions);
api.delete({params: {userId: '1234'}});
api.deleteAll(['1234', '4556']);
Passing through data
//It's not generally recommended, but any primitive data is just passed through as-is
const api = new ApiTree('http://backend', {limit: 10, name: 'my great api', enabled: true});
console.log(api.limit); // 10
console.log(api.name); // 'my great api'
console.log(api.enabled); // true
Validate your network requests before they're dispatched
import ApiTree from '@zakkudo/api-tree';
import ValidationError from '@zakkudo/api-tree/ValidationError';
// Add a json schema which will be executed before the network request is sent out
const api = new ApiTree('https://backend', {
users: {
get: ['/v2/users/:userId', {}, {
$schema: "http://json-schema.org/draft-07/schema#",
type: 'object',
properties: {
params: {
type: 'object',
required: ['userId'],
properties: {
userId: {
type: 'string',
pattern: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
},
},
},
},
}],
}
});
// Try fetching without an id
api.users.get().catch((reason) => {
if (reason instanceof ValidationError) {
console.log(reason); // "params: should have required property 'userId'
}
throw reason;
})
// Try using an invalidly formatted id
api.users.get({params: {userId: 'invalid format'}}).catch((reason) => {
if (reason instanceof ValidationError) {
console.log(reason); // "params.userId: should match pattern \"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\""
}
throw reason;
});
// Skip the validation by passing false to the network call
api.users.get({params: {userId: 'invalid format'}}, false).catch((reason) => {
if (reason instanceof HttpError) {
console.log(reason.status); // 500
}
throw reason;
});
Handling network errors
import ApiTree from '@zakkudo/api-tree';
import HttpError from '@zakkudo/api-tree/HttpError';
const api = new ApiTree('https://backend', {
users: {
get: ['/v2/users/:userId', {}, {
$schema: "http://json-schema.org/draft-07/schema#",
type: 'object',
properties: {
params: {
type: 'object',
required: ['userId'],
properties: {
userId: {
type: 'string',
pattern: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
},
},
},
},
}],
}
});
// Force execution with an invalidly formatted id
api.users.get({params: {userId: 'invalid format'}}, false).catch((reason) => {
if (reason instanceof HttpError) {
console.log(reason.status); // 500
console.log(reason.response); // response body from the server, often json
}
throw reason;
});
Overriding options
import ApiTree from '@zakkudo/api-tree';
import HttpError from '@zakkudo/api-tree/HttpError';
import ValidationError from '@zakkudo/api-tree/ValidationError';
const api = new ApiTree('https://backend', {
users: {
post: ['/v1/users', {method: 'POST'}, {
$schema: "http://json-schema.org/draft-07/schema#",
type: 'object',
properties: {
body: {
type: 'object',
required: ['first_name', 'last_name'],
properties: {
first_name: {
type: 'string'
},
last_name: {
type: 'string'
},
},
},
},
}],
get: [
['/v1/users'], //Endpoint overloading. If userId is provided as a param, the
//second endpoint is automatically used
['/v2/users/:userId', {}, {
$schema: "http://json-schema.org/draft-07/schema#",
type: 'object',
properties: {
params: {
type: 'object',
required: ['userId'],
properties: {
userId: {
type: 'string',
pattern: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
},
},
},
},
}],
],
}
}, {
headers: {
'X-AUTH-TOKEN': '1234'
},
transformError(reason) {
if (reason instanceof HttpError && reason.status === 401) {
login();
}
return reason;
}
});
//Set headers after the fact
api.options.headers['X-AUTH-TOKEN'] = '5678';
//Get 10 users
api.users.get({params: {limit: 10}}).then((users) => {
console.log(users); // [{id: ...}, ...]
});
//Create a user
api.users.post({first_name: 'John', last_name: 'Doe'}).then((response) => {
console.log(response); // {id: 'ff599c67-1cac-4167-927e-49c02c93625f', first_name: 'John', last_name: 'Doe'}
});
// Try using a valid id
api.users.get({params: {userId: 'ff599c67-1cac-4167-927e-49c02c93625f'}}).then((user) => {
console.log(user); // {id: 'ff599c67-1cac-4167-927e-49c02c93625f', first_name: 'john', last_name: 'doe'}
})
// Override the global options at any time
api.users.get({transformResponse: () => 'something else'}).then((response) => {
console.log(response); // 'something else'
});
API
@zakkudo/api-tree~ApiTree ⏏
Kind: Exported class
- ~ApiTree
- new ApiTree(baseUrl, tree, [options])
- ~FetchFunction : function
- ~Options : Object
new ApiTree(baseUrl, tree, options)
Returns: Object - The generated api tree
Param | Type | Description |
---|---|---|
baseUrl | String | The url to prefix with all paths |
tree | * | The configuration tree for the apis. Accepts a deeply nested set of objects where array are interpreted to be of the form [path, options, schema] . Thos array are converted into api fetching functions. |
options | Options | Options modifying the network call, mostly analogous to fetch |
ApiTree~FetchFunction : function
Executes the network request using the api tree configuration. Generated from the triplets of the form
[url, options, jsonschema]
where only url is required.
Kind: inner typedef of ApiTree
Param | Type | Default | Description |
---|---|---|---|
options | Options | The override options for the final network call | |
validate | Boolean | true | Set to false to force validation to be skipped, even if there is a schema |
ApiTree~Options : Object
Options modifying the network call, mostly analogous to fetch
Kind: inner typedef of ApiTree
Properties
Name | Type | Default | Description |
---|---|---|---|
options.method | String | 'GET' | GET, POST, PUT, DELETE, etc. |
options.mode | String | 'same-origin' | no-cors, cors, same-origin |
options.cache | String | 'default' | default, no-cache, reload, force-cache, only-if-cached |
options.credentials | String | 'omit' | include, same-origin, omit |
options.headers | String | "application/json; charset=utf-8". | |
options.redirect | String | 'follow' | manual, follow, error |
options.referrer | String | 'client' | no-referrer, client |
options.body | String | Object | JSON.stringify is automatically run for non-string types | |
options.params | String | Object | Query params to be appended to the url. The url must not already have params. The serialization uses the same rules as used by @zakkudo/query-string | |
options.unsafe | Boolean | Disable escaping of params in the url | |
options.transformRequest | function | Array.<function()> | Transforms for the request body. When not supplied, it by default json serializes the contents if not a simple string. Also accepts promises as return values for asynchronous work. | |
options.transformResponse | function | Array.<function()> | Transform the response. Also accepts promises as return values for asynchronous work. | |
options.transformError | function | Array.<function()> | Transform the error response. Return the error to keep the error state. Return a non Error to recover from the error in the promise chain. A good place to place a login handler when recieving a 401 from a backend endpoint or redirect to another page. It's preferable to never throw an error here which will break the error transform chain in a non-graceful way. Also accepts promises as return values for asynchronous work. |
@zakkudo/api-tree/ValiationError~ValidationError ⇐ Error ⏏
An error used to represent a list of validation issues generated from a JSON schema.
Kind: Exported class
Extends: Error
new ValidationError(url, errors, schema)
Param | Type | Description |
---|---|---|
url | String | The url when the validation errors were found |
errors | Array.<String> | The list of the validation errors |
schema | Object | The JSON schema used to generated the validation errors |
validationError.errors
The list of validation errors
Kind: instance property of ValidationError
validationError.schema
The JSON schema used to generated the validation errors
Kind: instance property of ValidationError
@zakkudo/api-tree/HttpError~HttpError ⏏
Aliased error from package @zakkudo/fetch/HttpError
Kind: Exported class
@zakkudo/api-tree/UrlError~UrlError ⏏
Aliased error from package @zakkudo/fetch/UrlError
Kind: Exported class
@zakkudo/api-tree/QueryStringError~QueryStringError ⏏
Aliased error from package @zakkudo/fetch/QueryStringError
Kind: Exported class
5 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago