Quick start
Before start: for better understanding, I will talk about only http APIs using fetch adapter.
Just keep in mind that:
import { apicase } from '@apicase/core'
import fetch from '@apicase/adapter-fetch'
const doRequest = apicase(fetch)
Here I will show you core features of Apicase based on "Create post" example.
Events-based requests handling
Why events?
Apicase follows "business logic failures are not exception" principle
So we need at least 3 states for that:
/* Do you recognize axios? */
const payload = {
url: '/api/posts',
method: 'POST',
body: {
title: 'Hello, Apicase',
text: 'This is my favourite post'
}
}
const request = doRequest(payload)
/* Request is done */
request.on('done', postSubmitted)
request.onDone(postSubmitted)
/* Request is failed but it's OK */
request.on('fail', invalidData)
request.onFail(invalidData)
/* Unexpected error (may be code error or smth else) */
request.on('error', logError)
/* Bonus #4: cancel event */
request.on('cancel', undoSubmit)
You still can use it as Promise or with async/await
:
const { success, result } = await doRequest(payload)
if (success) {
/* resolved */
} else {
/* rejected */
}
– Looks easy
– But we have no authentification
– Just scroll down :)
Middlewares to change-on-fly/resolve/reject/retry API calls
How it works?
Apicase creates an async queue that consists of before hooks, request, resolve and reject hooks.
Look at this diagram:Hook is an asynchronous function that accepts payload and some callbacks.
Use next
to go to the next step (or next hook)
Use done
to call done hooks and resolve request
Use fail
to call fail hooks and reject request
Use retry
to start call again with another payload
Hooks are passed in hooks
property
Why it's important?
Still suffer with authentification? Just look at this:
const call = doRequest({
url: '/api/posts',
method: 'POST',
body: {
title: 'Hello, Apicase',
text: 'This is my favourite post'
},
hooks: {
/* Add client token */
before ({ payload, next }) {
/* Example uses Ramda */
next(R.assocPath(['headers', 'token'], localStorage.getItem('token'), payload))
},
/* If it's failed, try to refresh token */
fail ({ payload, result, retry, next }) {
if (result.status !== 401) return next(result)
const { success } = await refreshToken.doRequest()
if (success) {
retry(payload)
} else {
next(result)
}
}
}
})
/* Just listen to it */
call.on('done', postPublished)
call.on('fail', shitHappened)
– Easy-easy!
– Should I write this on each call?? And... wait, what is
refreshToken
?– You'll find out it soon :)
Services with unlimited inheritance
What is this?
Services allow you to partially put payload, hooks and all of request params to service and then call it.
You can extend services as much as you want.
Services also have global events listeners that allows you to do some cool things.
Example of services usage
Let's move our refresh-token logic into a separated file (e.g. api.js):
import fetch from '@apicase/adapter-fetch'
import { ApiService } from '@apicase/services'
/* Base options for all services */
export const BaseService = new ApiService({
adapter: fetch,
url: '/api'
}).on('error', globalErrorLogger)
/* Yep, it's here */
export const ReloginService = BaseService.extend({
url: 'auth/refresh',
hooks: {
before ({ payload, next }) {
next(R.assocPath(['headers', 'refresh'], localStorage.getItem('refresh_token'), payload))
}
}
})
ReloginService.on('done', res => {
localStorage.setItem('token', res.body.token)
})
/* API services with auth logic */
export const WithAuthService = BaseService.extend({
hooks: {
/* Add client token */
before ({ payload, next }) {
next(R.assocPath(['headers', 'token'], localStorage.getItem('token'), payload))
},
/* If it's failed, try to refresh token */
fail ({ payload, result, retry, next }) {
if (result.status !== 401) return next(result)
const { success } = await ReloginService.doRequest()
if (success) {
retry(payload)
} else {
next(result)
}
}
}
})
And then, our API request now looks like in the first code example:
import { WithAuthService } from './api'
WithAuthService.doRequest({
url: 'posts',
method: 'POST',
body: {
title: 'Hello, Apicase',
text: 'This is my favourite post'
}
})
Meta information
Also, you can pass meta
property to request. It will be available in hooks only (not in adapter).
You can use it to check whether request should use auth logic or not:
ApiService.doRequest({
url: 'posts',
method: 'POST',
meta: { authReqired: true },
body: {
title: 'Hello, Apicase',
text: 'This is my favourite post'
}
})
/* or even better */
const WithAuthService = ApiService.extend({
meta: { authRequired: true }
})
WithAuthService.doRequest(/* ... */)
It's not all surprises
Now, let's imagine that we have 2 requests running at the same time.
And both of them are failed because of outdated token.
What's now? Both of them will call refreshToken
service?
No. Apicase solves that problem for you.
Services contain queue
with all requests that are currently running.
And it has some methods to deal with it:
- doRequest - just do request. It will push it to queue and remove on finish
- pushRequest - do request after queue is finished
- doSingleRequest - do request only if there are no currently running requests. Otherwise, it will return currently running request that you can subscribe to
- doUniqueRequest - do request only if there are no currently running request with the same payload. Otherwise, it will return this request that you can subscribe to
So, to solve the problem, you can just replace doRequest
to doSingleRequest
. That's all you need to be happy :)
Adapters instead of concrete tools
What does it mean?
Apicase core doesn't know the ways to work with API. Instead, it just uses adapters.
It makes Apicase much more flexible, because It's not limited by http-only requests.
How adapter looks?
Adapter is an object with this structure:
const adapter = {
/* Initial response state */
createState: () => /* ... */,
/* Callback that accepts payload and resolve/reject methods */
callback ({ payload, resolve, reject }) { /* ... */ },
/* Method that prepares payload before start */
convert: payload => /* ... */,
/* Merge strategy for adapters payload (used in services */
merge: (from, to) => /* ... */
}
This object just should be passed to apicase
or ApiService
:
/* Just request */
apicase(adapter)(opts)
/* Service */
new ApiService({ adapter, ...opts })
As you can see at the start we used @apicase/adapter-xhr
What adapters we have for now?
For now, apicase has only fetch
and xhr
official adapters.
But we are planning to add websocket
support too and maybe some another cool things.