Skip to main content
Version: 13.x (Current)

Create a custom Microservice

In this tutorial, we will see how to create and develop a new microservice starting from one of the templates, offered by the Mia-Platform Marketplace, up to the actual coding of the endpoints. We will also deep dive into the functionalities that Mia-Platform Console offers to the developer during the whole software lifecycle.

What we will Build

We will develop a simple microservice that exposes a route returning the shipping cost depending on whether the user is a new customer or not. Moreover, the values returned by the endpoint will be based on the value of some environment variables.

We will also create a CRUD service to store the data related to customers and orders, and we will see how to communicate between different services, the one we are developing and the curd-service.

In the final part, we will go over how to check the logs and status of the services, and how to release the final version into production using semantic versioning.

Prerequisites

Before starting, we will assume that, you already have a clean project in Mia-Platform Console. To know how to create a project on Mia-Platform Console, read this guide.

The project must:

  • Be integrated with a deploy pipeline;
  • Have an ingress route with "api-gateway" as service;
  • Be aware of have configured your project domain to be called. If the project links have been configured, you can find them in the "Environments" section of the "Project Overview" area, under the "Application" column;
  • Have a properly configured API Portal.
  • Have a properly configured CRUD Service.

Better to have:

  • Some familiarity with API and REST concepts.
  • Some familiarity with at least one among the following:
    • Node.js and Fastify,
    • Java and Spring Boot.
tip

If your are using a Mia-Platform Console in PaaS and the project has been created using the "Mia-Platform Basic Project Template", the project is already configured as needed.

Create CRUD collections

Firstly, open your project, and create the following MongoDB CRUD collections:

  • Create customers CRUD collection, download the schema here or copy it from below;

    Click to see the customers schema:


  • Create orders CRUD collection, download the schema here or copy it from below.

    Click to see the orders schema:


info

You don't know how to import CRUD schema? Give a look here

Expose CRUD endpoints

Once the collections have been created, they need to be exposed through an Endpoints.

  1. Create customers CRUD endpoint:

    • Base path: /customers;
    • Type: in the dropdown menu, select "CRUD";
    • CRUD Base Path: in the dropdown menu, select /customers.
  2. Create orders CRUD endpoint:

    • Base path: /orders;
    • Type: in the dropdown menu, select "CRUD";
    • CRUD Base Path: in the dropdown menu, select /orders.
info

You don't know how to import CRUD schema? Give a look here

Create the Microservice from a Marketplace template

A Microservice can be created starting from existing templates. Starting from a Mia-Platform template or using Mia-Platform service libraries has many advantages and allows your microservices to be compliant with all best practices (such as pipelines, logging, health routes, etc.).

In the Marketplace you can find a list of Examples and Templates powered and supported by Mia-Platform that allows you to set up microservices with a tested and pre-defined function.

We will create a microservice from Node.js template:

  1. Go to Microservices section and click Create a Microservice;

  2. Search the template Node.js Template, then select it;

  3. Now enter the following information:

    • Name: shipping-cost;
  4. In the detail, go to the Microservice configuration section and set Memory Request Limit to 150 and CPU Limit to 100.

Now you have to expose the microservice:

  1. Go to Endpoints section and click Create new endpoint. Then enter the following information:

    • Base path: /shipping-cost-service;
    • Type: in the dropdown menu select "Microservice";
    • Microservice: in the dropdown menu select shipping-cost.
  2. Click Create button;

  3. Commit the changes.

info

If you have difficulty creating the microservice or exposing it, check out the first tutorial!

Let's code!

The newly created microservice repository can be accessed from the microservice details page, by clicking on View Repository button in the upper right-corner. The Clone button allows you to easily see SSH and HTTPS urls to clone the repository in your local machine or to directly open it with your preferred editor.

Now, we want to implement the route that calculates the shipping cost.

Since Mia-Platform strongly encourages the use of Test Driven Development, we will start by writing the tests. This approach allows the developer to focus on the needed requirements and, in addition, makes the code easier to evolve over time and durable to mistakes.

Write the tests

Let's start writing a simple test for GET /shipping-cost route. It will receive an email in the query string as the customer identifier and return the correct shipping cost.

First of all, open the test/index.test.js file and add, after the require instructions, the lines to include Nock and disable real HTTP requests. We will use this module to mock calls to the CRUD simulating responses from HTTP requests.

After that let's write the real test, replacing the "Insert your tests" comment with the highlighted code:

"use strict";

const t = require("tap");
const lc39 = require("@mia-platform/lc39");
const nock = require("nock");
nock.disableNetConnect();

async function setupFastify(envVariables) {
const fastify = await lc39("./index.js", {
logLevel: "silent",
envVariables,
});
return fastify;
}

t.test("shipping-cost", async (t) => {
// silent => trace for enabling logs
const fastify = await setupFastify({
USERID_HEADER_KEY: "userid",
GROUPS_HEADER_KEY: "groups",
CLIENTTYPE_HEADER_KEY: "clienttype",
BACKOFFICE_HEADER_KEY: "backoffice",
MICROSERVICE_GATEWAY_SERVICE_NAME: "microservice-gateway.example.org",
});

t.teardown(async () => {
await fastify.close();
});

t.test("Default customer shipping cost", async (t) => {
const NEW_CUSTOMER_SHIPPING_COST = 5.99;
const DEFAULT_SHIPPING_COST = 10;
const CRUD_BASE_URL = "http://crud-service";
const getCustomerScope = nock(CRUD_BASE_URL)
.get(`/customers/`)
.query({ email: mockedCustomer.email })
.reply(200, [mockedCustomer]);

const getOrderScope = nock(CRUD_BASE_URL)
.get(`/orders/count`)
.query({ customerId: mockedCustomer._id })
.reply(200, 1);

const response = await fastify.inject({
method: "GET",
url: "/shipping-cost",
query: {
customerEmail: mockedCustomer.email,
},
});
t.equal(response.statusCode, 200);
t.same(JSON.parse(response.payload), {
shippingCost: DEFAULT_SHIPPING_COST,
});

// Check if the mocked requests have been really called by the handler
getOrderScope.done();
getCustomerScope.done();
});

t.end();
});

In this test we tested only the "happy path", that is the case in which returning the correct shipping cost for a new customer. To achieve full coverage, we need to also test different inputs and the return of the default shipping cost. You can implement the full test suite on your own or copy the complete test code below:

Tests to get full coverage:

/tests/index.test.js
"use strict";

const t = require("tap");
const lc39 = require("@mia-platform/lc39");
const nock = require("nock");
nock.disableNetConnect();

async function setupFastify(envVariables) {
const fastify = await lc39("./index.js", {
logLevel: "silent",
envVariables,
});
return fastify;
}

const NEW_CUSTOMER_SHIPPING_COST = 5.99;
const DEFAULT_SHIPPING_COST = 10;
const CRUD_BASE_URL = "http://crud-service";

t.test("shipping-cost", async (t) => {
// silent => trace for enable logs
const fastify = await setupFastify({
USERID_HEADER_KEY: "userid",
GROUPS_HEADER_KEY: "groups",
CLIENTTYPE_HEADER_KEY: "clienttype",
BACKOFFICE_HEADER_KEY: "backoffice",
MICROSERVICE_GATEWAY_SERVICE_NAME: "microservice-gateway.example.org",
});

t.teardown(async () => {
await fastify.close();
});

t.test("GET /shipping-cost", (t) => {
const customerId = "1";

const mockedCustomer = {
_id: customerId,
email: "customer@email.com",
};

t.test("404 - No customer found", async (t) => {
const getCustomerScope = nock(CRUD_BASE_URL)
.get(`/customers/`)
.query({ email: mockedCustomer.email })
.reply(404, {});

const response = await fastify.inject({
method: "GET",
url: "/shipping-cost",
query: {
customerEmail: mockedCustomer.email,
},
});
t.equal(response.statusCode, 404);
t.same(JSON.parse(response.payload), {
error: "Customer does not exist",
});

getCustomerScope.done();
});

t.test("503 - error getting orders count", async (t) => {
const getCustomerScope = nock(CRUD_BASE_URL)
.get(`/customers/`)
.query({ email: mockedCustomer.email })
.reply(200, [mockedCustomer]);

const getOrderScope = nock(CRUD_BASE_URL)
.get(`/orders/count`)
.query({ customerId: mockedCustomer._id })
.reply(500, 0);

const response = await fastify.inject({
method: "GET",
url: "/shipping-cost",
query: {
customerEmail: mockedCustomer.email,
},
});
t.equal(response.statusCode, 503);
t.same(JSON.parse(response.payload), {
error: "Error in Order collection",
});

// Check if the mocked requests have been really called by the handler
getOrderScope.done();
getCustomerScope.done();
});

t.test("New customer shipping cost", async (t) => {
const getCustomerScope = nock(CRUD_BASE_URL)
.get(`/customers/`)
.query({ email: mockedCustomer.email })
.reply(200, [mockedCustomer]);

const getOrderScope = nock(CRUD_BASE_URL)
.get(`/orders/count`)
.query({ customerId: mockedCustomer._id })
.reply(200, 0);

const response = await fastify.inject({
method: "GET",
url: "/shipping-cost",
query: {
customerEmail: mockedCustomer.email,
},
});
t.equal(response.statusCode, 200);
t.same(JSON.parse(response.payload), {
shippingCost: NEW_CUSTOMER_SHIPPING_COST,
});

// Check if the mocked requests have been really called by the handler
getOrderScope.done();
getCustomerScope.done();
});

t.test("Default customer shipping cost", async (t) => {
const getCustomerScope = nock(CRUD_BASE_URL)
.get(`/customers/`)
.query({ email: mockedCustomer.email })
.reply(200, [mockedCustomer]);

const getOrderScope = nock(CRUD_BASE_URL)
.get(`/orders/count`)
.query({ customerId: mockedCustomer._id })
.reply(200, 1);

const response = await fastify.inject({
method: "GET",
url: "/shipping-cost",
query: {
customerEmail: mockedCustomer.email,
},
});
t.equal(response.statusCode, 200);
t.same(JSON.parse(response.payload), {
shippingCost: DEFAULT_SHIPPING_COST,
});

// Check if the mocked requests have been really called by the handler
getOrderScope.done();
getCustomerScope.done();
});
t.end();
});
});

Write the handler

Once we have created the test it's time to write the handler:

In order to do it, we need to create and navigate to the handlers folder add a new file called getShippingCost.js.

Firstly, we define the schema for the request and the response:

const schema = {
querystring: {
type: "object",
properties: {
customerEmail: { type: "string" },
},
},
response: {
200: {
type: "object",
properties: {
shippingCost: { type: "number" },
},
},
"4xx": {
type: "object",
properties: {
error: { type: "string" },
},
},
"5xx": {
type: "object",
properties: {
error: { type: "string" },
},
},
},
};

As written in the schema, the route accepts a customerEmail as query string parameter and return the associated shipping cost if all goes well, otherwise it returns an appropriate error response.

Now we need to write the code for handler itself, so that it will:

  1. Read from customers CRUD to retrive the _id of the customer related to the mail;
  2. Read from orders CRUD to count the number of orders placed by the customer;
  3. Return the shipping cost, eventually appling a discount if it is the first order.

The handler code will be the following one:

async function handler(req, rep) {
const DEFAULT_SHIPPING_COST = 10;
const NEW_CUSTOMER_SHIPPING_COST = 5.99;
const CRUD_BASE_URL = "http://crud-service/";
// Get client to interact with the CRUD Service
const crudClient = req.getHttpClient(CRUD_BASE_URL);

// Get query params
const { customerEmail } = req.query;

let customerCrudRes;
try {
customerCrudRes = await crudClient.get(`/customers/`, {
query: { email: customerEmail },
});
} catch (error) {
return rep.code(404).send({ error: "Customer does not exist" });
}

const [customer] = customerCrudRes.payload;

let orderCrudRes;
try {
orderCrudRes = await crudClient.get(`/orders/count`, {
query: { customerId: customer._id },
});
} catch (error) {
return rep.code(503).send({ error: "Error in Order collection" });
}

const numberOfOrders = orderCrudRes.payload;

const shippingCost =
numberOfOrders > 0 ? DEFAULT_SHIPPING_COST : NEW_CUSTOMER_SHIPPING_COST;

return rep.code(200).send({ shippingCost });
}

As you can see, the Mia-Platform CustomPluginLib for node.js is included by default in the template.

tip

If you have an on-premise installation, you can add your custom templates to the Marketplace in order to have a uniform codebase. You can also create your template for PaaS Marketplace and request to add it by opening a Marketplace Contribution issue in Mia-Platform Community GitHub repository.

In particular, the getHttpClient is used to get an HTTP client to make calls to the CRUD service API following Mia-Platform standards (e.g. injecting some headers used by Mia-Platform plugins).

CRUD Service

To learn more how to use API exposed by the CRUD Service, check out the CRUD Endpoints Documentation

The last thing left to do is to export the function:

module.exports = {
handler,
schema,
};
Complete snippet:

/handlers/getShippingCost.js
"use strict";

const schema = {
querystring: {
type: "object",
properties: {
customerEmail: { type: "string" },
},
},
response: {
200: {
type: "object",
properties: {
shippingCost: { type: "number" },
},
},
"4xx": {
type: "object",
properties: {
error: { type: "string" },
},
},
"5xx": {
type: "object",
properties: {
error: { type: "string" },
},
},
},
};

async function handler(req, rep) {
const DEFAULT_SHIPPING_COST = 10;
const NEW_CUSTOMER_SHIPPING_COST = 5.99;
const CRUD_BASE_URL = "http://crud-service/";
// Get client to interact with the CRUD Service
const crudClient = req.getHttpClient(CRUD_BASE_URL);

// Get query params
const { customerEmail } = req.query;

let customerCrudRes;
try {
customerCrudRes = await crudClient.get(`/customers/`, {
query: { email: customerEmail },
});
} catch (error) {
return rep.code(404).send({ error: "Customer does not exist" });
}

const [customer] = customerCrudRes.payload;

let orderCrudRes;
try {
orderCrudRes = await crudClient.get(`/orders/count`, {
query: { customerId: customer._id },
});
} catch (error) {
return rep.code(503).send({ error: "Error in Order collection" });
}

const numberOfOrders = orderCrudRes.payload;

const shippingCost =
numberOfOrders > 0 ? DEFAULT_SHIPPING_COST : NEW_CUSTOMER_SHIPPING_COST;

return rep.code(200).send({ shippingCost });
}

module.exports = {
handler,
schema,
};

Add the route

Now it is time to register the route with the handler build previously.

Go back to index.js file and add the route GET /shipping-cost handled by the function written previously:

index.js
"use strict";

const customService = require("@mia-platform/custom-plugin-lib")();
const getShippingCost = require("./handlers/getShippingCost");

module.exports = customService(async function index(service) {
service.addRawCustomPlugin(
"GET",
"/shipping-cost",
getShippingCost.handler,
getShippingCost.schema
);
});

Now run the tests we have implemented previously by running the properly NPM script command:

npm run test

Finally commit and push changes to master.

caution

As previously mentioned Mia-Platform cares about TDD programming so you will not be able to commit until 100% coverage is reached, to check the current coverage you can run:

npm run coverage

Try the Microservice

Now we can deploy the service, so we can test it. Be careful to flag the "Always release services not following semantic versioning" checkbox, we will see the semantic versioning later in the tutorial.

After the deploy has been finished, open the API Portal and use the Customers CRUD routes to add this document:

Click to see the customer schema:

{
"name": "Awesome",
"surname": "Customer",
"email": "awesome.customer@email.com",
"phone": 1234567890
}

Now you can try the GET /shipping-cost route and you will see that it will return the NEW_CUSTOMER_SHIPPING_COST value which will be 5.99, if you followed the tutorial.

Now use the Orders CRUD routes to add this document:

Click to see the orders schema:

{
"customerId": "<_id>",
"productId": 1234,
"quantity": 2,
"expectedShipmentDate": "2023-10-10",
"statusHistory": ["received"]
}

caution

Get the response _id from the creation of the client and replace the "<_id>" placeholder in customerId in the previous json.

Now if you try again the GET /shipping-cost route you will see that it will return the DEFAULT_SHIPPING_COST value which will be 10, because it will be the second order.

Environment variable

You should know, as a developer, that writing values directly into the code it isn't a good practice, so we want to extract the CRUD_BASE_URL, DEFAULT_SHIPPING_COST and NEW_CUSTOMER_SHIPPING_COST variables as environment variables.

An environment variable is a variable whose value is set outside the microservices made up of a name/value pair. You can set a different value based on the environment (Development, Production, etc.).

Create an environment variable

In order to do it, follow these steps:

  1. Open your project in Mia-Platform Console;
  2. Go to Project Overview area;
  3. Click on Public Variables tab and click Add variable;
  4. Fill in the following values:
    1. Insert DEFAULT_SHIPPING_COST as key
    2. Insert 12 as Development value;
    3. Insert 300 as Production value;
  5. Confirm

In this way, we have created the DEFAULT_SHIPPING_COST that values 12 in the Development environment and 300 for Production, so it is possible to use different values based on the environment.

Add the ENV to the microservice

Now we have to add the environment variable to the microservice:

  1. Go to Microservices and select the get-shipping-cost service.
  2. Go to section Environment variable configuration click Add Environment Variable
  3. Create the DEFAULT_SHIPPING_COST and enter {{DEFAULT_SHIPPING_COST}} as value;
  4. Create the NEW_CUSTOMER_SHIPPING_COST and enter 7 as value;
  5. Insert the key CRUD_BASE_URL and http://crud-service as value.
  6. Commit the changes

In this way the microservice can access to DEFAULT_SHIPPING_COST ENV. Using {{env_name}} Mia-Platform automatically interpolates the correct value for each environment where the microservice will run.

Edit microservice

Now we need to modify the microservice so that it uses environment variabiles.

As the first thing adjust the tests in /tests/index.test.js file injecting the variables in the setupFastify method:

t.test('shipping-cost', async t => {
// silent => trace to enable logs
const fastify = await setupFastify({
USERID_HEADER_KEY: 'userid',
GROUPS_HEADER_KEY: 'groups',
CLIENTTYPE_HEADER_KEY: 'clienttype',
BACKOFFICE_HEADER_KEY: 'backoffice',
MICROSERVICE_GATEWAY_SERVICE_NAME: 'microservice-gateway.example.org',
NEW_CUSTOMER_SHIPPING_COST,
DEFAULT_SHIPPING_COST,
CRUD_BASE_URL,
})
...
})

After we have to edit index.js and set the schema of the environment variables used by the microservice:

...
const customService = require("@mia-platform/custom-plugin-lib")({
type: "object",
required: [
"CRUD_BASE_URL",
"DEFAULT_SHIPPING_COST",
"NEW_CUSTOMER_SHIPPING_COST",
],
properties: {
CRUD_BASE_URL: {
type: "string",
description: "Base url where CRUD exposes its APIs",
},
DEFAULT_SHIPPING_COST: {
type: "number",
description: "Default shipping cost",
},
NEW_CUSTOMER_SHIPPING_COST: {
type: "number",
description: "Shipping cost for new clients",
},
},
});
...

Finally change the /handlers/getShippingCost.js handler in order to use the ENV, this will be available under config property of the request:

async function handler(req, reply) {
const { NEW_CUSTOMER_SHIPPING_COST, DEFAULT_SHIPPING_COST, CRUD_BASE_URL } = this.config
req.log.info({ value: CRUD_BASE_URL }, 'CRUD_BASE_URL value')
req.log.info({ value: DEFAULT_SHIPPING_COST }, 'DEFAULT_SHIPPING_COST value')
req.log.info({ value: NEW_CUSTOMER_SHIPPING_COST }, 'NEW_CUSTOMER_SHIPPING_COST value')
// Get client to interact with the CRUD Service
const crudClient = req.getHttpClient(CRUD_BASE_URL)

...
}

Furthermore we will log the value of the ENV.

Logging

Check out the Guidelines for logs for more details about the log instance.

Now you can commit and push the code to the master branch.

Final test

We deploy the project to try the microservice.

After the deploy, open the Documentation Portal and try the GET /shipping-cost route, it will return the new values:

API Portal try /shipping-cost

Health routes

Let's go back to the Console, specifically go to the microservice detail page. You can see several configuration fields (check out this page for more details), in particular the probes section define the health routes for the Kubernetes pod:

  • The Readiness path provides k8s with information about when the container is ready to receive incoming traffic. The default route is /-/ready.
  • The Liveness path provides k8s with information about when the container is in a healthy state. The default route is /-/healthz.

These routes are called automatically by Kubernetes and the microservice you just created has a default implementation provided by the Mia-Platform custom plugin libs: Node.js, Go and Java implementations.

info

Check out Kubernetes official documentation for more details about probes.

Those information are shown also in the runtime section to know if a service is healthy or not.

Runtime

In this tutorial, we have created a microservice perfectly integrated with Mia-Platform Console, as the last step, we will find out how to monitor microservices logs:

  1. From your project, go to section Runtime.
  2. From here, you can see select the environment and see the list of current running pods. Select Development as environment and search for get-shipping-cost-... pod.
  3. Click on the pod name to see all logs. Here you should see the logs with the value of the CRUD_BASE_URL, DEFAULT_SHIPPING_COST and NEW_CUSTOMER_SHIPPING_COST environmental variables.

Microservice logs

Tag and deploy the first release

Now the last thing to do is to use semantic versioning on the console release, so after those changes we are ready to tag this version.

To do so:

  • Open the design section;
  • In the upper right-corner open the git section;
  • Click the tag icon;
  • In the form:
    • Create from: master;
    • Tag name: v0.1.0;
    • Short description: first project release.
  • Click the create tag button.

Now to deploy it go to the Deploy section, select the tag v0.1.0 and deploy!