Premium lesson

The Three-Layer Architecture

Layered Architecture·20 min read·Jan 1, 2025

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.

This architecture follows the separation of concerns principle, where each layer is given a specific set of responsibilities and can only access the one below it or at the same level.

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.

Overview of the architecture

In the three-layer architecture, the application is divided into:

  1. The Router layer

  2. The Service layer

  3. The Data Access Layer

Example

Let's consider the following server that implements a GET /article/:slug route in charge of sending an article back based on the slug provided in the route parameters.

Whenever a request is routed to the GET /article endpoint, it is immediately forwarded to the Router Layer through 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 Router layer is the first layer that contains the API routes of the application.

It is responsible for:

  1. Parsing and validating the payload of incoming requests sent by the client.

  2. Forwarding the parsed data to the Service layer responsible for performing specific tasks.

  3. Sending the result of the call made to the Service layer as a valid HTTP response to the client.

Example

Let's consider this implementation of the controller() function of the Router layer:

// File: controller.js

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

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

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

module.exports = controller;

When executed, it will:

  1. Parse the request by extracting the slug variable from the route parameters.

  2. Invoke the Service layer through the service() function in charge of retrieving the article identified by the slug.

  3. Respond to the client by sending the article in the JSON format using res.json() or a standard HTTP error using res.sendStatus().

The Service Layer

The Service Layer is located between the Router Layer and the Data Access Layer.

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

  1. 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 session tokens, sending emails and so on).

  2. Calling the Data Access layer in the case it needs to communicate with an external component, such as a database or a cache.

Example

Let's consider this implementation of the service() function of the Service layer:

// 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;

When executed, it will:

  1. Invoke the Data Access layer through the dataAccess() function in charge of querying the database.

  2. Forward the article object to the Router layer if found or a null value otherwise.

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 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.

Example

Let's consider this implementation of the dataAccess() function of the Data Access layer:

// 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) {
  const result = articles[slug] || null;

  return Promise.resolve(result);
};

module.exports = dataAccess;

When executed, it will:

  1. Initialize the result variable with an object from the articles object if the slug matches one of its keys or a null value otherwise.

  2. Simulate a database call by returning a resolved Promise containing the result variable.

🎯 Learning objectives

The goal of this branch is to build a production-ready microservice in Node.js using the three-layer architecture.

Everything you'll learn can be applied to most microservices; however, to keep the focus on the incremental building process from start to finish, you'll build a lightweight authentication microservice that mainly implements two functionalities:

  • A sign up route responsible for storing an email-password pair into a MySQL database.

  • A log in route responsible for retrieving an email-password pair from the database, and generating an access token.

By the end of this branch, you'll know how to:

  • Create a modular and robust API in Node.js using well-known patterns, such as modules, factory functions, dependency injection, and so on.

  • Manage and dynamically load configuration files for multiple deployment environments.

  • Parse and validate the payload of incoming HTTP requests using middleware functions and schema validators.

  • Connect to a MySQL instance to safely store and retrieve user data, and generate encoded access tokens.

  • Automate the testing of your application's components with unit testing.

  • Write proper documentation for both the application and its API using Readme files and the OpenAPI standard.

icon light bulb key

Unlock the Build Layered Services in Node.js module

Learn how to build well-structured, easily maintainable, and resilient microservices with the three-layer architecture.

You get immediate access to:

  • 10 focused lessons across the three-layered architecture
  • 1 real-world commented, step-by-step project
  • Ongoing updates to this bundle
  • Lifetime access to this bundle
Unlock this module