@ciklum/waas
v1.9.2
Published
lightweight library for working with api
Readme
WAAS
Web App Api Service (WAAS) - lightweight library that organizes your APIs. It is based on the next principles:
- event-based request handling
- services with unlimited inheritance
- adapters instead of concrete tools
Table of contents
Installation and usage
Set private registry by adding registry=http://npm.pp.ciklum.com/ to your .npmrc file
Install WAAS library by running
npm i -S @ciklum/waas
Now you can use assets provided by WAAS:
import { AxiosHttpAdapter, ApiService, REQUEST_EVENTS } from '@ciklum/waas'
ApiService
ApiService class allows you to move the most of your payload outside your main code and care only
about business logic.
HttpAdapters
Adapters tell ApiService how to work with some API. They should implement HttpAdapter interface
to provide executeRequest and cancelRequest methods.
The default adapter, provided by the package, is AxiosHttpAdapter - an axios wrapper.
ApiService configuration
ApiService configuration should be one of type:
- a service's URL string (parametrized URLs are allowed)
- an object with two keys:
url- a mandatory key of type 'string' that represents API URL (parametrized URLs are allowed)headers- an optional object with HTTP headers
ApiService creation
To create an ApiService you should provide HttpAdapter and configuration to the constructor:
import { AxiosHttpAdapter, ApiService } from '@ciklum/waas'
const axiosHttpAdapter = new AxiosHttpAdapter({
baseURL: 'https://ciklum.com', // optional axios' configuration parameter
})
const config = {
url: 'api',
headers: {
'content-type': 'application/json; charset=utf-8',
},
}
const rootApiService = new ApiService(axiosHttpAdapter, config)Service inheritance
ApiServices are extendable. You can create child ApiService, that extends parent's functionality
using extend method.
For example, you can create a service for endpoint that needs authorization token:
// We use rootApiService created in the previous example
const authConfig = {
headers: {
Authorization: `Bearer ${getToken()}`,
},
}
const protectedApiRoot = rootApiService.extend(authConfig)So all requests sent with protectedApiRoot contain Content-Type (from rootApiService) and
Authorization headers.
Then you can create a service to work with employees that needs authorization:
// We use protectedApiRoot created in the previous example
const employeeService = protectedApiRoot.extend('employee')All requests sent with employeeService contain Content-Type (from rootApiService) and
Authorization (from protectedApiRoot) headers. employeeService's URL is
https://ciklum.com/api/employee.
ApiService's extend method concatenates URLs from configuration (from parent to children)
and merges headers. If parent's header has the same name as child's header, child header's value
should be set as the value of the header.
An argument provided to extend method should be
- a string in case of changing URL only
- an object with the same keys as for
constructor, except all keys are optional.
Request API
ApiService provides request API:
get- send a GET request,post- send a POST request,put- send a PUT request,patch- send a PATCH request,delete- send a DELETE request.
Request configuration object could be provided to the method.
Configuration object with optional keys headers and params could be passed to the get and
delete methods.
Configuration object with optional keys headers, params and data could be passed to the
post, put and patch methods.
params represents an object, containing params for URL and query. For example:
// We use protectedApiRoot created in the previous example
const employeeConfig = {
url: 'employee/:employeeId',
}
const employeeService = protectedApiRoot.extend(employeeConfig)
employeeService.get({
params: {
employeeId: 123,
openedTab: 'general',
}
})
// Request URL: https://ciklum.com/api/employee/123?openedTab=generaldata field represents the data to be sent as the request body.
Additionally request could be cancelled by calling cancel method:
const employeeRequest = employeeService.get({ params: { employeeId: 123 } })
employeeRequest
.on('cancel', () => { console.log('Request cancelled') })
employeeRequest.cancel()
Events
Event types
WAAS provides six types of custom events:
before- special type of event that used to modify request's configuration on-the-flyrequest- event emitted before the request starts runningsuccess- event emitted on successful responseerror- event emitted on client errors (HTTP status code 4xx)failure- event emitted on all errors except client errors (server errors, network errors etc.)cancel- event emitted on cancelling the requestafter- event emitted after all
You can use events by their names or import REQUEST_EVENTS constant:
import { REQUEST_EVENTS } from '@ciklum/waas'
// These two subscriptions to 'success' event are equivalent
apiService.get()
.on('success', sucsessHandler)
.on(REQUEST_EVENTS.success, sucsessHandler)Events diagram

Event listeners
Method on provided by ApiService and request should be called to subscribe to the event.
Events request and cancel provide no payload to the listeners.
Events error and failure provide to the listeners HttpError that contains fields:
messagestatusstatusText.
Additionally these events provide function to retry request again.
employeeService.get({ params: { employeeId: 123 } })
.on('error', (error, retry) => {
if (error.status === 401) { // 401 Unauthorized
updateToken() // update token
return retry()
}
this.loggerService.error(error)
})Event success provides to the listeners an object of shape:
data- the response that was provided by the servermetastatus- the HTTP status code from the server responsestatusText- the HTTP status message from the server responseurl- the HTTP request URLheaders- the headers that the server responded with (all names are lower cased)contentType
Special event 'before'
This special event lets you modify request configuration "on-the-fly". It provides and object with
request configuration (payload key) and function to call the next before listener or to
launch the HTTP request (next key).
For example if the authorization token is returned asynchronously:
import { ApiService, AxiosHttpAdapter, mergeConfig, REQUEST_EVENTS } from '@ciklum/waas'
const apiRoot = new ApiService(new AxiosHttpAdapter(), { url: '/api' })
const protectedApiRoot= apiRoot.extend()
.on(REQUEST_EVENTS.before, ({ payload: prevConfig, next }) => {
getToken()
.then((token) => {
const nextConfig = mergeConfig(
prevConfig,
{
headers: {
Authorization: `Bearer ${token}`,
},
},
)
next(nextConfig)
})
})Important: next should be called with transformed request configuration. This value will be
passed to the next listener or to the adapter's executeRequest method. before events are emitted
synchronously: from the root service to the last child and then to the request's listeners. HTTP
request will be sent only after the last before listener executed.
Service event listeners
Services may have their own event listeners. It's just like default events but you apply it to services. All requests made with this service will have these events.
For example, we can implement logging:
import { ApiService, AxiosHttpAdapter,REQUEST_EVENTS } from '@ciklum/waas'
const apiRoot = new ApiService(new AxiosHttpAdapter(), { url: '/api' })
apiRoot
.on(REQUEST_EVENTS.error, loggerService.error)
.on(REQUEST_EVENTS.failure, loggerService.error)
.on(REQUEST_EVENTS.success, loggerService.info)So all requests made by apiRoot or by it's children will log error, failure and success
events.
Request event listeners
Request listeners are similar to the Service event listeners but connected to the HTTP request only.
employeeService.get({ params: { employeeId }})
.on('success', setEmployeeAction)
.on('error', setEmployeeErrorAction)
.on('failure', setEmployeeFailureAction)Events sequence
Listeners could be registered to the same event for ApiServices and request. They are emitted from
parent to child in order they were registered.
import { ApiService, AxiosHttpAdapter } from '@ciklum/waas'
const rootService = new ApiService(new AxiosHttpAdapter(), { url: '/api' })
rootService
.on('request', () => console.log('rootService::request:1'))
.on('request', () => console.log('rootService::request:2'))
const protectedConfig = {
headers: {
Authorization: getToken(),
},
}
const protectedService = rootService.extend(protectedConfig)
protectedService
.on('request', () => console.log('protectedService::request:1'))
.on('request', () => console.log('protectedService::request:2'))
const employeesConfig = {
url: 'employees'
}
const employeesService = protectedService.extend(employeesConfig)
employeesService
.on('request', () => console.log('employeesService::request:1'))
.on('request', () => console.log('employeesService::request:2'))
employeesService.get()
.on('request', () => console.log('employeesService.get::request:1'))
.on('request', () => console.log('employeesService.get::request:2'))The output for the example:
rootService::request:1
rootService::request:2
protectedService::request:1
protectedService::request:2
employeesService::request:1
employeesService::request:2
employeesService.get::request:1
employeesService.get::request:2Working with local copy of module
When you want to develop new features for module, this section will be helpful for you. Package linking is a two-step process which solves this need. You need npm link for this.
Steps:
Create global link. It will be available in folder were your npm modules are.
npm linkLinks to the global installation target from your front end app.
npm link @ciklum/waas
Publish a package
Publish a package to the registry by running
npm run build
npm publish