LEAN-CODERS Logo

Blog

LEAN-CODERS

Microservices with nodejs and express

Abstract Lines in front of Skyscrapers by night
What it's all about

Microservices with nodejs are a great way to manage your backend - with the same code base that powers your frontend.

Let's dig into the details how to setup a lightweight yet flexible microservice architecture that scales and provides production resilience to your services.

All code samples in this blog post are available in the repo, so if you are in a rush head over to
https://github.com/lean-forge/microservices-nodejs-express, clone it and start it.

Info: This blog post does not cover NestJS. So if you are searching for a quick start with NestJS I highly recommend NestJS First steps series.

The stack

In this blog post we focus on:

  • Node.js - the server runtime

  • Express - the Node.js framework for building web APIs

  • TypeScript - typed JavaScript

  • TSyringe - lightweight dependency injection framework for TypeScript

As you might already noticed: The focus of this article is to provide a lightweight microservice scaffold that you can use right away and that ensures scalable yet simple architecture. So let's take a look.

The beginning

We start off with a very simple Node.js app with express-routes - nothing new. You can find the code with commit-ID abe1fb7.

We implemented on (very) basic CartController:

Typescriptimport express, { Request, Response } from "express"; 

export const CartController = express.Router(); 

CartController.post("/", (_req: Request, res: Response) => { 
   res.send("CartController.post");
}); 

CartController.get("/", (_req: Request, res: Response) => { 
   res.send("CartController.get");
});

Additionally we prepared an interface IDataSource that is used in the MongoDBService - a simple but often used dependency:

Typescriptimport { IDataSource } from "./DataSource"; 

export default class MongoDBService implements IDataSource {}

So here comes the first challenge: How can we improve code quality with this relationship?
A very popular - and de-facto standard - choice to decouple this relationship is Dependency Injection. Don't get me wrong - there is nothing wrong with this implementation. When it comes to testing it might get tricky. And dependency injection can help with that. Let's see how:

The improvements

For a full list of changes please refer to commit 4853d10 for a detailed list of files (package-lock.json, package.json, etc.) - I will focus just on the relevant ones here.

Let's add tsyringe and jest to enable Dependency Injection with TypeScript and a Testing framework.

The first thing to note is that by using tsyringe we need to:

Typescriptimport 'reflect-metadata';

in app.ts as tsyringe as "the Reflect polyfill should be added once, and before DI is used". For detailed installation instructions refer to the tsyringe Github page.

After integrating tsyringe we are ready to actually make use of Dependency Injection:

Typescriptimport express, { Request, Response } from "express";
import { container } from "tsyringe";
import MongoDBService from "../datasource/MongoDBService";

export const CartController = express.Router();

CartController.post("/", (_req: Request, res: Response) => {
  res.send("CartController.post");
});

CartController.get("/", (_req: Request, res: Response) => {
    const db = container.resolve(MongoDBService);
    console.log(db.getAllItems());
    res.send("CartController.get");
});

In CartController.ts we integrate the MongoDBService-resolution via the tsyringe container. This enables us to replace the MongoDBService during testing with different mocks as we will see in a minute - and is the key benefit of using Dependency Injection in apps.

Let's see what we changed to enable the MongoDBService-resolution via the tsyringe container:

At first we added an @injectable() DataSource class which implements the IDataSource-interface:

Typescriptimport { injectable } from "tsyringe";
import { CartItem } from "../models/CartItem";

export interface IDataSource {
    getAllItems(): CartItem[];
};

@injectable()
export class DataSource implements IDataSource{
    getAllItems(): CartItem[] {
        return [];
    }
}

With the @injectable attribute - more accurate: a class decorator factory - tsyringe registers this class to be injectable at runtime. TSyringe relies on several decorators, to get to know all of them refer to the TSyringe Github page.

The same attribute is applied to the MongoDBService class:

Typescriptimport { injectable } from "tsyringe";
import { CartItem } from "../models/CartItem";
import { DataSource } from "./DataSource";

@injectable()
export default class MongoDBService {
  constructor(database: DataSource) {
    this.database = database;
  }

  private database: DataSource;

  getAllItems(): CartItem[] {
    return this.database.getAllItems();
  }
}

With this setup we are ready for the next step (and the best reason to use Dependency Injection)

Writing clean tests

All that is left now is to define a mock to be injected instead of the previously defined DataSource. Let's just to this - again by the use of @injectable:

Typescriptconst MOCK_ITEMS = [
  {
    id: "1",
    productId: "Item 1",
    quantity: 1,
  },
  {
    id: "2",
    productId: "Item 2",
    quantity: 2,
  },
];

@injectable()
class MockDataSource implements IDataSource {
  getAllItems(): CartItem[] {
    return MOCK_ITEMS;
  }
}

Now we are ready to write clean test cases:

Typescriptdescribe("MongoDBService", () => {
  
  let dbService: MongoDBService;
  
  beforeEach(() => {
    container
    .registerInstance(DataSource, new MockDataSource());
    dbService = container.resolve(MongoDBService);
  });

  test("should be defined", () => {
    expect(dbService).toBeDefined();
  });

  test("should return all items", () => {
    expect(dbService.getAllItems()).toEqual(MOCK_ITEMS);
  });
});

In the Jest lifecycle method beforeEach we register the MockDataSource for the DataSource. So TSyringe now resolves the DataSource to MockDataSource instead of the "real" DataSource - that might actually change items, alter tables or might have other side effects.

Wrap up

A lightweight yet scalable stack for microservices is a rather important factor to keep services manageable. With the proposed setup you might meet these requirements - but still: Beware of microservices that might grow into monoliths over time ;)

Zurück zur Übersicht

Get inTouch

Diese Seite wird durch reCAPTCHA geschützt. Es gelten Googles Datenschutzbestimmungen und Nutzungsbedingungen.
Adresse:
Hainburger Straße 33, 1030 Wien