node.jsexpressmicroservicescode-sharing

Nodes js, Microservices and share app.listen?


My company has started with a relatively small project using microservices. As one can imagine the hot topic of code sharing came up, and what to and not to share. One point of contention was sharing the actual express server listen() code in a lib and use in multiple services. I feel this is wrong as I believe this tightly couples the two services by sharing the init code. My colleague days that we are already coupled with express. Is this shared approach the right thing to do? In addition he has not segregated routes and controllers to their structure and yes the code is small but not sure how well one could find routes fast.

Below is what is being shared. I am interested in what people with more experience think about this approach.

let app = express();

let app_ready = false;
function check_alive(request, response, callback?:(req:any, resp:any) => void) {
    console.log("alive state requested");
    if (callback === undefined) {
        // no alive_check callback provided
        // use default
        if (app_ready) {
            response.status(200);
        } else {
            response.status(503);
        }
        response.json({ready: app_ready});
    } else {
        callback(request, response);
    }
}

function check_ready(request, response, callback:(req:any, resp:any) => void) {
    console.log("ready state requested");
    callback(request, response);
}

exports.instance = app;
exports.init = (ready_check: (req:any,resp:any) => void, alive_check?:(req:any,resp:any) => void) => {
// basic routing
    app.get("/liveness", (req, res) => {check_alive(req, res, alive_check);});
    app.post("/liveness", (req, res) => {check_alive(req, res, alive_check);});

    if (ready_check == undefined || ready_check == null) {
        app.get("/readiness", (req, res) => {check_alive(req, res);});
        app.post("/readiness", (req, res) => {check_alive(req, res);});
    } else {
        app.get("/readiness", (req, res) => {check_ready(req, res, ready_check);});
        app.post("/readiness", (req, res) => {check_ready(req, res, ready_check);});
    }
};

exports.listen = (port, callback) => {
        return app.listen(port, () => {
        app_ready = true;
        callback();
    });
}

exports.get = (url:string, callback:any) => {
    app.get(url, callback);
}

exports.post = (url:string, callback:any) => {
    app.post(url, callback);
}

EDIT: So while this code is technically correct, should the app initialization function be wrapped up in a shared lib? If no, why not and what should and shouldn't be shared?

I have read lots of article in medium, devops and this site saying as long as you can deploy, restart, etc independently then it is a microservice.

Thank you in advance for any insight.


Solution

  • When a monolithic codebase becomes too large to manage and to understand for a team, (i.e. bringing in new developers is a strong pain point), something has to be done.

    Many approaches are possible, but most are the "divide and conquer" type: create black boxes that have documented and versioned APIs so that programmers don't have to know the details of everything. Those black boxes can be libraries, npm packages, microservices... whatever as long as they are versioned and documented.

    In the case of a NodeJS Web API you could imagine splitting your application into multiple properly documented npm packages that implement most of the logic and then writing an API which imports those.

    Each team, either working on the npm packages or in the Web API which use them does not need to know inner workings of the other projects. The result would be the same.

    Microservices, (and SOA before it) took that to the next level. Instead of using libraries to divide and conquer, they do it by defining multiple web services that interconnect.

    This has some benefits but also many drawbacks.

    You pay the (big) performance penalty of RPC so that code doesn't end up in the same process, need to write way more code, refactoring is a pain (it's harder to move features from one microservice to another than to rename and move around classes...), on nontrivial projects, you'll need to embrace eventual consistency (no more foreign keys!).

    But then, you can use the "best tool for each job" (machine learning service in Python, API in Node, etc...). Better tooling to document and version Web APIs than libraries (Swagger, GraphQL, ...). Can be easier to scale horizontally (even if this is rarely needed in most projects).

    To me, what really matters is:

    This is a long answer but my point would be, don't be pedantic about if your microservices share 10 lines of code or not. IMHO "it does not matter".

    Disclaimer: I believe that microservices are a nice pattern as long as they solve a real problem for a given project but that it is WIDELY overused because of the hype, and that in most projects I've worked on, they were not needed and added a lot of complexity and development pain for little gain (I'm a freelancer).