Node.js Custom Plugin Lib
This library is available on GitHub
In addition to standard components (e.g., CRUD), you can create your own microservices that encapsulate ad-hoc logics that are autonomously developed and integrated. Your microservice receives HTTP requests, its cycle of use and deploy is managed by the platform.
A microservice encapsulates ad-hoc business logics that can be developed by any user of the platform and potentially in any programming language. However, to facilitate its adoption and use, Mia-Platform team has created the Mia Service Node.js Library, a library in node.js, based on fastify. Using Mia Service Node.js Library
it is possible to create your own microservice by following these steps:
- Implement HTTP routes handlers
- Handling different clients and users with different roles
- Sending requests to other services on the platform
- Implementing PRE and POST decorators
The remaining part of this guide will describe how to develop, test and deploy your microservice in Node.js to the platform ecosystem using the Mia Service Node.js Library
library.
Installation and Bootstrap
Install from Marketplace template
From the microservices area it is possible to add a new service starting from the Node.js template that is already set up and configured to use the Mia Service Node.js Library
.
Check out the Marketplace Documentation for further information on how to install and bootstrap a service from a Marketplace template.
Manual installation on a new repository
To start developing your custom service, first make sure to have Node.js installed on your local machine. Then, initialize a Node project with the following commands:
mkdir my-custom-plugin
cd my-custom-plugin
npm init -y
Open the package.json
file and modify the name
, description
fields according to your needs.
We suggest setting the value of the version
field to 0.0.1
to get started.
Mia Service Node.js Library
can be installed withnpm
, along with its fastify-cli
dependency, necessary for bootstrapping and executing the microservice.
npm i --save @mia-platform/custom-plugin-lib
The library can be used to instantiate an HTTP server.
To start developing with Mia Service Node.js Library
the variables need to be available to the Node.js process environment:
USERID_HEADER_KEY
= miauseridUSER_PROPERTIES_HEADER_KEY
= miauserpropertiesGROUPS_HEADER_KEY
= miausergroupsCLIENTTYPE_HEADER_KEY
= client-typeBACKOFFICE_HEADER_KEY
= isbackofficeMICROSERVICE_GATEWAY_SERVICE_NAME
= Microservice-gateway
Among these variables, the most interesting is MICROSERVICE_GATEWAY_SERVICE_NAME
, which contains the host name (or IP address) pointing to the microservice-gateway
and is used for internal communication with other services in your project namespace. This implies that MICROSERVICE_GATEWAY_SERVICE_NAME
allows the user to configure their microservice to call a specific microservice inside their Mia-Platform project. For example
MICROSERVICE_GATEWAY_SERVICE_NAME = "microservice-gateway"
To instantiate the HTTP server you can paste the following snippet to the service entrypoint (typically the index.js
file).
const customPlugin = require('@mia-platform/custom-plugin-lib')()
module.exports = customPlugin(async service => {
// at the GET request on /status/alive route, respond with the JSON object { "status": "ok" }
service.addRawCustomPlugin(
'GET',
'/status/alive',
async (request, reply) => ({
status: 'ok'
})
)
})
To start the microservice, simply edit the package.json
file in this way
//...
"scripts": {
// ...
"start": "fastify start src/index.js",
//...
You can now run the command npm start
and open a browser at the url http://localhost:3000/status/alive
, to get a response.
Factory exposed by Mia Service Node.js Library
Mia Service Node.js Library
exports a function which creates the infrastructure to accept the definition
of routes and decorators. This code snippet illustrates its use.
const customPlugin = require('@mia-platform/custom-plugin-lib')()
module.exports = customPlugin(function(service) { })
The argument passed to the customPlugin
function is a declaration function which accepts as argument an object that allows the user
to define routes and decorators.
Routes
Mia Service Node.js Library
allows to define the behavior of the microservice in response to HTTP requests, in a declarative style.
For this purpose, the addRawCustomPlugin
method is used as shown below:
service.addRawCustomPlugin(httpVerb, path, handler, schema)
The addRawCustomPlugin
method accepts the following parameters:
httpVerb
- the HTTP verb of the request (e.g.,GET
)path
- the route path (e.g.,/status /alive
)handler
- function that handles the incoming request. It must respect the same interface defined in the documentation of the handlers of fastify.schema
- definition of the request and response data schema. The format is the one accepted by fastify
Example
const customPlugin = require('@mia-platform/custom-plugin-lib')()
// behavior in response to the query
async function aliveHandler(request, reply) {
return { status: 'ok' }
}
// response schema
const aliveSchema = {
response: {
200: {
type: 'object',
properties: {
status: { type: 'string' },
},
},
},
}
// wiring and route declaration
module.exports = customPlugin(async function(service) {
service.addRawCustomPlugin('GET', '/status/alive', aliveHandler, aliveSchema)
})
Handlers
A handler
is a function that respects the handler interface of fastify and
accepts a Request and a Reply.
In addition to the fastify Request interface, Mia Service Node.js Library
decorates the Request instance with information related to the Platform. This information includes the id
of the user currently logged in, its groups, the type of client that performed the HTTP request and specifies whether the request comes from the CMS.
Furthermore, the Request instance is also decorated with methods that allow HTTP requests to be made to other services deployed on the Platform.
User and Client Identification
The instance of Request
(the first argument of a handler) is decorated with the following functions:
getUserId
- returns the user's id if logged in ornull
if notgetUserProperties
- returns the properties of the logged user if logged in, otherwise returnsnull
getGroups
- returns an array containing strings that identify the groups to which the logged in user belongsgetClientType
- returns the type of client that performed the HTTP requestisFromBackOffice
- returns a boolean indicating whether the HTTP request comes from the CMS
Example
async function helloHandler(request, reply) {
// access to the user id (passed as header inside the platform)
return `Hello ${request.getUserId()}`
}
Context
Inside the handler scope it's possible to access the service fastify instance using this
.
Example
module.exports = customPlugin(async function(service) {
// decorating custom environment variable
service.decorate('decoratedService', service.config.DECORATED_SERVICE)
// creating custom route
service.addRawCustomPlugin('GET', '/hello', helloHandler)
})
async function helloHandler(request, reply) {
// `this` references the fastify context
this.decoratedService // access custom fastify decoration
this.config["LOG_LEVEL"] // access configured environment variable
}
Endpoint queries and Platform services
Both from the Request
(the first argument of a handler) and from the Service
(the first argument of the declaration function) it is possible to obtain a proxy object to call other endpoints or services running in the Platform project. For example, if you need to connect to a CRUD, you have to use a Proxy towards the crud-service
. These proxies are already configured to automatically include all necessary platform specific headers.
There are two types of proxies, returned by two distinct functions:
getServiceProxy(options)
- proxy passing throughmicroservice-gateway
getDirectServiceProxy(serviceName, options)
- direct proxy to the service
The fundamental difference between the two proxies is that the first one triggers all the logics that are registered in microservice-gateway
,
while the second does not. For example, if a resource exposed by the CRUD service is protected by ACL, this protection will come
bypassed using the direct proxy.
For the direct proxy it is necessary to specify the serviceName
of the service to be queried. The port cannot be specified in the serviceName
but must be passed in the port
field of the options
parameter. In the case of getServiceProxy
, you should not specify the name of the service as it is implicitly that of the microservice-gateway
.
The options
parameter is an object with the following optional fields:
port
- an integer that identifies the port of the service to be queriedprotocol
- a string that identifies the protocol to use (onlyhttp
andhttps
are supported, default value ishttp
)headers
- an object that represents the set of headers to forward to the serviceprefix
- a string representing the prefix of the service call path
Potentially, the getDirectServiceProxy
method allows you to also query services outside the platform. In this case, however, it is necessary to bear in mind that the platform headers will be automatically forwarded.
Both proxies, by default, forward the four mia-headers to the service called. To do this, the following environment variables must be present:
- USERID_HEADER_KEY
- GROUPS_HEADER_KEY
- CLIENTTYPE_HEADER_KEY
- BACKOFFICE_HEADER_KEY
The values of these variables will specify the key of the four mia-headers.
In addition, other headers of the original request can also be forwarded to the named service. To do this it is necessary to define an additional environment variable, ADDITIONAL_HEADERS_TO_PROXY
, whose value must be a string containing the keys of the headers to be forwarded separated by a comma.
Both proxies expose the following methods:
get(path, querystring, options)
post(path, body, querystring, options)
put(path, body, querystring, options)
patch(path, body, querystring, options)
delete(path, body, querystring, options)
The parameters of these methods are:
path
- a string that identifies the route to which you want to send the requestbody
- optional, the body of the request which can be:querystring
- optional, an object that represents the querystringoptions
- optional, an object that admits all theoptions
listed above for thegetServiceProxy
andgetDirectServiceProxy
methods (which will eventually be overwritten), plus the following fields:returnAs
- a string that identifies the format in which you want to receive the response. It can beJSON
,BUFFER
orSTREAM
. DefaultJSON
.allowedStatusCodes
- an array of integers that defines which status codes of the response are accepted. If the response status code is not contained in this array, the promise will be rejected. If this parameter is omitted, the promise is resolved in any case (even if the interrogated server answers 500).isMiaHeaderInjected
- Boolean value that identifies whether Mia's headers should be forwarded in the request. Defaulttrue
.
Example
// Example of a request towards `tokens-collection` endpoint passing through Microservice Gateway
async function tokenGeneration(request, response) {
const crudProxy = request.getServiceProxy()
const result = await crudProxy
.post('/tokens-collection/', {
id: request.body.quotationId,
valid: true
})
// ...
}
// Example of a request towards `tokens-collection` endpoint bypassing Microservice Gateway
async function tokenGeneration(request, response) {
const crudProxy = request.getDirectServiceProxy('crud-service')
const result = await crudProxy
.post('/tokens-collection/', {
id: request.body.quotationId,
valid: true
})
// ...
}
PRE and POST decorators
Through Mia Service Node.js Library
it is possible to declare PRE and POST decorators. From a conceptual point of view, a decorator
of (1) PRE or (2) POST is a transformation applied from microservice-gateway
to (1) a request addressed
to a service (original request) or (2) to the reply (original reply) that this service sends to
caller. From a practical point of view, decorators are implemented as HTTP requests in POST
to a specified microservice. In order to use the decorators it is important to configure them also in the console. More information are available in the Decorators docs.
The declaration of a decorator using Mia Service Node.js Library
occurs in a similar way to the declaration of a route
service.addPreDecorator(path, handler)
service.addPostDecorator(path, handler)
Example
module.exports = customService(async function(service) {
// Examples of a PRE and a POST decorator definition using `Mia Service Node.js Library`.
service.addPreDecorator('/is-valid', handler) // PRE
service.addPostDecorator('/is-valid', handler) // POST
})
Effective received HTTP request
PRE and POST decorator receive a POST HTTP request from microservice-gateway
with the following json body:
PRE decorator schema
{
"method": "GET",
"path": "/the-original-request-path",
"headers": { "my": "headers" },
"query": { "my": "query" },
"body": { "the": "body" },
}
POST decorator schema
{
"request": {
"method": "GET",
"path": "/the-original-request-path",
"query": { "my": "query" },
"body": { "the": "body" },
"headers": { "my": "headers" },
},
"response": {
"body": { "the": "response body" },
"headers": { "my": "response headers" },
"statusCode": 200,
}
}
Access and Handling of the Original Request With Pre decorator
The utility functions exposed by the Request
instance (the first parameter of a handler) are used to access the original request
getOriginalRequestBody()
- returns the body of the original requestgetOriginalRequestHeaders()
- returns the headers of the original requestgetOriginalRequestMethod()
- returns the original request methodgetOriginalRequestPath()
- returns the path of the original requestgetOriginalRequestQuery()
- returns the querystring of the original request
In addition to the methods described above, the Request
instance exposes an interface to modify the original request, which will come
forwarded by microservice-gateway
to the target service. This interface is accessible using the Request
instance method
changeOriginalRequest
which returns an object by the following methods:
setBody(newBody)
- change the body of the original requestsetHeaders(newHeaders)
- modify the headers of the original requestsetQuery(newQuery)
- modify the querystring of the original request
To leave the original request unchanged, the leaveOriginalRequestUnmodified
function is used instead.
In all cases the PRE decorator handler must return either the object returned by changeOriginalRequest
or the object returned byleaveOriginalRequestUnmodified
.
Example of PRE Decorators
// this PRE decorator reads a header of the original request
// and converts it to a querystring parameter
async function attachTokenToQueryString(request, response) {
const originalHeaders = request.getOriginalRequestHeaders()
const token = originalHeaders['x-token']
if(token) {
return request
.changeOriginalRequest()
.setQuery({ token })
}
// in case the token was not specified in the headers
// the original request is left unchanged
return request.leaveOriginalRequestUnmodified()
}
Access and Manipulation of the Original Response With POST Decorator
As with the original request, the Request
instance (the first parameter of a handler) is decorated with useful functions for
also access the original service original response information (these are available only for POST decorators)
getOriginalResponseBody()
- returns the body of the original responsegetOriginalResponseHeaders()
- returns the headers of the original responsegetOriginalResponseStatusCode()
- returns the status code of the original response
In addition to the functions described above, the Request
instance exposes an interface to modify the original response, which will come
forwarded by microservice-gateway
to the calling client. This interface is accessible using the function
changeOriginalResponse
concatenating it with invocations to functions
setBody (newBody)
- change the body of the original answersetHeaders (newHeaders)
- modify the headers of the original answersetQuery (newQuery)
- modify the querystring of the original answersetStatusCode (newStatusCode)
- change the status code of the original response
To leave the original answer unchanged, instead, the leaveOriginalResponseUnmodified
function is used.
In all cases the decorator handler must return either the object returned by changeOriginalResponse
or the object returned byleaveOriginalResponseUnmodified
.
Example of POST Decorators
// this POST decorator reads a token from the original reply body
// and converts it into a header.
async function attachTokenToHeaders(request, response) {
const originalBody = request.getOriginalResponseBody()
const token = originalBody.token
if (token) {
return request
.changeOriginalResponse()
.setHeaders({
...request.getOriginalResponseHeaders(),
"x-token": token,
})
}
// in case the token is not present in the body of the answer
// the original answer remains unchanged
return request.leaveOriginalResponseUnmodified()
}
Decorator Chain Stop
Through microservice-gateway
it is possible to define a sequencer of decorators, so that the output of a
single decorator is passed to the next decorator. In special cases, however, it may be necessary
interrupt the chain and return a response to the original caller.
For this purpose, the Request
instance (the first argument of a handler) exposes the function
abortChain (finalStatusCode, finalBody, finalHeaders)
Example
// this PRE decorator verifies that a token is present
// in the header of the original request. If it is not present
// break the chain by returning an error 401 to the client
async function validateToken(request, response) {
const headers = request.getOriginalResponseHeaders()
const token = headers['x-token']
if(!token) {
return request.abortChain(401)
}
return request.leaveOriginalRequestUnmodified()
}
Route Diagram and Documentation
A microservice developed with Mia Service Node.js Library
automatically also exposes the documentation of the routes and decorators that
are implemented. The documentation is specified using the OpenAPI 2.0 standard
and exhibited through Swagger. Once the microservice is started, its documentation can be accessed at
route http://localhost:3000/documentation. The specification of the request scheme
and responses to a route must conform to the format accepted by
Fastify.
Example
const schema = {
body: {
type: 'object',
properties: {
someKey: { type: 'string' },
someOtherKey: { type: 'number' }
}
},
querystring: {
name: { type: 'string' },
excitement: { type: 'integer' }
},
params: {
type: 'object',
properties: {
par1: { type: 'string' },
par2: { type: 'number' }
}
},
headers: {
type: 'object',
properties: {
'x-foo': { type: 'string' }
},
required: ['x-foo']
}
response: {
200: {
type: 'object',
properties: {
responseKey: { type: 'string' },
otherResponseKey: { type: 'number' }
}
}
},
}
Environment Variables
Like any service on the Platform, a microservice must be set up to be released in different environments, starting from the local environment (the development machine) to development, test and production environments. The differences between various environments are managed through the mechanism of environment variables.
In addition to the mandatory ones, using Mia Service Node.js Library
it is possible to define other environment variables based on
needs of the single microservice, to then access them and use their values in the code of the handlers. For the definition yes
use the JSON schema format.
If the correct set of environment variables is not supplied to the microservice, the microservice does not start by returning in output which environment variable is missing.
Example
// the env var VARIABLE will be available at runtime
const serverSchema = {
type: 'object',
required: ['VARIABLE'],
properties: {
VARIABLE: {
type: 'string',
},
},
}
const customPlugin = require('@mia-platform/Mia Service Node.js Library')(serverSchema)
module.exports = customPlugin(async service => {
// in the config it is possible to find the declared env vars
const VARIABILE = service.config.VARIABILE
service.addRawCustomPlugin(
'GET',
'/variable',
async function (request, reply) {
return {
// it is possible to access to the configuration through `this.config`
secret: this.config.VARIABLE,
}
}
)
})
Testing
Mia Service Node.js Library
is built on fastify and therefore integrates with testing tools
made available by the framework. A complete example of this type of test is present online in the repository of
Mia Service Node.js Library
on GitHub.
Integration and Unit test
The testing of a microservice built with Mia Service Node.js Library
can be performed at multiple levels of abstraction. One of
Possibility is to use a technique called fake http injection for which it is possible to simulate
receiving an HTTP request. In this way, all the microservice logic is exercised from the HTTP layer to the handlers and
this is an example of Integration Testing.
Example Integration Test
In the example below the test framework Mocha.
'use strict'
const assert = require('assert')
const fastify = require('fastify')
const customPlugin = require('@mia-platform/custom-plugin-lib')()
const index = customPlugin(async service => {
service.addRawCustomPlugin(
'GET',
'/status/alive',
async (request, reply) => ({
status: 'ok'
})
)
})
const createTestServer = () => {
// silent => trace for enabling logs
const createdServer = fastify({ logger: { level: 'silent' } })
createdServer.register(index)
return createdServer
}
describe('/status/alive', () => {
it('should be available', async () => {
const server = createTestServer()
const response = await server.inject({
url: '/status/alive',
})
assert.equal(response.statusCode, 200)
})
})