Table of Contents
- Introduction to API Gateway and Edge Node.js architecture
- The Node.js architectural stack
- Node.js development environment setup
- Creating your first Node.js microservice
- Deploying microservices in different environments
- Introduction to Express Gateway
- Installation and configuration of Express Gateway
- Security Best Practices (TLS/SSL)
- Proxy and API management with OAuth 2
- Pipelines Express Gateway
- Routing and Load Balancing Overview
- Scaling Techniques
- Multi-threaded distribution with Cluster module
- Load balancing with Express Gateway
- Node server pools with NGINX
- Authentication and authorization strategies and techniques
- Session-based authentication
- Passwordless Authentication
- Road protection with RBAC
- Why rate limiting and quotas?
- Rate limiting concepts, techniques and tests
- Rate limiting global versus specific routes
- Edge Architectures and Design Principles
- Cache policies at the edge layer
- Node.js cache options: in-memory, distributed, solutions/databases
- Demo: single-route vs multi-route cache
- Performance, bottlenecks and optimization
- Performance Optimization Techniques
- Clustering for CPU-intensive tasks
- Parallelism in Node.js
- Unified log files
- Memory leak monitoring
1. Course Overview
Hello, everyone. My name is Brian Letort, and welcome to my course, Node.js Microservices: API Gateway and Edge Services. I am the chief architect over product and AI at Digital Realty, also an author and adjunct instructor at Southern New Hampshire University. Embark on a journey uncovering the technologies driving global applications, such as Netflix, Slack, and Uber. In this course, we’ll explore the secrets behind the Node.js underpinnings that lead to their seamless performance, allowing you a chance to replicate their success. Some of the major topics we’ll cover in this course include managing Node.js APIs and implementing middleware with an API gateway, Node.js API rate limiting, quotas, and load balancing for performance and availability, API authentication and authorization techniques, and performance optimization such as parallelism and caching. By the end of this course, you’ll know the benefits of centralized management of APIs with an API gateway and how to optimize Node.js systems for performance at the edge. Before beginning this course, you should be familiar with Node.js, APIs, and REST basics. I hope you’ll join me on this journey to learn about managing Edge Services with an API gateway with the Node.js Microservices: API Gateway and Edge Services course here, at Pluralsight.
2. Setting up the development environment and first Node.js microservice
Total module duration: 30m 41s
Versions of software used in this course
| Software | Release |
|---|---|
| Node.js LTS | 18.17.1 |
| VS Code | 1.8.2 |
| Express.js | 4.18.2 |
| ExpressGateway | 1.16.10 |
2.1 Introduction to API Gateway and Edge Node.js architecture
Hello everyone, and welcome. In this module, our topic is setting up the development environment and building your first Node.js microservice. Let’s take a quick look at the versions of the key software components we’ll be using throughout the course. As a quick reminder and version check of the main software components used, we’ll be using Node.js LTS version 18.17.1. We’ll be using VS Code version 1.8.2 for our integrated development environment. We’ll be using Express.js 4.18.2 as the web application framework for building the server-side components of web apps and APIs. Express Gateway 1.16.10 will be used as the API gateway technology that will provide the various capabilities that we’ll be reviewing throughout the course, such as routing, security, authentication, authorization, load balancing, caching, and many others. Depending upon when you view this course, these versions may evolve slightly. However, the features we cover should still be supported with subsequent versions as we avoid using features that are set to be deprecated. The main focus of this module is to set up the development environment to build our first Node microservice. However, we will cover a few foundational elements that will assist us with building this microservice. First, we’ll delve into the Node.js API gateway and Edge architecture where we’ll understand the core components and their significance. Next, we’ll unravel how microservice architectures leverage an API gateway and Edge services highlighting how these architectures enhance scalability, security, and performance. Then we’ll cover setting up the Node development environment. Finally, we’ll build our first Node microservice and explore the various options for deployment. So let’s explore the fundamental concepts that comprise the API gateway Edge services architecture. As we can see in the diagram, we have a few microservices provided as API providers which are accessible by the Edge clients through the API gateway. At its core, the API gateway acts as a gatekeeper orchestrating the flow of requests and responses between clients and back-end services. Let’s quickly explore some of the key capabilities provided by an API gateway. API gateways handle the API routing of incoming requests to the appropriate microservices. You can think of the yellow boxes as the public side and the blue boxes as the private side. For an example, if you have an app with many microservices, the gateway is the traffic director to process incoming HTTP requests and send to the proper back-end microservice. For load balancing, they distribute traffic across multiple instances of a microservice, ensuring even load distribution and high availability. For a simple example, if we have three microservices running on say three ports, port 5000, 5001, 5002, and we can use one of the available algorithms such as round-robin to determine which port they connect, spreading the workload across the microservices. API gateways manage user authentication and enforce access control policies for authorization. They provide security features like encryption, denial-of-service protection, and often web application firewall, capabilities to protect against common security threats. To prevent against abuse or overuse of your services, API gateways allow you to set rate limits on API requests, ensuring fair usage and system stability. As an example, we could set a rate limit of five client requests over a period of time, such as 2 minutes. This is very useful to help harden and protect our gateway. By caching frequently-requested data and responses, API gateways reduce the load on back-end services, improving response times and overall system performance. As an example, we can cache an HTTP response for 10 seconds, and after that 10-second time-to-live expires, the cache would be refreshed. This can assist with overcoming thrashing and frequently accessing resources while providing improved performance. Many additional API gateway capabilities are provided as well, which we’ll cover in subsequent modules throughout this class. Now, let’s talk about the public client and Edge components on the right side of the diagram. Edge services are strategically positioned at the network’s edge, providing various essential capabilities. Edge services are geographically distributed, ensuring the content and services are delivered as close to the end users as possible. The reduction in latency directly translates into a better user experience, faster loading web pages, smoother video streaming, and responsive applications, ensuring users are engaged and satisfied. Edge services can scale to handle surges in traffic and offload resource-intensive tasks from the origin server. At the Edge, security features such as denial-of-service protection and web application firewall capabilities protecting against malicious traffic safeguarding the applications backend. These Edge services bring speed, security, and reliability to the forefront, creating an improved user experience while ensuring your application can handle the demands of the modern high speed and data-intense internet. Next, let’s talk about the left API provider and microservices side and discuss a bit about why they are unique when compared to traditional server-side APIs. Microservices allow individual components to scale independently. Each microservice can be deployed using different technologies and databases allowing you to choose the right tool for each job. Microservices are self-contained units, reducing the risk of single component failure affecting the entire application. This insulation enhances system resilience and fault tolerance. Microservices promote agility by enabling teams to develop test and deploy independently. To summarize, microservices offer a paradigm shift in application architecture. They allow the development team to build, scale, and maintain applications more effectively and provide greater flexibility and technology choices over traditional monolithic application architectures. Up next, we’ll delve into the Node architectural stack and components utilized in an API Gateway Edge services architecture.
2.2 The Node.js architectural stack
Next, let’s explore the Node architectural stack and considerations for robust and scalable solutions. Here, we have a simple diagram that illustrates the Node architectural stack, so let’s decompose what this entails. At the top in the Node application, this is your code modules and where the Node built-in modules reside written in or compiled in Java. This is basically anything installed from Node packet manager or that you’ve written yourself. Next are the bindings. A binding is a wrapper written in one language that exposes the library to another language so that code written in a different language can communicate. a unique characteristic of Node is that it’s built upon a single threaded event loop architecture which supports handling multiple concurrent clients efficiently. The underlying architecture leverages an event loop enabling Node to perform non blocking io operations by delegating tasks to the system’s kernel. The system’s kernel being multi threaded allows it to manage various operations simultaneously in the background. Once an operation is completed, the kernel informs Node and the relevant callback is placed in the pole queue for execution upon Node, start up, it initializes the event loop processes are provided input scripts and then enters the event loop processing each phase in the event loop maintains a first in first out cue of callbacks for execution. When this queue is empty or the callback limit is reached the event loop transitions to the next phase and this cycle continues use next. Let’s discuss a few key activities within this approach. In the context of a single thread, event loop architecture, a few key phases and activities are provided that contribute to the efficient handling of asynchronous operations. The timer phase is responsible for executing callbacks scheduled through functions like set time out and set interval. This could be useful in the case of session timeouts for user authentication. For instance, automa logging a user out after 15 minutes of inactivity. The pending callback phase is dedicated to IO callbacks deferred from the previous loop cycle. It processes callback functions that result from IO operations such as reading from databases files or other network requests. An example may be waiting until multiple files have been read before processing. Let’s say we’re waiting for five files to be uploaded and processed. The pending callback be used to ensure data processing only begins when all file read operations have been finished. The poll phase is where IO operations are checked if any IO operations are ready, their callbacks are added to the pole quee. So while waiting for IO operations, this phase also checks for new timers and executes any immediate callbacks that were scheduled. An example may be a Node application that is a real time monitoring system watching for changes and log files and processing the new log entries as they are written in this scenario. The pull phase is used to watch for file changes and enable the ability to read this data in real time. The check phase is responsible for executing callbacks that were scheduled through set immediately. An example may be a real time chat application such as slacker teams when a user sends a message and you want to immediately notify the connected clients about the message set. Immediate can be used to broadcast the message. The closed callback phase handles resource clean up such as closing file handlers or perhaps even network sockets, basically executing callbacks that are related to closing resources. For example, if you have a web server that serves out dynamic content, this could help with proper resource clean up and saving of data when the server is shutting down these key activities within the single threaded event loop, enable the amount of concurrent client requests over traditional multi threaded request response models. So in addition to discussing the previous event loop examples, let’s take a quick look at a few code snippets that illustrate this functionality here. I’m just going to show a few lines of JavaScript code to reinforce and provide concrete examples of the asynchronous activities that we just discussed. First, let’s look at the timer phase in this JavaScript file. We have a simple application that sets a timer for 15 minutes and uses the set time out. Exercising the timer phase, user activity resets the timer and upon time out the user is automatically logged out. Next, let’s look at the pending callback phase. In this example, we have three files to read and process asynchronously. So we need to ensure that all file reads have complete. Before we proceed to data processing, we have a few processing functions up top a loop to iterate through the files to read. And down below is the process. Next tick function to exercise the pending callback phase, making sure all files have been read. Third, let’s look at the po phase example in the poll example, this is a simple example of the pull phase by watching new log entries as they occur. We can see this in the read line event handler that processes log entries in real time. Next, let’s look at a check phase example here is our simple real time chat application similar to something like teams or slack. We can see a few functions up top to handle events such as sending messages and disconnecting. To illustrate our example, if we look within the sending message event, we can use set media to broadcast a message exercising the check phase. Finally, let’s look at the close callback phase. For this example, we are exercising cleanup activities. When a server shuts down in our function, perform clean up, we can see our closed callback phase activities. Many notable apps as shown here on the right benefit from the Node highly scalable and highly concurrent architecture. So let’s discuss a few of the scalability considerations and recommendations plan for horizontal scaling. Adding more server instances to meet increased demand containerization with Docker and orchestration with Kubernetes are valuable for managing multiple instances, providing flexibility and ease of expanding, implement load balancing to distribute incoming requests across Node. Instant popular load balancing options include en gen xh A proxy which we’ll discuss later in the course. Embrace the event driven asynchronous nature of Node use events and callbacks for managing concurrency, especially in scenarios with real time requirements. As we just viewed in our demo, implement caching strategies to reduce the load on your data base and improve response times later. In the course, we’ll dive into caching of edge services, divide your application into smaller independent microservices. Each microservice can be separately deployed, scaled and maintained, enhancing overall scalability up. Next, we’ll discuss setting up the development environment of Node and key libraries for building microservices.
Layers of the Node.js architectural stack
| Layer | Description |
|---|---|
| Application node | Your code, built-in Node modules, npm packages |
| Bindings | Wrapper allowing communication between different languages |
| Event Loop | Non-blocking single-threaded architecture |
| System Kernel | Multi-threaded, handles background I/O operations |
Key phases of the Event Loop
- Timer Phase: Execution of scheduled callbacks with
setTimeout()andsetInterval() - Pending Callbacks: I/O callbacks deferred from the previous iteration
- Poll Phase: Retrieving new I/O events
- Check Phase: Execution of
setImmediate()callbacks - Close Callbacks: Closing sockets and resources
2.3 Setting up the Node.js development environment
Up next, during this video, we are going to cover the Node development environment and key modules for building microservices. One of the many strengths of Node is its widespread support across various operating systems. Node is very lightweight and does not require large workstation resources. Even most miniature computers, such as Raspberry Pi, support Node. Node is designed to be versatile and platform agnostic, ensuring you can develop and deploy your applications on a broad range of operating systems. Node is natively supported on Windows, macOS, and Linux. This cross-platform compatibility is a significant advantage allowing developers to work on their preferred operating system while ensuring seamless development across various platforms. When installing, another consideration is the version to install. Node provides users with two distinct version, the long term support, or LTS version, and the current version. The LTS version characterized by its extended presence in the market comes with comprehensive support and a wealth of community resources. The LTS version is generally recommended for most users due to its maturity and support cycle. On the other hand, the current version represents the most recent Node release featuring the latest updates and features. However, the current version has a shorter support span, and being newer, has the slight chance of the presence of quality issues or bugs. If used, most recommend using the current version exclusively for front-end development and LTS for back-end development. Visual Studio Code, often referred to as VS Code, is a popular, versatile, and powerful code editor, and it seamlessly integrates with Node making it great for Node application development. With VS Code, you can easily create, edit, and debug your Node applications. Git and GitHub are both integral to modern software development. Git is the version control system allowing you to track changes in your software code locally. GitHub, on the other hand, is the cloud-based platform that hosts Git repositories, enabling teams to work together seamlessly. It’s where your code is stored, shared, and reviewed by your coworkers and peers. As for Node, we’re going to be using the current LTS, and we’ll walk through the install shortly, but most installed Node first followed by VS code, and if working in a team environment, you may want to add Git with the GitHub which integrates directly into VS code and Node. If we navigate to Nodejs.org, we’ll see this landing page which provides the two versions we spoke about, the LTS and current version. After we click on these, it’ll download an MSI file to install your chosen version. And next, the wizard opens, starting the install process. First, we’ll accept the license agreement, choose our installation location, choose which features we’d like installed here. We’re taking the defaults. We have an option to install additional tools to compile some additional modules from Node Package Manager. And then finally, we’ll click install which will install the software. After we have Node installed, then we can navigate to code.visualstudio.com, which provides options for download on the front page. After we choose our version to install, we’ll download, and a wizard will begin. First, we’ll accept the agreement. Next, we’ll choose some additional options such as creating a desktop icon and opening code within Windows Explorer. Finally, we’ll click Install to install the software, and with our software installed, now we can open VS Code. You can choose so by navigating to your desktop icon or simply entering code in a command prompt. Here, we have VS Code open with some sample JavaScript files. Just to note, over on the left at the top is our Explorer pointing to our project folders. If we are using Git, this is where our repos would be located. One additional feature to point out is the use of terminals built into the IDE. This is very common within Node so that we can run simple commands to interact with our application. Node empowers developers with a large ecosystem of libraries, so let’s explore a few of these libraries and see how they can enhance your Node projects. Node includes the powerful built-in HTTP module at its core. This module is the backbone of any web server you create within Node. It enables you to handle HTTP requests and responses easily making it an essential foundation for your web application. When it comes to web application frameworks, Express is quite popular. It simplifies the often complex task of routing, handling middleware, and managing HTTP requests. Express provides an intuitive way to structure your application, serving as a robust and flexible choice for building APIs, web services, and even full-fledged web applications. As we know, security is paramount in today’s digital landscape, and Helmet is helpful in this regard. Helmet is a middleware for Express that offers essential security headers. These headers protect your application from common web vulnerabilities, such as cross-site scripting and even clickjacking. By simply integrating Helmet, you can significantly enhance the security of your Node applications. Memcache is a high-performance, in-memory caching system. It’s perfect for storing frequently-accessed data, such as database queries or rendered HTML pages and responses. Memcache accelerates response times and reduces the load on your databases, ultimately, enhancing the overall performance and responsiveness of your application. We will be leveraging these, as well as a few others in the subsequent modules as we delve into the other subtopics. During our next video, we will create a simple Node.js microservice to exercise some of these discussed concepts.
Essential tools for Node.js development
- Node.js: Server-side JavaScript runtime (LTS recommended for back-end)
- VS Code: Code editor with native Node.js integration
- Git / GitHub: Version control and collaboration
- Postman: Testing and simulating HTTP/REST requests
- Docker: Containerization of microservices
Key Node.js modules for microservices
# Installation des dépendances de base
npm install express # Framework web
npm install nodemon --save-dev # Rechargement automatique en développement
npm install express-gateway # API Gateway
npm install express-rate-limit # Rate limiting
npm install jsonwebtoken # JWT pour l'authentification
npm install helmet # Sécurité des en-têtes HTTP
2.4 Creating your first Node.js microservice
Next, we’ll be going step-by-step through the creation of a simple Node microservice. For our example, we’ll use VS Code to create a new project that hosts this simple microservice. It’s hosted behind Express as a simple HTTP route hosted on port 5000. And for this simple example, the microservice just returns a simple static text. Let’s quickly walk through the steps of creating this simple Express-hosted Node microservice, then we’ll switch over and actually go through the steps. In Step 1, we’ll create the directory for our project data. In Step 2 within Visual Studio, we’ll create app.js as the root of the directory. This will be the entry point file for our application. In Step 3, we’ll install the necessary dependencies, Express and Nodemon, using either the integrated terminal in Visual Studio or a command prompt. In Step 4, we’ll edit package.json to specify the entry point as app.js. We’ll also configure the script property to specify the start in dev directing it to the entry point of our application and using Nodemon for restarting when files change in the directory. In Step 5, we’ll edit app.js and add our code for the simple microservice. In Step 6, within the same IDE, in Step 6, within the same integrated terminal, we’ll run and test the new microservice using Nodemon app.js, and we’ll start our app by issuing the command npm run dev. Just to highlight these two dependencies, Express is the popular framework for building web applications, and Nodemon is the development tool that monitors for changes in your source code and automatically restarts the node application when change is detected. We’ll create this file in a moment, but to just show the manifest file package.json which contains critical project metadata, the areas to emphasize here is the start and dev variables which are set under scripts. So we’ll ensure start and dev are pointed at our new file app.js, which we’ll edit next. And to quickly show the code, we’ll review this momentarily in VS Code where I’ll explain steps along the way, but to summarize, the code creates a basic web server using Express listening on port 5000 and responds with a simple message when someone accesses the root URL. Now, let’s see a demo of building this microservice. First, we’re going to start with a command prompt, then we’re going to create a folder for our project. Now, we have a folder called M2Example. Let’s start in Visual Studio Code. Now we can see that we have a new empty folder within Explorer. So first, we’re going to start by creating our app.js file, and now we have an empty app.js file. Next, we’re going to install our dependencies. First, we’re going to install Express by typing the command npm install express. Next, we’ll install nodemon. Now with both dependencies installed, we have our manifest file created. So next, we’re going to edit and add a bit more metadata. Here, we have our scripts. We’ve added two new parameters, start and dev. The script property contains commands that are run at various times in the lifecycle of the application, so we’ve specified two lifecycle events of start and dev on starting file and then using Nodemon for incremental updates. Next, we’re going to edit our app.js file and put our code in for our simple microservice. Next, we’re going to edit our app.js file and put our code in for our microservice. So here’s how it works. First, the script starts by importing the express module. Next, it creates an express application object. The object called app will be used to define routes and configure the server. We define a route for handling GET requests to the root URL. When someone accesses this root URL, a callback function is executed. This function sends a response to the client with a message, a simple node app is running on this server, and then ends a response. The script then sets the port variable which determines the port on which the server will listen for incoming requests. If it’s not specified, it will also default to port 5000. Finally, the server is set up to listen on the specified port. It also logs a message to the console indicating that the server has started and is listening on the chosen port. This message will display the current port number in use. Now, we’re ready to run our app. We can do so by issuing the command npm run dev. And just to exercise Nodemon, if we change the port number, hit File, Save, we’ll see the server restarting down below. And if we open a web browser and navigate to our localhost port 5005, we’ll receive the simple response. This sums up the demonstration of building our first microservice. Up next, we’ll discuss deploying microservices, such as the one we just built, to different environments.
Steps to create a simple Node.js microservice
Step 1: Create the project directory
mkdir my-microservice
cd my-microservice
Step 2: Initialize the project and install dependencies
npm init -y
npm install express
npm install nodemon --save-dev
Step 3: Configure package.json
{
"name": "my-microservice",
"version": "1.0.0",
"main": "app.js",
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
},
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"nodemon": "^3.0.0"
}
}
Step 4: Create app.js - the microservice entry point
const express = require('express');
const app = express();
const PORT = 5000;
app.get('/', (req, res) => {
res.send('Hello from my first Node.js Microservice!');
});
app.listen(PORT, () => {
console.log(`Microservice running on port ${PORT}`);
});
Step 5: Start the microservice
npm run dev
2.5 Deploying microservices in different environments
Now that we’ve built a Node microservice, let’s discuss deploying them to different environments. These environments could be intended for different purposes such as development, staging, and production. Additionally, these environments could be hosted a bit differently using various technologies, such as a local server, a virtual machine, a container, a cloud serverless service, as well as many others, so let’s dive in. When building a production solver application with Node, as well as others, it’s common to work in three different environments, the development environment, the quality assurance and staging environment, and the production environment. Each environment serves a specific purpose in the software development lifecycle. The development environment is where developers write and test code. It’s the creative playground where new features are developed and bugs are fixed. It’s typically the most flexible and dynamic environment. The QA and staging environment is where software is thoroughly tested before it’s released into production. This environment is intended to mimic production. The production environment is where the application is accessible to all end users. It’s the live mission critical environment. Any changes or updates must be carefully planned and coordinated to minimize downtime and avoid user disruption. DevOps and Infrastructure as Code, often called IaC, are crucial aspects of modern software development and deployment. They enhance efficiency, scalability, and maintainability of software applications. DevOps within a Node application provides a few key benefits, which are improved automation, collaboration, monitoring, and security. From an automation perspective, DevOps automates the deployment of the Node application to the development, QA staging, and production environments. Tools like Jenkins, Travis CI, Circle CI, and GitHub actions enable automatic testing and continuous integration of code changes to the intended environment. Infrastructure as Code is the practice of treating the underlying infrastructure configuration as software code, allowing developers to define and provision infrastructure resources using code and scripts rather than manual processes and steps. Infrastructure as Code is a fundamental concept in modern DevOps and cloud computing basically enabling efficient management of the underlying infrastructure at scale. Tools like Docker can help automate the deployment of code to the various environments. Node offers quite a bit of flexibility and scalability in deploying microservices to various environments. For local development and testing, a local server serves as a familiar and cost-effective choice. Traditional hosting on virtual private servers remains a robust option for full control over the infrastructure, serverless platforms like AWS Lambda and cloud platforms like AWS, Azure, or Google Cloud offers scalability and ease of management, making them suitable for production deployments. Docker containers provide portability and isolation, enabling consistent deployment across various environments. Then using continuous integration and continuous deployment, or CI/CD pipelines, we’re able to streamline the deployment across these environments.
The three deployment environments
| Environment | Purpose | Features |
|---|---|---|
| Development | Writing and testing code | Flexible, dynamic, errors tolerated |
| QA / Staging | Tests before production | Mirror of production |
| Production | Accessible to end users | Mission-critical, high availability |
Deployment Technologies
- Local server: For development
- Virtual machine (VM): Isolation and flexibility
- Container (Docker): Portability and consistency between environments
- Serverless Cloud: Automatic scalability (AWS Lambda, Azure Functions)
- Kubernetes: Container orchestration in production
Example of Dockerfile for a Node.js microservice
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 5000
CMD ["node", "app.js"]
2.6 Module 2 Summary
Next, let’s have a quick summary and recap of everything covered in this module. Module one was a comprehensive introduction to Node microservices covering many critical aspects of the Node architecture. We began by exploring the Node architectural stack, understanding the core capabilities of Node.js, it’s asynchronous nature, the event-driven architecture, which makes it a robust choice for building high-performing applications. We also covered the API gateway and Edge architectures emphasizing their importance in managing, securing, and optimizing the flow of data in microservices. We constructed a simple microservice, then covered the various deployment options, embracing concepts like DevOps and containerization, and orchestration with Docker and Kubernetes. To summarize, we established the foundation for us to expand upon in subsequent modules, such as using API gateways, leveraging load balancers and caching, ensuring proper security is applied, and learning how to create scalable Edge microservices. I hope you found this module helpful. Up next, we’ll delve into the role of API gateways by implementing Express gateway to learn and understand the critical role of gateways in highly-scalable Edge services.
3. The role of an API Gateway
Total module duration: 19m 9s
3.1 What is an API Gateway and why is it useful?
Up next, we’ll discuss the role of an API gateway and Node API gateway Edge service solutions. To expand on the role of an API gateway, we’ll cover four primary areas which include first, what is an API gateway, and why is it useful, second, what is the role of an API gateway in Edge services, third, routing in Express API gateway applications, and last, but not least, the role of middleware in API gateways. If we consider a Node solution, both with and without a gateway, the emergence and presence of these gateways offer many benefits. To highlight a few of these, first, they simplify complexity, giving the nature of microservices and the fact that the architecture can be complex when we have numerous services in the order of dozens or perhaps even hundreds, and API gateway simplifies the client’s interactions by providing a single entry point. It can also offer benefits to manage the routing between these microservices. API gateways enhance security by providing authentication, authorization, and encryption capabilities with various approaches and options which will cover during the course. API gateways ensure even distribution of requests among microservices, preventing overloading of any single service. This is particularly important in large-scale applications with low latency requirements. API gateways can translate between different communication protocols, making it easier to connect clients and services. Next, let’s touch on some of the key functions that are implemented and configured in API gateways. Express allows you to find custom routing rules for incoming API requests. You can specify how requests should be directed to the appropriate back-end service based on the request URL HTTP method and other criteria. API gateways provide robust security features, including authentication and authorization mechanisms. As part of this, you can integrate it with various authentication providers such as OAuth JWT tokens, LDAP, or even custom authentication methods to ensure that only authorized users or applications can access your APIs. API gateways help with establishing rate limits on API endpoints to control the number of client requests can make within a specified time period helping prevent abuse and ensuring fair use of your APIs. API gateways support load balancing to distribute incoming API requests across multiple instances of your back-end services. This improves performance, availability, and scalability of your APIs. API gateways allow you to modify the request and response data as it passes through the gateway. You can perform tasks like request response transformations, logging, error handling, and data manipulation to tailor the API data to your needs. And there are many more capabilities, such as logging and analytics, horizontal scalability, and distributing the architecture for redundancy, performance, and fault tolerance. Next, let’s talk about some of the use cases that are often implemented in API gateways, API routing and request forwarding, API gateways route requests from clients to specific microservices, ensuring efficient and organized communication. Second, aggregating and composing responses. API gateways can collect and combine responses from multiple microservices to providing a comprehensive answer to the client. This can be useful when a client request depends on multiple back-end microservices. As an example, let’s assume we have an ecommerce site for a page load activity that requires the product information, the inventory, and the review microservices. API gateways offer valuable insight into the performance and usage of your microservices helping you both in troubleshooting and optimization. API gateways provide granular access control by authenticating clients and authorizing them to access specific microservices or data. Next, let’s take a quick look at Express gateway, the popular open source API gateway built upon Node.js and Express.js. Policies in Express gateway are predefined sets of rules or functionalities that can be applied to the incoming request or response. These policies define actions like authentication, rate limiting, transformations, security, and more. Each policy handles a specific aspect of the API request or response flow. Conditions are used to determine when a policy should be executed. They act as a trigger or criteria that decide when a policy should be applied to an incoming request. Actions are the actual functionalities or transformations that policies perform when conditions are met. For example, if the condition for an authentication policy is fulfilled, the action could involve verifying user credentials or tokens. Middleware in Express gateway refers to the function or handlers that intercept the incoming request in response while performing various tasks. Parameters are the configurations or settings used to define how policies, conditions, and actions behave. They specify the details or options for each policy or action. For instance, a rate limiting policy might have parameters defining the number of requests allowed per minute or per hour. Express gateway uses routing to direct incoming requests to the appropriate policies and actions. Router functions determine the flow of requests based on conditions. Now that we’ve talked through API gateways, let’s see them in action via building an app that exercises many of these functions. For example, we’ll create an application that leverages an API gateway. We’re going to add rate limiting and set a value of 10 requests per minute. We’ll use morgan middleware to log the HTTP requests. We’ll create three services to load balance across and use a random generator to route to one of these three. Let’s see this in action. To set up our example, we’ll create a directory, initiate the project, and install the dependencies. As you can see, we created a directory, initiated the project with the default through the command npm init -y, and then we installed our dependencies. Next, let’s start VS Code within this project. What we’ll do is we’ll place our code in a new file called app.js. So let’s talk through these code segments. At the top, we have our dependencies. Below that, we have our server set up where we create an app as an instance of an express object to create the server, and the port is set to run on 3000. Next is the rate limiting middleware. Here, we specify the middleware that limits requests to 10 per second. We’ll print a message when the limit has been exceeded, and below that, we apply the use of the limiter to all requests. Next is the morgan middleware that sets the logging for all incoming requests. We’ll see in a moment how these are logged to the console. Next, we define our routes with the route simply displaying, Welcome to the gateway. The next few sections are for our load balancing. We start by creating a proxy to handle the load balancing, setting an array of 3 services as we can see service 1, 2, and 3, and we have a simple load balancer that randomly chooses between 1, 2, or 3. The back-end services for these three services are below where they print the corresponding service name to the screen. Finally, we start the server listening to the port defined above and create a log entry that the gateway is initiated and running. Next, let’s run the app. We’ll start the app by typing node app.js. As we can see, now our server is running on port 3000, and I’m going to place my window above VS Code so we can see the terminal behind it. First, we’ll make a request to the default port. Here, we receive our response, Welcome to the gateway, while also seeing the log to the terminal. Next, we’ll choose one of our services and see how it’s randomly routed to the back end. First, it routes to service 1 response, second, service 3, third, service 2, and so on and so forth. After I refresh this 10 times, we’ll get a response back saying that we’ve reached the limit. Now, we’ve exceeded the limit set as 10. And as I try to refresh the page, the gateway doesn’t process any incoming requests. So now we’ve seen an API gateway in action. Up next, we’ll delve into the many options available to route requests to these back-end microservices.
Main benefits of an API Gateway
| Profit | Description |
|---|---|
| Simplification | Single entry point for dozens or hundreds of microservices |
| Security | Centralized authentication, authorization, encryption |
| Load distribution | Prevents single service overload |
| Translation of protocols | Facilitates communication between customers and services |
| Observability | Centralized logging, monitoring and analytics |
Key functions configurable in an API Gateway
- Custom routing: Redirection of requests according to URL, HTTP method, criteria
- Security: OAuth authentication, JWT, API keys
- Rate Limiting: Limiting the request rate
- Load Balancing: Distribution of traffic between multiple instances
- Transformation: Modification of requests and responses
- Caching: Caching responses to improve performance
- Analytics & Logging: Logging and metrics
3.2 Routing in an API Gateway
Routing is one of the key functions provided by API gateways, so let’s delve into what this entails, review the various routing schemes, and build us an example of one of the most popular schemes. Here, we show Express, the popular open-sourced API gateway that is often used in Node solutions. From a routing perspective, we can think of this as acting as the coordinator of public API endpoints with back-end microservices. So what exactly is a route? A route is a section of Express code that associates a HTTP verb, such as GET, POST, PUT, or DELETE or associates a URL path with a function that is called to handle the request. So how is a route processed? First is route reception where the route processing begins when the API gateway receives an incoming request, typically HTTP, but could be a WebSocket or other communication protocol. First is a request reception where the route processing begins when the API gateway receives an incoming request, typically HTTP, but could be a WebSocket or other communication protocol. Second, the request is analyzed to extract key information, such as the request method which could be GET, POST, PUT, or DELETE, it also extracts the request URL and path headers, parameters, and payload or request body, if applicable. Third, the API gateway consults its routing table or configuration to determine how to handle the request. The routing table contains predefined routing rules that specify how requests should be routed based on their attributes. Fourth, if load balancing middleware is used, the API gateway identifies the routing to multiple instances or nodes using a load balancing algorithm as we saw in the previous example. Fifth, before the forwarding of the request, the API gateway often performs security checks, which could include authentication, authorization, and rate limiting. Finally, if the API gateway cannot find a matching route or if the intended microservice is unavailable or encounters an error, the API gateway handles the situation gracefully via proper error handling. The API gateway handles the routing schemes which are the mechanisms to direct incoming API requests to the proper back-end service or resource. It is essentially a set of rules and configurations defined when configuring the coordination of back-end services, so let’s take a look at the various routing schemes. Path-based routing is one of the more straightforward routing schemes. It involves matching the URL path of an incoming request to the predefined routing patterns in the API gateway’s configuration. Host-based routing relies on the host name or domain name of the request URL to determine the destination microservice. Each microservice may have its own subdomain or custom domain. The HTTP method-based routing takes into account the HTTP request method, such as GET, POST, PUT, or DELETE, to decide where to route the request. Different methods can trigger different actions or microservices. Header-based routing involves inspecting the specific headers in the request to make routing decisions. Headers, such as accept, content type, or even custom headers can be used to route requests. In parameter-based routing, the API gateway examines the query parameters form data or other parameters within the request to determine the routing destination. It’s particularly useful when requests carry parameters that indicate their target. Content-based routing is based on the content or payload of the request. It allows the API gateway to make routing decisions based on the content’s characteristics such as keywords or data patterns. This is useful when processing specific types of content, such as images or natural language. Well, these are the primary, a few specific scenarios may be used as well, such as version-based, weighted routes, or even dynamic routing which we’ll delve into further in an upcoming module. Now that we’ve discussed routing schemes, let’s look at them in action. In our demo, we’ll demonstrate path-based routing schemes which is one of the more common and popular approaches. We’ll accomplish this by creating three services which include a user service, product service, and order service, and then we’ll create the gateway code to exercise routing to each of these based on the path. So let’s get started. As you can see, I’ve created a directory called Mod3Routing, moved it to the directory, initiated the project with the defaults, and then installed Express through the command npm install express. Next, let’s start VS Code. And as you can see, I’ve created a folder for services with three files within them for each of our microservices, user service, product service, and order service. I’ve also created a file app.js which contains our gateway code. Let’s take a look at these. First, we’ll look at userService. Up top, we initialize Express by creating a router to be used to define routes. Below this, we define a path to profile using the HTTP GET Method. It responds with a simple JSON containing the message, user, or profile data. Next, if we look at the product service, it’s very similar, but creates a path for list with a similar approach of HTTP GET. Continuing on, if we look at the orderService, we see a path to history and the HTTP GET Method. Now, let’s look at our gateway code in app.js. Up top, first, we import our microservices by pointing the microservices to where they reside in the services folder. App.use defines the path-based routing of each of these services. And below, we can see our server is listening on port 3000. Next, let’s run our app. As we can see, our gateway is now listening on port 3000. First, let’s look at a route going to our user service. If we enter the URL localhost/user/profile, we’ll see a simple JSON message return. Similar, similar, if we look at our product service via entering the URL get for products and list, we’ll see a similar JSON returned. And finally, to take a look at our orders, we’re routed to the JSON that sends back the Order History Data. Up next, let’s look into the role of middleware in extending the API gateway capabilities.
What is a road?
A route is a section of Express code that associates an HTTP verb (GET, POST, PUT, DELETE) with a URL path with a processing function.
Route processing process
- Request receipt: The gateway receives an HTTP (or WebSocket) request
- Analysis of the request: Extraction of the method, URL, headers, parameters, body
- Consultation of the routing table: Determination of the appropriate handler
- Load Balancing: If configured, distribution to multiple instances
- Security checks: Authentication, authorization, rate limiting
- Error handling: If no route matches, return an appropriate error
Express Routing Example - Simple Proxy
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
// Route vers le service utilisateurs
app.use('/api/users', createProxyMiddleware({
target: 'http://localhost:3001',
changeOrigin: true,
pathRewrite: { '^/api/users': '' }
}));
// Route vers le service produits
app.use('/api/products', createProxyMiddleware({
target: 'http://localhost:3002',
changeOrigin: true,
pathRewrite: { '^/api/products': '' }
}));
app.listen(8080);
3.3 Middleware in an API Gateway
So let’s delve a bit deeper into API gateway middleware. When middleware is applied to API gateways, they extend functionality and enable much more granular control over configurations and management of your microservices. Let’s look at them a bit further. Express middleware refers to the set of functions and components that can be used to process and manipulate incoming HTTP requests and outgoing HTTP responses in an Express application. Middleware functions can be used to add features, perform tasks, modify data as requests and responses flow through the application. Express middleware can be used to define routes for specific URL paths which helps route requests to the appropriate handling functions. Middleware can handle user authentication and authorization ensuring that only authenticated and authorized users can access certain routes or resources. Middleware can log requests, perform data validations, sanitize input, or transform data before it reaches the route handlers. Middleware can capture and handle errors that occur during request processing. Custom error handling middleware can be defined to centralize error handling, so let’s use middleware to apply rate limiting and logging to our application. As you can see, I’ve created a folder mode3Middleware, I’ve initialized the project with the default parameters, and installed the dependencies, Express and Express rate limit. Now let’s start VS Code. Let’s create our gateway file app,js. Let’s dive into the code of app.js. First, we create the express application using the express library and import the express-rate-limit middleware for rate limiting. We define a rate limit of 10 requests per minute using the rateLimit middleware. This middleware will limit the number of resources to routes in which it is applied. We implement custom middleware for request logging. This middleware logs the incoming requests, including the timestamp HTTP method and path. We apply the rate limiting middleware to a specific route limited, that includes a sample microservice route for limited/service, and when the API gateway is started, it listens on port 3000. To test the rate limiting and logging, we access the limited route and send more than 10 requests within a minute, so let’s try this out. We’ll start our application by typing node app.js. As we can see, our gateway is listening to port 3000. We’ll test our service by entering the URL to limited service. And as we can see, a simple JSON is returned and also the logging occurs in the terminal. And if we refresh this multiple times, we’ll eventually exceed the threshold. And here, we’ve entered the eleventh request and get the response back, too many requests, please try again later. This code demonstrates a simple use of middle in an API gateway providing rate limiting and request logging. Let’s summarize everything covered in this module. We reviewed the various benefits of API gateways to demonstrate the utility and benefits they provide. We examined the various routing schemes, and we explored and demonstrated how API gateways can be extended with middleware. Up next, we’ll delve further into implementing and configuring Express Gateway.
Typical uses of Express middleware
- Routing: Define routes for specific URL paths
- Authentication / Authorization: Road access control
- Logging: Logging incoming requests
- Data validation: Input sanitation
- Error management: Centralization of error processing
- Rate Limiting: Limiting the number of requests
Example of middleware with rate limiting and logging
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
// Middleware de rate limiting : 10 requêtes par minute
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 10,
message: 'Trop de requêtes, veuillez réessayer dans une minute.'
});
// Middleware de logging
const requestLogger = (req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
};
// Application du middleware
app.use(requestLogger);
app.use('/api', limiter);
app.get('/api/data', (req, res) => {
res.json({ message: 'Data retrieved successfully' });
});
app.listen(8080);
4. Implementing Express Gateway
Total module duration: 29m 11s
4.1 Introduction to Express Gateway
Our next topic is implementing and using Express Gateway within a Node microservices architecture. Express Gateway is an open-sourced API gateway designed specifically for microservices. It’s built on top of Express.js, the popular web application framework for Node.js. This gateway includes essential functionalities for managing and securing microservices in a distributed architecture, so let’s look at the subtopics of Express Gateway. To cover the topic of implementing Express Gateway, we’ll cover five areas. First, we’ll cover an introduction to Express Gateway. Second, we’ll go through the installation, configuration, and setup steps. Third, we’ll use Helmet as the middleware to secure our gateway. Fourth, we’ll expose public APIs while enabling access with OAuth 2. Fifth, we’ll use pipelines to create public APIs that interact with these private APIs. If we consider the common front-end and back-end API services ecosystem, we often have our microservices managed behind the gateway. This allows us to centralize management and coordinate all interactions and sequences of events that occur while interacting with these back-end services. Then we have our public API endpoint as the entry point to the back-end coordination and management. At the edge where various devices, clients, and services connect, an API gateway ensures streamlined communication by handling tasks like authentication, rate limiting, routing, and protocol translation. In this common scenario, the API Gateway assists in managing Edge services because it acts as a centralized entry point for managing, securing, and optimizing interactions between clients and the distributed network of services at the Edge. So we can gather the primary goal of Express Gateway is to simplify and unify the complexities of managing APIs by providing a comprehensive, yet intuitive and easy-to-use platform. Express Gateway offers many benefits. It offers a centralized control point for API traffic, promoting better security through policy enforcement, authentication, and authorization mechanisms. Its extensible architecture and flexibility empowers developers to adapt and scale their API infrastructure according to evolving business needs. Up next, we’ll install and configure Express Gateway and exercise some basic gateway functionality to verify our new gateway is functioning as intended.
Benefits of Express Gateway
- Centralized: Unified management of all APIs from a single point
- Open-source: Built on Express.js, very extensible
- Integrated security: Authentication, authorization, encryption
- Pipeline: Flexible processing architecture
- Ready-to-use policies: Rate limiting, OAuth, JWT, proxy, etc.
4.2 Installation and configuration of Express Gateway
Now that we’ve discussed the benefits of Express Gateway, let’s install and build a simple application to test the functionality. In our demonstration, first, we’re going to install Express Gateway globally on our system. Second, we’ll configure a public endpoint to route to a back-end service. We’ll have a public endpoint called API exposed through Express Gateway on port 8080. I think it will route to the private microservice running on the same localhost on port 3000. With Express Gateway handling this routing, we have our centralized management and pipeline definitions that we will create to execute the sequences of policies we’d like to implement, such as authentication, logging, rate limiting, and many others. So let’s install Express and test it out. First, we’ve created a folder called ExpressTest. Here’s where we’ll create our project. And to install Express Gateway globally on the system, we’ll issue the command npm install -g express-gateway. And after a few moments, we can see that Express Gateway has been installed. Next, let’s create our gateway project. We’ll do so by issuing the command eg gateway create, which is going to start a small wizard. First, it’s going to ask us to name our gateway. We’ll just call it ExpressTest, and we’ll install it in a folder, ExpressTest. Next, it’s going to ask us what type of gateway do we want to create? We have two options here, Getting Started with Express Gateway or Basic default pipeline with proxy. We’ll choose the first of Getting Started. The Getting Started with Express Gateway option is a bit more comprehensive as a template that includes some basic policies and plugins applicable to most use cases. Now we have our gateway installed, and we’ll start VS Code so we can edit our YAML file and configurations. When we start VS Code, we can see we have our folder, ExpressTest. Underneath that, we have a config folder which contains the gateway.config.yml file. This YAML file contains the configurations where we define our public API endpoints, our back-end services, pipelines, and policies. And if we look at the gateway.config.yml file, again, this is the configuration used by Express Gateway. So let’s review the edits. At the top, it’s using the default port 8080, and we’ve created a public apiEndpoint of /API. Below that, we’ve created a private or service endpoint as localhost on port 3000. We’ll keep the default policies here, but in an upcoming video, we’ll go into the details of pipelines with the specific policies. If we scroll down below, this is where we define our pipelines and the sequence of events and policies that occur whenever the public APIs are referenced. So now, we’ll switch back to our command prompt and start our gateway. We’ll do so by issuing the command npm start. And as we can see, our server is listening on port 8080, as well as our admin port listening on 9876. So let’s go back to Visual Studio and define our microservice. We’ll create a new file called app.js. Within our app.js, we have our endpoint defined as /api, which simply returns text, Hello, this is your simple Node.js app. And we also write to the log that the server is listening, so let’s start this back-end microservice. We’ll do so by issuing the command node app.js, and we can see the server is now listening on port 3000. Let’s test our public API endpoint. We’ll do so by issuing localhost port 8080/api, and as we can see, it’s been routed to the back-end service on port 3000 of our localhost and returns, Hello, this is your simple Node.js app. We can also test our API using curl and passing our URL with our API endpoint. And as we can see, it also returns our simple text routing the public API endpoint of 8080/API to our back-end service running on port 3000. Up next, we’ll look at Express Gateway and some security best practices.
Installing Express Gateway
# Installation globale
npm install -g express-gateway
# Création d'un projet gateway
eg gateway create
# Nom: ExpressTest
# Type: Getting Started with Express Gateway
cd ExpressTest
Express Gateway project structure
ExpressTest/
├── config/
│ ├── gateway.config.yml # Configuration principale
│ └── system.config.yml # Configuration système
└── server.js # Point d'entrée
Example of gateway.config.yml file
http:
port: 8080
admin:
port: 9876
host: localhost
apiEndpoints:
api:
host: '*'
paths: '/api/*'
serviceEndpoints:
myMicroservice:
url: 'http://localhost:3000'
policies:
- proxy
- rate-limit
- log
pipelines:
main:
apiEndpoints:
- api
policies:
- proxy:
- action:
serviceEndpoint: myMicroservice
changeOrigin: true
Starting the gateway
npm start
4.3 Security best practices (TLS/SSL)
Up next, we’ll discuss security best practices while delving into the importance of secure connections within our APIs and microservices. This is accomplished by implementing Transport Layer Security, often just called TLS, or its predecessor, Secure Socket Layer, often called SSL. So let’s delve into what these entails. As we all know, given the large surface attack area of Edge API systems that leverage an API gateway and the associated back-end microservices, security is paramount in modern web service architectures. Encryption ensures that data transmitted between clients and the Express Gateway and associated back-end microservices remain confidential and restricted to the intended users or systems. By implementing encryption in Express Gateway and microservices, we are able to share yield sensitive information from threats safeguarding user credentials and data integrity. To fortify these communication channels, Express Gateway offers seamless TLS/SSL configuration. Implementing TLS or SSL guarantees encryptic connections between clients and the API gateway and the associated back-end microservices. Additionally, adherence to TLS/SSL best practices, not only fortifies security, but also ensures compliance within industry regulations, fostering a resilient and trustworthy digital ecosystem. In essence, encryption with the configuration of TLS/SSL are essential elements weaving a robust security fabric around our API applications and instilling confidence in users and stakeholders alike. So let’s take a look at how Express and back-end services leverage TLS over HTTPS. To accomplish this, we’ll want to obtain a certificate and private key from a certificate authority. For instance, if working in an enterprise environment, you may work with your information security team or perhaps even your DevSecOps team to acquire such a certificate. For testing, we could use OpenSSL to create a self-sign key as well, so let’s create one of these and test it out. First, we’ll create our gateway issuing the command, eg gateway create. Now we have a new gateway created called HTTPS example. Next, let’s use OpenSSL to create our certificate and key. We’ll do so by issuing the openssl command specifying that our private key name, private key-pem where our certificate is named certificate.pem, and when we issue this command, it’s going to ask us a few prompts, and just entering some sample information, we’ve now created our certificate and key. Next, let’s start VS Code. Now we’re ready to create our back-end service that leverages this key and certificate. So we’ll create a new file called app.js. And to walk through this code, if we look at the top, we see that we import the required modules of express https and fs for the file system. Below this, we create the express application and set the port to 3000. Next, we create our default response whenever the API is used and prints back Hello, Express Gateway with TLS/SSL. Below this, we can read in our SSL certificate and key files. Then we create our HTTPS server, passing the SSL certificate and private key as options and set the Express app as the request handler for the server. Finally, we start the server and write to the log file that the server is running on a secured port. So let’s test our app by starting it using the command node app.js. And to test our application, we will enter the URL https://localhost port 3000. And since this is a self-sign cert, our browser is going to interpret the response as a non-private connection. However, we can proceed to it if we click the Proceed button, and we can see that it returns our response Hello Express Gateway with TLS/SSL. So this is a short example of how to create a secure back-end microservice. Up next, we’ll get into extending that by including the Express Gateway portion to include our proper routing and pipelines of orchestrating our public API endpoint connecting to this back-end secured microservice. Helmet.js is an open source JavaScript library to help secure the Node application by setting several HTTP headers making it more difficult for attackers to exploit known vulnerabilities. The GitHub repo has over 9000 stars and over 4 million weekly downloads illustrating its popularity. Helmet helps prevent cross-site scripting attacks by implementing a content security policy specifying which content sources are allowed and mitigating the risks of injecting malicious scripts into web pages. HTTPS enforcement is enforced by Helmet instructing browsers to connect only through secure encrypted connections, reducing the risk of man-in-the-middle attacks. Clickjacking prevention is set by Helmet to prevent clickjacking attacks ensuring that web pages cannot be embedded within an iframe protecting against user interface manipulation. Helmet mitigates the risks of MIME-type sniffing attacks by setting the X content type options, instructing browsers not to perform MIME-type sniffing and reducing the risk of certain types of attacks. Refer policy control is implemented by Helmet to control how much refer information is included in the request headers, enhancing user privacy and restricting the information sent to other sites when a user clicks on a link. Helmet manages DNS prefetching by setting the X-DNS-prefix-control, this controls browser DNS prefetching to prevent potential malicious domain resolution. Up next, we’ll explore using Express Gateway to proxy and manage APIs with OAuth 2.
Why is TLS/SSL essential?
- API gateways have a large attack surface
- Encryption ensures confidentiality of data in transit
- TLS/SSL protects credentials and data integrity
- Compliance with industry regulations (GDPR, PCI DSS, etc.)
HTTPS configuration with OpenSSL (self-signed certificate for testing)
# Génération d'une clé privée et d'un certificat auto-signé
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
Enabling HTTPS in Express
const https = require('https');
const fs = require('fs');
const express = require('express');
const app = express();
const options = {
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
};
app.get('/', (req, res) => {
res.send('Secure connection via HTTPS');
});
https.createServer(options, app).listen(443, () => {
console.log('HTTPS server running on port 443');
});
Using Helmet.js to secure HTTP headers
const helmet = require('helmet');
app.use(helmet());
// Ajoute automatiquement: X-Content-Type-Options, X-Frame-Options,
// Strict-Transport-Security, Content-Security-Policy, etc.
4.4 Proxy and API management with OAuth 2
Next, we’ll discuss using OAuth 2 with an Express Gateway to proxy and manage APIs. OAuth 2 is an open standard and authorization protocol that provides a secure framework for granting third-party applications limited access to user-protected services without exposing user credentials. It’s widely used for delegated authorization and designed to be simple and scalable for securing APIs, so let’s dive in. Many times, API gateways are just used as a central place that routes and manages back-end traffic, as shown in our diagram, and we’ll build this in our demo here shortly. We have an app that is only accessible via a proxied, secure API endpoint. OAuth is an open standard framework that allows users to grant third-party applications limited access to their resources without exposing their credentials. Set another way, OAuth is all about delegation. It is commonly used to enable secure access to APIs and widely used for delegated authorization, such as social media logins, mobile applications, and other web services. API gateways serve a critical role in proxying and delegating OAuth authentication and authorization processes, enhancing the security scalability and manageability of microservices, and even broader distributed systems. In the context of OAuth, an API gateway acts as an intermediary between clients and resource servers and authorization servers. When a client application seeks access to protected resources, the API gateway can proxy the request to the appropriate authorization server managing the OAuth flow on behalf of the client. This involves handling the redirections token exchanges and ensuring the security of the communication. Once a client obtains an access token, the API gateway can delegate the responsibility of verifying the token validity to the authorization server or perform token introspection itself. This ensures that only valid and authorized requests proceed to the resource servers. Additionally, API gateways can enforce OAuth scopes and policies restricting access based on the permissions associated with the provided tokens, so this provides very granular control over our back-end services, so let’s get into the demo. In our scenario, we’re going to proxy and delegate using OAuth, a redirect URL behind Express Gateway. For this, we’ll redirect to google.com to exercise the functionality, but we could redirect to any URL or API endpoint. First, we’ll create a new gateway using the Express Gateway generator. As you can see, we have the command npm i -g express-gateway. Next, we’ll create our gateway project using the command eg gateway create. We’ll provide the name SecureGW. Now that our gateway is installed, we’ll change directories and start VS Code. To enable OAuth 2, we are going to edit our gateway.config.yml file. We will scroll down to the default pipeline section and add the parameter for OAuth 2. Once we’ve added it, we can hit Save and start our gateway starting our gateway with the command npm start. Now that our server is running, we are going to connect to the default port 8080/IP, and as we can see, we receive an unauthorized since we have OAuth enabled. To access this OAuth-enabled secure endpoint, next, we’ll create new Express Gateway users in an app and then access them through the proper URL. Going back to a command prompt, we’re going to create our users using the command eg users create. We’ll use these in a moment to access the secured endpoint, and it’s going to prompt us for a few parameters to create our accounts. Now we have a new user created called bletort. Next, we’ll create the credentials for OAuth 2, as well as basic auth. To create the OAuth credentials, we’ll use the command eg credentials create -c bletort -t oauth2. Next, we’ll create our basic auth credentials using the command eg credentials create -c bletort -t basic auth -p with a password of Node4me. Next, we need to create the Express application. In the context of Express Gateway, an application typically refers to the credentials that a client uses to authenticate and interact with the API gateway. These are created for OAuth 2 based authentication flows in Express Gateway. So next, we’ll create an app as a consumer of the API. This would be similar to how a social media login requests access to user objects, such as photos, so let’s create a test app and redirect to google.com. We’ll do so by issuing the command eg apps create -u bletort, and we’ll name our app secureapp, and we’ll provide the redirect URL to http://google.com. Now the app has been created and we can see our unique identifier that is created along with it starting with the numbers 54d. We’re going to go back to our browser and enter a URL using the information we just created. We are going to use localhost port 8080/oauth 2, and for the parameters, we are going to use a response type of token and the client ID came from the previous screen, copied and pasted from the app that was created, the 54d number I just briefly covered. And as part of that, you will see the redirect at the end to google.com, and once we issue the URL command, it will route us to the login screen. Here, we’ll use the login that we created of bletort with the password of Node4me. Next, we can see that Express Gateway is using our new app, secureapp, asking us to approve access. Once we hit Approve, we’ll be redirected to google.com. The benefits of utilizing API gateways for proxying and delegation in OAuth scenarios are multifaceted. First, it centralizes the management of OAuth processes, simplifying the development and maintenance of client applications. Second, it enhances security by offloading token validation and ensuring consistent enforcement of access policies. Third, API gateways improve scalability by handling the intricacies of OAuth, allowing back-end services to focus on their core functionalities. Overall, API gateway serve as a robust abstraction layer streamlining OAuth-related tasks and contributing to the efficiency, security, and reliability of distributed systems. Up next, we’ll delve into defining Express Gateway pipelines which are the sequence of policies to be applied as public API paths are processed through the gateway.
What is OAuth 2?
OAuth 2 is an open authorization protocol that allows third-party applications to access protected resources without exposing user credentials.
Typical OAuth 2 flow through an API Gateway
- The client sends a request to the gateway
- The gateway redirects to the authorization server
- User authenticates and accepts permissions
- The authorization server returns an authorization code
- The gateway exchanges the code for an access token
- The gateway validates the token for each subsequent request
- The gateway transfers the request to the back-end microservice if the token is valid
OAuth 2 configuration in gateway.config.yml
policies:
- oauth2
pipelines:
secured-pipeline:
apiEndpoints:
- api
policies:
- oauth2:
- action:
jwt:
secretOrPublicKey: your-secret-key
- proxy:
- action:
serviceEndpoint: myMicroservice
4.5 Pipelines Express Gateway
Up next, we’ll explore the use of pipelines within our Express Gateway applications. In Express Gateway, pipelines are a fundamental concept that enables developers to structure and manage the flow of HTTP requests through a series of defined stages or middleware. A pipeline represents a sequence of processing steps that your request goes through before reaching its final destination. Each step in the pipeline, known as a policy, performs a specific function, such as authentication, authorization, logging, rate limiting, or even custom business logic. Express Gateway pipelines are highly configurable allowing developers to customize the order and execution of policies to meet the specific requirements of their application. The gateway.config.yml file serves as the central configuration file that defines the entire setup of the API gateway including the configuration for pipelines. In the pipeline section of the file, we define sequences of policies that dictate how requests are processed. Each pipeline can be associated with specific routes or services. Within a pipeline, you specify the policies that should be executed for each request. The order of policies in a pipeline determines the sequences of their execution. This allows you to create a structured flow for processing requests. For example, authentication policies will likely come before authorization policies. In addition to pipeline-specific policies, you can also define global policies that apply to all pipelines. Now, let’s build an example Express Gateway pipeline. Navigating to the folder of our new project, we’re going to install Express Gateway. Next, we’ll create our gateway using the command eg gateway create. We’ll name our gateway Microservice-App. Next, we’ll change directories into our new app. Then, we’ll install Express, issuing the command npm install express. Next, let’s start VS Code, edit our gateway.config.yml file, and create our microservices. For our example, we’re going to create two microservices, one called user, which simply returns a list of student names and a JSON array, second, we’ll create a microservice called class that also returns three classes as standard JSON text, then, we’ll configure our pipeline to route through a reverse proxy to both of those as part of the pipelines. And if we look at our user.js code, up top, we have our require function to import the express.js module and assign the functionality to the express variable. Next, we create the express application called app that represents the web app used to configure routes. Below this, we define a route for the user’s path that issues a call back to send a JSON array with a few students name hardcoded. Req is the HTTP request object, res is the HTTP response object. Next is the function that passes control to the next middleware in the stack. Then down at the bottom, we start the server listening on port 3000. Next, we’re going to create our class.js file. If we review our class file, it’s very similar. The primary difference is the app.get method where we use the class path, and we also use a res.status setting it to 200 indicating a successful response. Then we send a hardcoded response as programming 1, 2, and 3 as a JSON array in the response. And below this, we listen on port 4000, instead of 3000. To tie it together, we’ll edit our gateway.config.yml file. And to review the changes we’ve made to our gateway.config.yml file, we’ve defined two new public API endpoints of user and class specifying the host of localhost and the paths to user and class. Below this is our back-end service of user service and class service defined to localhost running on the respective ports 3000 and 7000, and if we scroll down to the pipeline section, this is where we connect the dots. We’ve created two new pipelines of user pipeline and class pipeline. For each, we’ve placed the proxy policy acting as a reverse proxy too for the incoming request to the back-end service. This is how the public endpoint is routed to the back-end service running on either port 3000 or 4000. And if we scroll to the bottom, we can see both for user pipeline and class pipeline. Now, to test this out, we need to start both of our microservices user in class, as well as our gateway, and since we have two microservices and our express-gateway running, rather than opening three command prompts, we can edit our server.js file and include both the user and class files to be executed upon startup of the express-gateway. Doing so, we’re able to start all of our back-end microservices when Express Gateway is started, and to start our application, we’ll use the command npm start. As we can see, both of our microservices are now running on port 3000 and 4000, as well as our gateway listening on port 8080. To test our pipeline, we’re going to issue the URL http://localhost8080 path user, and as we can see, it’s been routed through the reverse proxy to the user’s back-end microservice, returning the simple JSON text, Student1, 2, 3, 4, 5. Likewise, we can test our public class API endpoint through issuing the command localhost 8080, and similar, the gateway serves as a reverse proxy executing the pipeline to route to the back-end microservice returning a simple JSON text, Programming 1, 2, and 3. Now we’ve successfully built a simple pipeline with a reverse proxy using Express Gateway. In this scenario, Express Gateway acts on behalf of the server handling the requests from the client and forwarding them to the appropriate back-end service. Within this module, we covered the many facets of Express Gateway. We discussed the many benefits of Express Gateway, installed, configured, and built a few sample applications. We discussed Helmet.js and the use of certificates to secure and harden our environment. We used OAuth 2 as a mechanism to authenticate and delegate APIs through the gateway. We built Express Gateway pipelines to coordinate the sequence of actions in processing our API requests. In the next module, we’ll explore the various routing options and implement load balancing as an approach to improve performance, reliability, and scalability of our Node applications.
Pipeline concept
A pipeline in Express Gateway is a sequence of processing steps (policies) that each HTTP request must pass through before reaching its final destination.
Policies available in a pipeline
| Policy | Function |
|---|---|
proxy | Forwards the request to a back-end service |
rate-limit | Limits query throughput |
oauth2 | OAuth 2 Authentication |
jwt | Validation of JWT tokens |
basic-auth | Basic Authentication |
key-auth | API key authentication |
log | Query logging |
Complete pipeline example
# Installation
npm install -g express-gateway
eg gateway create
# Nom: Microservice-App
cd Microservice-App
npm install express
# gateway.config.yml - Pipeline avec authentification et rate limiting
http:
port: 8080
apiEndpoints:
public-api:
paths: '/api/*'
serviceEndpoints:
userService:
url: 'http://localhost:3001'
orderService:
url: 'http://localhost:3002'
pipelines:
main:
apiEndpoints:
- public-api
policies:
- rate-limit:
- action:
max: 100
windowMs: 60000
- log:
- action:
message: "${req.method} ${req.path}"
- proxy:
- condition:
name: pathMatch
match: '/api/users*'
action:
serviceEndpoint: userService
- condition:
name: pathMatch
match: '/api/orders*'
action:
serviceEndpoint: orderService
5. Routing and load balancing
Total module duration: 34m 19s
5.1 Overview of Routing and Load Balancing
Our next module covers routing and load balancing. Routing and load balancing in Node optimizes the distribution of incoming requests across multiple servers for improved performance and scalability. Using advanced routing strategies and load balancing techniques, these systems efficiently manage API traffic, ensuring high availability, low latency, and seamless scalability for modern dynamic applications, so let’s review the subtopics we’ll be covering in this module as we delve into both load balancing and routing. First, we’ll provide an overview of both routing and load balancing. Second, we’ll discuss the various scaling techniques. Third, we’ll discuss multi-threaded work distribution using the cluster module, and then testing with AutoCannon to simulate a stress test to evaluate the performance. Fourth, we’ll discuss Express Gateway and load balancing. Last, we’ll discuss fronting Node server pools with a load balancer, in this case, we’ll use NGINX. So to summarize, with load balancing and routing, we have several options, and our specific application requirements will help us determine which approach is suitable. For simple applications, the cluster module can be sufficient so that we can take full advantage of all the CPUs and cores on a machine. For advanced load balancing, NGINX provides advanced configurations and excels in scenarios where we have a cluster of servers or a server pool available to host our node microservices, so let’s review the details of routing and load balancing. First, load balancing offers improved performance and scalability. From a traffic distribution perspective, load balancing evenly distributes incoming requests across multiple servers. This prevents any single server from becoming a bottleneck, ensuring optimal resource utilization and improving the overall performance of a system. From a scalability perspective, load balancing facilitates horizontal scaling by allowing new servers to be easily added to the server pool. This elasticity ensures that the system can handle the increased load gracefully, providing a scalable architecture. Second, load balancing offers enhanced reliability and availability. From a redundancy perspective, load balancing introduces redundancy by distributing traffic across multiple servers. If one server fails or experiences issues, the load balancer redirects traffic to healthy servers minimizing downtime and enhancing the reliability of the system. For fault tolerance, load balancers often perform health checks on servers. If a server is identified as unhealthy, the load balancer can automatically redirect traffic away from that server maintaining overall system stability and availability. Third, load balancing offers optimized resource utilization. They do this by allocating requests based on server capacity and load. This ensures that each server operates within its optimal range, preventing overload loading and maximizing resource utilization. This also provides cost efficiency by optimizing resource usage and preventing overprovisioning of servers. Load balancing contributes to cost efficiency in terms of hardware, overall infrastructure, and operational expenses. Next, let’s discuss routing. First, routing offers flexible endpoint management. From a path-based routing perspective, this routing allows for us to organize the NAT services and endpoints based on their paths. This flexibility makes it easier to manage and structure the APIs, providing a clear and intuitive way to handle different types of requests. And for header-based and query parameter-based routing, these routing decisions are based on the headers or query parameters that provide dynamic control over how the requests are handled. This flexibility is valuable for scenarios where different behaviors are required based on specific conditions. Second, routing offers improved resource utilization. From a dynamic endpoint handling perspective, wildcard routes and dynamic endpoint handling capabilities enable efficient management of diverse endpoints without the need for extensive configuration. This simplifies the handling of complex routing scenarios. Routing also helps with optimized decisions. Effective routing ensures that requests are directed to the most appropriate back-end service. This optimization contributes to the efficient resource utilization, reducing unnecessary processing and enhancing overall system performance. Third, routing offers improved maintainability and adaptability. Routing provides a clear and structured way to organize and manage different services and their endpoints. This improves maintainability and makes it easier for developers to understand and work with the APIs. It also allows for the adaptability to changes. Dynamic configuration options and routing allow for real-time adaptation to changes in the system. This ensures that routing strategies can be adjusted without requiring system downtime supporting continuous deployment and agile development practices. Next, let’s discuss effective routing strategies. First is path-based routing. In path-based routing, these routes are defined based on the path specified in the URL. Requests are directed to the back-end services based on the URL path allowing for a straightforward mapping between endpoints and services. Next is header-based routing. With header-based routing, routing decisions are based on the values of the headers in the HTTP request. This allows for more dynamic routing strategies when specific headers determine the destination service for a given request. Third is query parameter-based routing. This is similar to header-based routing, but routing decisions are made based on the value of the query parameters in the URL. This provides flexibility in handling requests by considering the values of specific parameters. Now, let’s put it all together in an architecture. When an architecture has multiple Express Gateways, it provides the ability to balance the workload and provide scalability options. Load balancing in a Node API gateway involves distributing incoming traffic across multiple back-end servers to optimize resource utilization, enhance system performance, and ensure high availability. This is particularly important in scenarios where the API gateway is handling a large number of concurrent requests or serving multiple microservices concurrently. Load balancing ensures that the incoming requests are evenly distributed among the available back-end servers. This prevents any single server from becoming bottlenecked and optimizes response times. We have various load balancing algorithms that determine how requests are distributed. Common algorithms include round robin, least connections, weighted round robin, and algorithms based on dynamic factors like server health. Scaling in a Node API gateway involves adjusting the system’s capacity to handle varying levels of traffic or resource demands. This can be achieved through horizontal scaling, basically adding additional servers, or vertical scaling, upgrading individual servers to have higher resources. In a horizontally-scaled system, adding instances of an API gateway are added to the infrastructure to distribute the incoming load. This is particularly effective when dealing with a large number of concurrent requests. Vertical scaling involves upgrading the resources, such as the CPU or memory, of an existing server to handle increased demand. While this approach has its limits, it can be effective in improving the performance of individual servers. Autoscaling allows the system to automatically adjust its capacity based on predefined criteria such as traffic volume or resource usage. This ensures the system dynamically scales up or down as needed. Up next, we’ll delve into the various scaling techniques and options.
Advantages of load balancing
| Advantage | Description |
|---|---|
| Performance | Fair distribution of requests, prevention of bottlenecks |
| Scalability | Dynamically adding new servers according to demand |
| High availability | Redundancy: if one server goes down, the others take over |
| Efficiency | Optimization of resource use |
Load balancing algorithms
- Round Robin: Cyclic distribution of requests between servers
- Weighted Round Robin: Round robin with weighting according to server capacity
- Least Connections: Redirection to the server with the fewest active connections
- IP Hash: The same client is always redirected to the same server
5.2 Scalability techniques
Up next, we’ll discuss the techniques that can be used to scale a Node microservice system. As discussed in the prior video, the primary goal of load balancing is to optimize resource utilization, prevent server overload, ensure high availability, and enhance the overall performance and reliability of the system. With Node, two common load balancers are often used in front of Express Gateway, which include NGINX and HA proxy. NGINX is a versatile web server and reverse proxy server that is widely used for load balancing, it supports various load-balancing algorithms and can be configured to distribute traffic across multiple Node instances or back-end servers. HA proxy is a high performance open sourced load balancer that can handle TCP and HTTP-based application. It also offers advanced load-balancing algorithms, SSL termination, and health checks. HA proxy is often used to distribute traffic across multiple Node servers or microservices. Horizontal scaling allows us to distribute the underlying workload. This is accomplished by adding more instances of the application or service to handle increased load. Load balancers distribute incoming requests among these instances preventing any single server from being overwhelmed. When scaling horizontally, it’s critical to consider shared state issues. To be mindful of this, we’ll ensure that shared resources like databases or caches can handle the increased number of connections and maintain data consistency across instances. A newer technique that’s becoming common is the use of stateless architectures where each request contains all the information needed to properly process. This simplifies horizontal scaling. Each instance can handle any request without relying on shared state. Vertical scaling involves increasing the resources such as CPU or memory of existing servers. Load balancing can still be beneficial in the scenario by distributing incoming requests among the upgraded servers. In some cases, a combination of vertical and horizontal scaling may be employed. Vertical scaling can be used to handle increased resource demands for individual instances, while horizontal scaling provides additional capacity. Now, let’s discuss some of the common load balancer routing algorithm. With random routing, the routes are randomly determined for each request. This algorithm is simple and effective in scenarios where all servers have similar capabilities. Round Robin distributes client requests evenly across all servers. Least connection routes new requests to the server with the fewest current connections. This algorithm is useful when application’s workloads are unevenly distributed across the servers. IP hashing direct requests from the same IP address to the same server ensuring session persistence. This algorithm is useful when the application requires session persistence. Least time routes request to the server with the lowest average response time. This algorithm is useful when the response time is critical. Least bandwidth routes requests to the server with the lowest bandwidth usage. This algorithm is useful when the application’s workload is unevenly distributed across the servers. Up next, we’ll delve into improving performance with clustering, specifically the cluster module which allows us to take advantage of all of the processors and cores on the underlying machine.
Load common Node.js balancers
- NGINX: Versatile web server and reverse proxy, supports many algorithms
- HAProxy: High performance load balancer for TCP and HTTP
Horizontal vs. vertical scalability
| Type | Description | Advantages | Disadvantages |
|---|---|---|---|
| Horizontal | Add more instances | Almost unlimited scalability, fault tolerance | Shared State Management |
| Vertical | Increase server resources | Easier to implement | Physical limit of the material |
Stateless architecture to facilitate scalability
In a stateless architecture, each request contains all the information necessary for its processing. This greatly simplifies horizontal scaling.
5.3 Multi-threaded distribution with the Cluster module
Now, let’s explore how Node performance can be improved with the use of the cluster module. This module will allow us to take full advantage of multicore architectures of most modern workstations and servers. In a single-threaded execution model in Node, the application runs on a sole thread leading to certain performance limitations. Under a heavy load, the system experiences increased response time as it struggles to handle concurrent requests effectively. With a single thread, the application’s ability to concurrently process multiple tasks is restricted resulting in delays during high-demand scenarios. Additionally, the single-threaded nature of Node.js leads to limited utilization of multicore CPUs. Modern servers commonly have multiple CPU cores, but a single-threaded node application cannot fully leverage this hardware architecture missing out on the opportunity for parallel processing. As a consequence, the system may not scale properly, especially in environments where high concurrency and efficient use of available computational resources are crucial for achieving optimal performance, and multi-threaded execution models facilitated by the cluster module in Node, the application benefits from improved parallel processing and enhanced scalability overcoming some of the limitations of single-threaded execution. The cluster module enables the creation of multiple child processes or workers, each running on a separate thread and capable of handling concurrent tasks independently. This results in improved parallelism as the application can execute multiple operations simultaneously significantly reducing response times under heavy load. Moreover, the adoption of multi-threading enhances scalability by efficiently utilizing multiple CPU cores available on modern servers. Each worker can be assigned to a specific core allowing the application to scale horizontally and handle increased workloads more effectively, and as we evaluate scenarios to utilize multi-threading, there are a few metrics that are useful. First is the response time which measures the time taken to respond to requests. Second is the throughput which evaluates the number of request processed per second. Third is latency which assesses the time taken for a request to be acknowledged. Autocannon is an open source tool that is useful for testing the performance of HTTP-based applications such as Node.js, its goal is to simulate and measure the behavior of a web server under different load conditions helping identify performance optimization opportunities. When used within the command line, autocannon has many available options. Some of the common parameters we specify include the duration, connections, and workers. For instance, we may specify a duration of 10 seconds, 100 connections, and 6 workers. As we’ll see in our demonstration shortly given my workstation has six cores, this aligns with the underlying architecture. In a moment, we’ll see how autocannon can evaluate the performance of single-threaded and multi-threaded applications. For our demonstration, first, we’re going to build both single and multi-threaded Node applications. Then we’ll test the performance with autocannon, and as we will see the cluster module can offer improved performance with autocannon being useful to provide comparative metrics, so let’s get into the demo. Once we create the folder for our project, we’ll initiate our application and install Express. Now, let’s start VS Code and build our applications. First, we’ll create our single-threaded application in a file called single.js. Now, let’s take a quick look at our code. First, we import the modules to create the HTTP server and handle the HTTP requests. Then, we create our HTTP server called server using the createServer method from the HTTP module. Next, the callback function with request and response handles the incoming HTTP request, setting the response header with a status code of 200 and content type as plain text, then sending the response single-threaded app with response .n. Below this, we set our port to listen on 3000, then we use the listen function of the HTTP module using our defined port while also issuing a callback that logs a message to the console once the server starts listening. Next, let’s create our multi-threaded Node application. Now let’s look at our code intended to utilize all the processes and cores on my workstation. First, we import our modules of cluster, and HTTP cluster is what will allow us to create child processes, often referred to as workers. Next, we determine the number of CPU cores available and place them in num CPUs using the OS module. Next, we check if the current process is the master, and if so, log a message indicating that the master process is running. Below this, we have a for loop that forks worker processes based on the number of CPU cores available. Cluster.on sets up an event listener for the exit event, logging a message when a worker process exits. Below this is our else logic and is where worker process handling occurs. This code in the else block is executed by each of the worker processes. The const server = http createServer is what creates an HTTP server with a callback function to handle incoming requests. We send a simple response to the client that says Clustered App. Then we set our port to 3000, and finally, we tell the server to listen on the specified port and log a message indicating that the worker process is listening on that port. Now, let’s run our applications and test the performance. We will start our single application by issuing the command node single. We can test our application by entering the curl command with our server and port, and as we can see with our response, single-threaded app, our application is running. Now let’s use autocannon to simulate a workload. With this, we’re going to specify 100 connections over 10 seconds, and as we can see, 194,000 requests were processed in 10.08 seconds. Now, let’s switch over, run our multi-threaded app, and compare the results. We’ll stop our single app, then start multi. Now, we can see not only the master is running, but six worker nodes, and to quickly illustrate the number of cores on my workstation, as we can see down on the bottom, we have six cores. And to test our multi-threaded app, we’ll use the curl command with our server and port. And as we can see, the response of clustered app illustrates it is up and running. Next, let’s simulate a workload with autocannon, and this time, we’re able to see 333,000 requests in 10.9 seconds, so let’s compare them side-by-side. Here, we have our single-threaded results from autocannon at the top with our multi-threaded response from autocannon at the bottom, and as we compare them, if we look at the average requests per second, single-threaded was only 19,354, whereas multi-threaded was 33,243. So as we can see, this is a very helpful tool to evaluate the performance and simulate expected loads and response times for particular microservices. Up next, we’ll discuss load balancing with Express.
Node.js single-threaded model issue
- Node.js runs on a single thread, limiting the use of multi-core CPUs
- Under heavy load, response times increase
- Modern servers have several unused CPU cores
Solution: the Cluster module
const cluster = require('cluster');
const http = require('http');
const os = require('os');
const numCPUs = os.cpus().length;
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
console.log(`Starting ${numCPUs} workers...`);
// Fork un worker par CPU
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died. Restarting...`);
cluster.fork(); // Redémarrage automatique
});
} else {
// Les workers créent le serveur HTTP
http.createServer((req, res) => {
res.writeHead(200);
res.end(`Worker ${process.pid} handled this request`);
}).listen(8000);
console.log(`Worker ${process.pid} started`);
}
Performance test with AutoCannon
# Installation d'AutoCannon
npm install -g autocannon
# Test de charge : 100 connexions pendant 30 secondes
autocannon -c 100 -d 30 http://localhost:8000
5.4 Load balancing with Express Gateway
Our next topic is load balancing with Express Gateway. First, we’ll quickly review the Express Gateway load balancing features, then we’ll build a simple application that uses round robin load balancing and iterates through our back-end microservices. As discussed previously, Express Gateway is an API gateway solution built on top of the Express.js web framework. While it offers a range of features for API management, security, and analytics. Its load balancing capabilities are particularly valuable for distributing incoming traffic across multiple back-end services or servers, so let’s discuss how Express Gateway is used for load balancing. Routes in Express Gateway specify how incoming requests are mapped to back-end services. For load balancing, we can define routes that point to the same service, but have different configurations to distribute the load effectively. Express Gateway supports various load balancing policies that determine how traffic is distributed among the available back-end servers. Common load balancing policies include round robin, weighted round robin, least connection, and IP hash. Express Gateway can be deployed in a clustered configuration where multiple instances of the gateway run simultaneously. This allows for horizontal scaling and improve handling of increased request volumes. Next, let’s create a node’s system with a few back-end services that Express Gateway load balances across. We’ll do so by implementing round robin load balancing, so let’s get started. Once we’ve created our directory, we’ll install Express Gateway using the command eg gateway create, and we’ll name our Express Gateway LB-App. Now that that’s installed, we have a new subdirectory that we can change directory to and start VS Code. First, we’ll create our three files that will house our back-end API microservices. Now, let’s look at our code for our back-end microservice. First, we import the express framework, creating an instance of the express application and setting our port to 3001. Next, we define a route for the root path so that when a get request is issued, a callback prints, Hello from App 1. Next, we start our server listening on the defined port and issue a callback to write to the console that our server is running and listening on the defined port. If we look at app2.js, you’ll see it is very similar with the main difference being the port is 3002 and the callback prints, Hello from app 2. Likewise with app 3, we set our port to 3003 and print with our callback Hello from app 3. Next, we’re ready to edit our gateway.config.yml file. If we look at our edits at the top under API endpoints, we set our public path to the localhost in the root folder. Below this is where we create the appServices endpoint that will be used below in the pipelines. Here is where we could use the cluster module or other techniques to spin up multiple copies on different servers or ports. Listing these sequentially by default as the round robin approach, it will start at the first one iterate through to the last, then loop back and start again. And if we scroll down to the pipeline section, we define our appPipeline with the proxy policy pointing to our appService defined above with the three APIs on separate ports. Next, we’ll edit server.js so that our three microservices start whenever the API gateway starts. Now, we’re ready to start our system with the command npm start. Now, we can see that all three of our back-end microservices are running, as well as the gateway. To test our app, we can use the curl command and enter the URL localhost with port 8080 as the Express Gateway public endpoint. And as you can see, I’ve issued this command a few times and it’s iterating through the back-end services in order. Once it reaches app 3, it circles back and starts over again. So with this, we can see how Express Gateway becomes very powerful by using load balancing algorithms, such as round robin, to orchestrate commands with our back-end services. Up next, we’ll discuss fronting Node server pools with NGINX load balancing.
Configuring round robin in Express Gateway
# gateway.config.yml - Round Robin Load Balancing
http:
port: 8080
apiEndpoints:
api:
paths: '/*'
serviceEndpoints:
app1:
url: 'http://localhost:3001'
app2:
url: 'http://localhost:3002'
app3:
url: 'http://localhost:3003'
pipelines:
lb-pipeline:
apiEndpoints:
- api
policies:
- proxy:
- action:
serviceEndpoint: app1
changeOrigin: true
- action:
serviceEndpoint: app2
changeOrigin: true
- action:
serviceEndpoint: app3
changeOrigin: true
loadBalancingStrategy: round-robin
Simple back-end microservice
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3001;
app.get('/', (req, res) => {
res.json({ message: `Hello from App on port ${PORT}`, pid: process.pid });
});
app.listen(PORT, () => console.log(`App running on port ${PORT}`));
5.5 Node server pools with NGINX
Up next, we’ll discuss the benefits of having a load balancer in front of Express API Gateway, then we will build an application to demonstrate and exercise these capabilities. We will start a few back-end services to simulate what could either be Node server pools or Express Gateway pools. Then we’ll place NGINX in front of it and demonstrate four algorithms which include round robin, weighted round robin, least connections, and IP hash. Let’s get started. In this scenario, we are going to focus on the highlighted area. Here, we have NGINX in front of a pool of servers. NGINX would work as a load balancer for direct back-end microservices or in the front of Express Gateway, depending on the scenario. If Express Gateway were present, it would provide the centralized management and the ability to define pipelines for orchestration among the APIs. With both approaches, the NGINX configuration will define the load balancing strategy and forward requests to the defined back-end IP addresses and ports. This configuration allows NGINX to distribute incoming requests among the defined servers providing a scalable and fault-tolerant solution. We will configure our nginx.conf configuration file that contains the specification for how load balancing is to occur by defining both our server pools, as well as our load balancing algorithm, so let’s get into the demonstration where we’ll install NGINX and place it in front of three microservices. First, we’re going to continue from our prior demonstration using App 1, App 2, and App 3. With each of these, we have a very simple app that’s running on each port, port 3001, port 3002, and port 3003. Each of those simply returns Hello from the app and the provided number corresponding to which microservice is called. And after we start these microservices, we’re ready to install configure and use NGINX. I’m going to use NGINX for Windows, and you can see the URL nginx.org/en/download.html, and for this, I’ll be using the stable version 1.24.0. Once I click on this, it will download a zip file. Once I extract the contents of the zip file, then I’ll navigate to it with a command prompt. And here is a quick look at our NGINX folder which we’ll navigate to next on a command line so we can edit the configuration and start the server. And as we can see, if we look at the contents of the folder with the DIR command, we can see the nginx executable, as well as the sub folders. Next, we’ll start VS Code. The configuration for NGINX is placed in the conf folder with the sub file, nginx.conf. Now we’ve edited our configuration file and added a code block to the http section. If you look at the code block for upstream processing called lb_server, this contains our load balancing information. When listed such as this, the default is round robin where it iterates through them sequentially. As discussed previously, round robin works well when the back-end servers have similar specifications, we’ll often refer to this as a homogeneous cluster since all the backend servers are similar. And if we scroll down a bit further, we add two additional lines for proxy pass and proxy set header. For proxy pass, we have our lb_server which is defined above. Going back to a command prompt, we can start NGINX by issuing the command start nginx. And if we’d like to see the subprocesses running, we can use the tasklist command as shown. With this, we can see the main process, as well as worker process. Now, we can test our load balancer using the curl command. And after I issue the order a few times, we can see it iterating through in a round robin fashion. Next, let’s adjust our configuration file to account for different weights so that we can implement a weighted round robin. Here, I’ve added three separate weights, weight 1 for our first app, weight 10 for our second, and weight 50 for our third. This could account for different specifications and compute size, memory, and others for the back-end servers. So with this, once we reload our configuration, we would expect apps 3 and 2 to occur more than 1, and to reload our new configuration, taking advantage of the weights, we’ll use the command nginx -s reload. And if we go back to a command prompt and use the curl command, we should be routed two. Now, we can go back and use the curl command two test weighted routing. As we can see, it’s routed to App 3 in almost every scenario with 2 coming back on one occasion. With App 1 having the weight of 1, the likelihood of it being routed to it is much lower. Now, we can go back and edit our NGINX configuration file. We’ll remove the weights and add fewer connections. You can see that through least_conn. Least connection is useful when we have heterogeneous server environments or perhaps with long lived connections so that we orchestrate new requests to those with the least amount of current activity. Now, we can reload our configuration and test the application using least connections issuing the command a few times or routing to the server with the least connections. Now, we can go back to our NGINX config file and replace least connections with ip_hash, and this will provide session persistence with IP hashing. Maintaining session persistence is something to be mindful of when designing a load performance strategy. Many applications preserve login sessions or monitor the progress of larger or longer. Running back-end services, these will benefit from session persistence. So again, we’ll accomplish this by adding ip_hash. Next, we can reload our configuration and test with curl. And as you can see, I’ve issued the curl command many times and the load balancer is routing it to App 2 in every scenario. So now, we’ve built a fully-functioning load balancer with NGINX and demonstrated a few different load balancing algorithms. And in our next module, we’ll delve into authentication and authorization within our Node applications and uncover how API gateways can assist with standardizing and implementing this across a portfolio of managed back-end microservices.
NGINX configuration for load balancing
# nginx.conf - Load balancing avec 4 algorithmes
upstream backend_servers {
# Round Robin (défaut)
server localhost:3001;
server localhost:3002;
server localhost:3003;
}
upstream weighted_servers {
# Weighted Round Robin
server localhost:3001 weight=3;
server localhost:3002 weight=2;
server localhost:3003 weight=1;
}
upstream least_conn_servers {
# Least connections
least_conn;
server localhost:3001;
server localhost:3002;
server localhost:3003;
}
upstream ip_hash_servers {
# IP Hash (sticky sessions)
ip_hash;
server localhost:3001;
server localhost:3002;
server localhost:3003;
}
server {
listen 80;
location / {
proxy_pass http://backend_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
6. Authentication and authorization
Total module duration: 29m 1s
6.1 Authentication and authorization strategies and techniques
Welcome everyone. In our next video, we’ll cover authentication and authorization in our Node.js applications. For our agenda, we have four topics that we’ll cover. First, we’ll discuss strategies and techniques for authentication and authorization. Second, we’ll discuss session-based authentication. Third, we’ll cover password-less authentication. And fourth, we’ll cover the protection of route with role-based access control. So let’s get into our first topic which covers the strategies and techniques for authentication and authorization. Authentication and authorization are intertwined to establish security within our application and microservices. Authentication is the process of verifying the identity of a user, system, or entity attempting to access a resource or perform an action. It ensures that the user is who they claim to be before granting access to the requested service or data. So we have a few different verification methods. First is the use of passwords. Second is the use of biometrics, which could be physical or behavioral traits such as fingerprints or facial recognition. Third is two-factor authentication which requires two forms of identification for added security. And fourth is multi-factor authentication involving multiple layers of verification. For authentication protocols, we also have a few options. First is OAuth used with third-party authorization, often seen in social media logins. Second is OpenID Connect built on top of OAuth which focuses on authentication. Third is SAML, Security Assertion Markup Language, which enables single sign-on across multiple services. And for some considerations with authentication, we have a few, such as balancing security and user experience, also protecting against password-related vulnerabilities and continuous monitoring for suspicious activities. Next, let’s discuss authorization. So authorization is the process of granting or denying access to specific resources or actions based on the authenticated users permissions. It ensures that the authenticated users have the appropriate level of access to perform their tasks, preventing unauthorized actions. For access control, we would have a few options such as role-based access control, which is the most prevalent assigning permissions based on things like job roles. Another approach is the use of attribute-based access control, which considers attributes like time, location, and other user characteristics and attaches the access controls to those aspects. And third could be mandatory access controls which assigns access based on security labels. For authorization mechanisms, there is a couple to consider, such as access control lists, sometimes called ACLs, which are lists defining permissions attached to an object. Second is capability-based security where the user possesses a token providing their authorization. And third is policy-based access control where the access is determined by predefined policies. And for the considerations, we have a few such as ensuring the principle of least privilege, managing permissions as the user roles evolve and change, and also monitoring and auditing for unauthorized access. Authentication and authorization play key roles in ensuring the security and integrity of our known microservice ecosystem when centrally managed through an API gateway such as Express Gateway. Authentication verifies the identity of the users or services which safeguards against unauthorized access and potential security threats. Authorization, on the other hand, that takes the level of access granted to those authenticated entities preventing unauthorized actions and enforcing proper permissions. By serving as a centralized management point, Express Gateway simplifies the implementation and enforcement of authentication and authorization across the microservices, promoting consistency, scalability, and ease of maintenance. The common scenario is for an end user or system to authenticate first and then use authorization policies to determine if adequate credentials exist. So for authentication, Express Gateway offers a few benefits, such as middleware that can be useful for authentication. Express API Gateway can leverage middleware functions to handle this authentication. Commonly used middleware for this includes Passport.js which supports various authentication strategies, also supported is token-based authentication. Many microservice architectures use tokens such as JSON Web Tokens, or often called JWT tokens. The gateway can validate incoming tokens to ensure the authenticity of the user. This often involves decoding the token, verifying the signature, and checking additional claims. And then finally, single sign-on. For systems implementing single sign-on, the gateway can act as a central authentication point. It validates user’s credentials once and issues tokens for subsequent requests to microservices. This reduces the need for repetitive authentication checks. Once users or our systems are authenticated, then the authorization policies and approaches can be utilized. Similar to authentication, Express Gateway offers many benefits to implementing authorization such as role-based access control. Express Gateway can enforce role-based access control by examining the user roles contained in the authentication token. Based on the user’s role, the gateway can decide whether to allow or deny access to specific microservices or endpoints. Custom middleware or third-party authorization middleware can be employed to enforce access controls. These middleware components Analyze the users, details, and makes decisions about whether the user has necessary permissions to access a resource. In some scenarios, API keys are used for authorization. Express API Gateway can check the validity of the API keys associated with incoming requests before allowing access to the underlying microservices. Express Gateway can serve as a central point for defining and enforcing authorization policies. Policies can specify which users or roles have access to specific microservices or endpoints providing a fine-grained control mechanism. So let’s discuss a few techniques and best practices of authentication. OAuth 2 can be used for secure delegation of authorization. Express Gateway supports OAuth 2 flows including authorization code, implicit resource owner password credentials, and client credentials. OpenID Connect can be leveraged for authentication on top of OAuth 2. Express Gateway acts as an OpenID Connect provider enabling features like single sign-on and identity federation. We can use JWT tokens for stateless authentication. Express Gateway can validate and decode JWT tokens to authenticate users. This is often employed in token-based authentication scenarios. We can implement API key authentication for simple token list access to APIs. Express gateway can validate API keys associated with incoming requests. And for simplicity, Express Gateway supports basic authentication; however, it’s recommended to use more secure methods for production environments due to the inherent limitations and vulnerabilities of Basic Auth. So let’s discuss a few best practices for authentication. First, we can use Express Gateway as a centralized point for authentication to avoid duplicating authentication logic across microservices. This simplifies management and ensures consistency. When dealing with tokens, we’ll need to ensure proper validation. For JWT tokens, validating the signature, expiration, and issuer can be used to enhance security. It’s a best practice to implement rate limiting and throttling to protect against brute force attacks and other malicious activities. Express Gateway has built-in plugins for rate limiting. For secure communication, ensuring that communication between clients, Express Gateway, and microservices is secure. We can use HTTPS to encrypt data in transit and employ secure channels. We can consider integrating multi-factor authentication for additional security layers, especially in scenarios where higher assurance is needed. If the microservices are using API keys, implementing key rotation practices regularly will enhance security, and we can implement RBAC, or role-based access control, to manage access control efficiently. Express Gateway allows you to define roles and the associated permissions for fine-grained control over who can access specific resources. And finally, implementing security headers can enhance the security posture of our APIs setting headers like content security policy, strict transport security, and others can protect against various types of attacks. Up next, we’ll delve into session-based authentication and build a sample working application.
Difference between authentication and authorization
| Concept | Definition | Key question |
|---|---|---|
| Authentication | Identity Verification | ”Who are you ?” |
| Permission | Checking permissions | “What can you do?” |
Identity Verification Methods
- Passwords: Traditional method, to be coupled with secure hashing
- Biometrics: Fingerprints, facial recognition
- Two-Factor Authentication (2FA): Two forms of identification
- Multi-Factor Authentication (MFA): Multiple layers of verification
Authentication protocols
- OAuth 2: Delegation of authorization (social logins)
- OpenID Connect: Based on OAuth, focused on authentication
- SAML: Single Sign-On between multiple services
- JWT (JSON Web Token): Self-contained tokens for stateless authentication
6.2 Session-based authentication
Up next, we’ll review a few different authentication approaches and then implement a session-based application example. So let’s look at a few different authentication methods. First is session-based authentication. This is the prevailing form of authentication which involves the submission of a username and password matched against a stored database record. Successful entry results in initiation of a session assigned by a specific identifier. Termination of a session occurs upon user logout. A key security measure in the implementation of sessions is expiration which is programmed to automatically conclude after a predetermined duration. This practice is particularly prevalent in financial applications, including banks and trading platforms. If you’ve used a banking or an investment app, you’ve probably been logged out after a few minutes of inactivity. Next is token-based authentication. Rather than employing tangible credentials for request authentication, token-based authentication provides users with a transient token stored in the browser. Although some implementations may use credentials to grant the token, typically, this is a JWT token encapsulating all requisite information for endpoint validation. Each user-initiated request incorporates this token. A notable advantage of token-based authentication lies in its capacity to embed details regarding a user’s role and permission, removing the need to retrieve such information from a database. This feature contributes to increased security as even in the event of token theft, attackers are afforded limited access to critical information. Third is password authentication. This authentication paradigm distinguishes itself from conventional methods. The absence of required credentials for a login sets it apart. Users only need an associated email address or phone number prompting the creation of a magic link or one-time password for each login attempt. Clicking the link seamlessly redirects the user to the application already authenticated and renders the magic link invalid for subsequent use. Concurrently generated with the magic link is a JWT token constituting the authentication mechanism. So let’s take a quick look at the flow of activities of how session-based authentication works, then we’ll get into a demonstration. First, the user enters a website with a username/password text box, enters the information, and issues an HTTP Post with credentials. Upon validating the username and password, the server responds with a session cookie. Now, the user has an authenticated HTTP request with the session cookie. And the HTTP server can respond with a 200 or OK status that the session is valid and usable for subsequent requests. So let’s build a session-based authentication system and test it out. First, we are going to create the folder structure to place our application, then initiate our project. Next, we’ll install our dependencies. We’ll start VS Code so we can create our application. First, we’re going to create an environment variable where we’ll place our secret session variable. We’ll do so by creating a file called .env, and in this file, we’re going to create our environment variable. Sometimes we’ll often place other variables here such as the host name, ports, or even database connection information. So next, we’ll create our application file. So let’s take a look at our code. Up top is our Express setup and port specification. For the session setup, it’s going to retrieve a secret key from the environment variable we just created, the .env file. For production, we’d probably use Mongo or Redis, but for the demo, this will illustrate the functionality. Next is the authentication middleware that checks if the user is authenticated by looking at the authenticated property in the session. If not authenticated, it responds with a 401, otherwise it proceeds. App.use is our session middleware configuration specifying the options. And below that, we have our routes. The root route simply responds with a greeting message. The login route handles the user authentication. If the user is not authenticated, it sets the authenticated property in the session to true, otherwise, it indicates that the user is already authenticated. Below that is our logout route which uses the protect middleware to ensure that only authenticated users can log out, and if so, destroys the session. The protected route also uses protect, ensuring that only authenticated users can access the protected service. Finally, we tell our servant to listen on the defined port. So let’s test out our application. And as we can see, our server is up and running on port 3000. So first, let’s navigate to our root path, and we receive our welcome message. Next, let’s try the protected path, and as we can see, since we do not have a session, we receive an unauthorized. So to illustrate the creation of a session, let’s navigate to the login endpoint. In a production environment, we may use passwords, multi-factor, or some other more secure approach, but for the example, we’ll just navigate to the route and have it create our session, and we receive successfully authenticated. If we refresh this page, we receive an already authenticated. So now, let’s navigate back to protected and see if we have access. And here we can see we have access to the protected route with the response, Hello from the protected service user. And to just demonstrate killing the session, if we navigate to the logout endpoint, we’ll receive a successfully logged out. To verify this, we can go back to the protected endpoint, and we are unauthorized again. So now we’ve built an application that implements session-based authentication. Up next, we’ll look at how passwordless authentication works and build a working example.
Comparison of authentication methods
| Method | Description | Advantages | Disadvantages |
|---|---|---|---|
| Session-based | Session with ID stored server side | Simple, immediate revocation | Server-side state, complex scaling |
| Token-based (JWT) | Client-side self-contained token | Stateless, scalable | Complex revocation |
| Passwordless | Magic link / OTP by email/SMS | Improved UX | Dependence on communication channels |
Example of session authentication with Express
const express = require('express');
const session = require('express-session');
const app = express();
app.use(express.json());
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS uniquement
httpOnly: true, // Inaccessible via JavaScript
maxAge: 15 * 60 * 1000 // 15 minutes
}
}));
app.post('/login', (req, res) => {
const { username, password } = req.body;
// Vérification dans la base de données (exemple simplifié)
if (username === 'admin' && password === 'secret') {
req.session.user = { username, role: 'admin' };
res.json({ message: 'Connecté avec succès' });
} else {
res.status(401).json({ error: 'Identifiants invalides' });
}
});
app.get('/protected', (req, res) => {
if (!req.session.user) {
return res.status(401).json({ error: 'Non authentifié' });
}
res.json({ message: `Bonjour ${req.session.user.username}` });
});
app.post('/logout', (req, res) => {
req.session.destroy();
res.json({ message: 'Déconnecté' });
});
app.listen(3000);
6.3 Authentication without password (Passwordless)
Now, let’s review passwordless authentication. If you’ve ever received a one-time link or password via SMS text or email, you were probably using passwordless authentication, so let’s take a look at what these details. Next. Let’s look at the process flow of events that occur between the client and server for passwordless authentication. First, the user provides information to ensure the system recognizes who they are, and it’ll send them a link to their preferred method, email or SMS text. For instance, the system may have a valid phone number and email, and if the end user provides one that matches and recognizes it, they are sent a valid link. Next, the server response with the one-time magic link or even could be a code that is used for login. Then the user clicks the link, logging them in and making the link invalid for subsequent use, so let’s build a passwordless application. For this, we’ll use ngrok so that we have a public URL so that it is accessible from the outside. Also, rather than creating web-based forms SMTP servers to relay email, we’ll just print our magic link to the screen similar to what we did with the session-based authentication. Essentially, we want to demo how we can generate the random link and verify it works as intended. We’ll also see how the magic link creates a JWT token that we can use for authentication, so let’s get into the demonstration. After we have created our project folder and directory to contain our files, we’ll issue the npm initiation command. Next, we’ll install our dependencies, express cors body parser and JSON Web Token. Now, let’s open VS Code and create our application. As mentioned using ngrok, you can see the executable within our project structure. I have it started in a separate window. As you can see with the command start ngrok, it will initiate provide your public IP address, as well as responses that are received. So let’s create our application file. Now let’s review our code. First, we have our secret key and public URL. As we just reviewed, we can copy that public address and place it here in this variable. Next, we include our dependencies and server configuration such as the port. Then we start our middleware setup with cors and body parser to parse the JSON data in the request body. Then next is our endpoints. First is send magic link. This is the endpoint that generates a JWT token and sends the magic link to the response. The JWT has the issuer expiration date and subject. And as you can see, we print it to the console, as well as returning it to the screen. Below this, we have authenticated user. Authenticate user handles the authentication when the user clicks on the magic link. Verifying the JWT token sets the cookie for the user information and session id then redirects the user to the welcome endpoint. The welcome endpoint simply returns a welcome message. Then we have two blocks of code with the app.get that serves as the error handling middleware with one for 404 errors and the other as sort of a catch all. If an error occurs, it clears the cookies and sends an error response. So let’s start our app and test it out. And if interested in the ngrok executable, it is simply an exe file that can be downloaded directly from the website ngrok.com/download. I simply download it here and placed it in the project folder. Okay, so let’s test our application. First, we’ll try to access the authenticated user endpoint, then we receive a Not Authorized. So to receive access, we are simply going to connect to the send magic link endpoint. And just to flip back to the console, as we can see, it printed the HTML to generate the Click Me link, which we also saw in the screen and we’ll go click in a moment, but essentially for the parameters, you can see token = the dynamic token that was generated. So now that we have our magic link, we can click on it, and since this is our first time routing through ngrok, we’ll hit Visit Site, and it will take us directly to our endpoint Welcome, revealing that we are now logged into the app using the JWT token. So as you can see, the only way to connect to this endpoint is through the magic link with the random-generated token. And now we’ve created the core elements needed to have a passwordless authentication mechanism. Up next, we’ll discuss protecting routes with role-based access control.
Passwordless authentication flow
- User provides email or phone number
- The server generates a unique magic link or OTP code
- The link / OTP is sent by email or SMS
- User clicks link / enters code
- The server validates and generates a JWT token
- The link becomes invalid (single use)
Passwordless implementation example with JWT
const express = require('express');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const SECRET = 'your-jwt-secret';
const magicLinks = new Map(); // En production: utiliser Redis
app.post('/auth/request', (req, res) => {
const { email } = req.body;
// Générer un token unique
const token = crypto.randomBytes(32).toString('hex');
magicLinks.set(token, { email, expires: Date.now() + 15 * 60 * 1000 });
// En production: envoyer par email via Nodemailer ou SendGrid
const magicLink = `http://localhost:3000/auth/verify?token=${token}`;
console.log('Magic link:', magicLink); // Simulé pour la démo
res.json({ message: 'Lien magique envoyé' });
});
app.get('/auth/verify', (req, res) => {
const { token } = req.query;
const data = magicLinks.get(token);
if (!data || Date.now() > data.expires) {
return res.status(401).json({ error: 'Token invalide ou expiré' });
}
magicLinks.delete(token); // Usage unique
// Générer le JWT d'authentification
const jwtToken = jwt.sign({ email: data.email }, SECRET, { expiresIn: '1h' });
res.json({ token: jwtToken });
});
app.listen(3000);
6.4 Road protection with RBAC
Next, we’ll cover the use of role-based access control to protect our routes. Doing so, we’re able to utilize groups to protect routes and assist with securing our endpoints. So let’s discuss the benefits of using RBAC and our microservices architecture. Group-based roles involves categorizing users into logical groups based on their responsibilities, functions, or permissions within an organization. Each group is associated with a specific role that defines its access rights and privileges. This simplifies the management of user access, provides scalability, and allows for granular control over endpoints and routes. Central configuration involves consolidating access control and policies into a single centralized location. This centralization ensures that access roles are defined consistently and can be easily managed. This is beneficial by being consistent and providing easier maintenance. Controlling access to routes involves specifying which users or groups are allowed to access particular routes or endpoints within a Node.js application. RBAC is used to define and enforce these access controls dynamically enabling enhanced security, dynamic management, and fine-grained control. So let’s review the high-level steps needed to implement route protection with RBAC. First, we create the roles and configure the policies for their use. Second, we associate particular users with each of the roles we’ve created. Third, we apply the roles and the associated policies to the routes. And fourth, we integrate with the authentication method. So let’s take a quick look at a demo where we’ll implement these steps. For this demonstration, we are going to create three JavaScript files. First will be our roles file that will create and describe the roles we’ll be using. Second is the authentication file which generates and verifies our tokens. Then we create our main application with our endpoints and having roles defined to the routes. After creating the directory structure to house our application, we’ll issue the npm init -y command to initiate our project. Next, we’ll install our dependencies which include Express and JSON Web Token. Then we’ll start VS Code and create our three files. First, we’ll create roles.js. In this file, we describe the two roles that we’ll be using. The role has two properties, user and admin, and we export the roles objects so that other modules can use, which we’ll see in a moment. Next, we’ll create our auth.js file. Within our Auth-Code, we first import our dependencies, create our secretKey for signing, and verifying JWT tokens and have functions to generate and verify tokens. GenerateToken is going to use the jwt.sign method to create the JWT token which expires in 1 hour. The verify function takes a JWT as an argument and a verification is successful. The decoder payload is returned and the role information is extracted. Then the module is exported so that it can be used in app.js. So next, let’s create app.js, and if we look at our JavaScript code, first, we import our dependencies up top and initialize Express, as well as set the port to 3000. Then we generate the JWT for the admin role and assign to the admin role and token variable. We log the generated tokens, then we have middleware for JSON pairing with express.json. Next, we have our two routes, public and admin. For our public route, it is accessible to all users and responds with a simple JSON message. For our admin route, it requires authentication based on the user’s profile. It does so by calling the authenticateUser function. And below that, we can see the authenticateUser function, which is the middleware that checks for a valid JWT token in the authorization header of the request. It verifies the token using auth.verifyToken and extracts the user’s role. If the token is missing or invalid, it returns a 401. If the user’s role is not valid or doesn’t match, it returns a 403. If the authentication is successful, it calls the next middleware or route handler. Finally, below this, the server is started. Any message is sent to the log indicating the server is running. So let’s start and test our application. To test it, we are going to use the curl command. First, we’ll navigate to the public endpoint, and as we can see, a simple JSON is returned which prints public route. Next, let’s attempt to connect to our admin endpoint. Doing so, we receive an unauthorized. So to authorize, we need to pass our authorization token with the curl command. We can pull that from our console as it printed whenever the application started. Now, we’ll use the curl command using our token. We issue the command curl -H Authorization, then our token, then our URL and the admin endpoint. And as we can see, we receive a response from the admin route successfully accessing it. So now we’ve used the admin role, applied it to the admin route, and this is just a simple demonstration of how roles can be applied to these endpoints. Up next, in our next module, we’ll discuss rate limiting and quotas.
Benefits of RBAC (Role-Based Access Control)
- Simplified management: Permissions are assigned to roles, not individual users
- Scalability: Easily add new users to the right group
- Granular control: Fine-grained protection of each route/endpoint
- Centralized maintenance: Changing permissions at role level only
RBAC Implementation Steps
- Create roles and their permissions
- Associate users with roles
- Apply roles to protected routes
- Integrate with authentication method
RBAC Implementation Example
// roles.js - Définition des rôles et permissions
const roles = {
admin: ['read', 'write', 'delete', 'manage'],
editor: ['read', 'write'],
viewer: ['read']
};
module.exports = roles;
// auth.js - Génération et vérification des tokens JWT
const jwt = require('jsonwebtoken');
const SECRET = 'your-secret-key';
const generateToken = (user) => {
return jwt.sign({ id: user.id, email: user.email, role: user.role }, SECRET, { expiresIn: '1h' });
};
const verifyToken = (req, res, next) => {
const token = req.headers['authorization']?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Token manquant' });
try {
req.user = jwt.verify(token, SECRET);
next();
} catch {
res.status(403).json({ error: 'Token invalide' });
}
};
module.exports = { generateToken, verifyToken };
// app.js - Application principale avec RBAC
const express = require('express');
const roles = require('./roles');
const { verifyToken, generateToken } = require('./auth');
const app = express();
app.use(express.json());
// Middleware de vérification des permissions
const checkPermission = (permission) => (req, res, next) => {
const userPermissions = roles[req.user.role] || [];
if (!userPermissions.includes(permission)) {
return res.status(403).json({ error: 'Permission refusée' });
}
next();
};
app.get('/api/data', verifyToken, checkPermission('read'), (req, res) => {
res.json({ data: 'Données accessibles aux lecteurs' });
});
app.delete('/api/data/:id', verifyToken, checkPermission('delete'), (req, res) => {
res.json({ message: `Élément ${req.params.id} supprimé` });
});
app.listen(3000);
7. Rate Limiting and Quotas
Total module duration: 21m 17s
7.1 Why rate limiting and quotas?
Up next, we’ll cover rate limiting and quotas in our Node APIs and microservices, so let’s get started. For the agenda of rate limiting and quotas, we’ll have three subtopics. First, we’ll cover what rate limiting and quotas are and why we may need them in our applications with a heavy focus on the benefits they provide. Second, we’ll discuss rate limiting concepts, techniques, and testing approaches. With this, we’ll cover the different rate limiting algorithms and then demonstrate a few rate limiting techniques and test them with Postman. Third, we’ll discuss the boundaries for applying rate limiting which could be on a route-by-route basis or globally across the application. So let’s get into our first topic which covers what is rate limiting and why might we need it in our applications. First, let’s touch on a few reasons why we may want to use rate limiting and quotas in our APIs. Rate limiting and quotas ensure stable and reliable API performance by preventing sudden spikes in traffic that could overwhelm the system. By controlling the rate of incoming requests, these mechanisms maintain consistent service levels, preventing performance degradation and downtime during periods of excessive demand. Rate limiting and quotas act as a protective barrier against abuse and security attacks by restricting the volume of requests a client can make within a specific timeframe. This prevents malicious attackers from overloading the system with excessive traffic safeguarding against distributed denial of service, sometimes seen as DDoS, attacks, brute force attacks, and other forms of abuse that could compromise the API’s integrity and availability. Rate limiting and quotas contribute to the overall scalability and availability of the system by preventing resource exhaustion. By distributing and managing API usage efficiently, these mechanisms ensure that the system resources are allocated judiciously preventing individual clients from monopolizing resources and enabling the system to scale effectively to meet growing demands while maintaining high availability. Now, let’s look at both rate limiting and quotas and review how they can help protect against attacks. First, let’s look at rate limiting. By restricting the number of requests from a single client within a timeframe, rate limiting helps mitigate DDoS attacks. It makes it difficult for attackers to overwhelm the server with a high volume of requests as each client is limited to a certain number of requests per second. Rate limiting protects against brute force attacks where an attacker tries to gain unauthorized access by repeatedly attempting different combinations of usernames and passwords. Rate limiting limits the number of login attempts within a given time period, making it more challenging for attackers to guess credentials. Rate limiting helps protect value resources by preventing excessive consumption. This is crucial in scenarios where APIs have limited resources, and abuse or malicious activities can affect the overall performance and availability. Rate limiting protects against resource exhaustion attacks where an attacker tries to consume all available resources leading to service unavailability. By limiting the rate of incoming requests, the API gateway can prevent such attacks from overwhelming the system. Now, let’s review quotas. Enforcing quotas on API usage helps prevent abuse and unauthorized access. By setting limits on the number of requests a client can make within a specified time window, quotas ensure that only legitimate users or applications can access the API at the expected rate. Enforcing quotas can be crucial for compliance with service-level agreements and regulatory requirements. It ensures that clients adhere to predefined usage limits and prevent scenarios where a single client monopolizes resources. Rate limiting and quotas are often heavily used when assembling the pricing strategy and various consumption options, so let’s review a few of these considerations. Enforcing quotas allows API providers to structure pricing models based on the volume of API requests. By setting limits on the number of requests or data transfers allowed within a specific time period, providers can monetize their services effectively and ensure fair compensation for usage. Rate limiting and quotas support tiered pricing models where different levels of service come with different limits. For example, a free tier might have lower limits while a premium or enterprise tier enjoys higher quotas and more generous rate limits. This approach encourages users to upgrade for increased capacity. Usage-based billing is often directly tied to the number of requests made to an API. By implementing rate limiting, API providers can structure their pricing strategy around the actual usage of the services. This ensures that users are billed based on the resources they consume. Rate limiting and quotas provide granular control over API usage. This granularity enables API providers to offer customized pricing plans tailored to the specific needs of different clients. Clients with higher demands can subscribe to plans with elevated limits. Enforcing quotas is not only a security measure, but also a way to prevent resource abuse in the context of a pricing strategy. It ensures that clients cannot excessively consume resources without adhering to their allocated limits, protecting the API provider’s infrastructure, and optimizing resource allocation. By incorporating rate limiting into the pricing strategy, API providers can encourage clients to use the API efficiently. Clients who stay within their allocated rate limits may benefit from lower costs creating an incentive for responsible and optimized usage. Rate limiting and quotas can be adjusted dynamically based on real-time usage patterns. This adaptability allows API providers to implement dynamic pricing strategies responding to the changes in demand, resource availability, and market conditions. Establishing quotas is not only about pricing, but also about ensuring compliance with service-level agreements. It sets expectations for the level of service a client can expect based on their subscription tier. Within node, we have the ability to apply rate limiting within our individual files, such as Express web apps, as well as those placed behind Express Gateway. When used in Express Gateway, we’ll edit our gateway.config.yml file and place it within the specific route, or we can provide it as the default for all routes. In the context of Express Gateway, rate limiting offers substantial benefits for controlling the flow of incoming requests. Express Gateway allows developers to integrate and configure rate limiting policies, setting restrictions on the number of requests a client can make within a specific timeframe seamlessly and centrally. Express Gateway excels in implementing quotas providing a powerful mechanism for managing API consumption based on predefined usage limits. Within Express Gateway, administrators can set quotas at the client or application level allowing for customized access based on subscription tiers or individual user needs. Up next, we’ll delve into the rate limiting concepts, techniques, and testing. We’ll do so by exploring the various algorithms, building a few, and then testing them out.
Main reasons to use rate limiting
| Reason | Description |
|---|---|
| Stability | Preventing traffic spikes that overload the system |
| Security | Protection against DDoS attacks, brute force and abuse |
| Scalability | Preventing server resource exhaustion |
| Equity | Fair distribution of resources between users |
| Economic model | Basis for freemium/premium subscription levels |
7.2 Rate limiting concepts, techniques and tests
Now, let’s review some of the rate-limiting algorithms. We will implement sliding window and token buckets, then test them in Postman, which is great for simulating a load with subsequent requests. So let’s review the various rate-limiting algorithms. First is the sliding window algorithm which controls the number of requests a user or IP address can make within a specific timeframe. Second is the token bucket algorithm where we allocate tokens at a fixed rate and requests can only be made if there are available tokens. Third is leaky bucket algorithm where we will allow a fixed number of requests per unit of time. Excess requests are stored in a bucket and processed at a steady rate. Fourth is burst versus sustained limits where we distinguish between the maximum number of requests allowed in a short burst and the sustained rate over a longer period. Soft and hard limits and rate limiting refers to two different approaches to handling excessive requests. Soft limits are threshold set on the rate of incoming requests, and they are typically configured to allow a brief period of exceeding the limit without immediate consequences. Hard limits, on the other hand, are strict thresholds that, when crossed, immediately trigger consequences such as request rejection, HTTP error responses, or other predefined actions. Now, let’s demonstrate using both sliding window and token bucket. We can use Postman to execute more thorough and advanced testing, so let’s get started. After we create our folder to house our application, we’ll initiate our project with npm init -y. Next, we’ll install our dependencies, Express, Express rate limit, and then rate limiter flexible. Express rate limit will be used for our sliding window, and flexible rate limiter will assist with our token bucket. Now let’s start VS Code and create our application. We’ll create a file called app.js to replace our code. Now, let’s review our code. Up top, we import our dependencies and set up Express and our port. Below this, we are using rate limiter flexible which will assist in implementing the token bucket approach. Then, we set up our token bucket with the rate limiter variable. For this, we specify a capacity of five points to be added to the bucket after every 4 seconds. This is applied to the bucket route below, so let’s scroll down and take a look. Within the bucket route, when a request is issued, it will consume one token. If enough tokens are available, the request is allowed, otherwise, a 429 response is sent. Now, let’s scroll back up and look at our sliding window setup. Below our token bucket, we have our sliding window rate limiter where we set up the limiter variable specifying 10 requests allowed during a 1-minute timeframe or window. This is applied to the limited route down below, and we have custom middleware with app.use to log the details of each incoming request, including the path and method. Scrolling to the bottom, we can see we start our app with app.listen using our port we defined above of 3000, so let’s start our app and test this out. I’m going to split the screen so we can watch the console of our token bucket rate limiter, and we’ll use the curl command issuing our server name port and route to bucket. As we can see, we received a response. This is a limited microservice route using token bucket. If I issue this multiple times, we’ll see if rate limiting works. And as we can see after five requests, our token bucket was empty telling us too many requests were sent. We also printed to the console information about our bucket. Now let’s try the curl command on our sliding window route. This should work 10 times before the limit has been reached. And as we can see, too many requests, please try again later. And for more advanced testing, we can use Postman. So what I’m going to do is create a new collection and place both GET requests within it. I’ll name my collection Rate-Limit Test. Now, I can add a request to it by clicking the link, add a request. I’ll name the request bucket and then enter my URL localhost port 3000 bucket, then click Send to confirm that it works. And we can see with our response, this is a limited microservice route using token bucket. Next, we’ll save it and create our second test for our sliding window, and we’ll click send to test it out, and we can see it’s working. Now, I’ll save my request as SlidingWindow. Now, within my collection, I have both SlidingWindow and bucket. Next, I’ll click Rate-Limit-Test over here on the right, Run collection. Next, I’ll click on the Performance tab where I can specify the performance parameters. With this, we’ll mimic and increment up to 20 users over a test duration of 1 minute, and we’ll click Run and watch our test occur. And I’ve slid the window up so we can see what’s occurring in the background. So what’s occurring is our sliding window worked for the very first part, then reached the limit and has issued errors ever since. whereas our token bucket is reset every 5 seconds, adding more tokens to the bucket, and we can see that through the peaks and valleys. So now, we’ve successfully built and tested both sliding window and token bucket rate limiting algorithms and tested them with both curl and Postman. Up next, we’ll demonstrate individual versus global routes. First, we’ll look at this within a Node application, then we’ll review from Express Gateway and see how the central management occurs.
Rate limiting algorithms
| Algorithm | Operation | Use cases |
|---|---|---|
| Sliding Window | Control of the number of requests. in a sliding window | General APIs |
| Token Bucket | Tokens allocated at a fixed rate, consumed per request | Bursts allowed |
| Leaky Bucket | Constant treatment, queue for excess | Traffic smoothing |
| Fixed Window | Counter reset at fixed intervals | Simple to implement |
Soft limits vs Hard limits
- Soft limits: Thresholds with grace period before consequences
- Hard limits: Strict thresholds - any violation immediately triggers an HTTP 429 rejection
Installing dependencies
npm init -y
npm install express express-rate-limit rate-limiter-flexible
Implementation - Sliding Window
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
// Sliding Window : 100 requêtes par minute
const slidingWindowLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => {
res.status(429).json({
error: "Too Many Requests",
message: "Limite dépassée. Réessayez dans 1 minute.",
retryAfter: res.getHeader("Retry-After")
});
}
});
app.use('/api/sliding', slidingWindowLimiter);
app.get('/api/sliding/data', (req, res) => {
res.json({ message: "OK - Sliding Window" });
});
app.listen(3000);
Implementation - Token Bucket
const { RateLimiterMemory } = require('rate-limiter-flexible');
// Token Bucket : 10 requêtes par seconde
const tokenBucketLimiter = new RateLimiterMemory({
points: 10,
duration: 1
});
const tokenBucketMiddleware = async (req, res, next) => {
try {
await tokenBucketLimiter.consume(req.ip);
next();
} catch (err) {
res.status(429).json({ error: "Token bucket épuisé" });
}
};
app.use('/api/token', tokenBucketMiddleware);
app.get('/api/token/data', (req, res) => {
res.json({ message: "OK - Token Bucket" });
});
Testing with Postman
Postman allows you to simulate a load to test the limits:
- Collection Runner: Executing queries repeatedly
- Checking HTTP code 429: Confirmation that rate limiting works
- Response headers:
X-RateLimit-Limit,X-RateLimit-Remaining,Retry-After
7.3 Rate limiting global versus specific routes
Next, we’re going to demonstrate applying rate limiting globally versus specific routes. As mentioned previously, within Node, we have quite a bit of flexibility in applying rate limiting. For Express web applications, we can apply across all routes or within specific ones as we saw in the last demonstration where we used two separate rate-limiting algorithms. In Express Gateway, we configure this in our gateway.config.yml file where it can also define it for both routes or globally. So first, we’ll need to determine which approach works best with our requirements. When applied to specific routes, we have fine-grained controls and are able to adapt rate limits based on the nature of the data or functionality exposed by the route, providing fine-grain optimization of our resources. When applied to specific routes, we gain fine-grained controls are able to adapt rate limits based on the nature of the data or functionality exposed by the route, and we also gain optimized resource utilization. On the other hand, global rate limiting simplifies the configuration providing uniform protection and a bit easier maintenance; however, we do lose a bit of granular control if that is needed. Up next, we’ll demonstrate applying rate limiting individually versus global routes. First, we’ll look at this within a Node Express application, then we’ll review from an Express Gateway and see how central management occurs. We are going to utilize the same application we created in our last demonstration, so let’s start VS Code and alter our code so that we apply rate limiting globally, and this is the same Node application that we demonstrated in the last clip that reviewed both token bucket and sliding windows. Next, we’ll update this so that all global routes use the same approach. First, we’ll create a new file called ratelimiter.js, and within this file, we’re going to set up sliding window just like we did in the other application; however, we’re going to specify it at five requests per minute. We also export it so that we can use it in our app.js file. Next, we’ll add one line of code to pull in our new rate limiting file, and we’ll place it in rateLimitMiddleware. Now, we’ll add the line of code with app.use, applying our new rateLimitMiddleware applying it to all routes, so let’s test out the application. We’ll open a terminal and start our application, and we’ll use curl to test it and see if our rate limit is reached after five requests, and we’ll try it with our other route limited service. As we can see, they share the same response. Next, let’s look at an application using Express Gateway and modify it to use individual versus global routes. We are going to use an application demonstrated earlier where we demonstrated pipelines. Within this application, we have two endpoints, one for user and one for class. To briefly review them, if we click on user.js, we can see the endpoint with a simple JSON response. With this, the server is listening on port 3000. If we look at our class endpoint, we also use a simple JSON response. And in this case, the app is listening on port 4000. And in our gateway.config.yml file, we have both endpoints specified with our pipelines down below, so let’s add rate limiting to the user endpoint. Lines 49 through 53 represent the new edition. For this route, we’ve defined one request every 3 seconds, so let’s test this out. We can use the curl command to test our user service, and after two requests, we can see we’ve received a too many requests. Now, let’s apply it to all routes. We’ll stop our application and modify our code. We’ll do so by moving the code up to our default pipeline. Now, we can start our gateway again, so let’s test it on both routes. So we’ll use curl command and connect to our user endpoint, then switch to our class endpoint, and as we can see, too many requests, please try again later. So now we’ve demonstrated applying rate limiting to specific routes, as well as globally with an Express Gateway simply by adjusting our config.yml file. In our next module, we’ll cover caching in Edge services.
Comparison of approaches
| Approach | Advantages | Disadvantages |
|---|---|---|
| Specific routes | Fine control, optimization by route | More complex configuration |
| Overall | Simple Setup, Uniform Protection | Less granularity |
Rate limiting global in Express
// ratelimiter.js
const rateLimit = require('express-rate-limit');
const globalLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
message: { error: "Global rate limit exceeded" }
});
module.exports = globalLimiter;
// app.js
const express = require('express');
const globalLimiter = require('./ratelimiter');
const app = express();
app.use(globalLimiter); // Appliqué à toutes les routes
app.get('/api/users', (req, res) => res.json({ data: 'Users' }));
app.get('/api/orders', (req, res) => res.json({ data: 'Orders' }));
app.listen(3000);
Rate limiting in Express Gateway
# gateway.config.yml
pipelines:
main:
apiEndpoints:
- api
policies:
- rate-limit:
- action:
max: 100
windowMs: 60000
rateLimitBy: "${req.ip}"
- proxy:
- action:
serviceEndpoint: myService
8. Caching in Edge Services
Total module duration: 33m 18s
8.1 Edge Architectures and Design Principles
Up next, we’ll discuss and demonstrate caching in Edge Services. Caching in Edge Services is essential to reduce latency, optimize bandwidth, enhance scalability, and improve overall user experience by storing frequently-requested content closer to the end user at Edge locations, so let’s get started. For our agenda, we have four topics which include first, Edge architectures and design principles, second, caching strategies, the pros and cons of them at the Edge, third, node caching options which include in-memory, distributed, and caching solutions and databases. And finally, we’ll demonstrate single versus multi-route caching using a few different libraries. So let’s start our first topic which includes Edge architectures and design principles, and Edge network is a distributed network infrastructure that brings computing resources closer to the end user and devices, reducing latency and improving performance. The Edge network consists of various components each serving a specific purpose. The three key layers of an Edge network are the provider, enterprise core, service provider, and device Edge. Of course, we have our last mile networks also in the Edge solution, which is the final leg that connects the end user or IoT devices to the nearest Edge server or point of presence. So let’s look at the details of each of these layers. First is the enterprise core and cloud layer which is the central part of the network responsible for high speed, long distance communication, and routing between different parts of the network. The characteristics of it often include being of high speed with high capacity links, typically located in large regional data centers or other centralized facilities. This layer handles large volumes of traffic between different geographical locations. Below this, we have our service provider layer which is the boundary between the core network and the external network such as the internet and other service provider networks. As far as the characteristics within this layer, we often implement security measures and policies to protect the core network, we may perform functions like traffic shaping, quality of service, and network address translation, and this layer connects the core network to external networks and ensures proper data exchange. At the bottom is the device Edge. The device Edge is the point where individual devices connect to their local network, often at the edge of the network architecture. For these characteristics, they often represent the interface between the users devices and the local network, some examples would include smartphones, laptops, IoT devices, and other connected gadgets. These may have built-in networking capabilities or connect through intermediary devices like routers or access points. Next, let’s discuss the importance of Edge Services in modern web development. By bringing computing resources closer to the end user, Edge Services minimizes the time it takes for data to travel between the user’s device and the service. This results in significantly reduced latency leading to faster loading times for web apps and improved user satisfaction. For enhanced performance, Edge Services can optimize content delivery, cache frequently accessed data, and offload computational tasks to geographically distributed nodes. This optimization contributes to improved overall performance making these web apps more responsive and efficient. For scalability and reliability, Edge Services can help distribute workloads across multiple locations, improving scalability and ensuring high availability. This distribution of resources reduces the risk of single points of failure and enhances the system’s ability to handle increased traffic and demand. For bandwidth efficiency, Edge Services can intelligently manage and optimize the use of bandwidth by caching and delivering content locally. This reduces the load on central servers and minimizes the consumption of network resources. For security and privacy, Edge Services play a crucial role in enhancing security by implementing closer measures to the user. This includes features such as distributed denial-of-service protection, encryption, and security protocols contributing to a more robust and secure environment. For support of emerging technology Edge Services are well suited to support these emerging technologies like Internet of Things and real-time applications. The proximity of computing resources to end users and devices facilitates the efficient processing of data generated by those technologies. So next, let’s talk about some common use cases and scenarios that leverage Edge architectures and the associated design considerations. Microservice architectures often involve the use of micro APIs, which are small, independently deployable components. Caching in micro APIs helps reduce latency by storing and serving frequently-requested data at the Edge. Consider a social media platform with a micro API responsible for fetching user profiles. Caching user profiles at the Edge can significantly reduce the response time for profile requests. Single-page apps rely heavily on client-side processing, but they still need to fetch data from APIs. Caching at the Edge can store API responses, HTML, CSS, and JavaScript files speeding up the loading time for end users. For example, an ecommerce website built as a single-page app can cache product information at the Edge, ensuring quick access to product details and images without making repeated requests to the server. Server-side rendering generates HTML on the server and sends it to the client, which can be cached at the Edge. This improves the initial load time for users and reduces the load on the server for subsequent requests. For an example, a news website using server-side rendering can cache rendered articles at the Edge, ensuring fast content delivery to users and reducing the server load during peak traffic. Static sites consists of pre-rendered HTML, CSS, and JavaScript files. Caching at the Edge can store these files reducing the time it takes to load pages and improving the overall user experience. For an example, a blog hosted as a static site can benefit from Edge caching by storing posts and pages at the Edge, ensuring quick access for visitors without the need to fetch data from the origin server. Real-time applications, such as chat applications or collaborative tools require low latency. Caching at the Edge can store real-time data, reducing the roundtrip time for requests and improving the overall responsiveness of the application. As an example, a messaging application can cache recent chat messages at the Edge ensuring that users receive updates quickly without relying solely on back-end servers for real-time communication. Up next, we’ll discuss caching strategies, the pros and cons of its use at the Edge.
Layers of an Edge Network
| Layer | Role | Features |
|---|---|---|
| Enterprise Core / Cloud | Long distance communication | High speed, large data centers |
| Service Provider | Interface between core and external network | ISP Interconnect |
| Device Edge | Closer to users | Points of Presence (PoP) |
| Last Mile | End user/IoT connection | Wi-Fi, 5G, fiber optic |
8.2 Cache strategies at the Edge layer
Next, we’ll discuss caching strategies at the Edge along with the pros, cons, and best practices. A well-designed caching strategy for edge services is essential to optimize performance by minimizing latency, reducing bandwidth usage, and enhancing scalability while carefully considering the tradeoffs to ensure efficient and effective delivery of our applications. Caching is a critical aspect of improving the performance and responsiveness of web applications. Here are some caching strategies commonly employed in node edge services. For page caching, we can cache the entire HTML page to serve them quickly without regenerating the content for each request. This can be effective for static content or content that doesn’t change frequently. For object caching, we can cache specific objects or data structures such as database query results or API responses. This helps reduce the need to repeat expensive computations or database queries. We can leverage content delivery networks to cache and distribute specific assets, such as images, stylesheets, and scripts, globally. This helps reduce latency by serving content from servers close to the user’s geographical location. We can also cache specific fragments or components of a page allowing for partial page updates. This can be useful for dynamic pages where only certain sections change frequently. We can store frequently-accessed data in the application’s memory for quick retrieval. This can provide low latency access to data, but is limited to the available memory. We can share data across multiple servers or instances to maintain consistency and improve scalability. This helps ensure that all instances can access and update a shared cache. Cache expiration is crucial to ensure that cache data remains up to date. Different caching systems employ various expiration mechanisms. With time-based expiration, we can set a predefined time limit for how long an item should remain in the cache, and after the expiration time, the cache is considered stale and a fresh copy is fetched. For event-based expiration, we can invalidate the cache in response to specific events such as data updates or changes. This helps ensure that the cache is refreshed when the underlying data changes. Another strategy is to only load data into cache when requested. This can help minimize the chances of serving outdated information. Optimizing caching involves maximizing the efficiency and performance benefits of caching strategies. We can use HTTP cache headers, such as cache control and expires, to control caching behavior to help us specify cache lifetimes and conditions for revalidation. We can add version numbers or unique identifiers to asset URLs to force cache invalidation when content changes. This can help ensure that users receive the latest version of assets. We can compress cache content to reduce storage requirements and improve retrieval speed. This is commonly applied to text-based assets like CSS and JavaScript files. We can choose what to cache based on the nature of the content. This helps avoid caching sensitive or user-specific information that may lead to privacy issues. We can monitor cache hit rates, misses, and expiration rates to optimize caching policies by adjusting cache strategies based on usage patterns and evolving application requirements. We can configure CDN settings to maximize the efficiency of content delivery by utilizing features like edge caching and intelligent routing to optimize for performance. While caching has numerous advantages, there are potential downsides and challenges associated with its implementation. The first disadvantage is cache poisoning. Cache poisoning occurs when malicious code or incorrect data is stored in the cache leading to subsequent retrieval and use by the client. How this occurs, an attacker may inject false or harmful data into the cache leading to the distribution of incorrect or harmful content to the users, so the users may be exposed to misinformation, security threats, or unintended behaviors due to the poisoned cache data. To prevent, we can implement thorough input validation, employ secure cache strategies, and regularly audit cache data for integrity. Second is fragmentation. This happens when the cache becomes filled with small noncontiguous pieces of data making it challenging to utilize available cache space efficiently. How this occurs, small fragmented pieces of data may occupy cache space leading to suboptimal performance as larger data structures cannot be cached. The reduced cache hit rates and increased cache misses results in diminished effectiveness of the caching system. To mitigate, we can implement cache eviction policies that consider the size and frequency of access to cached items to minimize fragmentation. Third is stale content. This refers to cache data that has expired or become outdated, but is still served to users without being refreshed. So users may receive inaccurate or obsolete information if the cache content is not regularly updated or invalidated. With reduced data freshness, this leads to a degraded user experience and potential issues in scenarios where real time or frequently changing data is involved. As far as resolution, we can implement effective cache expiration and invalidation mechanisms based on time events and other triggers to ensure the timely removal of stale content. Now, let’s discuss some general best practice and risk mitigation strategies. We should periodically review and refresh cached content to ensure it remains accurate and up to date and implement automated processes to detect and remove poisoned and outdated cache entries. We should apply security measures to prevent cache poisoning such as input validation, proper handling of user inputs, and sanitation of cache data. We should use cache eviction policies that consider the size and frequency of access to minimize fragmentation and ensure efficient use of cache space. And we can employ strategies for intelligent cache and validation based on changes in the underlying data or application state to prevent the serving of stale content. Up next, we’ll discuss cache options in node, which includes in-memory, distributed, and caching solutions and databases.
Cache policies
| Strategy | Description | Use cases |
|---|---|---|
| Page Caching | Hide full HTML page | Static content |
| Object Caching | Cache specific objects (DB results) | Expensive data |
| CDN Caching | Geographically distributed assets | Images, CSS, JS |
| Fragment Caching | Page Component Cache | Partially dynamic pages |
| In-Memory | Cache in application memory | Frequent data |
| Distributed | Multi-instance shared cache | Scalable environments |
HTTP Cache Important Headers
Cache-Control: public, max-age=3600
ETag: "abc123hash"
Last-Modified: Mon, 20 Jun 2026 10:00:00 GMT
Expires: Mon, 20 Jun 2026 11:00:00 GMT
Vary: Accept-Encoding
8.3 Node.js cache options: in-memory, distributed, solutions/databases
Up next, we’ll discuss node caching options which include in-memory, distributed, and caching solutions and databases. So let’s get started. In-memory caching involves storing frequently-accessed data directly in the application’s memory. This provides fast retrieval as the data is retrieved from memory faster compared to fetching data from databases or external sources. It provides low latency access to this cached data. In-memory caching is best suited for small to moderately-sized datasets that can fit into the available memory. Distributed caching involves storing cache data across multiple servers or nodes within a network. This allows for distributing the caching load across multiple servers and proving scalability. It reduces the risk of a single point of failure, ensuring high availability. Distributed caching is ideal for large-scale applications where caching needs to be shared and synchronized across multiple instances. Another option is caching solutions such as Redis. Redis, which is commonly used within node for caching, is an in-memory data structure store that can be used as a caching solution. It supports various data structures such as strings, hashes, lists. and sets. Redis is versatile as it supports complex data structures, making it suitable for a wide range of use cases. It offers options for data persistence, allowing data to be stored even if the server restarts. Redis and similar caching solutions are well suited for scenarios requiring fast and efficient caching such as session caching, real-time analytics, and frequently-changing data. So let’s discuss what data should be cached. We can identify data that is accessed frequently, but doesn’t change frequently. Some examples include configuration settings, reference data, or static content. We can cache the results of computationally expensive operations to avoid repeating the computation for every request. Some examples include the results of database queries, API responses, or complex calculations. We can cache components or fragments of a page that are reused across multiple requests. Some examples may be navigation menus, header and footer components, or common UI elements. We can cache static assets, such as images, stylesheets, and scripts to reduce the load on web servers and improve loading times. Some examples may be logo images, CSS, and JavaScript libraries. We can cache session-related data to enhance the performance of our user sessions. Some examples may be user preferences, authentication tokens, or session-specific information. We can cache data that is used for reference and doesn’t change frequently. Some examples may include country lists, product categories, or other static reference data. And we can cache aggregate or summarize data to speed up reporting or analytics. Some examples may include aggregated sales data, statistical summaries, or pre-computed metrics. So let’s discuss a few other considerations when designing our caching solutions. For data volatility, we can consider how frequently the data changes when deciding on the caching strategy. Dynamic data may require more sophisticated cache expiration and invalidation mechanisms. For data size, we can assess the size of the data to be cached and choose an appropriate caching solution based on available memory and scalability requirements. For security concerns, we can be cautious when caching sensitive or private data to avoid potential security risks. We can implement secure caching practices and consider encryption for sensitive information. Up next, we’ll demonstrate single and multi-route caching with a few different node libraries.
Comparison of cache solutions
| Solution | Type | Advantages | Disadvantages |
|---|---|---|---|
| Native In-Memory | Process room | Ultra-fast | Lost on reboot |
| node-cache | In-memory | Simple, key-value API | A single process |
| apicache | In-memory HTTP | Easy Express Middleware | Less suitable for distributed |
| Redis | Distributed | Persistent, scalable, rich structures | Infrastructure required |
| Memcached | Distributed | High performance, multi-server | No persistence |
Data to cache
- Configuration data (rarely changed)
- Reference data (lists, catalogs)
- Static content (HTML, CSS, images)
- Costly calculation results (ML inference, aggregations)
- Frequently read database query results
8.4 Demonstration: single route vs multi-route caching
Up next, we’ll demonstrate single versus multi-route caching. We will also demonstrate three separate libraries, which includes apicache, node-cache, and Memcached, to exercise a few of the different caching approaches. Apicache is a lightweight in-memory caching solution specifically designed for caching API responses. It is usually used for caching HTTP responses in web applications, especially in the context of API routes. It provides a middleware that can be easily integrated into Express.js applications. You can define caching rules based on routes or specific criteria. For cache storage with apicache, it stores it in memory within the node.js process. For cache storage, apicache stores data in-memory within the node process. This technique is ideal for caching API responses to reduce server load and improve response times. This approach is commonly used to cache API responses at the Edge data center and is effective for reducing latency and server load at the edge. The downside is that we are limited to in-memory caching within the Node process which might be less suitable for edge applications where data needs to be distributed across multiple locations. Node-cache is a general purpose in-memory caching module for Node. It can be used for various caching scenarios not limited to HTTP responses. You can cache any type of data. It provides a simple key value store API allowing you to store and retrieve data using keys. For storage node-cache stores data in memory within the node process. Node-cache is suitable for caching arbitrary data in Node applications, such as configuration settings, database query results, or other frequently-accessed information. Node-cache is suitable for in-memory caching within individual Edge servers. As far as the drawbacks, node-cache may not be optimized for scenarios where caching needs to be shared across a distributed edge network. Memcache is a distributed caching system that can be used by multiple applications and across multiple servers. It is designed for distributed and scalable caching needs. It’s suitable for large scale applications where caching needs to be shared across different servers and instances. Memcached is a separate service that runs independently of your node application. The application communicates with the Memcache service over a network. For storage, data is stored in the memory of the Memcached server which can be shared across multiple instances of your application. Memcached is best suited for scenarios where caching needs to be distributed and the application is scaled across multiple servers. It’s well suited for distributed caching across multiple servers and locations. It can also handle large-scale caching needs in Edge architectures. As far as the drawbacks, it does require a separate Memcached service which may add complexity to the architecture, the overhead of the network communication between the Node application, and the Memcached service. We also have the added overhead of the network communication between the Node application and the Memcached service. Up next, we’re going to demonstrate three separate libraries, apicache, node-cache, and memcached, so let’s get started. After we create our directory to house our application, we’ll initiate our project. Next, we’ll install our dependencies of Express and apicache. Then we’ll start VS Code. We’ll create an app.js file to place our code. Now, let’s look at our apicache code. Up top, we import our express module for the server and HTTP routes and apicache for the caching of the HTTP responses. Below that, we create our express app and set up our caching middleware and place it in the variable cache. We define our routes below this with the profile route. We specify a caching duration of 2 minutes. With list, we specify 1 day, and with history, we have no caching, and I have app.use with cache specified as 1 minute commented out. We will uncomment that in a moment where it will apply it globally to all routes. Down below this, we have app.get for our root folder which simply returns hello from the root path. Finally, we start the app on our specified port, so let’s start the application. Next, we’ll test with the curl command, passing a -i to receive extra information about the response, and we’ll connect it to our history endpoint. History does not have caching defined for it. Next, if we do the same command to our profile endpoint, we should get a different response. Here, we can see cache-control of a max-age of 120 indicating 2 minutes. If we connect to the list endpoint, it had a cache specified of 1 day, so we can see the cache-control with an age of 86,400 seconds. Now let’s go back to our application and uncomment the code to set a global parameter of 1 minute caching for all routes. On Line 24, once we uncomment it, we’ll specify caching as 1 minute, and this will be applied to all routes where it’s not overridden and specified within each route. Said another way, list and profile will still retain their caching specifications of 1 day and 2 minutes respectively, and history now will have a 1-minute caching. So let’s stop our service and restart it. Now, we’ll run our curl command on our history endpoint and see if it responds with a cache of 60 seconds and our response with a max age of 60. Next, we’re going to demonstrate the use of node-cache. Once we create our project folder to house our code, we’ll initiate with the npm init -y command. Next, we’ll install our two dependencies of express and node-cache. Then we’ll start VS Code, and we’ll create an app.js file to place our code. Now, let’s review our code. Up top, we import our libraries of express and node-cache as express will define the routes and node-cache will allow us to store key value pairs in memory. Below this, we create an app as an instance of the express application. Then we create cache as an instance of node-cache which will be used to store and retrieve data. Next, we define the route for data. When a request is made to the data endpoint, the server checks if the requested data is in the cache. If the data is found in the cache, it is returned to the client and a log message is printed. If the data is not in the cache, a new set of data is created, stored in the cache, and then returned to the client. A log message is printed to indicate that the data was fetched from the server and cached. Below this, we start the server and listen on port 3000. So in a moment, we’ll test this out and see our data cached within 10 seconds. So let’s test our application. To test our caching, we’ll issue a curl command to localhost 3000 and the data endpoint, and as we can see, the data was fetched from the server, and in the console behind it, we can also see it printed. And after 10 seconds, the data was refreshed as we see down below in the console message data served from cache. So after 10 seconds, the data is refreshed and written to the cache. So now, I’ve run the curl command a few times, and if we run it within the window of 10 seconds, we’re able to see that the data is served from cache, otherwise, it’s fetched from the server and restored into the cache. Next, let’s look at Memcached. Memcached is an open source high performance distributed memory caching system that is used to speed up dynamic applications by temporarily storing and managing frequently-accessed data and RAM reducing the need to repeatedly fetch data from back-end databases, and Memcached for Windows can be found at this URL. Once the zip file is downloaded, there is a memcache.exe housed within it. To start the Memcached service, simply run memcache -d start. After we’ve created our folder to house our Memcache solution, we’ll initiate with the npm init -y command. Next, we’ll install our dependencies of express and memjs, then we’ll start VS Code, and we’ll create an app.js file to put our code. Now let’s review our code. First, we import our library’s express and memjs. Similar to the first two demonstrations, express creates the server and routes. In this case, the memjs is the client library that allows us to interact with the Memcached server. Below this, we create app as our instance of the Express application. MemcachedClient is an instance of the memjs client class, which is used to interact with the Memcached server. The Memcached server address is specified either in the process.env.MEMCACHED_SERVERS environment variable, or as in this case, to the default server and port at localhost 11211. Next is our data route. When a request is made to data, the server tries to get the data from the Memcached cache using await memcached .get data. If the data is found in the cache, it is returned to the client and a log message is printed. If the data is not found in the cache, a new set of data is created stored in the cache with the key data and returned to the client. The data is also JSON stringified before storing it to cache. The cache entry is set to expire after 10 seconds, so we’ve set it low just to demonstrate caching here in a few moments. We have error handling down below so that when an error is encountered, it is logged and a 500 Internal Server Error response is sent to the client. Finally, we start the server and listen on port 3000, so let’s test our application. We’ll use the curl command and connect to our server and data endpoint. And remember, if we issue the command within 10 seconds, the subsequent commands after the first, before 10 seconds is reached, should provide a cached response. After 10 seconds, it would be refreshed and then cached again for another 10 seconds. So I’ve issued the command quickly a few times just to illustrate that it’s serving it from cache. Now, after the 10-second window, the data was fetched from the server and recached. So now, we’ve seen caching from a few different approaches within Node using a few different libraries. Up next, we’ll delve into performance optimization in Edge services.
1. Cache with apicache
npm install express apicache
const express = require('express');
const apicache = require('apicache');
const app = express();
const cache = apicache.middleware;
app.get('/api/data', cache('5 minutes'), (req, res) => {
res.json({ data: "Expensive data", timestamp: Date.now() });
});
app.get('/api/products', cache('10 minutes'), (req, res) => {
res.json({ products: ["Widget A", "Widget B"] });
});
app.listen(3000);
2. Cache with node-cache
npm install express node-cache
const express = require('express');
const NodeCache = require('node-cache');
const app = express();
const myCache = new NodeCache({ stdTTL: 300, checkperiod: 60 });
app.get('/api/config', (req, res) => {
const cached = myCache.get("app-config");
if (cached) return res.json({ source: "cache", data: cached });
const config = { theme: "dark", language: "fr", maxItems: 100 };
myCache.set("app-config", config);
res.json({ source: "database", data: config });
});
app.listen(3000);
3. Cache with Redis (recommended in production)
npm install express ioredis
const express = require('express');
const Redis = require('ioredis');
const app = express();
const redis = new Redis({ host: "localhost", port: 6379 });
app.get('/api/data/:id', async (req, res) => {
const cacheKey = `data:${req.params.id}`;
const cached = await redis.get(cacheKey);
if (cached) return res.json({ source: "redis", data: JSON.parse(cached) });
const data = { id: req.params.id, value: Date.now() };
await redis.setex(cacheKey, 300, JSON.stringify(data));
res.json({ source: "database", data });
});
app.listen(3000);
4. Cache with Memcached
npm install express memcached
const express = require('express');
const Memcached = require('memcached');
const app = express();
const memcached = new Memcached("localhost:11211");
app.get('/api/products', (req, res) => {
memcached.get("products-list", (err, data) => {
if (!err && data) return res.json({ source: "memcached", products: data });
const products = ["Produit A", "Produit B", "Produit C"];
memcached.set("products-list", products, 300, () => {});
res.json({ source: "database", products });
});
});
app.listen(3000);
9. Performance Optimization in Edge Services
Total module duration: 46m 21s
9.1 Performance, bottlenecks and optimization
Welcome. In this module, we’ll cover performance optimization in edge services. Optimizing performance in Edge Services is crucial to enhance user experience by reducing latency, accelerating content delivery, and ensuring responsiveness, ultimately improving the overall efficiency and reliability of Node applications at the network’s edge. And let’s take a quick look at our agenda. First, we have performance, bottlenecks, and optimization. Second, we’ll discuss performance optimization techniques. Third, clustering for CPU-intense tasks. Fourth, we’ll discuss parallelism within Node.js. Fifth, we’ll discuss unified log files, and last, we’ll cover monitoring for memory leaks. So let’s get started with our first topic, performance, bottlenecks, and optimization. In Edge Services, performance is a critical factor that influences various aspects of user experience, operational efficiency, and overall service reliability. Speed is often measured as the rate at which Node API Edge Services process requests and deliver responses to clients. Faster speed directly contributes to a better user experience. Users expect quick interactions, and a speedy response time enhances satisfaction. In a competitive landscape, services with faster response time stand out and attract more users. For some Node speed considerations, of course, Node utilizes a non-blocking event-driven architecture making it inherently fast and efficient in handling concurrent operations. Leveraging asynchronous programming allows Node to perform multiple tasks concurrently without waiting for each to complete, which optimizes speed. A second performance indicator is responsiveness, which is the ability of Node’s API Edge services to promptly react to user inputs or requests, providing real-time feedback. Responsive services contribute to a more engaging user experience, particularly in applications where real-time updates are crucial. Prompt responses build trust and users are more likely to rely on and return to services that demonstrate responsiveness. For Node responsiveness considerations, Node excels in event-driven scenarios allowing for quick responses to user-triggered events. Node supports WebSockets facilitating bidirectional communication for real-time applications and further enhances responsiveness. And a third performance consideration is scalability, which is the ability of Node API Edge services to handle an increasing number of requests and scale resources efficiently. Scaling services can adapt to changing traffic patterns, ensuring consistent performance during both peak and off-peak periods. Efficient scalability leads to optimized resource usage, minimizing costs while maintaining high performance. For scalability considerations, Node facilitates horizontal scaling by allowing the development of multiple instances, distributing the load, and enhancing scalability. Breaking down services into smaller manageable components, such as microservices, it supports easier scalability and maintenance. Next, let’s discuss some approaches to identify bottlenecks in Node applications. We have a few performance monitoring tools available for our use with Node. Node provides a built-in performance module that allows us to measure the duration of certain operations. This module exposes a performance.now method monitoring code level insights, alerting and end to end performance. Visibility load testing is a critical step in identifying bottlenecks in a node application, it involves subjecting the application to various levels of stress to understand how it performs under certain conditions. Load testing involves simulating real world conditions by generating artificial user traffic on your application. This helps understand how the application performs under various loads. Load testing helps determine the maximum capacity your application can handle before experiencing performance. Degra stress testing involves pushing the system to its limits or beyond to identify breaking points. This can reveal how your application behaves under extreme conditions and helps identify potential bottlenecks load testing. Tools often provide insight into resource utilization including CPU usage, memory consumption and network activity monitoring. These metrics during the test can help identify resource related bottlenecks. Load testing tools measure response times for different types of requests, analyzing response times under varying loads helps identify which parts of the applications are slowing down. Benchmarking enables performance baselines for our applications. By running benchmarks under normal conditions, we can create a reference point to compare against when making changes or optimizations. Benchmark is useful for comparing different configurations, code versions or dependencies. It helps identify how changes impact performance and whether they introduce bottlenecks. Benchmarking tools can highlight performance hotspots in our code. This is essential for identifying specific functions or modules that may be causing performance bottlenecks. In addition to overall application benchmarks, we can use micro benchmarking to focus on specific parts of our code. This helps identify bottlenecks at a more granular level. Benchmarking tools often integrate with profilers and tracing mechanisms. Profilers help identify which functions consume the most resources and tracing allows us to visualize the flow of function calls and identify bottlenecks in the execution path. Logging and tracing are crucial tools for identifying bottlenecks and debugging performance issues and not applications. These techniques provide visibility into the flow of execution allowing developers to track the sequence of events and identify areas where the application may be experiencing slowdowns. From a coding perspective. We can use event time stamps and execution. Flow logging allows us to time stamp events and trace the execution flow of our application by logging the start and end of critical operations or functions. We can identify which parts of our code is taking the most time to execute. We can log key performance metrics such as response time, database query duration and other relevant information. Analyzing these metrics over time can reveal patterns and trends that indicate potential bottlenecks in microservice architectures or distributed systems. Tracing becomes even more critical. Distributed tracing allows us to follow the requests journey over multiple services helping identify latency and bottlenecks in the entire transaction. And many operations are asynchronous tracing tools can help us track the flow of asynchronous operations making it easier to identify areas where callbacks are taking longer than expected. Memory usage analysis is a critical aspect of identifying and resolving bottlenecks and node applications as well. Memory related issues can lead to performance Degra increased response times and even application crashes. Tools like the Chrome developer tool operations, we can enable garbage collection profiling to gain insight into memory allocations, object lifetimes and the impact of garbage collection on the applications performance. Utilizing tools like heap dump or memory leak detector to automate the process of capturing heap dumps and analyzing them for memory leaks up. Next, we’ll delve into node edge architecture performance optimization techniques.
Key performance indicators
| Indicator | Description | Objective |
|---|---|---|
| Speed | Request processing rate | Maximize throughput |
| Responsiveness | Reaction time to user inputs | Minimize perceived latency |
| Reliability | Consistency and precision of responses | Maintaining quality under load |
| Availability | Service Uptime | Reach 99.9%+ SLA |
| Scalability | Ability to handle increased traffic | Horizontal/vertical elasticity |
Common Bottleneck Types
- CPU-bound: Intensive calculations blocking the event loop
- I/O-bound: Waiting on files, database, network
- Memory-bound: Memory leaks, data not freed
- Network-bound: Network latency, insufficient bandwidth
Node.js profiling tools
# Profiling intégré Node.js
node --prof app.js
node --prof-process isolate-*.log > profile.txt
# Clinic.js - Diagnostic complet
npm install -g clinic
clinic doctor -- node app.js
clinic flame -- node app.js
# AutoCannon - Test de charge
npm install -g autocannon
autocannon -c 100 -d 30 http://localhost:3000
9.2 Performance Optimization Techniques
Next, let’s delve into some Node performance optimization techniques. Caching is a critical aspect of optimizing the performance of Node API Edge Services. By strategically caching data, we can reduce the response time for frequently-requested information alleviating server load and enhancing the overall user experience. As we saw in our last module, Node provides simple in-memory caching modules that can be used for quick and lightweight caching. This is suitable for caching small amounts of data that can be stored in memory. We can leverage HTTP caching headers, such as cache control, expires ETag, or last modified, to instruct client browsers and intermediate caching proxies to cache responses. This reduces the needed repeated requests for the same resources. We can generate a content-based hash, we can generate a content hash-based on the response content and use it as part of the cache keys. This way, if the content changes, the cache key changes and the client fetches the updated content. We can implement distributed caching using external caching systems like Redis or Memcached. Like we discussed and demonstrated in the last module, these tools provide a centralized and scalable caching solution that can be shared across multiple instances of your Node API. We can use the cache-aside pattern where our application code is responsible for reading and writing to the cache. This allows for more control over caching logic and can be integrated with external caching systems. We can implement conditional requests using ETags. Clients can make requests with an if-none match header and the server responds with a 304 not modified status if the content hasn’t changed since the last ETag. We can utilize content delivery networks for Edge caching, CDNs, cache content at geographically distributed Edge locations, reducing latency and offloading traffic from your Node server. We can implement caching at the application level for specific resources or data. This can be especially useful for data that doesn’t change frequently, but is expensive to compute or retrieve. And we can also cache the results of frequent or expensive database queries to avoid redundant queries for the same data. While database optimization is an entire field within itself, we’ll highlight a few key areas to contribute to the performance optimization of our Node applications. Regularly analyzing execution plans of our queries helps identify missing indexes. Lack of proper indexes can lead to full table scans and significantly impact performance. Consider creating composite indexes for queries that involve multiple columns in the WHERE clause or JOIN condition. Composite indexes can be more efficient than having separate indexes on each column. The data distribution and access pattern in our application may change over time, so regularly review and update indexes to ensure they remain effective as your database grows and evolves. We should carefully craft our SQL queries to be as efficient as possible. Avoid using select * when only specific columns are needed and ensure that WHERE clauses are selective and can leverage existing indexes. Be mindful of N+1 query problems, especially in object-relational mapping frameworks ensuring that our queries fetch necessary data in a single query rather than resulting in multiple queries for each related record. We should be cautious on join operations, especially in scenarios involving large tables. Use appropriate indexing and consider denormalizing if necessary to reduce the need for complex joints. And last, we should ensure that database operations are performed asynchronously in a non-blocking manner, leveraging the asynchronous nature of node. This prevents a database query from blocking the event loop and allowing the application to handle multiple concurrent requests efficiently. Load balancing involves distributing incoming network traffic across multiple servers to ensure no single server bears too much load. With round robin, requests are distributed in a cyclical order to each server in a pool. It’s a simple strategy suitable for environments with similar server capabilities. With least connections, the load balancer directs traffic to the server with the fewest active connections. This strategy helps distribute load more evenly. For IP hashing, traffic is directed based on a hash of the client’s IP address. This ensures that a specific client is always directed to the same server which can be beneficial for stateful applications. With weighted round robin least connections, we assign different weights to servers based on their capabilities. Servers with higher weights receive more requests allowing for a symmetric load distribution. Horizontal scaling involves adding more servers or nodes to the system to handle increased load. In a Node context, this means running multiple instances of your application across different machines. We can use containerization and orchestration like Docker and Kubernetes to facilitate the deployment and management of multiple instances of our Node application across a cluster of machines. Vertical scaling involves increasing the resources, such as CPU or RAM, on a single server to handle more load. While Node applications benefit from horizontal scaling, vertical scaling can still be used for smaller applications or during the early stages of growth. We can implement autoscaling policies to dynamically adjust the number of running instances based on demand. Cloud platforms like AWS, Azure, and Google Cloud provide autoscaling features that can automatically add and remove instances based on predefined rules. If the application uses session data, ensuring that session information is either stateless or stored externally such as in a centralized cache or database. This allows requests to be distributed seamlessly across multiple instances without relying on sticky sessions. We’ll need to ensure that the database can handle increased load implementing strategies such as sharding, replication, and read replicas to scale your database horizontally or vertically based on your application’s requirements. When using a database, implement connection pooling to efficiently manage database connections. We can consider breaking down the application into microservices and horizontally partitioning your data to scale specific components independently. Each microservice can be scaled independently based on its load. A content delivery network, or CDN, is a distributed network of servers strategically placed at multiple geographic locations to deliver web content more efficiently to users. CDNs, cache, and store static assets like images, stylesheets, and scripts, closer to end users, reducing latency and accelerating content delivery. CDNs bring content closer to end users by caching it at the edge. This reduces the physical distance between the user and the server leading to lower latency and faster load times. Within CDNs, properly configuring cache control headers to control how the CDNs cache and serve content. We should adjust the cache expiration time based on the nature of our content. For static assets that rarely change, use longer cache durations while dynamic content might have shorter cache durations. Ensuring that our CDN supports and enforces HTTPS, encrypting traffic is crucial for security and SEO rankings. Additionally, optimized TLS configurations for faster handshake times. Use modern cipher suites and protocols supported by both the CDN and the client. We should implement smart purging strategies to efficiently manage cache and validation. CDNs typically provide options for purging specific files, paths, and even wildcard purges. Consider using versioning in URLs or employing cache-busting techniques to force browsers and CDNs to fetch updated content. Up next, we’ll cover clustering for CPU-intensive tasks.
Node.js optimization strategies
- Caching: In-memory cache, Redis, CDN for frequently accessed data
- HTTP Cache Headers:
Cache-Control,ETag,Last-Modifiedfor clients - Content Hashing: Content-based cache keys for automatic invalidation
- Distributed Cache: Redis/Memcached to share the cache between instances
- Cache-Aside Pattern: The application manages reading/writing to the cache
- Conditional Requests:
If-None-Matchwith ETags for 304 Not Modified - CDN: Static content distributed geographically
- Compression: gzip/Brotli to reduce response size
- Connection Pooling: Reuse of database connections
- Lazy Loading: Delayed loading of non-critical resources
Compression with Express
const express = require('express');
const compression = require('compression');
const app = express();
// Activer la compression gzip/Brotli pour toutes les réponses
app.use(compression({
threshold: 1024, // Compresser seulement les réponses > 1KB
level: 6 // Niveau de compression (1-9)
}));
app.get('/api/large-data', (req, res) => {
const largeData = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
value: Math.random()
}));
res.json(largeData);
});
app.listen(3000);
HTTP caching with ETags
const express = require('express');
const crypto = require('crypto');
const app = express();
app.get('/api/data', (req, res) => {
const data = { message: 'Hello World', version: '1.0' };
const content = JSON.stringify(data);
// Génération du hash ETag basé sur le contenu
const etag = crypto.createHash('md5').update(content).digest('hex');
// Vérification si le client a déjà la version courante
if (req.headers['if-none-match'] === etag) {
return res.status(304).end(); // Not Modified
}
res.setHeader('ETag', etag);
res.setHeader('Cache-Control', 'public, max-age=3600');
res.json(data);
});
app.listen(3000);
9.3 Clustering for CPU-intensive tasks
Up next, we’re going to discuss clustering for CPU-intensive tasks which is commonly used in Node Edge scenarios. We’ll extend the clustering app we built and demonstrated in module five to add some computationally-intense work and watch new threads be created and work on these activities, so let’s get started. Designed properly, Node can utilize clustering to take advantage of parallelism provided by modern server environments. Some of these examples may be high-definition image processing at the edge, machine learning inference, cryptographic operations such as hashing, encryption, and decryption, as well as blockchain-based applications, and to expand on machine learning and AI given its current popularity and value it provides, TensorFlow.js, Brain.js, and Synaptic are a few example libraries that are commonly used. So for these workloads, we’ll want to take advantage of multicores and multiprocessors, and we can do so with both Cluster and PM2. Cluster is the built-in module in Node that allows the creation of multiple processes or workers to handle incoming requests or tasks in a cluster. It is particularly useful for leveraging multicore systems and parallelizing workloads to improve performance and scalability. PM2 is an advanced process manager for Node applications. It simplifies the deployment monitoring and management of Node processes in production environments. PM2 is helpful for autoscaling to automatically scale the number of application instances based on system resources and demand. Node is single-threaded and uses an event-driven non-blocking model; however, many modern servers and computers come with multiple cores. Clustering allows node to create a pool of workers each running on a separate core. This enables the application to fully utilize the potential of all available hardware. CPU-intensive tasks often involve calculations, data processing, and other operations that can be parallelized. By running multiple worker processes in parallel, clustering enables the application to distribute the workload, reducing the overall time required to complete CPU-intensive tasks. Clustering improves the throughput of the application by enabling it to handle more requests concurrently. As the number of cores increase, the application scalability improves allowing it to efficiently handle a higher volume of CPU-intensive tasks. Clustering allows for efficient utilization of system resources. While one worker is busy with a CPU-intense task, other workers can handle additional requests or tasks. This helps maximize the overall resource utilization of the server. So now, we’ll demonstrate clustering with tasks that are a bit CPU intense simulating a scenario where we can put a small load on multiple cores. We’ll be using multi.js that we created in module five to demonstrate the clustering. In that video, we spawn processes based on the number of processors and cores available on the machine, so we’ll be extending that code to simulate computationally-intense processes. So for this application, I’ve made a copy of multi.js named it multiCPU.js and added two things, first, code that is computationally intense, and second, simulation of the completion of worker processed tasks, so let’s review the code. Up top, we set up our dependencies. Cluster.isMaster checks if the current process is the master as it creates and manages worker processes. We can get the number of CPUs with the os.cpus.length. We log the master process information, then we have a for loop to create worker processes using fork to create the new worker threads or processes. Cluster.on handles the exit of worker processes. When they exit via completing a task or encountering an error, the event is triggered, so we log the information about the exited worker process and restart a new worker using process.4. The else block is executed in the worker process. It’s the code that runs in parallel to the main process and is responsible for performing the specific tasks, in this case, calculating prime numbers within a given range. Function.isPrime checks if a given number is a prime number. The first two conditions are the base cases, and if less than are equal to one, it is not a prime. If less than or equal to three, it is a prime number. The % 2 line checks if the number is dividable by 2 or 3. If so, returns false. Then the for loop iterates over potential divisors starting from 5 and incrementing by 6 for each iteration, taking advantage of the fact that all prime numbers greater than five can be expressed in the form of 6 K + or -1. If divisible by any of these potential divisors, the function returns a false. Else, it’s a prime number and returns true. Calculate primes and range takes a start and end and calculates all prime numbers within the range using isPrime. Prime numbers are then stored in a prime’s array. StartRange and endRange are the range of numbers where the prime numbers will be calculated. Changing this value can increase the duration and time it takes making it more computationally intense. Console.time starts a timer labeled with the worker process ID using the time taken for the prime number calculation. Result involves the calculatePrimesInRange and console.in time sets the timer that was started using the console.time as the time elapsed since console.time. Doing so, we’re able to see the duration of the task. The console.log will log to the console information about the worker process ID, the range of prime number calculations, and the results. The setTimeout simulates a delay of an even longer running task or computation. After the delay, the worker process exits simulating the completion of the worker task, so let’s test this out, and we’ll do so by typing Node MultiCPU. Okay, now I’ve stopped it after one iteration. And essentially, what it does is quickly spin up processes, calculate the prime numbers, and then ends in restarts. As we can see, the duration is also printed to the console. We’ve designed it so that it will do this indefinitely just so that we can demonstrate spinning up worker threads, doing some work ending, and then new work occurring. So by using techniques like cluster, we are able to better utilize our underlying resources. Given my machine has six cores, it’s able to have six workers and assign work to each, thereby increasing performance. Up next, we’ll discuss and demonstrate parallelism in Node by creating a few parallel tasks that use promises and avoid synchronous functions by using async await.
Examples of CPU-intensive Edge tasks
- High definition image processing (resizing, compression)
- ML/AI Inference (TensorFlow.js, Brain.js, Synaptic)
- Cryptographic operations (hash, encryption, decryption)
- Blockchain applications (block validation, proof of work)
- Data compression (real-time gzip, streaming)
Cluster for CPU-intensive tasks
const cluster = require('cluster');
const http = require('http');
const crypto = require('crypto');
const os = require('os');
const numCPUs = os.cpus().length;
if (cluster.isMaster) {
console.log(`Master PID: ${process.pid}, CPUs: ${numCPUs}`);
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker) => {
console.log(`Worker ${worker.process.pid} died - restarting`);
cluster.fork();
});
} else {
http.createServer((req, res) => {
// Simulation d'une tâche CPU-intensive: hachage
if (req.url === '/hash') {
const data = 'large-data-to-hash'.repeat(10000);
const hash = crypto.createHash('sha256').update(data).digest('hex');
res.writeHead(200);
res.end(JSON.stringify({ hash, worker: process.pid }));
} else {
res.writeHead(200);
res.end(JSON.stringify({ status: 'ok', worker: process.pid }));
}
}).listen(8000);
console.log(`Worker ${process.pid} started`);
}
PM2 - Process Manager for Node.js in production
# Installation de PM2
npm install -g pm2
# Démarrage en mode cluster (utilise tous les CPUs)
pm2 start app.js -i max
# Monitoring
pm2 monit
# Redémarrage automatique après crash
pm2 restart app
# Configuration dans ecosystem.config.js
// ecosystem.config.js
module.exports = {
apps: [{
name: 'my-api',
script: 'app.js',
instances: 'max', // Utiliser tous les CPUs
exec_mode: 'cluster',
watch: false,
env: {
NODE_ENV: 'production',
PORT: 3000
},
max_memory_restart: '500M' // Redémarrage si > 500MB RAM
}]
};
9.4 Parallelism in Node.js
Up next, we’ll discuss parallelism and Node Edge Services. Parallelism and Node Edge Services is important for handling concurrency, improving response times, achieving scalability, and optimizing resource utilization, all of which are crucial aspects in providing efficient and responsive services at the network’s edge, so let’s get started. From an application architecture perspective, there are a few things we can do to improve our performance and design, so let’s look at a few. Node is designed to be non-blocking, allowing it to handle many concurrent connections without the need for threads. Synchronous functions can block the event loop making the application less responsive, especially dealing with I/O operations. Synchronous functions can make an application appear unresponsive, especially when dealing with time-consuming tasks. Asynchronous programming with tools like async/await enables developers to write code that remains responsive, providing a better user experience. Asynchronous operations in Node allows the application to continue executing other tasks while waiting for I/O operations to complete. This improves overall performance by maximizing CPU utilization and reducing idle time. In Node, async/await and promises are mechanisms for handling asynchronous code, making it more readable and manageable. A promise is an object representing the eventual completion or failure of an asynchronous operation. It has three states, first is pending, this is the initial state, the promise is neither fulfilled nor rejected. The second state is fulfilled where the operation completed successfully and the promise has a resulting value. Third is rejected. This is where the operation failed and the promise has a reason for failure. Async await is a modern JavaScript feature that simplifies asynchronous code, making it more readable and maintainable. It allows developers to write asynchronous code in a style similar to synchronous code, reducing the cognitive load associated with callbacks and promises. To elaborate a bit further, async/await is a full syntax built on top of promises that simplifies asynchronous code. The async keyword is used to define a function that returns a promise and the await keyword is used to pause the execution of the function until the promise is resolved. In a moment, we’ll discuss the axios library which fully supports asynchronous programming using async/await syntax. Axios is designed to work seamlessly with promises, and it returns promises for all of its asynchronous operations. So next, let’s build an application using parallelism with async/await and promises. First, we’ll create our folder parallel, and we’ll initiate our project, then we’ll install our dependency which is axios. Next, I’ll start VS Code, and we’re going to create a file called parallelTasks.js. Okay. Let’s look at our code. First, we import the axios library for making HTTP requests used to fetch data from specific URLs. Below that, the async function, parallelTasks, has an array of URLs representing JSON data. Const promises uses the map function to create an array of promises. For each URL in the URLs array, it calls the fetchData function asynchronously creating an array of promises that will resolve with fetchedData. The try block has the promise.all used to wait for all the promises to resolve. This results in an array of resolved data from each asynchronous call. Results.forEach is the loop that iterates over the results array and logs the data along with the corresponding URL. Then we catch any errors that occur during the parallel execution and log the message. Finally, parallel tasks is called to initiate the execution of the asynchronous tasks in parallel, so let’s run our application, and as we can see, the parallel execution results in printing the three responses. For our data, we’re using the website jsonplaceholder.typicode.com. If we navigate to this web page, we can see there are other endpoints as well. The comments endpoint has 500 comments, and if I click on it, I can see it’s quite large. So let’s use this endpoint, put it in our app, and execute it in parallel with the others. And I’ll enter the URL right below posts/3. First, I’ll clear the screen, then we’ll execute our application. And as you can see, the response is returned quickly. So now we’ve demonstrated parallel tasks, avoiding synchronous functions and using async/await to execute outside of the main thread. Given that Node often interacts with various components, such as multiple node servers, various types of databases, internal and third-party API endpoints, and other third-party and internal systems, having a unified view of logging is advantageous. So up next, we’ll discuss the unification of log files.
Asynchronism and parallelism in Node.js
Node.js is designed to be non-blocking, but it is necessary to distinguish:
- Concurrency: Management of several interleaving operations (event loop)
- Parallelism: Simultaneous execution on several threads/processes (cluster)
The 3 states of a Promise
| State | Description |
|---|---|
| Pending | Initial state, neither fulfilled nor rejected |
| Fulfilled | Successful operation with resulting value |
| Rejected | Operation failed with error reason |
Examples of parallelism with async/await
const express = require('express');
const app = express();
// Simulation d'appels API asynchrones
const fetchUserData = () =>
new Promise(resolve => setTimeout(() => resolve({ id: 1, name: 'Alice' }), 100));
const fetchOrderData = () =>
new Promise(resolve => setTimeout(() => resolve([{ id: 1, total: 99.99 }]), 150));
const fetchInventory = () =>
new Promise(resolve => setTimeout(() => resolve({ items: 42 }), 80));
// ❌ Approche séquentielle - lente (330ms total)
app.get('/api/sequential', async (req, res) => {
const user = await fetchUserData(); // 100ms
const orders = await fetchOrderData(); // 150ms
const inventory = await fetchInventory(); // 80ms
res.json({ user, orders, inventory }); // Total: 330ms
});
// ✅ Approche parallèle - rapide (150ms total)
app.get('/api/parallel', async (req, res) => {
const [user, orders, inventory] = await Promise.all([
fetchUserData(),
fetchOrderData(),
fetchInventory()
]);
res.json({ user, orders, inventory }); // Total: 150ms (le plus long)
});
// Worker Threads pour les tâches CPU-intensives
const { Worker, isMainThread, parentPort } = require('worker_threads');
app.get('/api/cpu-task', (req, res) => {
const worker = new Worker('./heavy-task.js');
worker.on('message', (result) => {
res.json({ result });
});
worker.on('error', (err) => {
res.status(500).json({ error: err.message });
});
});
app.listen(3000);
Worker Threads (heavy-task.js)
const { parentPort } = require('worker_threads');
// Calcul CPU-intensif dans un thread séparé
let result = 0;
for (let i = 0; i < 1_000_000_000; i++) {
result += Math.sqrt(i);
}
parentPort.postMessage({ result });
9.5 Unified log files
Up next, we’ll discuss the unification of log files. By unifying the log files, we are able to have a comprehensive and centralized management making debugging, monitoring, and analysis of system behavior much easier when addressing issues, optimizing performance, and maintaining the overall health and reliability of an application, so let’s discuss some of the Node logging scenarios. Logging is invaluable for debugging during development and troubleshooting issues in production. It allows developers to trace the flow of execution, identify errors, and inspect variable values. Logs help in identifying errors and exceptions that occur during execution of the application. They provide valuable information about the context in which an error occurred, making it easier to diagnose and to fix issues. Logging is essential for monitoring the performance of your application. By analyzing logs, you can identify bottlenecks, track response times, and gain insight into the behavior of your application under different conditions. With libraries like Winston, we can configure multiple transport mechanisms, such as the console, files, and databases, to log information. This enables you to centralize logs from different parts of your application. Winston can also support a variety of transports or outputs for logs, including console, file, HTTP, and third-party services. This flexibility allows you to send log files to different destinations based on your needs, and there are many more benefits such as compliance and regulation, user behavior analysis, and even predictive maintenance. Now, we’ll create an app that creates a simple Express application with logging capabilities using winston logging libraries. We are going to create an application with middleware for logging details of incoming requests and log messages about our routes. The logs will then be output to both the console and a new file called combined.log, so let’s get started. After creating our folder to house our code, we’ll initiate our project. Next, we’ll install our dependencies which include express and winston. Then we’ll start VS Code. For this application, we are going to need two JavaScript files, first, our logger file, then our main Express application. So let’s start by creating logger.js. So let’s take a look at our logger.js code. First, we pull into the Winston library. Winston allows us to log messages to different transports such as consoles, files, and databases. Then we set up our winston logger and it is configured to log messages with the level of info or higher. The logs include a timestamp and are formatted as JSON. Two transports are set up, one for logging to the console and another for logging to a file name combined.log. The next block defines the custom middleware function for express. It logs information about incoming requests, including the request method, URL, client IP, request body, and query parameters. It then calls next to pass control to the next middleware in the stack. Then we export with module.export. Next, we’ll create our app with a few endpoints, so we’ll create a file app.js. So let’s review our code. First, we import the express framework, as well as our custom logger module which we just created. The next two lines create an express app instance and sets our port to 3000. Then with app.use, we tell express to use the custom middleware defined in the logger module. App.get defines a route for the root URL that responds with a Hello from the Root endpoint and writes to both logging mechanisms. App.get for the user endpoints responds with a Hello from the User’s endpoint and writes to both logging mechanisms. App.listen starts our server listening on port 3000, so let’s test our application. Now, I’ve opened a separate terminal window so that we can see the console behind it. We’ll use the curl command and connect to the root endpoint, and in the front, we get a response, Hello from the Root endpoint, and in the back, we can see the log written to the console. Next, we’ll try the user’s endpoint. In the front, we receive Hello from the User’s endpoint, and in the back, we can see the console, and in the back, we can see the user’s endpoint at the console. Now, let’s check our combined.log file. Here, we can see the initial console log that the server is up and running on port 3000, as well as the two get requests that were made. Let’s issue a few more commands. I’ll connect to the root and the users a few times to demonstrate expansion of the log file. And now, we can see with eight requests, we’ve generated 16 lines in our combined.log file. So to summarize, logger.js sets up our winston logger with specific configurations and custom middleware for logging express requests. App.js creates the Express app. Using the logging middleware defines a few simple routes and logs the routes when get requests are issued. With an approach such as this, we are able to log various aspects, such as APIs, databases, I/O operations, and other activities to a unified log file to assist with troubleshooting, monitoring, and performance analysis. Up next, we’ll explore and demonstrate monitoring for memory leaks.
Benefits of log unification
- Debugging: Tracing execution flow through microservices
- Error identification: Full context during exceptions
- Performance monitoring: Identifying bottlenecks
- Compliance: Retention of records for audit
- Behavioral analysis: Understanding user usage
Implementation with Winston
npm install express winston
const express = require('express');
const winston = require('winston');
const app = express();
// Configuration du logger Winston
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
// Sortie console
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}),
// Fichier de logs combinés
new winston.transports.File({ filename: 'logs/combined.log' }),
// Fichier d'erreurs uniquement
new winston.transports.File({ filename: 'logs/error.log', level: 'error' })
]
});
// Middleware de logging des requêtes
app.use((req, res, next) => {
logger.info('Incoming request', {
method: req.method,
url: req.url,
ip: req.ip,
userAgent: req.headers['user-agent']
});
const start = Date.now();
res.on('finish', () => {
logger.info('Response sent', {
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: `${Date.now() - start}ms`
});
});
next();
});
app.get('/api/data', (req, res) => {
logger.info('Data endpoint accessed');
res.json({ message: 'Hello World' });
});
// Middleware de gestion d'erreurs avec logging
app.use((err, req, res, next) => {
logger.error('Unhandled error', {
error: err.message,
stack: err.stack,
url: req.url
});
res.status(500).json({ error: 'Internal Server Error' });
});
app.listen(3000);
9.6 Memory leak monitoring
Up next, we’ll discuss monitoring for memory leaks. Monitoring for memory leaks in Node applications is critical to prevent gradual memory consumption growth, ensuring optimal performance and avoiding potential application crashes due to resource exhaustion, so let’s get started. Monitoring for memory leaks can help maintain application’s stability, prevent performance degradation, and optimize resource utilization for efficient and scalable code execution. Memory leaks can lead to a gradual increase in the application’s memory usage over time. If left unchecked, this can eventually lead to the exhaustion of available system resources, so continuous monitoring helps detect and address memory leaks before they impact the overall performance and stability of the application. Memory leaks can result in increased memory consumption which may lead to unexpected crashes or slowdowns. Identifying and fixing memory leaks in advance helps prevent unplanned downtime ensuring that the application remains available and responsive to users. Memory leaks can degrade the performance of an application. As memory usage increases, the garbage collector may have to work harder leading to longer response times and increased CPU usage. Monitoring and fixing memory leaks contribute to maintaining optimal performance levels. Identifying and addressing memory links is crucial when scaling applications. Memory leaks that go unnoticed can become more pronounced as the application scales affecting the performance and scalability of the entire system. Regularly monitoring helps ensure that the application can scale efficiently without being hindered by memory issues. Memory leaks can result in increased infrastructure costs as more resources are needed to support the growing memory footprint. By addressing memory leaks promptly, we can optimize resource utilization potentially reducing the need for additional hardware or cloud resources. Memory efficient applications contribute to a better user experience. Users are more likely to encounter smooth and responsive interactions when an application is free from memory-related issues. Monitoring for memory leaks helps maintain a positive user experience by preventing sluggish performance and unexpected downtime. Up next, we are going to create an HTTP server that intentionally induces a memory leak by continuously pushing large arrays into a leaky array variable. The server also periodically will monitor the heap size, and if it exceeds 10 MB, it captures a heap snapshot using the heap dump library, creating a file with a timestamp name for further analysis of the memory usage, so let’s get started. After creating our folder to house our application, we’ll million leak strings into leaky array. This creates the intentional memory leakage by continuously increasing the size of leaky array. Next, we create a basic http server using the HTP module and server responding with hello world. Then we start the server to listen on port 3000 and a call back is provided to log a message that the server is listening, then we have our set shots. So let’s test our application and our server is up and running on port 3000. After a few moments, we can see the heap size has exceeded the 10 megabit threshold and a message is log capturing it. Let’s stop our application and take a quick look at the log files. And with this file, it allows us to look for memory holding objects that should have been released but are still retained and ordered by some. We can see that strings and arrays are at the top. Given our intentional memory leak, we can also use the chrome developer tools to open this file and troubleshoot as well. We’ll do so by selecting more tools than developer tools. If we click on memory, we can load our newly created file. And as we see, this allows us to troubleshoot, we can also organize by containment and statistics. Now, we’ve demonstrated how to capture the heap dump using the heap dump middleware. This concludes our course. I hope you found the information valuable. If you have any questions, feel free to reach out to me on the discussions tab, you can also find me on linkedin if you search for Brian La Tot. Thanks again for listening and I hope you have fun enjoying the new node knowledge you may have gained.”
Why monitor memory leaks?
| Risk | Consequence |
|---|---|
| Increasing consumption | System resource exhaustion over time |
| Crashes | OOM (Out of Memory) errors and application crash |
| Degradation | Gradual increase in response times |
| Costs | Oversized infrastructure to compensate for leaks |
| Unable to scale | Leaks increase with increasing load |
Common causes of Node.js memory leaks
- Event listeners not deleted:
emitter.on()withoutemitter.off() - Accumulated closures: Variables captured indefinitely
- Unbounded Caches: Maps and objects growing without limit
- Timers not cleared:
setInterval()withoutclearInterval() - Circular references: Mutually referencing objects
Memory Monitoring Tools
# Clinic.js pour les fuites mémoire
npm install -g clinic
clinic heapprofile -- node app.js
# node --expose-gc pour forcer le GC
node --expose-gc app.js
# Monitoring avec process.memoryUsage()
Memory monitoring in application
const express = require('express');
const app = express();
// Surveillance périodique de la mémoire
const memoryMonitor = () => {
const usage = process.memoryUsage();
const formatMB = (bytes) => `${(bytes / 1024 / 1024).toFixed(2)} MB`;
const stats = {
rss: formatMB(usage.rss), // Total mémoire allouée
heapUsed: formatMB(usage.heapUsed), // Heap utilisé
heapTotal: formatMB(usage.heapTotal), // Heap total
external: formatMB(usage.external) // Buffers C++
};
console.log('[Memory Monitor]', stats);
// Alerte si le heap dépasse 500MB
if (usage.heapUsed > 500 * 1024 * 1024) {
console.warn('[ALERT] High memory usage detected!', stats);
// En production: envoyer une alerte (PagerDuty, Slack, etc.)
}
return stats;
};
// Vérification toutes les 30 secondes
setInterval(memoryMonitor, 30000);
// Endpoint de santé avec métriques mémoire
app.get('/health', (req, res) => {
const memory = memoryMonitor();
res.json({
status: 'ok',
memory,
uptime: `${(process.uptime() / 60).toFixed(2)} minutes`
});
});
// Exemple de fuite mémoire à éviter
const cache = new Map(); // ❌ Sans limite de taille
// ✅ Avec limite de taille (LRU Cache pattern)
class LRUCache {
constructor(maxSize) {
this.maxSize = maxSize;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return null;
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value); // Réinsérer pour marquer comme récent
return value;
}
set(key, value) {
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey); // Éviction LRU
}
this.cache.set(key, value);
}
}
const safeCache = new LRUCache(1000); // Max 1000 entrées
app.listen(3000, () => {
console.log('Server started with memory monitoring');
memoryMonitor();
});
10. Summary and key points
This training covered the entire Node.js ecosystem for managing microservices via an API Gateway and Edge Services. Here are the main lessons:
Mastered technologies and tools
| Domain | Technology / Tool |
|---|---|
| API Gateway | Express Gateway, Express.js |
| Load Balancing | NGINX, HAProxy, Cluster Module |
| Authentication | JWT, Sessions, OAuth 2, SAML, Passwordless |
| Permission | RBAC, Policies |
| Rate Limiting | express-rate-limit, rate-limiter-flexible |
| Caching | apicache, node-cache, Memcached, Redis |
| Security | Helmet.js, TLS/SSL, HTTPS |
| Performance | Clustering, Worker Threads, async/await, PM2 |
| Logging | Winston |
| Testing | Postman, AutoCannon |
Typical architecture of a high-performance Node.js Edge system
[Internet]
|
[CDN / WAF]
|
[NGINX / HAProxy]
(Load Balancer)
/ \
[Gateway 1] [Gateway 2]
(Express Gateway) (Express Gateway)
|
+---------+----------+
| | |
[Service A] [Service B] [Service C]
(Users) (Orders) (Products)
|
[Redis Cache] + [Database]
Best practices checklist
- TLS/SSL enabled on all exposed endpoints
- Rate limiting configured globally and per sensitive route
- Proper authentication (stateless JWT recommended for microservices)
- RBAC implemented for road access control
- Caching in place for frequently accessed data
- Clustering enabled in production to use all CPUs
- Centralized logging with Winston or equivalent
- Memory monitoring active with alerts
- Health endpoints exposed for monitoring
- Pipeline Express Gateway configured with appropriate policies
Search Terms
node.js · microservices · api · gateway · edge · services · apis · backend · full-stack · web · express · cache · authentication · limiting · rate · load · balancing · memory · microservice · performance · benefits · configuration · oauth · optimization