Services
What are services?
Services are made to reduce boilerplate code by partial passed payloads and hooks.
It allows you to move the most of your payload outside your main code and care only about business logic.
Also services may have global event listeners and some more good things.
How to use
Before use, you should install @apicase/services
package
It's quite simple:
import fetch from '@apicase/adapter-fetch'
import { ApiService } from '@apicase/services'
const SomeService = new ApiService({
adapter: fetch,
url: '/api/posts'
method: 'GET'
})
Then, you can call do requests using .doRequest()
method
// { "url": "/api/posts", "method": "GET", "query": { "userId": 1 } }
SomeService.doRequest({
query: { userId: 1 }
})
Global event listeners
It's just like default events but you apply it to services:
const ServiceWithLogger = new ApiService({
adapter: fetch
})
.on('done', logSuccess)
.on('fail', logFail)
Then, all requests are made using this service will have these events
Service requests store
Apicase services store currently running requests inside. And here some useful methods to work with queue:
Requests queue
If you need to keep correct requests order, you can do it using .pushRequest()
method
const Comments = new ApiService({
adapter: fetch,
url: '/api/comments'
})
Comments.pushRequest({ body: { text: 'Hello #1' } })
Comments.pushRequest({ body: { text: 'Hello #2' } })
Comments.pushRequest({ body: { text: 'Hello #3' } })
.pushRequest()
just adds a before
hook that awaits for previous request in queue.
If requests queue is empty, it just works like .doRequest()
Next request starts on finish
or cancel
events.
Only one request at the same time
So, imagine that we have authorization with refresh-token logic (if we get 401 error because of outdated token, we can try to refresh it using specific API route).
And, imagine that we have two or more parallel requests. Both of them are failed with 401 and try to refresh token.
Here we need to do only one request. So, services have .doSingleRequest()
method
const RefreshToken = new ApiService({
adapter: fetch,
url: '/api/auth/refresh',
method: 'POST',
hooks: {
before ({ next }) {
const payload = {
headers: {
token: localStorage.getItem('refresh_token')
}
}
next(payload)
}
}
}).on('done', res => { localStorage.setItem('token', res.body.token) })
RefreshToken.doSingleRequest()
RefreshToken.doSingleRequest()
RefreshToken.doSingleRequest()
RefreshToken.doSingleRequest()
RefreshToken.doSingleRequest()
.doSingleRequest()
creates a new request only if service queue is empty.
Otherwise, it returns currently running request that you can listen to.
Unique requests at the same time
And, if you need to prevent users from sending the same requests simultaneously, you can use .doUniqueRequest()
For example, you can block spamming some actions, if your application doesn't prevent it
const DeletePost = new ApiService({
adapter: fetch,
url: '/api/posts/:id',
method: 'DELETE'
})
DeletePost.doUniqueRequest({ params: { id: 1 } })
DeletePost.doUniqueRequest({ params: { id: 1 } })
DeletePost.doUniqueRequest({ params: { id: 1 } })
DeletePost.doUniqueRequest({ params: { id: 1 } })
DeletePost.doUniqueRequest({ params: { id: 1 } })
.doUniqueRequest()
creates a new request only if service queue doesn't contain currently running request with equal payload.
Otherwise, it returns this request (like .doSingleRequest()
)
Payload equality check is provided by
N.equals
Services inheritance
Services have .extend()
method that creates a new service with merged payloads and hooks/events concatenated.
const ApiRoot = new ApiService({
adapter: fetch,
url: '/api'
})
.on('done', successLogger)
.on('fail', failLogger)
.on('error', errorLogger)
/* This one will have fetch adapter, all events and url /api/posts */
const SomeApi = ApiService.extend({
url: 'posts'
})
Also, if adapter has his own merge strategy, it will be used here.
For example, fetch and xhr adapters allows you stack paths:
// url: /api
const Parent = new ApiService({
adapter: fetch,
url: '/api'
})
// url: /api/posts
const Child1 = Parent.extend({ url: 'posts' })
// url: /posts (because it starts with slash
const Child2 = Parent.extend({ url: '/posts' })
Services trees
You can use ApiTree
to define services list as JSON object:
Basic example
import fetch from '@apicase/adapter-fetch'
import { ApiTree } from '@apicase/services'
const api = new ApiTree(fetch, [
{ url: '/api', children: [
{ url: 'posts', children: [
{ name: 'getAllPosts', url: '', method: 'GET' },
{ name: 'createPost', url: '', method: 'POST' },
{ name: 'getOnePost', url: ':id', method: 'GET' },
{ name: 'updateOnePost', url: ':id', method: 'PUT' },
{ name: 'removeOnePost', url: ':id', method: 'REMOVE' }
] },
{ url: 'profile', children: [...] }
] }
])
api('getAllPosts').doRequest()
api('createPost').doRequest({ body })
Use service as a parent
You can also pass parent service instead of adapter. It may flatten structure
const Root = new ApiService({
adapter: fetch,
url: '/api'
})
const api = new ApiTree(Root, [
{ url: 'posts', children: [
{ name: 'getAllPosts', url: '', method: 'GET' },
{ name: 'createPost', url: '', method: 'POST' },
{ name: 'getOnePost', url: ':id', method: 'GET' },
{ name: 'updateOnePost', url: ':id', method: 'PUT' },
{ name: 'removeOnePost', url: ':id', method: 'REMOVE' }
] },
{ url: 'profile', children: [...] }
])
Shorter notation
api('someService', payload) === api('someService').doRequest(payload)