JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • 0
  • Score
    100M100P100Q22229F
  • 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. DO NOT USE (YET).

A response to the request for a great API service send from the future.

Before Apiator

// Example of using Fetch API:
async fetchBooks() {
  try {
    const response = await fetch('http://my-api.com/books', {
      method: 'GET'
    })
    if (!response.ok) {
      // Whoops.
      return
    }
    const books = await response.json()
    // Do stuff with `books`...
  } catch(err) {
    // Whoops.
  }
}

Now with Apiator

async fetchBooks() {
  const [err, books] = await api.get('books').send()
  if (err) {
    // Whoops.
    return
  }
  // Do stuff with `books`.
}
  • No boilerplate, easy-as-it-gets configuration.
  • In-built URL and data encoding.
  • No try/catch needed, no unhandled promise warnings.
  • Keep the error handling close to the API call.

And more...

  • Send multiple requests at once.
  • Painlessly abort any request.
  • Use events for global loader etc.
  • Customize every feature and add your own.

Install

yarn add apiator

- or -

npm install apiator

Quick start example

Init your customized API service.

// api.js
import Apiator from 'apiator';
const api = new Apiator({
  baseUrl: 'https://my-api.com/',
});
export default api;

Use it anywhere in your code.

import api from './api';

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

async findBooks() {
  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
  // ...
};

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

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

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

Documentation

Initialize API features

You can init the API by creating an instance and passing optional feature options. 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 "Features" section below.

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

  /**
   * `contentType` feature is a shortcut for setting the 'Content-Type' header of the request.
   * 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` feature 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 options are handled by Apiator features.
   *
   * 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` feature.
   *
   * Example: (current) => ({ ...current, 'My-Header': '123'  })
   */
  headers() {},

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

  /**
   * `debug` feature specifies if infos and warnings shall automatically be written to your
   * console for faster 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 sending other types of request methods you can use .call().

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

Features

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').send();
// => https://your-base-url.com/books

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

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

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

Content Type

The contentType feature manages the 'Content-Type' header for you.

// Set feature on init:
const api = new Apiator({
  contentType: 'auto', // Default option.
});

// Send form data:
const body = new FormData();
formData.append('title', 'New book');
const [err, books] = await api.post('books', { body }).send();
// => 'Content-Type' header is set to 'multipart/form-data'.

// Send JSON:
const body = { title: 'New book' };
const [err, book] = await api.post('books', { body }).send();
// => 'Content-Type' header is set to 'application/json'.

// Implicitly set content type:
const [err, song] = await api
  .post('songs', { body, contentType: 'audio/mpeg' })
  .send();

// Update `contentType` feature globally for all following requests:
api.set({
  contentType: false,
}); // Feature is now disabled.

Format

The format feature 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 native Fetch API response object. You can do it by simply setting format to false. See example:

const [err, response] = await api
  .get('my-html-endpoint', {
    format: false, // Disable `format` feature.
  })
  .send();
if (!err) {
  // Access the native Fetch API response:
  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' }),

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

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

asnyc logout() {
  api.set({
    headers: current => {
      delete current.Authorization
      return current
    }
  });
}

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

Options

The opts feature lets you define which default options will be passed on to the Fetch API request. [https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#supplying_request_options](See Fetch API docs) for a full list of available options.

NOTE: the options body, headers and method will be ignored since they are handled by Apiator features.

const api = new Apiator({
  opts: () => ({
    cache: 'force-cache',
  }),
});

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();

You can also globally change opts for every following request. Here is an example of adding another default option:

api.set({
  opts: current => ({
    ...current,
    credentials: 'omit',
  });
})

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.is.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 (NOT SUPPORTED YET)

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 handle yourself, this is just an example of how payload could be used to control event behavior.

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 = async ([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.is.ABORTED_ERROR) return;
    let errorMessage = 'Something went wrong.';
    if (err.is.HTTP_ERROR) {
      // If an HTTP_ERROR occured `response` will be available.
      // Assuming your server returns a message on what went wrong
      // you can extract it from the response.
      try {
        errorMessage = await response.text();
      } catch {}
    }
    // 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);

Custom features (NOT SUPPORTED YET)

You will be able to control everything that is happening through features. In fact Apiator is using its own core features under the hood.