Skip to content

effervescentia/apion

Repository files navigation

apion CircleCI branch npm code style: prettier docs

JavaScript API client generator

Inspired by theon but designed to be a simpler alternative.

Usage

Install the package apion using npm, yarn or your preferred package manager. For example:

npm install apion

Import in your code:

import apion from 'apion';
// or import * as apion from 'apion'
import { json } from 'apion/helpers';
const apion = require('apion');
const { json } = require('apion/helpers');

Examples

import apion from 'apion';
import { json } from 'apion/helpers';

const apiConfg = apion
  .config()
  .use(json)
  .url('https://example.com/api');

const profile = apion.action('profile', id => api => api.query(`id=${id}`));

const auth = apion
  .group('auth', token => ({ token }))
  .path('auth')
  .headers((prev, { token }) => ({ ...prev, Authorization: token }))
  .nest(profile);

const login = apion
  .action('login', (user, password) => api => api.body({ user, password }))
  .post();

const root = apion
  .group('root')
  .use(apiConfig)
  .nest(auth)
  .nest(login);

const client = root.build();

// implementation

const {
  body: { token },
} = await client.login('[email protected]', '123456');
/*
method: 'POST'
url: 'https://example.com/api/login'
headers: { 'content-type': 'application/json' }
body: '{"email":"[email protected]","password":"123456"}'
*/

const { body: profile } = await cient.auth(token).profile('abc123');
/*
method: 'GET'
url: 'https://example.com/api/auth/profile?id=abc123'
headers: { 'content-type': 'application/json', authorization: <token> }
body: -
*/

API

Methods exposed on apion.

Create a ConfigBuilder with an optional name

  • name String: Sets the name of the configuration for reference in debugging.
import apion from 'apion';

const myConfig = apion
  .config('my_config')
  .url('https://example.com/api')
  .headers({ 'my-header': 'some value' });

const isRunning = apion
  .action('isRunning')
  .use(myConfig)
  .path('_health')
  .build();

await isRunning();
/*
method: 'GET'
url: 'https://example.com/api/_health'
headers: { 'my-header': 'some value' }
body: -
*/

Create a GroupBuilder with a name and an optional constructor for dynamically setting context for nested builders

  • name String: Sets the name of the group which by default is used as the name of the property added when nesting under another group or action.
  • constructor Function: A callback used to dynamically set the context for nested builders.
import apion from 'apion';
import { json } from 'apion/helpers';

const login = apion
  .action('login', (email, password) => api => api.body({ email, password }))
  .post();

const publicClient = apion
  .group('public')
  .use(json)
  .url('https://example.com/api/public')
  .nest(login)
  .build();

await publicClient.login('[email protected]', '123456');
/*
method: 'POST'
url: 'https://example.com/api/public/login'
headers: { 'content-type': 'application/json' }
body: '{"email":"[email protected]","password":"123456"}'
*/

constructor

Used to dynamically set properties in the context for nested builders.

const profile = apion.action('profile').path('profile');

const authClient = apion
  .group('auth', token => ({ token }))
  .url('https://example.com/api/admin')
  .headers((prev, { token }) => ({ ...prev, Authorization: token }))
  .nest(profile)
  .build();

await authClient('XXX-000-AAA').profile();
/*
method: 'GET'
url: 'https://example.com/api/admin/profile'
headers: { authorization: 'XXX-000-AAA' }
body: -
*/

It can also be used to set more complex request properies by returning a callback which receives the containing GroupBuilder.

const profile = apion.action('profile').path('profile');

const userClient = apion
  .group('user', (user, password) => api =>
    api.path(user).headers({ Authorization: password })
  )
  .url('https://example.com/api/admin/user')
  .nest(profile)
  .build();

await userClient('greg123', '456789').profile();
/*
method: 'GET'
url: 'https://example.com/api/admin/user/greg123/profile'
headers: { authorization: '456789' }
body: -
*/
  • name String: Sets the name of the action which by default is used as the name of the property added when nesting under another group or action.
  • constructor Function: A callback used to dynamically set the context for nested builders.
  • requestBuilder RequestBuilder: An instance of a RequestBuilder used to inject a simple builder pattern when constructing complex request bodies.

requestBuilder

Automatically injects a builder to be used for creating requests.

import apion from 'apion';
import { json } from 'apion/helpers';

const requestBuilder = apion
  .builder()
  .with('range', (start, end) => ({ start, end }))
  .with('interval')
  .with('timezone');

const getTimeseries = apion
  .action('timeseries', requestBuilder)
  .use(json)
  .url('https://example.com/api/timeseries')
  .post();

await getTimeseries(req =>
  req
    .range(10, 200)
    .interval(25)
    .timezone('UTC')
);
/*
method: 'POST'
url: 'https://example.com/api/timeseries'
headers: { 'content-type': 'application/json' }
body: '{"start":10,"end":200,"interval":25,"timezone":"UTC"}'
*/

Create a RequestBuilder with an optional formatter

  • formatter Function: Sets a formatting callback to construct the final request body.
import apion from 'apion';

const searchRequestBuilder = apion
  .builder()
  .with('query')
  .with('attributes', (...attributes) => ({ attributes }))
  .with('range', (start, end) => ({ range: { start, end } }));

/*
creates request bodies of the form:

{
  query: any,
  attibutes: any[],
  range: { start: any, end: any }
}
*/

formatter

Used to format the generated request body.

import apion from 'apion';

const searchRequestBuilder = apion
  .builder(payload => ({ type: 'search', payload }))
  .with('query')
  .with('pageSize')
  .with('sort', (attribute, order) => ({ sort: { attribute, order } }));

/*
creates request bodies of the form:

{
  type: 'search',
  payload: {
    query: any,
    pageSize: any,
    sort: { attribute: any, order: any }
  }
}
*/

The base class for ConfigBuilder, GroupBuilder and ActionBuilder. Contains utility methods for managing request properties.

url(url | transform)

  • url String: An full URL to set for the request, overrides the existing url.
  • transform Function: A callback to transform the previous value to the next value.
import apion from 'apion';

apion.config().url('https://example.com');
transform

A callback which is passed the previous value and the context object and should return the next value.

import apion from 'apion';

apion
  .config()
  .ctx({ path: 'some/path' })
  .url('https://example.com')
  .url((prev, ctx) => `${prev}/${ctx.path}`);
  • port Number: A port to set for the request, overrides the existing port.
  • transform Function: A callback to transform the previous value to the next value. (see transform)
import apion from 'apion';

apion.config().port(8080);
  • query String: A query to set for the request, overrides the existing query.
  • transform Function: A callback to transform the previous value to the next value. (see transform)
import apion from 'apion';

apion.config().query('x=y&a=10');
  • path String: A path to set for the request, overrides the existing path. If the path starts with a forward slash (/) then the whole path will be replaced, otherwise it will be added to the existing path.
  • transform Function: A callback to transform the previous value to the next value. (see transform)
import apion from 'apion';

apion
  .config()
  .url('https://example.com/api')
  .path('my/path'); // 'https://example.com/api/my/path'

apion
  .config()
  .url('https://example.com/api')
  .path('/my/path'); // 'https://example.com/my/path'
  • method String: A method to set for the request, overrides the exting method.
apion
  .config()
  .url('https://example.com/api')
  .method('POST');

apion also exports its own Method object for easy use

import { Method } from 'apion';

apion
  .config()
  .url('https://example.com/api')
  .method(Method.POST);

Sets the request method to GET.

Sets the request method to POST.

Sets the request method to PATCH.

Sets the request method to PUT.

Sets the request method to DELETE.

  • headers Object: A headers to set for the request, overrides the existing headers.
  • transform Function: A callback to transform the previous value to the next value. (see transform)
import apion from 'apion';

apion.config().headers({ 'content-type': 'application/json' });
  • body Object: A body to set for the request, overrides the existing body.
  • transform Function: A callback to transform the previous value to the next value. (see transform)
import apion from 'apion';

apion.config().body('[email protected]:123456');

Add a callback to transform the request body before sending it.

  • formatter Function: A callback to transform the request body before sending it.
import apion from 'aption';

apion
  .config()
  .formatter(body => (typeof body === 'string' ? body : JSON.stringify(body)));

Add a callback to transform the response body after receiving it.

  • parser Function: A callback to transform the response body after receiving it.
import apion from 'aption';

apion
  .config()
  .parser(body => (typeof body === 'string' ? JSON.parse(body) : body));

Used to construct re-usable updates to context and request properties. Inherits all methods from HTTPBuilder.

Update the context by merging in a new object (uses the same logic as Object.assign()).

  • obj Object: An object that will be merged with the existing context.
import apion from 'apion';

apion.config().ctx({ id: 123 });

Add the configuration from the provided builder or the result of the createBuilder function to the builder instance.

  • builder ConfigBuilder: Configuration from this builder will be inherited by the builder instance.
  • createBuilder Function: A function which accepts the context object and should return an instance of a ConfigBuilder
import apion from 'apion';

const config = apion.config().url('https://example.com');

apion.action('isRunning').use(config);
createBuilder

Used to have full control over dynamically setting request properties based on the context object.

import apion from 'apion';

apion
  .action('login', user => ({ user }))
  .use(({ user }) =>
    user === 'admin'
      ? apion
          .config()
          .path('admin')
          .headers({ Authorization: 'skip' })
      : apion.config().path(`user/${user}`)
  )
  .url('https://example.com/api');

Like use() except that configuration transformations are pushed to the top of the builder's inheritance chain.

import apion from 'apion';

const config = apion.config().url('https://example.com');

apion
  .action('isRunning')
  .use(apion.config().path('api'))
  .inherit(config);

A utility method to help apply re-usable chained methods to a builder instance.

import apion from 'apion';

const configure = api =>
  api.url('https://example.com/api').headers({ 'my-header': 'some value' });

// hard to chain
const test = configure(apion.action('test'));

// easy to chain
const test = apion.action('test').pipe(configure);

Clone a builder instance in order to create an extended version of it.

import apion from 'apion';

const config = apion.config().url('https://example.com');

const extended = config.extend().path('api');

Used to construct groups of nested builders. Inherits all methods from ConfigBuilder.

Override or set the builder's constructor to be used to provide dynamic context properties.

  • constructor Function: A callback used to dynamically set the context for nested builders (see constructor).
import apion from 'apion';

apion.group('auth').ctor(token => ({ token }));

Add a nested builder to a GroupBuilder or ActionBuilder.

  • name String: A name to override the name on the builder.
  • builder GroupBuilder | ActionBuilder: A builder to be nested under this builder instance. If no name is provided, the name provided when constructing the builder will be used.

Build an API client with this builder as the root node.

  • fetch Function: A replacement for the fetch instance used under the hood when making requests (defaults to cross-fetch).

Used to construct action builders for sending requests over the network. Inherits all methods from GroupBuilder.

Override or set the builder's constructor to be used to provide dynamic context properties.

  • constructor Function: A callback used to dynamically set the context for nested builders (see constructor).
  • requestBuilder RequestBuilder: An instance of a RequestBuilder used to inject a simple builder pattern when constructing complex request bodies (see requestBuilder).

Used to construct chainable request builders to simplify the construction of complex request bodies.

Add a chainable method with the name name to the generated request builder. A custom handler can be provided to control how the final value will be set in the request body.

  • name String: The name of the method that will be generated. If no handler is provided then it will also be the name of the resulting property on the request body. The name build cannot be used as it is reserved for use by the library.

  • handler Function: A callback to merge the parameters of the builder method into the request body object.

import apion from 'apion';

apion
  .builder()
  .with('query')
  // which is the same as
  .with('query', query => ({ query }));

// with multiple arguments
apion.builder().with('range', (start, end) => ({ range: { start, end } }));

Inherit the property handlers from another RequestBuilder, useful if you have multiple endpoints that share common request properties.

  • builder RequestBuilder: The builder to inherit properties from.
import apion from 'apion';

const rootBuilder = apion
  .builder()
  .with('id')
  .with('parameters', (...parameters) => ({ parameters }));

const inheritingBuilder = apion
  .builder()
  .use(rootBuilder)
  .with('session', id => ({ session: { id } }));

Generate the client-facing request builder class. The only pre-determined property on the builder is build() which will return the final request body.

import apion from 'apion';

const SpecialRequestBuilder = apion
  .builder()
  .with('name')
  .with('properties', (...properties) => ({ properties }));

const builder = new SpecialRequestBuilder();

builder
  .name('ben')
  .properties('lastUpdated', 'id', 'title')
  .build();
/*
{
  name: 'ben',
  properties: ['lastUpdated', 'id', 'title']
}
*/

Helpers

import apion from 'apion';
import { json } from 'apion/helpers';

// JSON.stringify() request body
// JSON.parse() response body
// set 'Content-Type: application/json' header
apion.config().use(json);