Beginner

Node.js Microservices Fundamentals

Node.js is the ideal technology for building robust and scalable microservices. It is lightweight, cross-platform and offers excellent support for different database technologies.

Technologies: Node.js 20.6.1 · Express.js 4.x · MongoDB · Docker · Azure App Service


Table of Contents

  1. Course Overview
  2. Introduction
  1. Getting started with Node.js
  1. Design an API
  1. Build an API
  1. Working with databases

1. Course Overview

Welcome to this course, Node.js Microservice Fundamentals. Node.js is the ideal technology for building robust and scalable microservices. It is lightweight, cross-platform and offers excellent support for different database technologies.

In this course, we will design, build and deploy a simple but practical microservice using Node.js, Express.js and MongoDB.

Main topics covered

  • Design a RESTful API based on real business requirements
  • Build a microservice with Express.js
  • Integrate a microservice with MongoDB

Prerequisites

After completing this course, you will have a good understanding of how to design, build and deploy a microservice with Node.js and Express.js. Before you get started, you should be familiar with JavaScript and have basic programming skills.


2. Introduction

Module duration: 19m 29s

2.1 General Introduction

Node.js is an open source, cross-platform runtime environment that allows you to build server-side applications in JavaScript. But instead of building one large monolithic application, imagine building several smaller, specialized applications that work together. Each little application, or service, does its job really well and communicates with other services as needed. This is the essence of microservices.

This course provides the foundation needed to start building microservices with Node.js.

Note on versions: This course was created with specific versions of Node.js and Express.js. If you use different versions, some information may not apply. Before you begin, check the compatible versions indicated in the course slides.

The program for this course is as follows: we will first configure our development environment and see which tools to use. We will then understand some fundamentals of Node.js and discover why Node.js is a great platform for building microservices. Above all, we will delve deeper into what microservices are and why they are important.


2.2 Presentation of the scenario

To accompany us throughout this course, here is Kiran. Kiran is a software developer who has just started working for Globomantics. She has a few years of experience primarily as a front-end developer, but in her new role she will be building microservices with Node.js. Although she has experience in JavaScript, she has never worked with Node.js or built a microservice.

The Globomantics Project

Globomantics is launching a new vehicle rental business where customers can rent cars and vans, and manage everything through a web app and a soon-to-be-launched mobile app. The application will allow users to:

  • Search for vehicles
  • Reserve vehicles
  • Manage their payments

Monolithic architecture (initial problem)

In the past, such an application might have consisted of only a few components:

  • Mobile application or website: application entry point, the part with which the user interacts
  • Backend server: handles everything — orders, payments, vehicle searches, and more
  • Database: for example SQL Server, or perhaps an enterprise system like SAP or Salesforce

This type of architecture is called a monolithic application. A monolithic application is a single, unified software system where all of its components and functions are tightly intertwined and managed in a single code base, running as a single unit.

Advantages of monoliths:

  • Initially simpler to develop, especially for small applications

Disadvantages as the application grows:

  • Code base becomes more complex and difficult to maintain
  • Teams must coordinate changes in the same codebase
  • Limited scalability: you must scale the entire application even if only part requires more resources
  • A bug in one part can affect the entire application
  • Deployments are more risky and complex

2.3 Understanding microservices architectures

What is it?

Now reimagine our application using microservices. What would that look like? We still have our mobile and web apps, but instead of all the code being tightly tied together in a single app, we want many specialized services, often called domain services. These services only focus on performing a single task or only work with a certain type of data.

Examples of microservices in Globomantics

ServicesResponsibility
User ServiceAuthentication, user registration
Service DocumentDocument Storage and Retrieval
Order ServicePurchase order processing
Vehicle ServiceVehicle information
Payment ServicePayments and billing
Billing ServiceBilling

Lots of specialized services, little mini-applications that do one thing and do it well.

Vehicle Service in detail

We typically interact with a microservice by making HTTP requests. This is our API (Application Programming Interface). Think of it as a set of HTTP methods that you can call to perform various actions.

For our Vehicle Service, we might want:

  • Obtain a vehicle
  • Create a vehicle
  • Update a vehicle
  • Delete a vehicle

These operations are called CRUD — Create, Read, Update, Delete — and they are present in virtually all microservices. In addition to this, we might want to search for a vehicle by registration number or price.

Our Vehicle Service exposes a set of functionalities via its API, and we call these entry points endpoints. You can have as many or as few as you need. The only rule here is that they only deal with vehicles, nothing else. Things get messy quickly if you try to add operations that are outside its scope.

Architecture of a microservice

Behind each of these endpoints, there is typically a service layer. The service layer contains all the business logic of the microservice. Then, there is often a data layer which links the service and the database.

Key Features of Microservices

Microservices have several important characteristics:

  • Independence: Each microservice is a separate process. You can deploy, modify, and scale it independently of other services.
  • Loose coupling: The services do not directly depend on each other.
  • High cohesion: Each department has a unique and well-defined responsibility.
  • Communication via API: Services communicate via APIs, typically HTTP/REST or messaging systems.

Advantages of microservices over monoliths

CriterionMonolithMicroservices
ScalabilityAll applicationService by service
DeploymentRisky, all or nothingIndependent by service
MaintainabilityDifficult as it gets biggerEvery service remains small
TechnologySingle stackBest-fit by service
ResilienceA bug can affect everythingFault isolation

2.4 Introduction to Node.js

What is Node.js?

Node.js is a runtime environment that allows you to run JavaScript code on the server side. Before the advent of Node.js, JavaScript was a browser-only language. In fact, JavaScript is used on almost every website, providing dynamic content and improving user experience. When Node.js arrived, developers were able to write server-side code using a language they already knew.

When we say that Node is a runtime environment, we mean that it provides the tools, parameters and conditions necessary to run JavaScript outside of a browser environment.

Key Features of Node.js

1. Non-blocking and asynchronous

Node.js is designed to be non-blocking and asynchronous. This means that instead of waiting for a task to complete, such as reading a file or querying a database, Node.js can move on to the next task. This design is particularly beneficial for I/O-bound operations and is one of the reasons why Node.js can handle many connections simultaneously. This makes Node a great choice for any application requiring solid performance.

2. Event architecture (event-driven)

Node uses an event-driven architecture, which means it can handle many connections simultaneously without multiple execution threads.

3. NPM (Node Package Manager)

For many, Node’s big selling point is the NPM package manager. NPM stands for Node Package Manager. With NPM, developers have access to a large repository of libraries and modules, making it easy to integrate various features into your application without building everything from scratch.

All of this makes Node a great environment for building server-side applications with JavaScript.

Why Node.js is particularly suitable for microservices

One of the main advantages of Node for microservices in particular is the lightweight runtime environment. Node.js is cross-platform and can run on any operating system — Windows, macOS or Linux. It also has excellent support for NoSQL databases, SQL and REST APIs.


2.5 Demo: Exploring the Vehicle Service API

In this demo, the instructor demonstrates the complete Vehicle Service microservice that will be built during the course. This service is a real-world, working example, with a GitHub repository and deployment pipeline.

Deployment

The Vehicle Service is deployed on an Azure App Service instance — essentially a Platform as a Service (PaaS) web host. The API is documented with Swagger, a technology that helps describe APIs in a human- and machine-readable way.

The 8 Vehicle Service endpoints

#MethodEndpointDescription
1GET/Health check — verifies that the service is working
2POST/vehicleCreate a new vehicle
3PUT/vehicle/:idUpdate a vehicle by ID
4DELETE/vehicle/:idDelete a vehicle by ID
5GET/vehicle/:idObtain a vehicle by ID
6GET/vehiclesGet all vehicles
7GET/vehicle/search/:registrationNumberSearch by registration number
8GET/vehicles/price/:maxPriceSearch by maximum price

Operation via Swagger

The Swagger interface allows you to interact directly with the API from a web page. For each endpoint:

  • We see the request model: the payload that the API expects
  • We see the possible answers

For example, calling the POST endpoint /vehicle:

  1. Click on “Try it out”
  2. Edit the request body with the desired data
  3. Click “Execute”
  4. Get a response with a code 201 (content created)
  5. The response includes the data created, the _id property (the ID of the document in the database) and the _v property (the version of the document)

GitHub connection and CI/CD pipeline

The service uses a GitHub Actions pipeline that:

  • Build the Docker image automatically on each push
  • Push image to Container Registry
  • Notify Azure that a new image is available

3. Getting started with Node.js

Module duration: 31m 32s

3.1 Introduction

Before we start building microservices, we need to set up our development environment properly and review some basic Node.js concepts.

Each demo in this course comes with a set of demo files that you can use to follow along with the exercises. These files include a starting solution (before) as well as a complete solution (after). Demo files are organized by module. For module 3, the first set of files is called M3_1.

What Kiran needs

Node.js is a very lightweight and flexible runtime. In theory, all you need is to install the Node runtime and use a basic text editor like Notepad. But this is not enough for a professional developer. Here’s what Kiran needs:

ComponentUtility
Node.js runtimeRun JavaScript code
NVM (Node Version Manager)Manage multiple versions of Node.js
Visual Studio CodeProfessional IDE
VS Code ExtensionsImprove the development experience
GitVersion control
Docker DesktopContainerization
MongoDBNoSQL Database

3.2 Install Node.js

On Windows and macOS

  1. Go to the Node.js website: nodejs.org
  2. Select the version to install (recommended: LTS — Long-Term Support)
  3. Choose the installer corresponding to your platform
  4. Download and run the installer
  5. Check the installation:
node -v
npm -v

If everything went well, the version numbers are displayed.

On Linux (Ubuntu)

# Mettre à jour le gestionnaire de paquets
sudo apt update
sudo apt upgrade

# Installer Node.js
sudo apt install nodejs

# Installer NPM
sudo apt install npm

# Vérifier les versions
node -v
npm -v

Node Version Manager (NVM)

NVM is a great tool for managing multiple versions of Node.js on the same machine. It allows you to easily switch from one version to another depending on the projects.

Installing NVM (macOS/Linux):

# Télécharger et exécuter le script d'installation depuis GitHub
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash

Using NVM:

# Lister les versions disponibles
nvm list available

# Installer une version spécifique
nvm install 20.6.1

# Utiliser une version spécifique
nvm use 20.6.1

# Vérifier la version active
node -v

Windows Note: On Windows, there is a different version called nvm-windows, available on GitHub.

Final check:

nvm list
node -v
npm -v

3.3 Choosing a development environment

Node.js developers have several code editors and Integrated Development Environments (IDEs) to choose from, each offering unique features and tools.

Visual Studio Code (VS Code)

  • IDE used by Kiran in this course – Popular choice among developers
  • IntelliSense for autocompletion
  • Built-in GitHub integration
  • Debug support
  • Wide range of extensions
  • Free

Sublime Text

  • Lightweight and fast editor with a clean and simple UI
  • Highly customizable with packages and plugins
  • Requires manual configuration for Node.js development

Atom

  • Free text editor developed by GitHub – Excellent GitHub integration – Integrated Package Manager and Customizable UI
  • Strong community support with many packages for Node.js

WebStorm – Powerful IDE developed by JetBrains

  • Deep JavaScript ecosystem support
  • Intelligent coding assistance
  • Integrated tools for debugging and testing
  • Requires paid license

ESLint

  • Powerful tool for identifying and reporting patterns in JavaScript code
  • Helps write cleaner code and maintain code standards
  • Automatic code analysis, customizable rules, and inline code corrections

Prettier

  • Code formatter for VS Code
  • Automatically formats code in a consistent style

GitLens

  • Powerful Git extension
  • Provides rich code information like author annotations, commit history, and more

Docker Extension

  • Makes it easier to manage and deploy Docker applications
  • Provides tools to build, manage and deploy Docker containers directly from VS Code

REST Client

  • Allows sending HTTP requests directly from VS Code
  • Lightweight alternative to tools like Postman for testing APIs

3.4 Demo: Configure Node.js

In this demo, the instructor sets up a development environment from scratch on macOS (the process is the same on Windows).

Steps Completed

1. Install Node.js

  • Go to nodejs.org
  • Choose LTS version
  • Run installer (accept terms and complete installation)
  • Check in terminal:
node -v

2. Install NVM

As the installed LTS version is not necessarily the version used in this course (20.6.1), you must install NVM to manage the Node version.

# Installer NVM via curl (script depuis GitHub)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash

# Après installation, fermer et rouvrir le terminal, puis vérifier
nvm -v

# Installer la version de Node requise pour le cours
nvm install 20.6.1

# Utiliser cette version
nvm use 20.6.1

# Vérifier
node -v

3. Install Visual Studio Code – Download from VS Code official website

  • Run installer
  • Open VS Code and install recommended extensions (ESLint, Prettier, etc.)

4. Create a Git repository

  • Create a new repository on GitHub
  • Clone repository locally
  • Open cloned folder in VS Code

Tip: The instructor strongly recommends using Git for your code, even for small projects. This allows you to track the history of your changes and go back if necessary.


3.5 Understanding the basics of Node.js

Structure of a Node.js application

The basic structure of a Node.js application includes several key components that work together to handle web requests, process data, and interact with external databases or services.

1. The entry point: app.js or index.js

This is the main file of your application. It initializes the server and often contains the main logic. In a simple application, this file can configure an HTTP server, listen on a port, and define routes or endpoints.

2. The package.json file

This file is the heart of your Node.js application. It includes:

  • Project metadata (name, version, entry point)
  • Managing project dependencies
  • Scripts (start the application, run the tests)
  • The list of dependencies (dependencies) and development dependencies (devDependencies)
{
  "name": "vehicles",
  "version": "1.0.0",
  "description": "A service to retrieve vehicle rental data",
  "main": "vehicles.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node vehicles.js"
  },
  "author": "James Millar",
  "license": "ISC"
}

3. The node_modules folder

This folder contains all NPM packages (libraries and dependencies) installed for your project. It is autogenerated when installing dependencies and is not included in version control (ignored via .gitignore).

4. Additional files depending on complexity

Depending on the complexity of the application, we may have other files such as model files and services.

Fundamental JavaScript Concepts

Although the goal of this course is to teach you how to build microservices with Node.js, it is important to have a good grasp of several fundamental JavaScript concepts:

  • Asynchronous JavaScript: callbacks, promises, async/await — crucial for the service to manage I/O operations without blocking
  • Error handling: essential in a distributed environment like microservices
  • Event-driven programming: a central aspect of Node.js

3.6 Understanding Asynchronous JavaScript

Asynchronous JavaScript allows you to perform operations in a non-blocking manner, allowing code to execute without having to wait for other operations to complete. This is crucial in microservices development, where waiting for operations like network requests, file system operations, or database queries can significantly impact performance.

Evolution of asynchronous patterns

1. Callbacks (old approach)

Initially, JavaScript relied heavily on callback functions. A callback function is passed as an argument to an asynchronous function and is executed after the operation completes.

// Exemple de callback
function greet(name) {
    console.log(`Hello, ${name}!`);
}

function processUserInput(callback) {
    // Simule la récupération d'un nom
    const name = "Kiran";
    callback(name);
}

processUserInput(greet);

The problem: Callback Hell

The problem with callbacks is that they create all sorts of complexity and maintainability issues. We often hear about “callback hell” or “pyramid of doom” — nested callbacks that become impossible to read and maintain.

2. The Promises (intermediate approach)

To work around this issue, a new feature called promise has been introduced. A promise is an object representing the eventual completion or failure of an asynchronous operation.

Analogy: Imagine you go to your favorite coffee shop and order a coffee. The barista tells you your coffee will be ready soon and gives you a buzzer. This buzzer is like a promise — it’s a promise that your coffee will eventually be ready. If all goes well, the promise is resolved with your coffee. If something goes wrong, the bride is rejected.

// Exemple de Promise
function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const data = { id: 1, name: "Tesla Model Y" };
            resolve(data);
            // En cas d'erreur : reject(new Error("Erreur de récupération"));
        }, 1000);
    });
}

fetchData()
    .then(data => console.log("Données reçues :", data))
    .catch(error => console.error("Erreur :", error));

3. Async/Await (modern approach)

Building on promises, the async/await syntax was introduced to handle asynchronous operations in a more seemingly synchronous manner. It’s really just syntactic sugar that makes asynchronous code easier to read and write.

// Exemple async/await
async function getVehicleData() {
    try {
        const vehicle = await fetchData();
        console.log("Véhicule récupéré :", vehicle);
    } catch (error) {
        console.error("Erreur :", error);
    }
}

getVehicleData();

Best practice: In this course, we mainly use async/await because it is the most modern and readable approach.


3.7 Handle errors

Error handling in JavaScript is a crucial practice for ensuring that your program behaves predictably and that errors are handled correctly. It’s about identifying when something is going wrong in a program and finding a way to handle those situations elegantly.

Try/Catch/Finally

The way we do this in JavaScript is by using the try/catch block. This is a fundamental error handling mechanism, and you’ll see this used in many programming languages.

try {
    // Code qui pourrait produire une erreur
    const result = riskyOperation();
    console.log("Résultat :", result);
} catch (error) {
    // Exécuté si une erreur se produit dans le bloc try
    console.error("Une erreur s'est produite :", error.message);
} finally {
    // Exécuté qu'une erreur se soit produite ou non
    // Idéal pour fermer des connexions de base de données, libérer des ressources
    console.log("Nettoyage effectué");
}
  • try block: contains code likely to generate an error. If an error occurs, execution of this block stops and control is passed to the catch block.
  • catch block: executed if an error occurs in the try block. The error object is accessible in this block.
  • finally block: the code executes whether or not an error has occurred. Useful for cleaning up resources or other finalization tasks, such as closing database connections.

Throw custom errors with throw

You can also throw your own errors using the throw instruction:

function validateNumber(number) {
    if (number < 0) {
        throw new Error("Le nombre ne peut pas être négatif");
    }
    return number;
}

try {
    const result = validateNumber(-5);
} catch (error) {
    console.error("Erreur de validation :", error.message);
    // Affiche : "Erreur de validation : Le nombre ne peut pas être négatif"
}

Important for microservices: Proper error handling is crucial, especially in a distributed environment. Each endpoint of our Vehicle Service uses try/catch/finally blocks to ensure that database connections are always closed, even in the event of an error.


3.8 Understanding event-driven programming

Event-driven programming is a central aspect of Node.js, allowing it to handle concurrent operations efficiently. It is a programming paradigm where the program flow is determined by events: user actions, I/O operations, timers, etc.

In Node.js, this pattern is widely used due to its non-blocking and asynchronous nature. It essentially allows you to create events in your code that other parts of your system can respond to, following a publish/subscribe pattern.

EventEmitter

In our code, we can use something called the Event Emitter to publish and subscribe to events that occur in our own code, as well as in any other modules we might use.

Published events are retrieved by something called the Event Loop. This is truly the beating heart of Node.js. It continually checks for events and distributes them to their respective subscribers when they occur.

Example of use

// Importer le module events et créer une classe qui étend EventEmitter
const EventEmitter = require('events');

class Logger extends EventEmitter {
    log(message) {
        // Émettre un événement 'log' avec le message
        this.emit('log', message);
    }
}

// Créer une instance
const logger = new Logger();

// Enregistrer un écouteur d'événements (subscriber)
logger.on('log', (message) => {
    console.log(`[LOG] ${message}`);
});

// Émettre l'événement (publisher)
logger.log("Application démarrée");
// Affiche : [LOG] Application démarrée
  • on(eventName, listener): registers an event listener
  • emit(eventName, ...args): triggers the event and calls all associated listeners

Event-driven programming in Node.js is particularly powerful for building scalable network applications. It efficiently handles asynchronous operations and allows developers to structure their applications in a way that responds to a multitude of different events.


3.9 Demo: Create a Node.js application

In this demo, the instructor creates and runs a simple Node application to verify that the environment is configured correctly.

Steps

1. Open the folder in VS Code

If you cloned the GitHub repository, navigate to the Module 3 > M3_2 > before folder. Otherwise, create a new folder on your file system and open it in VS Code.

2. Open a terminal

Go to the View menu, then Terminal.

3. Initialize the Node project

npm init

Follow the prompts (or press Enter to accept the defaults). A package.json file is created with the project metadata.

4. Create the app.js file

In VS Code File Explorer, create a new app.js file with the following content:

const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/plain');
    res.end('Hello Node\n');
});

server.listen(port, hostname, () => {
    console.log(`Server running at http://${hostname}:${port}/`);
});

5. Run the application

node app.js

The console displays: Server running at http://127.0.0.1:3000/

Open a browser and visit http://127.0.0.1:3000 — the message “Hello Node” is displayed.

6. Stop the server

Press Ctrl+C in terminal.

Note: This is not yet the recommended way to build a full service with pure Node.js — it’s just to verify that the environment works. We will use Express.js to build our real microservice.


4. Design an API

Module duration: 14m 5s

4.1 Introduction to API design

In this module we will look at what is involved in designing an API. Designing an API correctly is a multi-step problem, which includes:

  1. Understand the business problem that the API is there to solve (business requirements)
  2. Identify the resources and actions that the API must manage
  3. Choose a design principle (REST, GraphQL, SOAP…)
  4. Define endpoints
  5. Consider security (authentication, authorization)
  6. Document the API (with Swagger for example)
  7. Implement and test the API (iterative process)

Process overview

Exigences métier
       ↓
Identification des ressources et actions
       ↓
Choix du principe de conception
       ↓
Définition des endpoints
       ↓
Considérations de sécurité
       ↓
Documentation (Swagger)
       ↓
Implémentation et tests  ← (Retour au début si nouvelles exigences)

4.2 Understanding business requirements

As a software developer, it is likely that you will not have to manage business requirements in depth — this is often done by a business analyst. But if you’re building an API for yourself, it’s important to understand what problem it needs to solve.

Requirement Types

functional requirements

Defines how the system actually behaves.

Globomantics Example: “Customers should be able to add new vehicles.”

Non-functional requirements

Address other issues such as performance, reliability and compliance.

Globomantics Example: “Our API must be scalable and handle an increasing number of vehicles and users.”

Identify the target audience

With the requirements gathered, we need to identify the target audience — who will actually use our API. This can have a big impact on how we implement it. Some APIs are public, while ours is private and will only be used by our mobile and web applications.

Set clear goals

We need to define clear objectives for our API. This helps to delineate the scope and ensure that we don’t end up building overly complex features that we simply don’t need.


4.3 Exploring the design process

Identify resources and actions

Resources are the domain objects for which our service will be responsible. Our Vehicle Service only deals with vehicle-type resources, but it might also need to know about commands or users. Even if these resources will be managed by their own services, it is important to identify them.

Actions are operations that the service must be able to perform: create a vehicle, update a vehicle, delete a vehicle, etc.

Choose a design principle

In this course, we build a REST service. REST stands for Representational State Transfer — it is an architectural style that uses HTTP requests to access and manipulate data.

Here are other possible approaches:

| Approach | Description | Usage | |----------|------------||-------| | REST | Uses HTTP, multiple endpoints | Modern standard, our choice | | GraphQL | Query language, single endpoint | Maximum flexibility for the customer | | SOAP | XML-based, integrated security | Legacy enterprise applications |

GraphQL is a query language for APIs that allows clients to request exactly what they need and nothing more. Unlike REST which uses multiple endpoints, GraphQL only uses a single endpoint.

SOAP (Simple Object Access Protocol) has fallen somewhat out of favor in recent years, but you’ll probably encounter it at one point or another. It is XML-based and offers built-in security. Many enterprise applications still use it extensively.

Define endpoints

Several questions must be answered:

  • How many endpoints do we need?
  • What will they be named?
  • What naming convention?

It is important to establish a clear naming convention. Our service is just one of many microservices at Globomantics, and it is helpful to other developers that we follow a clear convention.

API versioning should also be considered — this facilitates future changes without impacting existing clients. (Not covered in this course.)

Request and response formats

We must also think about the request and response formats: the data we send and the data we receive. In this course, we use the JSON (JavaScript Object Notation) format.

Important HTTP Codes

CodeMeaningUsage
200OKSuccessful request
201CreatedResource created successfully
204No ContentSuccess without content (eg: DELETE)
400Bad RequestInvalid query
404Not FoundResource not found
500Internal Server ErrorServer error

4.4 Demo: Design an API

In this demo, the instructor analyzes the problem that the Vehicle Service needs to solve and identifies the key features it must have.

Analysis of requirements document

The course files for this module contain a requirements document (API_Design.md). The instructor uses it to plan the Vehicle Service API by determining:

  1. What does a vehicle look like? What properties does it have?
  2. What endpoints are needed?

Model Vehicle

According to the requirements, a vehicle has the following properties:

Modèle Vehicle :
- make            : string  (marque)
- model           : string  (modèle)
- registrationNumber : string  (numéro d'immatriculation)
- year            : number  (année de fabrication)
- rentalPrice     : number  (prix de location)

Endpoint design

The analysis result produces the following design:

# API Design

### Vehicle Model
- Make : string
- Model : string
- Registration : string
- Year : number
- Price : number

### Endpoints

1. Create Vehicle
HTTP POST
URL: /vehicle
Request body: Vehicle entity
Response:   201: Vehicle created
            500: Application error

2. Update Vehicle
HTTP PUT
URL: /vehicle/:id
Request body: Vehicle entity
Response:   200: Vehicle updated
            500: Application error

3. Delete Vehicle
HTTP DELETE
URL: /vehicle/:id
Response:   200: OK
            500: Application error

4. Get Vehicle by ID
HTTP GET
URL: /vehicle/:id
Response:   200: Vehicle updated
            404: Vehicle not found
            500: Application error

5. Get all Vehicles
HTTP GET
URL: /vehicles
Response:   200: A list of vehicles
            404: Vehicles not found
            500: Application error

Plus two additional endpoints:

  • GET /vehicle/search/:registrationNumber — search by registration number
  • GET /vehicles/price/:maxPrice — search by maximum price

5. Build an API

Module duration: 21m 27s

5.1 Building an API with Express.js

In this module, we will build our Vehicle Service API. To help us, we’re going to use a framework called Express.js, which offers a robust set of features for building APIs. We’ll also explore how to package and deploy our API using Docker.

What is Express.js?

Express.js, often simply called Express, is a lightweight and flexible web application framework for Node.js. Express offers a range of features for developing web applications, and it greatly simplifies API development. This means you can focus on the business logic of your API without having to worry about all the complicated server code.

More information about Express: expressjs.com

Two ways to get started with Express

Option 1: Add Express to an existing project

# Installer le package Express via NPM
npm install express

Option 2: Express Generator

The Express Generator creates an entirely new skeleton application that you can then modify as needed. It typically configures an application with a view engine that renders static content (web pages).

# Installer l'Express Generator globalement
npm install -g express-generator

# Créer une nouvelle application
express my-app

Note: In this course, we are not using the Express Generator because we are building a microservice, not an application that serves static content.


5.2 Demo: Getting started with Express.js

In this demo, the instructor creates a basic Express.js application from scratch.

Steps

1. Create the project

# Initialiser le projet avec les options par défaut
npm init -y

The -y flag tells NPM to accept all options by default.

2. Install Express

npm install express

3. Create the app.js file

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

app.get('/', (req, res) => {
    res.send('Hello Express.js!');
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

4. Launch the application

node app.js

Open a browser to http://localhost:3000 — the message “Hello Express.js!” is displayed.

package.json corresponding:

{
  "name": "basic-express-api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.18.2"
  }
}

5.3 Understanding Express.js

Routing

The main Express feature we use in this course is routing. Express provides a sophisticated routing mechanism to perform different actions depending on HTTP method and URL. This means we don’t have to write a lot of complicated server code, because Express handles all the routing for us.

Express works using middleware modules. Technically, Express is a routing and middleware framework that provides only minimal functionality.

Middleware

Middleware modules can:

  • Execute code
  • Modify request and response objects
  • End request-response cycle
  • Call the next middleware in the stack
// Exemple de middleware custom
app.use((req, res, next) => {
    console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
    next(); // Passer au middleware suivant
});

Structure of route definitions

The structure of a route in Express:

app.METHOD(PATH, HANDLER)

Where:

  • app: instance of Express.js
  • METHOD: HTTP method (get, post, put, patch, delete) in lowercase
  • PATH: URL path on the server
  • HANDLER: function invoked when the route matches

Example routes

// GET - retourne un texte
app.get('/', (req, res) => {
    res.send('Hello Node!');
});

// POST sur le même chemin
app.post('/', (req, res) => {
    res.send('POST request received');
});

// PUT avec paramètre de chemin
app.put('/vehicle/:id', (req, res) => {
    const id = req.params.id;
    res.send(`Updating vehicle ${id}`);
});

// DELETE
app.delete('/vehicle/:id', (req, res) => {
    const id = req.params.id;
    res.send(`Deleting vehicle ${id}`);
});

Query strings and parameters

// Paramètre de route : /vehicle/123
app.get('/vehicle/:id', (req, res) => {
    const id = req.params.id; // "123"
    // ...
});

// Query string : /vehicles?make=Toyota
app.get('/vehicles', (req, res) => {
    const make = req.query.make; // "Toyota"
    // ...
});

body-parser middleware

For POST and PUT requests that send a JSON body, we use the body-parser middleware:

npm install body-parser
const bodyParser = require('body-parser');
app.use(bodyParser.json());

// Maintenant req.body est disponible dans les handlers
app.post('/vehicle', (req, res) => {
    const { make, model, registrationNumber } = req.body;
    // ...
});

Documentation with Swagger

Swagger is a technology that helps describe APIs in a human- and machine-readable way. To use it with Express, install two packages:

npm install swagger-jsdoc swagger-ui-express

Swagger annotations are added directly in JSDoc comments:

/**
 * @swagger
 * /vehicle:
 *   post:
 *     summary: Create a new vehicle.
 *     tags: [Vehicle]
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: '#/components/schemas/Vehicle'
 *     responses:
 *       201:
 *         description: The created vehicle.
 */
app.post("/vehicle", vehicleService.createVehicle);

5.4 Demo: Create the Vehicle Service API

In this demo, the instructor takes the design created for the API and implements it in a new Express.js project.

Project structure

VehicleService/
├── Dockerfile
├── docker-compose.yml
└── src/
    ├── index.js           ← Point d'entrée, définition des routes
    ├── package.json
    ├── swagger.js         ← Configuration Swagger
    ├── data/
    │   └── seedData.js    ← Données de test
    ├── models/
    │   └── vehicleModel.js ← Classe Vehicle
    └── services/
        └── vehicleService.js ← Logique métier

models/vehicleModel.js — The Vehicle model

const { v4: uuidv4 } = require('uuid');

class Vehicle {
  constructor(registrationNumber, make, model, year, rentalPrice) {
    if (typeof registrationNumber !== 'string' || typeof make !== 'string' || typeof model !== 'string') {
      throw new Error('registrationNumber, make, and model must be strings');
    }
    if (typeof year !== 'number' || typeof rentalPrice !== 'number') {
      throw new Error('year and rentalPrice must be numbers');
    }
    this.id = uuidv4();
    this.registrationNumber = registrationNumber;
    this.make = make;
    this.model = model;
    this.year = year;
    this.rentalPrice = rentalPrice;
  }
}

module.exports = Vehicle;

Important points:

  • id property is automatically generated with uuidv4() (UUID v4 format)
  • Type validation is performed in the constructor (throw on error)
  • The uuid package must be installed: npm install uuid

data/seedData.js — Test data

const Vehicle = require("../models/vehicleModel");

const seedData = [];

const makes = ["Toyota", "Honda", "Ford", "Chevrolet", "Nissan"];
const models = ["Camry", "Civic", "F-150", "Silverado", "Altima"];
const years = [2021, 2022, 2023, 2024];

for (let i = 0; i < 50; i++) {
  const make = makes[Math.floor(Math.random() * makes.length)];
  const model = models[Math.floor(Math.random() * models.length)];
  const year = years[Math.floor(Math.random() * years.length)];
  const rentalPrice = Math.floor(Math.random() * 351) + 150;
  const vehicle = new Vehicle(`ABC${i}`, make, model, year, rentalPrice);
  seedData.push(vehicle);
}

module.exports = seedData;

This module generates 50 random test vehicles when the service starts.

services/vehicleService.js — The service layer

const Vehicle = require("../models/vehicleModel");
const seedData = require("../data/seedData");

const vehicleData = [];

function seed() {
  vehicleData.push(...seedData);
}

async function createVehicle(req, res) {
  try {
    const vehicle = new Vehicle(
      req.body.registrationNumber,
      req.body.make,
      req.body.model,
      req.body.year,
      req.body.rentalPrice
    );
    vehicleData.push(vehicle);
    res.status(201).send(vehicle);
  } catch (err) {
    res.status(500).send(err);
  }
}

async function updateVehicle(req, res) {
  try {
    const index = vehicleData.findIndex(vehicle => vehicle.id === req.params.id);
    if (index !== -1) {
      vehicleData[index] = {...vehicleData[index], ...req.body};
      res.send(vehicleData[index]);
    } else {
      res.status(404).send({message: 'Vehicle not found'});
    }
  } catch (err) {
    res.status(500).send(err);
  }
}

function deleteVehicle(req, res) {
  try {
    const index = vehicleData.findIndex(vehicle => vehicle.id === req.params.id);
    if (index !== -1) {
      vehicleData.splice(index, 1);
      res.status(204).send();
    } else {
      res.status(404).send({message: 'Vehicle not found'});
    }
  } catch (err) {
    res.status(500).send(err);
  }
}

function getVehicle(req, res) {
  try {
    const vehicle = vehicleData.find(vehicle => vehicle.id === req.params.id);
    if (!vehicle) {
      res.status(404).send("Vehicle not found");
    } else {
      res.send(vehicle);
    }
  } catch (err) {
    res.status(500).send(err);
  }
}

function getAllVehicles(req, res) {
  try {
    const vehicles = vehicleData;
    if (!vehicles || vehicles.length === 0) {
      res.status(404).send("Vehicles not found");
    } else {
      res.send(vehicles);
    }
  } catch (err) {
    res.status(500).send(err);
  }
}

function searchVehicleByRegistrationNumber(req, res) {
  try {
    const vehicle = vehicleData.find(
      vehicle => vehicle.registrationNumber === req.params.registrationNumber
    );
    if (!vehicle) {
      res.status(404).send("Vehicle not found");
    } else {
      res.send(vehicle);
    }
  } catch (err) {
    res.status(500).send(err);
  }
}

function lookupVehiclesByMaxRentalPrice(req, res) {
  try {
    const vehicles = vehicleData.filter(
      vehicle => vehicle.rentalPrice <= req.params.maxPrice
    );
    if (!vehicles || vehicles.length === 0) {
      res.status(404).send("Vehicles not found");
    } else {
      res.send(vehicles);
    }
  } catch (err) {
    res.status(500).send(err);
  }
}

module.exports = {
  createVehicle,
  updateVehicle,
  deleteVehicle,
  getVehicle,
  getAllVehicles,
  searchVehicleByRegistrationNumber,
  lookupVehiclesByMaxRentalPrice,
  seed,
};

swagger.js — Swagger Configuration

const swaggerUi = require("swagger-ui-express");
const swaggerJsdoc = require("swagger-jsdoc");

const options = {
  definition: {
    openapi: "3.0.0",
    info: {
      title: "Vehicle API",
      version: "1.0.0",
      description: "A simple Express API for managing vehicles",
    },
    servers: [
      {
        url: "https://vehicleservice.azurewebsites.net",
        description: "Production server",
      },
      {
        url: "http://localhost:3000",
        description: "Local server",
      },
    ],
  },
  apis: ["./models/vehicleModel.js", "./index.js"],
};

const specs = swaggerJsdoc(options);

module.exports = (app) => {
  app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(specs));
};

Swagger documentation can be accessed at: http://localhost:3000/api-docs

index.js — Entry point and route definition

const express = require("express");
const bodyParser = require("body-parser");
const vehicleService = require("./services/vehicleService");
const app = express();

app.use(bodyParser.json());

// Charger les données de test
vehicleService.seed();

// Configurer Swagger
require('./swagger')(app);

// Endpoint de health check
app.get("/", (req, res) => {
  res.status(200).send("Vehicle Service");
});

// POST /vehicle - Créer un véhicule
app.post("/vehicle", vehicleService.createVehicle);

// PUT /vehicle/:id - Mettre à jour un véhicule
app.put("/vehicle/:id", vehicleService.updateVehicle);

// DELETE /vehicle/:id - Supprimer un véhicule
app.delete("/vehicle/:id", vehicleService.deleteVehicle);

// GET /vehicle/:id - Obtenir un véhicule par ID
app.get("/vehicle/:id", vehicleService.getVehicle);

// GET /vehicles - Obtenir tous les véhicules
app.get("/vehicles", vehicleService.getAllVehicles);

// GET /vehicle/search/:registrationNumber - Recherche par immatriculation
app.get("/vehicle/search/:registrationNumber", vehicleService.searchVehicleByRegistrationNumber);

// GET /vehicles/price/:maxPrice - Recherche par prix maximum
app.get("/vehicles/price/:maxPrice", vehicleService.lookupVehiclesByMaxRentalPrice);

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`Vehicle Service is running on port ${PORT}`);
});

package.json from Vehicle Service

{
  "name": "vehicleservice",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.20.2",
    "express": "^4.18.2",
    "swagger-jsdoc": "^6.2.8",
    "swagger-ui-express": "^5.0.0",
    "uuid": "^9.0.1"
  }
}

Install dependencies

cd src
npm install
node index.js

5.5 Host a microservice

Now that our Vehicle Service is built, we need to think about how we are going to host it. We have several options.

Hosting Options

Platform as a Service (PaaS)

A type of cloud computing service that provides a platform allowing you to develop, run and manage an application without the complexity of building and maintaining the infrastructure. Examples:

  • Azure App Service (Microsoft)
  • Amazon Elastic Beanstalk (AWS)
  • Google Cloud App Engine (GCP)

PaaS removes much of the hosting complexity because you don’t have to manage and maintain the underlying infrastructure.

Containerization (Docker)

Containerization services like Docker allow you to package your microservice in a way that ensures consistent deployment in any environment. Services like Kubernetes and Docker Swarm then allow you to manage and scale your API.

Infrastructure as a Service (IaaS)

These are generally bare bones virtual machines. It’s like having your own physical server that you are responsible for managing and maintaining.

On-premise

Deploying on your own server. This is becoming less common as applications migrate to the cloud.

Containerization with Docker

In this course we use Docker. Here’s how it works.

Docker image

The first step is to package our application code into something called a Docker image. A Docker image is like a blueprint for a Docker container.

Analogy: If you want to build a house, the Docker image would be the detailed plans or instructions for building that house. The Docker image contains everything needed to run an application: the code, the Node runtime, libraries, environment variables, and configuration files.

You can then run this image on your local machine or any other machine, and the result will still be the same.

Docker Container

When you run a Docker image, it creates a Docker container. It is an isolated execution environment.

Dockerfile

To create a Docker image, we write a Dockerfile file:

# Use an official Node.js runtime as a parent image
FROM node:20.6.1

# Set the working directory to /app
WORKDIR /app

# Copy the current directory contents into the container at /app
COPY ./src .

# Install any needed packages specified in package.json
RUN npm ci

# Make port 3000 available to the world outside this container
EXPOSE 3000

# Define environment variable
#ENV NODE_ENV=production

# Run index.js when the container launches
CMD [ "node", "index.js" ]

Explanation of each instruction:

  • FROM node:20.6.1: official Node.js base image
  • WORKDIR /app: defines the working directory in the container
  • COPY ./src .: copies the source code into the container
  • RUN npm ci: installs the dependencies (“clean install” version, stricter than npm install)
  • EXPOSE 3000: expose port 3000
  • CMD: command executed when starting the container

docker-compose.yml

For simple deployment configuration:

version: '3'

services:
  web:
    image: jamesmillar/vehicleservice:latest
    ports:
      - "3000:3000"

5.6 Demo: Deploy a microservice

In this demo, the instructor demonstrates how to deploy the service on the cloud.

Automatic pipeline with GitHub Actions

For the main solution, the instructor uses a GitHub Action that automates deployment:

  • Each time code is pushed to the repository
  • Build the Docker image
  • The push towards the Container Registry
  • Inform Azure that a new image is available

Manual deployment with Docker

1. Build the Docker image

docker build -f Dockerfile -t vehicleservice .

2. Tag the image

docker tag vehicleservice username/vehicleservice:latest

3. Connect to the Container Registry

docker login

4. Push image

docker push username/vehicleservice:latest

The image now appears in Docker Hub with the latest tag.

Deploying to Azure App Service

When a new image is pushed to Docker Hub, a web hook notifies the Azure App Service that a new version is available.

In the Azure portal, in the Deployment Center:

  • Service is configured with Docker Compose
  • File specifies which Docker image to use and how to map ports
  • When a new image is available, Azure retrieves it from the registry and deploys it

Test the deployed service

To verify the deployment, open the Swagger documentation at the production URL and call a few endpoints to make sure everything is working correctly.


6. Working with databases

Module duration: 24m 21s

6.1 Introduction

Our Vehicle Service microservice as we designed and built it is not yet completely complete. At the moment we store our data in an array inside our code. Obviously, every time the application restarts, this data is cleared, so we need another way to store our data.

Why databases are important for microservices

Service independence

Typically, each microservice will have its own database. This ensures that services are loosely coupled and can operate independently.

Independent scalability

Microservices often need to scale independently based on demand. Having separate databases allows each microservice to scale its data storage and processing capabilities according to its needs without impacting the performance of other services.

Data isolation

By giving each microservice its own data store, we can ensure data isolation. This is particularly important for security and privacy concerns because data managed by one service is not directly accessible to other services.

Performance optimization

Different microservices may have different data storage requirements and access patterns. Dedicated databases allow each service to optimize its schema, indexes and queries according to its specific requirements.

Optimal technological choice

This also allows services to use the most appropriate data storage technology. Some services may need a traditional SQL database, while others may use a NoSQL database.

Database options for Node.js

Node.js supports virtually all database technologies. Here are some popular choices:

MongoDB (NoSQL)

  • Document-oriented NoSQL database
  • Very popular with Express.js (MEAN/MERN stack)
  • Ideal for flexible and unstructured data
  • Used in this course

PostgreSQL (Relational SQL)

  • Powerful open source SQL database system
  • Excellent support in the Node.js ecosystem

MySQL (Relational SQL) – Popular Relational Database

  • Widely used in web applications

Redis (cache)

  • In-memory database
  • Excellent for caching and sessions

In this course, we will use MongoDB with the Mongoose library.


6.2 Demo: Install MongoDB

In this demo, the instructor installs and configures MongoDB Community Edition. He also modifies the docker-compose.yml file so that the production environment includes a MongoDB server.

Installing MongoDB Community Edition

On Windows: 2. Select “Community Server” 3. Choose your platform and download the installer 4. Run the installer, making sure to run MongoDB as a service under the appropriate account 5. Verify that the service is started

On macOS (recommended via HomeBrew):

brew tap mongodb/brew
brew install mongodb-community
brew services start mongodb-community

On Linux: See the MongoDB documentation for instructions specific to your distribution.

MongoDB Compass

MongoDB Compass is a graphical interface for MongoDB that allows you to connect to a running MongoDB database and interact with the data.

Compass Features:

  • Create new databases and collections
  • Add documents via interface
  • View and query data
  • Monitor performance

Update docker-compose.yml

To include MongoDB in the Docker environment:

version: '3'

services:
  web:
    image: jamesmillar/vehicleservice:latest
    ports:
      - "3000:3000"
    depends_on:
      - mongo
    environment:
      - MONGO_URI=mongo

  mongo:
    image: mongo:latest
    ports:
      - "27017:27017"
    volumes:
      - mongo-data:/data/db

volumes:
  mongo-data:

Important points:

  • depends_on: mongo: web service starts after MongoDB is ready
  • environment: MONGO_URI=mongo: pass connection configuration
  • volumes: mongo-data:/data/db: persist MongoDB data even if the container stops

6.3 Understanding NoSQL

NoSQL vs SQL data structure

Traditional SQL Database

In a SQL database, if we want to store vehicle data, we could first define a table for vehicle manufacturers, with a unique record for each manufacturer and its own unique ID. Next, a vehicle table with a unique ID for each vehicle and a foreign key to describe the relationship between the vehicle and the manufacturer.

These tables and their relationships constitute a schema. In complex databases, this can include hundreds of tables.

MongoDB database (NoSQL)

With MongoDB, we do not define a schema because the data is not stored in rows and columns. MongoDB uses document-oriented storage:

  • Each record has a unique key associated with a specific object
  • Object is stored in BSON (Binary JSON) format — JSON extended to support additional data types like dates and binary data
  • A document can contain nested documents and tables
{
  "_id": ObjectId("507f1f77bcf86cd799439011"),
  "registrationNumber": "ABC123",
  "make": "Toyota",
  "model": "Camry",
  "year": 2022,
  "rentalPrice": 150
}

The Collections

Similar documents are grouped into collections. It is the equivalent of a table in an SQL database, but without a fixed schema.

CRUD operations with MongoDB

OperationMongoDBSQL
Createdb.collection.insertOne({...})INSERT INTO ... VALUES ...
Readdb.collection.findOne({...})SELECT ... FROM ...
Updatedb.collection.updateOne({...}, {...})UPDATE...SET...
Deletedb.collection.deleteOne({...})DELETE FROM ...

Mongoose — ODM for MongoDB

Mongoose is a JavaScript library that provides a simple schema-based solution for modeling data in your application. Mongoose is an Object Document Mapper (ODM) — it maps JavaScript objects to MongoDB documents.

Although MongoDB itself is schemaless, Mongoose allows you to define a schema for your collections, which helps maintain data consistency.

Advantages of Mongoose:

  • Defining schemas for data validation
  • Easy template creation
  • Simplified queries
  • Automatic management of IDs

Installation:

npm install mongoose

Define a Mongoose schema:

const mongoose = require("mongoose");

const vehicleSchema = new mongoose.Schema({
  registrationNumber: {
    type: String,
    required: true,
    unique: true,
  },
  make: String,
  model: String,
  year: Number,
  rentalPrice: Number,
});

module.exports = mongoose.model("vehicleModel", vehicleSchema);

Important points:

  • required: true: this field is mandatory
  • unique: true: unique values only
  • MongoDB automatically manages the _id field (no need to define it)

6.4 Demo: Integrate MongoDB

In this demo, the instructor modifies the Vehicle Service to store data in MongoDB.

Step 1: Install Mongoose

npm install mongoose

Step 2: Create the database client (dbClient.js)

const mongoose = require('mongoose');

const uri = 'mongodb://mongo:27017/vehicles';
const options = {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  serverSelectionTimeoutMS: 5000,
  socketTimeoutMS: 45000,
  autoIndex: true, // Gère automatiquement les propriétés id
};

async function connect() {
  try {
    await mongoose.connect(uri, options);
    console.log('Connected to MongoDB');
  } catch (err) {
    console.error(err);
  }
}

async function disconnect() {
  try {
    await mongoose.disconnect();
    console.log('Disconnected from MongoDB');
  } catch (err) {
    console.error(err);
  }
}

module.exports = { connect, disconnect };

Connection URI explanation:

  • mongodb://: MongoDB protocol
  • mongo:27017: Docker service name + MongoDB port (27017 by default)
  • /vehicles: database name

Note: In local development, the URI would be mongodb://localhost:27017/vehicles.

The autoIndex: true option tells Mongoose to automatically handle id properties on created documents.

Step 3: Update the Vehicle model (vehicleModel.js)

With MongoDB, we no longer need to manage the ID ourselves — MongoDB manages it. We replace the JavaScript class with a Mongoose schema:

const mongoose = require("mongoose");

const vehicleSchema = new mongoose.Schema({
  registrationNumber: {
    type: String,
    required: true,
    unique: true,
  },
  make: String,
  model: String,
  year: Number,
  rentalPrice: Number,
});

module.exports = mongoose.model("vehicleModel", vehicleSchema);

Step 4: Update test data (seedData.js)

const seedData = [];

const makes = ['Toyota', 'Honda', 'Ford', 'Chevrolet', 'Nissan'];
const models = ['Camry', 'Civic', 'F-150', 'Silverado', 'Altima'];
const years = [2018, 2019, 2020, 2021];

for (let i = 0; i < 50; i++) {
  const make = makes[Math.floor(Math.random() * makes.length)];
  const model = models[Math.floor(Math.random() * models.length)];
  const year = years[Math.floor(Math.random() * years.length)];
  const rentalPrice = Math.floor(Math.random() * 351) + 150;
  const vehicle = {
    registrationNumber: `ABC${i}`,
    make: make,
    model: model,
    year: year,
    rentalPrice: rentalPrice
  };
  seedData.push(vehicle);
}

module.exports = seedData;

Difference with the previous version: We no longer use the Vehicle class here, we return simple JavaScript objects that Mongoose will transform into MongoDB documents.

Step 5: Update the service (vehicleService.js)

The service is completely rewritten to use Mongoose instead of the in-memory array:

const Vehicle = require("../models/vehicleModel");
const seedData = require("../data/seedData");
const dbClient = require("../dbClient");

async function seed() {
  await dbClient.connect();
  await Vehicle.deleteMany();           // Supprimer tous les documents existants
  await Vehicle.insertMany(seedData);   // Insérer les données de test
  console.log("Test data inserted successfully");
  await dbClient.disconnect();
}

async function createVehicle(req, res) {
  try {
    await dbClient.connect();
    const vehicle = new Vehicle(req.body);
    await vehicle.save();
    res.status(201).send(vehicle);
  } catch (err) {
    res.status(500).send(err);
  } finally {
    await dbClient.disconnect();
  }
}

async function updateVehicle(req, res) {
  try {
    await dbClient.connect();
    const vehicle = await Vehicle.findByIdAndUpdate(req.params.id, req.body, {
      new: true,  // Retourner le document mis à jour
    });
    res.send(vehicle);
  } catch (err) {
    res.status(500).send(err);
  } finally {
    await dbClient.disconnect();
  }
}

async function deleteVehicle(req, res) {
  try {
    await dbClient.connect();
    await Vehicle.findByIdAndDelete(req.params.id);
    res.status(204).send();
  } catch (err) {
    res.status(500).send(err);
  } finally {
    await dbClient.disconnect();
  }
}

async function getVehicle(req, res) {
  try {
    await dbClient.connect();
    const vehicle = await Vehicle.findById(req.params.id);
    if (!vehicle) {
      res.status(404).send("Vehicle not found");
    } else {
      res.send(vehicle);
    }
  } catch (err) {
    res.status(500).send(err);
  } finally {
    await dbClient.disconnect();
  }
}

async function getAllVehicles(req, res) {
  try {
    await dbClient.connect();
    const vehicles = await Vehicle.find();
    if (!vehicles) {
      res.status(404).send("Vehicles not found");
    } else {
      res.send(vehicles);
    }
  } catch (err) {
    res.status(500).send(err);
  } finally {
    await dbClient.disconnect();
  }
}

async function searchVehicleByRegistrationNumber(req, res) {
  try {
    await dbClient.connect();
    const vehicle = await Vehicle.findOne({
      registrationNumber: req.params.registrationNumber,
    });
    if (!vehicle) {
      res.status(404).send("Vehicle not found");
    } else {
      res.send(vehicle);
    }
  } catch (err) {
    res.status(500).send(err);
  } finally {
    await dbClient.disconnect();
  }
}

async function lookupVehiclesByMaxRentalPrice(req, res) {
  try {
    await dbClient.connect();
    const vehicles = await Vehicle.find({
      rentalPrice: { $lte: req.params.maxPrice },  // $lte = less than or equal
    });
    if (!vehicles) {
      res.status(404).send("Vehicles not found");
    } else {
      res.send(vehicles);
    }
  } catch (err) {
    res.status(500).send(err);
  } finally {
    await dbClient.disconnect();
  }
}

module.exports = {
  createVehicle,
  updateVehicle,
  deleteVehicle,
  getVehicle,
  getAllVehicles,
  searchVehicleByRegistrationNumber,
  lookupVehiclesByMaxRentalPrice,
  seed,
};

Important points of the Mongoose methods used:

Mongoose methodDescription
Vehicle.find()Return all documents
Vehicle.findById(id)Return a document by its _id
Vehicle.findOne({...})Returns the first matching document
Vehicle.findByIdAndUpdate(id, data, opts)Updates and returns the document
Vehicle.findByIdAndDelete(id)Delete a document by its _id
Vehicle.deleteMany()Delete all documents
Vehicle.insertMany([...])Inserts multiple documents
new Vehicle(data).save()Create and save a new document

Pattern try/catch/finally:

All functions use finally to ensure that the MongoDB connection is closed, even in the event of an error:

try {
    await dbClient.connect();
    // ... opérations
} catch (err) {
    res.status(500).send(err);
} finally {
    await dbClient.disconnect(); // Toujours exécuté
}

package.json with Mongoose

{
  "name": "vehicleservice",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.20.2",
    "express": "^4.18.2",
    "mongoose": "^7.6.3",
    "swagger-jsdoc": "^6.2.8",
    "swagger-ui-express": "^5.0.0"
  }
}

6.5 Deploy and test the Vehicle Service

In this demo, the instructor deploys the completed Vehicle Service to Azure and tests it to make sure it works.

Preparing for deployment

For production deployment, we can comment out the seedData code to have an empty database initially:

async function seed() {
  await dbClient.connect();
  // await Vehicle.deleteMany();
  // await Vehicle.insertMany(seedData);
  // console.log("Test data inserted successfully");
  await dbClient.disconnect();
}

Automatic deployment

Changes are pushed to GitHub, which automatically triggers:

  1. Build the Docker image
  2. Push to Container Registry
  3. Deploying to Azure

Functional tests

1. Check an empty database

GET /vehicles → [] (tableau vide car la base est vide)

2. Create a vehicle (POST /vehicle)

{
  "registrationNumber": "XYZ789",
  "make": "Tesla",
  "model": "Model 3",
  "year": 2023,
  "rentalPrice": 200
}

Response: 201 Created with the returned document, including the MongoDB _id.

3. Update a vehicle (PUT /vehicle/:id)

{
  "rentalPrice": 150
}

Response: 200 OK with the updated vehicle.

4. Search by maximum price (GET /vehicles/price/100)

→ [] (aucun véhicule sous 100)
GET /vehicles/price/150
→ [{"registrationNumber": "XYZ789", ...}] (notre véhicule à 150)

5. Delete a vehicle (DELETE /vehicle/:id)

→ 204 No Content

6. Verify deletion (GET /vehicle/:id)

→ 404 Vehicle not found

7. Course Summary

What we covered

  1. Scenario Exploration: Kiran and Globomantics as real-world context throughout the course, making learning more concrete and easier to understand.

  2. Node.js and development environment: How to properly configure the development environment to ensure a good start with Node Version Manager, Visual Studio Code and recommended extensions.

  3. Fundamental concepts of Node.js: Asynchronous JavaScript (callbacks, promises, async/await), error handling with try/catch/finally, and event-driven programming with the Event Emitter.

  4. RESTful API design: How to analyze business requirements to produce an API design, defining the data model, endpoints and HTTP return codes.

  5. Building with Express.js: How to use Express.js to build a microservice, leveraging its routing system, middleware, body-parser and Swagger for documentation.

  6. Containerization with Docker: Creation of a Dockerfile and a docker-compose.yml to package and deploy the service.

  7. MongoDB Integration: How to integrate MongoDB with Mongoose to persist data, replace in-memory storage and use Mongoose CRUD operations.

Final architecture of the Vehicle Service

Client (Web/Mobile)
       ↓ HTTP Requests
   Express.js (port 3000)
       ↓ Routing
  VehicleService Layer
       ↓ Mongoose ODM
   MongoDB (port 27017)

Essential Docker Commands

# Construire une image
docker build -f Dockerfile -t vehicleservice .

# Lancer avec docker-compose
docker-compose up -d

# Voir les logs
docker-compose logs -f

# Arrêter les containers
docker-compose down

Additional Resources


Search Terms

node.js · microservices · fundamentals · apis · backend · full-stack · web · service · vehicle · api · design · mongodb · deployment · express.js · install · test · update · architecture · data · docker · microservice · model · mongoose · options

Interested in this course?

Contact us to book it or get a custom training plan for your team.