Mar 04, 2023

The Three-Layer Architecture for Node.js Application

The Three-Layer Architecture for Node.js Application

To improve their maintainability and flexibility, applications are often divided into several logical layers; a layer being an abstraction designed to satisfy a particular business need.

Each layer is given a specific set of responsibilities, and can only access the one below it or at the same level (i.e. separation of concerns).

On a practical level, this top-down approach allows developers to easily organize their code and change the implementation details of one or more layers, without impacting the entire application.

In this article, we'll discuss the three-layer architecture and illustrate it with a simple Express application.

Overview of the three-layer architecture

Three-layer architecture

The Router Layer

In a three-layer architecture, the Router Layer is the first layer that contains the API routes of the application and is responsible for:

  • Parsing and validating the payload of incoming requests sent by the client in order to strip them away from any HTTP-specific properties.
  • Forwarding the parsed data to the Service Layer responsible for handling the business logic of the application.
  • Translating the result of the call made to the Service Layer into a valid HTTP response before sending it back to the client.

The Service Layer

The Service Layer is located between the Router Layer and the Data Access Layer. It is completely agnostic from any transport mechanism such as HTTP or AMQP, which means that it can receive data from multiple sources and still process it effectively.

It contains the business logic of the application and is responsible for:

  • Performing application-specific tasks using the parsed and validated data sent by the Router Layer according to the defined set of business rules (e.g. generating new sessions tokens, sending emails and so on).
  • Calling the Data Access Layer in the case it needs to communicate with an external component such as a database.

The Data Access Layer

The Data Access Layer is the layer responsible for performing input/output operations outside of the application’s boundaries such as communicating with the database to carry out CRUD (i.e. Create, Read, Update, Delete) operations.

One of the easiest way to achieve that is to use an Object-Relational Mapper (ORM), which is a library that automates the transfer of data stored in relational database tables (e.g. MySQL, Postgres) into objects that are more commonly used in application code. An ORM therefore provides a high-level abstraction upon a relational database, that allows developers to write code instead of SQL statements or stored procedures to perform CRUD operations.

A simple application

Let's illustrate this architecture with a simple Express application that implements a GET /article/:slug route in charge of sending an article back based on the slug provided in the route parameters.

The application

Whenever a request is routed to the GET /article endpoint, it is immediately forwarded to the controller() function in charge of handling the request-response lifecycle.

// File: app.js

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

const controller = require('./controller');

app.get('/article/:slug', controller);

app.listen(8080);

The Router Layer

The controller() function parses the request by extracting the slug variable from the parameters, calls the service() function in charge of retrieving the article identified by the slug, and responds to the client by sending the article in the JSON format using res.json() or a standard HTTP error using res.sendStatus().

// File: controller.js

const service = require('./service');

async function controller(req, res) {
  const article = await service(req.params.slug);

  if (!article) {
    res.sendStatus(404);
  } else {
    res.json({ article });
  }
}

module.exports = controller;

The Service Layer

The service() function receives the slug, forwards it to the dataAccess() function in charge of querying the database, and returns the article to the upper layer if found or a null value otherwise.

// File: service.js

const dataAccess = require('./data-access');

async function service(slug) {
  try {
    const article = await dataAccess(slug);
    return article;
  } catch(error) {
    return null;
  }
}

module.exports = service;

The Data Access Layer

The dataAccess() function simulates a database call by returning a resolved Promise containing the requested article if the slug matches one of the keys of the articles object, or a null value otherwise.

// File: data-access.js

const articles = {
  'hello-world': {
    title: 'Hello World',
    content: 'A "Hello, World!" program is generally a computer program that ignores any input and outputs or displays a message similar to "Hello, World!".'
  }
};

function dataAccess(slug) {
  return Promise.resolve(articles[slug] || null);
};

module.exports = dataAccess;

Related posts