How to build microservices with Docker - The Services

sainig

Gaurav Saini

Posted on June 26, 2023

How to build microservices with Docker - The Services

Hello everyone and welcome back,
Today we'll cover the second part of the microservices application - the code for the individual services.
To quickly recall from the first part, the services we'll build today are:

  • Products: Used to list and search for products.
  • Orders: Used to place new orders. This is protected by the auth server so users can't place orders unless they're logged in.
  • Notifications: Used to notify users when a new order is placed.
  • Auth: Used to check user authentication before placing orders.

So, without any more delay, let's start

PS: The code here is incomplete, for the full code check out the Github repo


Products Service

This will be a simple Node, Express REST API. There's one GET endpoint to list all products. The code is like:

const express = require('express');
const app = express();

app.get('/', (req, res, next) => {
  const { ids } = req.query;
  let resultProducts = products;

  if (ids) {
    const productIds = ids.split(',');
    resultProducts = products.filter((p) => {
      return productIds.includes(p.id));
    }
  }
  res.status(200).json(resultProducts);
});
Enter fullscreen mode Exit fullscreen mode

You'll notice we have a query parameter named ids. We'll use this to fetch a subset of all the products by their id.

Also, for simplicity, the complete list of products is coming from a local variable here.


Orders Service

This is also a Node, Express REST API, but not quite as simple as the products one. There's still only one POST endpoint, but since it does multiple things, let's look at the code in pieces.

First, we extract the userId and productIds from the request body and use the productIds to fetch more product details (mainly prices and names) from the products service:

const { productIds, userId } = req.body;

const { data: orderedProducts } = await axios.get(
  `${PRODUCTS_APP_HOST}/?ids=${productIds.join(',')}`
);
Enter fullscreen mode Exit fullscreen mode

Then, we calculate the total amount for the order and a comma separated names list of the products:

let totalAmount = 0;
let orderedProductNames = '';

for (const product of orderedProducts) {
  totalAmount += product.price;
  orderedProductNames += orderedProductNames === '' ? product.name : `, ${product.name}`
}
Enter fullscreen mode Exit fullscreen mode

Lastly, we publish a message for the nats-broker (we'll look at the notification service next) and send a response back to the API request. Similar to the products service, there's no database here also, so we just return a plain JSON object with a random id in the response.

natsClient.sendMessage(
  NEW_ORDER_MESSAGING_CHANNEL,
  `Hello ${userId}, your order is confirmed. Products: ${orderedProductNames}`
);

res.status(201).json({
  productIds,
  totalAmount,
  userId,
  id: Math.floor(Math.random() * 9999)
});
Enter fullscreen mode Exit fullscreen mode

Notifications Service

This service is a Nats subscriber, listening for messages published to the "new order placed channel". For now, we just log the incoming messages to the console, but in a real world scenario we can notify users via SMS, Emails, etc.
The code consists of 2 functions, one is the handler for messages and another to start the subscriber.

function handleMessages(err, message) {
  if (err) {
    console.log(`Error while reading messages: ${err}`);
    return;
  }
  console.log(`Message received on channel "${NEW_ORDER_MESSAGING_CHANNEL}"`);
  console.log(message);
}

async function main() {
  await natsClient.startup();
  natsClient.subscribe(NEW_ORDER_MESSAGING_CHANNEL, handleMessages);
}

main().catch(console.log);
Enter fullscreen mode Exit fullscreen mode

The nats-client code

This is a common file and can be used by any service who wants to talk to the Nats broker.

const { connect, StringCodec } = require('nats');

const NATS_BROKER_URL = process.env.NATS_BROKER_URL;

class NatsClient {
  #conn;
  #codec;

  constructor() {
    this.#codec = StringCodec();
  }

  async startup() {
    this.#conn = await connect({ servers: NATS_BROKER_URL });
    console.log(`Connected to nats broker on "${this.#conn.getServer()}"`);
  }

  sendMessage(channel, message) {
    this.#conn.publish(
      channel,
      this.#codec.encode(message)
    );
  }

  subscribe(channel, cb) {
    this.#conn.subscribe(channel, {
      callback: (err, payload) => {
        cb(err, this.#codec.decode(payload.data))
      }
    });
    console.log(`Subscribed to messaging channel "${channel}"`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Currently, the orders service uses the sendMessage method and the notifications service uses the subscribe method.


Auth Service

This is again a REST API. But, just to mix things up a bit, it's built in Python using the FastAPI framework. Currently, there's only one endpoint to verify the Authorization header, which checks for a hardcoded token value.

@app.get("/verify")
def verify_token(authorization: Annotated[str | None, Header()] = None):
    if authorization != "secret-auth-token":
        return JSONResponse(
            status_code=status.HTTP_401_UNAUTHORIZED,
            content=None
        )

    return JSONResponse(
        status_code=status.HTTP_200_OK,
        content=None
    )
Enter fullscreen mode Exit fullscreen mode

Wrap up

That was all for the individual services code. In the next part we'll look at the root level docker-compose.yml files and the nginx.conf file to start all the components of the application and make things work.

I hope you'll enjoy this series and learn something new.

Feel free to post any questions you have in the comments below.

Cheers!

💖 💪 🙅 🚩
sainig
Gaurav Saini

Posted on June 26, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related