1.0.0 • Published 5 years ago

@zakkudo/api-tree v1.0.0

Weekly downloads
-
License
BSD-3-Clause
Repository
github
Last release
5 years ago

@zakkudo/api-tree

Make working with backend api trees enjoyable.

Build Status Coverage Status Known Vulnerabilities Node License

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

new ApiTree(baseUrl, tree, options)

Returns: Object - The generated api tree

ParamTypeDescription
baseUrlStringThe 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.
optionsOptionsOptions 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

ParamTypeDefaultDescription
optionsOptionsThe override options for the final network call
validateBooleantrueSet 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

NameTypeDefaultDescription
options.methodString'GET'GET, POST, PUT, DELETE, etc.
options.modeString'same-origin'no-cors, cors, same-origin
options.cacheString'default'default, no-cache, reload, force-cache, only-if-cached
options.credentialsString'omit'include, same-origin, omit
options.headersString"application/json; charset=utf-8".
options.redirectString'follow'manual, follow, error
options.referrerString'client'no-referrer, client
options.bodyString | ObjectJSON.stringify is automatically run for non-string types
options.paramsString | ObjectQuery 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.unsafeBooleanDisable escaping of params in the url
options.transformRequestfunction | 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.transformResponsefunction | Array.<function()>Transform the response. Also accepts promises as return values for asynchronous work.
options.transformErrorfunction | 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)

ParamTypeDescription
urlStringThe url when the validation errors were found
errorsArray.<String>The list of the validation errors
schemaObjectThe 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

1.0.0

5 years ago

0.0.25

6 years ago

0.0.24

6 years ago

0.0.23

6 years ago

0.0.22

6 years ago

0.0.21

6 years ago

0.0.20

6 years ago

0.0.19

6 years ago

0.0.18

6 years ago

0.0.17

6 years ago

0.0.16

6 years ago

0.0.15

6 years ago

0.0.14

6 years ago

0.0.13

6 years ago

0.0.12

6 years ago

0.0.11

6 years ago

0.0.10

6 years ago

0.0.9

6 years ago

0.0.8

6 years ago

0.0.7

6 years ago

0.0.6

6 years ago

0.0.5

6 years ago

0.0.4

6 years ago

0.0.3

6 years ago

0.0.2

6 years ago

0.0.1

6 years ago