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.
No boilerplate, easy-as-it-gets configuration.
In-built parameter encoding.
No try/catch needed, no unhandled promise warnings.
Keep the error handling close to the API call.
Send multiple requests at once.
Painlessly abort any request.
Table of contents
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;