@ciklum/waas
v1.9.2
Published
lightweight library for working with api
Downloads
3
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=general
data
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:
message
status
statusText
.
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 servermeta
status
- 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 ApiService
s 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:2
Working 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 link
Links 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