apiator
v0.0.6
Published
![The awaited response to the request for a shiny new API service.](cover.jpg)
Downloads
3
Maintainers
Readme
Apiator
Before Apiator
// Example of using Fetch API:
const fetchBooks = async () => {
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 🦾🤖
const fetchBooks = async () => {
const [err, books] = await api.get('books').send();
if (err) {
// Whoops.
return;
}
// Do stuff with `books`.
};
Core feautures include...
- No boilerplate, easy-as-it-gets configuration.
- In-built encoding.
- No try/catch needed, no unhandled promise warnings.
- Keep error handling close to the request.
And more...
- Send multiple requests at once.
- Painlessly abort any request.
- Use events for global loader, error handling etc.
- Customize every feature and add your own.
Install
yarn add apiator
- or -
npm install apiator
Quick start example
Setup your customized API service.
// api.js
import { createInstance } from 'apiator';
export default createInstance({
baseUrl: 'https://my-api.com/',
});
Use it anywhere in your code.
import api from './api';
const findBook = async (id = '123') => {
const [err, book] = await api.get('books/:id', {
params: {id},
}).send();
// => GET https://my-api.com/books/123
// ...
};
const findAllBooks = async () => {
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
// ...
};
const createBook = async () => {
const [err, book] = await api.post('books', {
body: { title: 'My Book' },
}).send();
// => POST `{"title": "My Book"}` to https://my-api.com/books
// ...
};
const updateBook = async (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/123
// ...
};
const deleteBook = async (id = '123') => {
const [err] = await api.delete('books/:id', {
params: {id}
}).send();
// => DELETE https://my-api.com/books/123
// ...
};
Table of contents
- Documentation
- Create API service with
createInstance()
orcreateInstanceWith()
- Update default configs with
set()
- Create requests with
get()
,post()
,put()
,delete()
orrequest()
- Send requests with
send()
- Abort requests with
abort()
- Core features
- Send and abort multiple requests with
requests()
- Handle events with
on()
andoff()
- Add and remove features with
use()
anddiscard()
- The request object
- The requests object
- Error types
- The error object
- Create API service with
- Deep dive
Documentation
Create API service with createInstance()
or createInstanceWith()
Apiator's flexible architecture lets you fully extend and manipluate its features. For your convenience, Apiator is shipped with commonly used core features that use the Fetch API for requests.
createInstance()
lets you create an API service with default features.
import { createInstance } from 'apiator';
const api = createInstance({
// Following are default configs...
// `baseUrl` will be automatically prepended to all your request URLs.
baseUrl: false,
// `contentType` feature is a shortcut for setting the 'Content-Type' header.
contentType: 'auto',
// `headers` lets you define any other headers.
headers: current => {},
// `opts` lets you set additional request options.
opts: current => {},
// `payload` carries any other info that you want to use for your request handling.
payload: undefined,
// `debug` feature automatically writes infos and warnings to your console.
debug: false,
// `format` feature specifies how the response shall be formatted.
format: 'auto',
// `fetch` lets you set the reference to Fetch API's `fetch` function.
// This is required on node.js. See 'Feature' section below for more.
fetch: fetch,
});
createInstanceWith()
lets you create an API service with any different bundle of feature than the default one:
import { createInstanceWith, CoreFeatures } from 'apiator';
import { MyFeatures } from 'somewhere';
const api = createInstanceWith([...CoreFeatures, ...MyFeatures]);
Both, features and configs, can be manipulated any time later on. You can also add or remove single features.
For more details on features see 'features' section of the documentation.
Update default configs with set()
You can extend your global configs with the set()
method. It will only overwrite the arguments you passed. The passed object of configs is equally structured as on the createInstance()
method (and as on any request as we will learn in a bit).
api.set({
baseUrl: 'https://my-api.com/authenticated/',
});
// `baseUrl` is now updated for all following requests.
// All other configs stay the same.
Create requests with get()
, post()
, put()
, delete()
or request()
Once your API service was created you can start sending a GET
, POST
, PUT
, DELETE
request with the get()
, post()
, put()
, delete()
method, respectively. They accept two parameters: a required url
and optional extraInput
. They return a request object with a send()
and abort()
method. Let's have a look:
Syntax:
const request = api[method](url [,extraInput])
Example:
// Create request:
const request = api.get('books', {
// Request configs:
query: { ID: 123 },
});
// Send request:
await request.send();
// Abort request:
await request.abort();
You can also use request()
, e.g. for sending other types of methods:
Syntax:
const request = api.request({
method,
url,
[...extraInput]
})
Example:
const request = api.request({
method: 'HEAD',
url: 'books/:id',
params: { id: 12 },
});
Send requests with send()
It is as simple as it gets:
const request = api.get('books');
const [err, books] = await request.send();
We can rewrite the above example in one line by immediately calling send()
:
const [err, books] = await api.get('books').send();
In this example send()
will send a GET request to https://your-base-url.com/books
and return a [err, books]
duplet. The first argument of the duplet holds an error object if something went wrong during sending. The second argument holds the output of the request if everything worked out fine.
send()
is an async function so don't forget to to use it with await
.
NOTE: Duplets are a core feature that can be disabled, meaning that you can also use your service with try/catch error handlers instead. See duplet
section for more details.
Abort requests with abort()
Any request object holds an abort()
method that will cancel the request for you.
let request;
const async abortBooksRequest = () => {
if (request) await request.abort();
};
const sendBooks = async () => {
request = api.get('books');
const [err, books] = await request.send();
if (err) {
// `err` will be defined if aborted.
if (!err.is.ABORT_ERROR) {
// This way you can only show an error warning if
// it was NOT due to aborting.
}
}
// ...
request = null;
};
Core features
baseUrl
Automatically prependeds to all your request URLs. This is optional and can be overwritten for every request.
Default: false
Accepts: false
, any URL string
// Set `baseUrl` globally for all following requests:
api.set({
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 per request:
const [err, books] = await api
.get('books', {
baseUrl: 'https://another-base-url.com/',
})
.send();
// => https://another-base-url.com/books
// Disable baseUrl per request:
const [err, books] = await api
.get('https://full-url.com/books', {
baseUrl: false,
})
.send();
// => https://full-url.com/books
// Disable gloablly:
api.set({ baseUrl: false });
params
Replaces :
-escaped placeholders in URL with a given value per request.
Default: undefined
Accepts: flat 'key/value' object
const [err, book] = await api
.get('books/:id', {
params: { id: '123' },
})
.send();
// => GET https://my-api.com/books/123
query
Appends encoded query string to the URL per request.
Default: undefined
Accepts: flat 'key/value' object
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
NOTE: Only flat arrays are supported as shown in the example above.
body
Sets the request body.
Default: undefined
Accepts: any type of data
const [err, book] = await api
.post('books/:id', {
params: {id: '123'}
body: { title: 'New title' }, // <- The post body.
})
.send();
contentType
Manages the 'Content-Type' header.
Default: 'auto'
Accepts: 'auto'
, false
, any other content type string
You can either pass the 'Content-Type'-value, e.g. 'audio/mpeg'
, you can set it to 'auto'
(default value) or disable it with false
.
The default value 'auto'
will let the feature try to set the right 'Content-Header' based on your provided input.
// Set feature:
api.set({
contentType: 'auto', // Default option.
// contentType: false, // Feature is now disabled.
// contentType: 'audio/mpeg', // Set the type directly.
});
// Send form data:
const body = new FormData(); // Feature will recognize form data.
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' }; // JSON will be recognized.
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();
format
Defines how the service should format the response body of the server before passing it back to you.
Default: 'auto'
Accepts: 'auto'
, false
, any Fetch API's body method such as 'json'
, 'text'
, 'arrayBuffer'
, 'blob'
, 'formData'
'auto'
will try to find an appropriate format based on the Content-Type
header of the server's response:
| 'Content-Type' header of response | Used Fetch API format method | | --------------------------------- | ---------------------------- | | application/json | json | | multipart/form-data | formData | | text/* | text | | other MIME types | blob |
You can overwrite the default format
with every request:
const [err, htmlString] = await api
.get('my-html-endpoint', {
format: 'text',
})
.send();
false
will not format anything and returns the native Fetch API response object instead:
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()
Defines default headers on init by returning them in the headers()
function.
Default: undefined
Accepts: flat 'header/value' object
api.set({
headers: () => ({
'X-App-Name': 'MyApp',
}),
});
You can alter the default headers for every request by setting a headers()
function. It receives 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 all default headers by ignoring the defaults.
const [err, books] = await api
.get('books', {
headers: () => ({ 'X-My-Header': '123' }),
// => { 'X-My-Header': '123' }
})
.send();
opts()
Lets you define additional options that will be passed on to the Fetch API request. See Fetch API docs for a full list of available options.
NOTE: if you pass options such as body
, headers
and method
that are handled by other features you will overwrite them.
You can globally change opts()
for every following request by defining an opts()
function.
api.set({
opts: () => ({
cache: 'force-cache',
}),
});
You can alter requests options by passing a opts
function.
const [err, books] = await api
.get('books', {
opts: current => ({
...current,
cache: 'no-cache',
}),
})
.send();
payload
Adds additional data to request and event context.
Default: undefined
Accepts: anything: string, object, number, ...
// Log request unless disabled.
const onResult = ({ context }) => {
const { payload } = context;
if (!payload.loggingDisabled) {
// Log the request...
}
};
api.on('result', onResult);
const [err] = await api
.post('analytics', {
body: { event: 'opened_settings' },
payload: { loggingDisabled: true },
})
.send();
duplet
Returns an error/response duplet instead of throwing errors.
Default: true
Accepts: true
, false
// Default behavior:
const [err, res] = await api.get('books').send();
// When disabled:
api.set({
duplet: false,
});
try {
const books = await api.get('books').send();
} catch (err) {
// ...
}
debug
Automatically logs requests/errors and their context in your console. Especially useful for non-browser environments like React Native and others.
Default: false
Accepts: true
, false
Send and abort multiple requests with requests()
fetch
Lets you set the Fetch API fetch
function used for handling the request. This is only required if your environment does not support Fetch API.
Default: undefined
Accepts: A Fetch API conform fetch
implementation.
This is necessary when you are in a node.js environment. Here is an example for using node-fetch
:
import * as fetch from 'node-fetch'; // <-- Import the Fetch API implementation.
import { createInstance } from 'apiator';
const api = createInstance({
baseUrl: 'https://my-api.com/',
fetch, // <-- Set the reference to package.
});
Send and abort multiple requests with requests()
You can send multiple requests with the requests()
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.requests([
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.
}
// ...
await 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 will contain an undefined
item for the request that failed.
Handle events with on()
and off()
Available events are send
and result
.
You can register events with the on()
method.
const onSend = async ({ request }) => {
// Do stuff...
};
const onResult = async ({ error, response, request }) => {
// Not all event data is always available. For instance
// `error` is undefined if everything worked out fine.
};
api.on('send', onSend);
api.on('result', onResult);
// Check out the 'deep dive' section for a practical example.
You can remove events with the off()
method.
api.off('send', onSend);
api.off('result', onResult);
Add and remove features with use()
and discard()
Features enable you to manipulate all inputs and outputs. In fact, Apiator is using its own features under the hood for its core functionality.
You can add and remove features any time. Take a look at this example:
import { CoolFeature } from 'somewhere';
import { DupletFeature } from 'apiator';
api.use(CoolFeature); // That's it!
api.discard(CoolFeature); // Aaand it's gone!
api.discard(DupletFeature); // No more duplet results.
The request object
| Property | Type | Description |
| -------------- | -------------- | ------------------------------------------------------------------------------------------------ |
| getInput()
| async function | Returns the data that was passed for creating the request. |
| getContext()
| async function | Returns the data that is used for handling the request, e.g.: method
, url
, headers
, body
|
| send()
| async function | Sends the request. |
| abort()
| async function | Aborts the request. |
The requests object
| Property | Type | Description |
| --------- | -------------- | ------------------------------------------ |
| list
| array | The list of all contained request objects. |
| send()
| async function | Sends all request. |
| abort()
| async function | Aborts all request. |
Error types
| Error type | Description |
| --------------- | ------------------------------------------------------------------ |
| SCRIPT_ERROR
| Something went wrong during execution. Check error message. |
| HTTP_ERROR
| The server returned a HTTP error code. |
| NETWORK_ERROR
| The server could not be reached. Most likely due to no connection. |
| ABORT_ERROR
| The request was aborted. |
The error object
| Property | Type | Description |
| ------------ | ------ | ---------------------------------------------------------------------------------------------------------------- |
| type
| string | One of the error types. |
| is
| object | Convenience object to check for error type, e.g. if (error.is.HTTP_ERROR) ...
|
| message
| string | Contains more details on what went wrong. |
| request
* | object | The request object that relates to this error. *Not available on SCRIPT_ERROR
. |
| response
* | object | The unformatted response (i.e. Fetch API response object). *Not available on SCRIPT_ERROR
or NETWORK_ERROR
. |
Deep dive
Handle loaders and errors via events
A standard use case is 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 = ({ request }) => {
const { payload } = await request.getContext();
// Check if loader was disabled in the payload...
if (payload?.noLoader) return;
// Code that shows a loader...
};
const onResult = async ({ error: err, response, request }) => {
let errorMessage
let defaultErrorMessage = 'Something went wrong.'
try {
const context = await request.getContext();
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.ABORT_ERROR) return;
// Define general error message.
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.
errorMessage = await response.text();
} else if (err.is.NETWORK_ERROR) {
// Show message that user should check internet connection.
errorMessage = 'No internet connection.';
} else {
// Show default error message.
errorMessage = defaultErrorMessage;
}
}
} catch {
// Show default error message.
errorMessage = defaultErrorMessage;
}
if (errorMessage){
// Code that shows an error warning with `errorMessage`...
}
};
api.on('send', onSend);
api.on('result', onResult);
Here is an example of how it works:
const createBook = async () => {
const [err, book] = await api
.post('books', {
body: { title: 'New book' },
payload: { noErrorWarning: true, successMessage: 'Book was created.' },
})
.send();
if (err) return;
// We do not need to care about handling the error message since
// the event handler is showing a warning automatically.
// In this example, however, we also told the event handler to show a
// success message and refrain from showing a warning for this request.
};
Handle authorization with headers
In some cases you will want to update the headers globally to authorize the user in all following requests:
const login = async () => {
const [err, res] = await api
.post('login', {
body: { email: '[email protected]', pw: 'a_secure_pw' },
})
.send();
if (err) {
// Login failed
} else {
api.set({
headers: current => ({
...current,
Authorization: 'Bearer ' + res.token,
});
})
// The 'Authorization' header will from now on automatically
// be set in all following requests.
}
}
const logout = () => {
api.set({
headers: current => {
delete current.Authorization
return current
}
});
// From now on 'Authorization' header will be gone on all
// following requests.
}
Omit error or response
You can send a one time attempt without caring for success and response by not assigning a duplet.
await api
.post('analytics', {
body: { action: 'opened_book', meta: { book_id: '123' } },
})
.send();
If you don't really care about the response 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.
}
Do not omit the error if you need to work with the response! Of course you don't need to do anything with the error really but at least check if there was one.
const showBooksMaybe = async () => {
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.
Custom features
Features are still undergoing development. We will provide detailed documentation once it is ready. If you like play around with features here is a full example of an intercepting feature:
const CoolFeature = () => {
const defaults = {
test: '123',
};
return {
name: 'custom/cool',
set: args => {
if (args.test) {
defaults.test = args.test;
}
},
intercept: {
request: {
input: input => {
// Do stuff...
return input;
},
context: (context, { input }) => {
context.test = input?.test || defaults.test;
return context;
},
response: (response, { context, input }) => {
// Do stuff...
return response;
},
},
},
on: {
send: ({ request }) => {
// Do stuff.
},
result: ({ error, response, request }) => {
// Do stuff.
},
},
};
};
Here is an example of a feature that disables the baseUrl
if a full URL was passed:
import { createInstance, Feature, BaseUrlFeature } from 'apiator';
const api = createInstance({
baseUrl: 'https://my-api.com/',
});
/**
* Disables base URL automatically if we have a full URL.
*/
const StreetSmartBaseUrlFeature: Feature = {
intercept: {
request: {
input: input => {
if (input.url?.startsWith('http')) {
// Disable baseUrl if it seems to be a full URL string.
input.baseUrl = false;
}
return input;
},
},
},
};
api.use(StreetSmartBaseUrlFeature);
// Now...
api.get('https://other-domain.com/test').send();
// ...will be send to 'https://other-domain.com/test'
// and NOT to 'https://my-api.com/https://other-domain.com/test'.
// It saves you from taking care of disabling baseUrl manually:
api.get('https://other-domain.com/test', { baseUrl: false }).send();
Intercept the response
You can intercept the response by writing a tiny feature.
/*
* Checks if response is wrapped in 'data' and returns accordingly.
*/
const InterceptResponseFeature = {
name: 'custom/intercept-response',
intercept: {
request: {
response: response => {
if (response.data) {
return response.data;
}
return response;
},
},
},
};
api.use(InterceptResponseFeature);