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
- Init with default configs
- Send requests
- Features
- Abort requests
- Send and abort multiple requests
- Omit the result entirely
- Omit the output
- Omit the error
- Events
- Interceptors (NOT SUPPORTED YET)
- Constants
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.