JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • 0
  • Score
    100M100P100Q19645F
  • License MIT

Package Exports

  • apiator

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (apiator) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

Apiator

WORK IN PROGRESS. DONT USE (YET).

Introducing Apiator, headache-free API service built upon Fetch API.

Install

yarn add apiator

- or -

npm install apiator

Quick start example

// @api.js
// Create your API service instance.
import Apiator from 'apiator';
const api = new Apiator({
  baseUrl: 'https://my-api.com/',
});
export default api;
// @somewhere in your code...
import api from './api';

async sendBook(id = '123') {
  const [err, book] = await api.get('books/:id', {
    params: {id},
  }).send();
  // => GET https://my-api.com/books/123

  if (err) {
    // Show warning...
    return;
  }

  // ...or do something with `book`.
};

async postBook() {
  const [err, newBook] = await api.post('books', {
    body: {title: 'My New Book'},
  }).send();
  // => POST https://my-api.com/books (with `{"title": "My New Book"}` in post body)

  // ...
};

async updateBook(id = '123') {
  const [err, newBook] = await api.post('books', {
    params: {id}
    body: {title: 'My Updated Title'},
  }).send();
  // => POST https://my-api.com/books (with `{"title": "My New Book"}` in post body)

  // ...
};

let cancelBooksCall = () => {}

async getBooks() {
  const [err, books] = await api.get('books', {
    query: {orderBy: 'latest', authors: ['max', 'john']},
  }).send();
  // => GET https://my-api.com/books?orderBy=latest&authors[]=max&authors[]=john

  // ...
};

Documentation

Init with default configs

You can init the API by creating an instance. All configs are optional. The values we pass below represent the default config. The configs can be overwritten in every single request you make. For more details and examples check out the configs section below.

const api = new Apiator({
  /**
   * `baseUrl` will be automatically prepended to all your request URLs.
   *
   * Example: 'https://my-api.com/'
   */
  baseUrl: false,

  /**
   * `contentType` is a shortcut for setting the 'Content-Type' header.
   * When set to `false` no 'Content-Type' will be set. When set to 'auto' the service will automatically
   * try to find the right 'Content-Type' header based on your request data.
   *
   * Example: 'application/json'
   */
  contentType: 'auto',

  /**
   * `format` specifies how the response body shall be formatted before it is returned to you.
   * When set to `false` the raw send API Response will be returned. See documentation for more details.
   */
  format: 'json',

  /**
   * `opts` lets you return Fetch API options that will be passed on to the `fetch` function for every
   * request, see:
   *  -  https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
   *  -  https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters
   *
   * NOTE: If you pass the `method`, `body` or `headers` option it will have no effect since
   * these features are handled by the service.
   *
   * Example: opts(opts) { opts.mode = 'no-cors'; return opts; }
   */
  opts(): {},

  /**
   * `headers` lets you define default headers that will be added to every request.
   *
   * NOTE: If your set 'Content-Type' header here it will be ignored since this is handled by the
   * `contentType` option below.
   *
   * Example: headers(headers) { headers['X-Api-Key'] = 'xyz123'; return headers; }
   */
  headers() {},

  /** `payload` can be any optional additional information that will be avaiable for you at any
   * request handling.
   */
  payload: undefined,

  /**
   * `debug` specifies if infos and warnings shall automatically be written to your
   * console for easier debugging. Set to `false` in production.
   */
  debug: false,

  /**
   * `fetch` lets you set the reference to Fetch API's `fetch` function. You only need to change
   * this when you are working in an environment where the Fetch API is not supported by
   * default. This is the case for node.js for instance. See documentation for more details.
   */
  fetch: fetch,
});

Send requests

You can send a GET, POST, PUT, DELETE request with the .get(), .post(), .put(), .delete() method, respectively. They accept two parameters: a required pseudoUrl and optional request specific configs. They return a request object with a send and abort method. Let's have a look:

// Syntax:
const request = api[method](pseudoUrl [,config])
const [error, output] = await request.send()
// ...and in one line:
const [error, output] = await api[method](pseudoUrl [,config]).send()
// The simplest possible example:
const request = api.get('books');
const [err, books] = await request.send();
// ...and in one line:
const [err, books] = await api.get('books').send();

It will send a GET request to https://your-base-url.com/books and return a response duplet [err, books]. The first argument of the duplet holds an error object if something went wrong during the request. The second argument holds the output of the request if everything worked out fine.

For programically setting the request methods you can use .call().

await api.call('HEAD', 'books').send();

Configs

Base URL

baseUrl will be automatically prepended to all your request URLs. This is optional and can be overwritten for every request.

const api = new Apiator({
  baseUrl: 'https://your-base-url.com/',
});

// Use default baseUrl:
const [err, books] = await api.get('books');
// => https://your-base-url.com/books

// Overwrite baseUrl:
const [err, books] = await api.get('books', {
  baseUrl: 'https://another-base-url.com/',
});
// => https://another-base-url.com/books

// Disable baseUrl:
const [err, books] = await api.get('https://full-url.com/books', {
  baseUrl: false,
});
// => https://full-url.com/books

// Update baseUrl globally for all following requests:
api.baseUrl = 'https://your-base-url.com/authorized/';

Format

The format option lets you define how the service shall format the response body of the server before passing it back to you.

The value needs to equal the one of Fetch API's Body methods: json, text, arrayBuffer, blob, formData.

const api = new Apiator({
  format: 'json',
});

You can overwrite the default format with every request.

const [err, htmlString] = await api
  .get('my-html-endpoint', {
    format: 'text',
  })
  .send();

In some scenerios you might want to work with the raw Fetch API response object. You can do it by simply setting format to false.

const [err, response] = await api
  .get('my-html-endpoint', {
    format: false,
  })
  .send();
if (!err) {
  const htmlString = await response.text();
}

Headers

You can define default headers on init by returning them in the headers function.

const api = new Apiator({
  headers: () => ({
    'X-App-Name': 'MyApp',
  }),
});

You can alter the default headers for every request by setting a headers function. It will pass an object with the current default headers that you can change and return back.

// Example of extending the default headers.
const [err, books] = await api
  .get('books', {
    headers: current => ({
      ...current,
      'X-My-Header': '123',
    }),
    // => { 'X-App-Name': 'MyApp', 'X-My-Header': '123' }
  })
  .send();

// Example of replacing default headers.
    headers: () => ({ 'X-My-Header': '123' }),

If you'd like to work with Fetch API Headers interface, here is an example:

const [err, books] = await api
  .get('books', {
    headers(current) {
      headers = new Headers(current);
      headers.append('X-My-Header', '123');
      return headers;
    },
  })
  .send();

In some cases you will want to update the headers globally, i.e. after a user was sucessfully authorized.

const [err, output] = await api
  .post('login', {
    body: { email: 'test@test.com', pw: 'a_secure_pw' },
  })
  .send();
if (err) {
  // Login failed
} else {
  api.headers(current => ({
    ...current,
    Authorization: 'Bearer ' + output.token,
  }));
}

The 'Authorization' header will from now on automatically be set in all following requests.

Options

You can alter Fetch API requests options by passing a opts function.

const [err, books] = await api
  .get('books', {
    opts: current => ({
      ...current,
      cache: 'no-cache',
    }),
  })
  .send();

Abort requests

The request object holds an abort method. If you require to cancel the request you can refrence to abort method and call it when needed.

let request

async sendBooks() {
  request = api.get('books');
  const [err, books] = await request.send();
  if (err) {
    // `err` will be defined if aborted.
    if (err.type === api.ABORTED_ERROR) {
      // You might not want to do anything.
    } else {
      // You might want to show a warning.
    }
  }
  // ...
  request = null
}

abortBooksRequest() {
  if (request) request.abort()
}

Send and abort multiple requests

You can send multiple requests with the .all() method. It works in accordance with sending a single request. The result will contain a duplet [errors, outputs] with a potential array of errors and the outputs.

const requests = api.all([
  api.get('books/:id', {
    params: { id: '123' },
  }),
  api.get('authors', {
    query: { books: ['123'] },
  }),
]);
const [errs, [book, authors]] = await requests.send(); // Sends all requests.
if (errs) {
  // An array of one or more error objects.
}

// ...
requests.abort(); // Aborts all requests at once.

NOTE: errors will only contain errors that occured, so it can be shorter than the amount of requests. Otherwise it is undefined. outputs will always be equal to the amount of requests but contain an undefined item for the request that failed.

Omit the result entirely

You can send a one time attempt without caring for success and output by not assigning a duplet.

await api
  .post('analytics', {
    body: { action: 'opened_book', meta: { book_id: '123' } },
  })
  .send();

Omit the output

If you don't really care about the output you can simply not assign it.

const [err] = await api
  .post('books', {
    body: { title: 'New book' },
  })
  .send();
if (err) {
  // Mmm, maybe we should try again. Let's show our user a "retry" button.
}

Omit the error

Well no, if you need to work with the output you can't! Of course you don't need to do anything with the error really but at least check there was one.

async showBooksMaybe() {
  const [err, books] = await api.get('books').send();
  if (err) return
  // Ok, now we can do something with `books`...
}

But what if we flip the duplet around? We know it is tempting but the motivation is to force you to actively think about what to do with a potential error so you won't forget it when you really shouldn't have.

Events

Available events are send, result.

A standard example could be a global loader and error handling that also checks for your custom payload options. Remember, payload is a feature that you need to design yourself, this is just an example of a payload implemetation.

const onSend = context => {
  const { payload } = context;
  // Check if loader was disabled in the payload...
  if (payload?.noLoader) return;

  // Code that shows a loader...
};

const onResult = ([err, output], context) => {
  const { payload } = context;

  // Code that hides the loader...

  if (!err) {
    if (payload.successMessage) {
      // Code that shows `payload.successMessage`.
    }
  } else {}
    // Handle error.
    // Check if error warning was disabled in the payload...
    // ...and do not show warning if user aborted.
    if (payload?.noErrorWarning || err.type === api.ABORTED_ERROR) return;
    let errorMessage;
    if (err.type === api.HTTP_ERROR) {
      // Assuming your server returns a message on what went wrong
      // you can access it in `output`.
      errorMessage = err.output;
    }
    if (err.type === api.SCRIPT_ERROR) {
      errorMessage = 'Something went wrong.';
    }
    // Code that shows an error warning with `errorMessage`.
  }
};

You can register events with the .on() method.

api.on('send', onSend);
api.on('result', onResult);

You can remove events with the .off method.

api.off('send', onSend);
api.off('result', onResult);

Interceptors (NOT SUPPORTED YET)

You can manipulate incoming and outcoming data through interceptors.

This feature is currently undergoing discussion and developement.

useCallInterceptors

const convertQueryBools = context => {
  let { method, query } = context;
  if (method === api.GET) {
  }
  return context;
};

//...
api.useCallInterceptors([convertQueryBools]);

useResultInterceptors

Allows you to manipulate what will be returned by the API service.

const wrapOutput = ([err, output], context) => {
  if (!err) {
    output = { data: output };
  }
  return [err, output];
};

//...
api.useResultInterceptors([wrapOutput]);

Constants

We recommend to always check against these constants for best compability for future versions:

// context.method
api.GET;
api.POST;
api.PUT;
api.DELETE;
api.HEAD;

// error.type
api.HTTP_ERROR;
api.SCRIPT_ERROR;
api.ABORTED_ERROR;