Skip to content

3. Chapter 2 - Installing the [NestJS] Server

The [Kotlin] client described in this document requires a server to query: the [NestJS] server for the [RdvMedecins] application, included with this document in the [rdvmedecins-nestjs-server] folder. Since this document focuses on [Kotlin], that server is treated as a “black box”: this chapter explains how to install it, configure it, create its [MySQL] database, launch it, and test it, but does not examine its code. Readers who wish to learn about [NestJS] can refer to the document “Introduction to the TypeScript Language and the NestJS Framework” (2026).

This chapter is an exact replica of Chapter 2 from the course “An Example of a Client/Server Application—Flutter / NestJS (2026)”: the server, its database, and its tests are exactly the same. Readers who have already installed this server for the Flutter course can skip directly to the last section of this chapter, “Launch the Kotlin Client for the First Time.”

3.1. Installing Laragon

To create the [MySQL] database used by the [NestJS] server, we’ll use the Laragon tool. As of September 2026, Laragon is available at URL [Download Laragon – Fast & Modern Dev Environment ]. Laragon includes tools that allow you to install a PHP server and the DBMS [MySQL] server:

Image

  • in [1], an Apache web server;
  • into [2], the DBMS [MySQL];
  • to [3], a messaging tool that we will not be using;
  • [4] starts all services (Apache, [MySQL], MailPit);
  • [5] displays the web page for URL [http://localhost]. By default, this URL refers to the <laragon-install>\www folder, where <laragon-install> is the Laragon installation folder;
  • in [6], launches the HeidiSQL tool, which allows you to manage MySQL databases. We will use this tool to create the database used by the [RdvMedecins] server;
  • In [7], open a terminal where all the binaries needed for Laragon are located in PATH;
  • In [8], open Windows File Explorer to the folder associated with URL and [http://localhost];

The Laragon installation folder contains the following items:

 
  • In [1], the [bin] folder contains numerous applications:
  • In [1], the Apache web server;
  • in [2], a command-line terminal;
  • [3], a client ([git]) that allows you to manage different versions of a project;
  • [4], a database manager;
  • [5], a mail manager;
  • [6], the DBMS [MySQL];
  • [7], another web server;
  • in [8], [node.js] to run JavaScript scripts;
  • [9], a text editor;
  • [10], the PHP interpreter;
  • [11], the [Python] interpreter;
  • [12], a cache server;

Laragon comes with a comprehensive development environment. In this course, we will use only the DBMS [MySQL] [6], and the [heidisql] tool to manage it graphically;

3.2. Installation and Configuration of the [NestJS] Server

The [rdvmedecins-nestjs-server] folder has the following directory structure:

rdvmedecins-nestjs-server/
├── package.json                  # dependencies and npm scripts
├── tsconfig.json                 # TypeScript compiler configuration
├── nest-cli.json                 # NestJS CLI configuration
├── .env.example                  # template for the .env configuration file
├── database/
│   └── dbrdvmedecins.sql         # database creation script
└── src/
    ├── main.ts                   # server entry point
    ├── app.module.ts             # root module
    ├── config/                   # database connection
    ├── entities/                 # TypeORM entities (one class per table)
    ├── domain/                   # business objects
    ├── repositories/             # data access (DAO layer)
    ├── business/                 # business layer
    ├── web/                      # JSON controller and response models
    └── auth/                     # JWT authentication and roles

Each subfolder of [src/] contains its own [README.md] file, which summarizes the purpose of the files it contains. The source code for these files is not covered in this document.

In the [rdvmedecins-nestjs-server] folder, type the following commands in a terminal:

npm install
cp .env.example .env

The [.env] file sets environment variables:

# -----------------------------------------------------------------------
# Sample configuration file.
# Copy this file to [.env] then adapt the values to your machine.
# The [.env] file must never be shared or committed (passwords).
# -----------------------------------------------------------------------

# listening port of the NestJS server (the original Spring server listened on 8080)
PORT=8080

# connection parameters for the MySQL database [dbrdvmedecins]
DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=
DB_DATABASE=dbrdvmedecins

# in development only: TypeORM can automatically create/update
# the tables from the entities. It is disabled here because we provide our
# own SQL script [database/dbrdvmedecins.sql], as the original Spring project did.
DB_SYNCHRONIZE=false

# -----------------------------------------------------------------------
# authentication (see src/auth/)
# -----------------------------------------------------------------------
# secret key used to sign/verify JWT tokens - MUST be changed
# in production (a long random string, never committed).
JWT_SECRET=change-me-to-a-long-random-string
# validity of a JWT token (see the documentation of the [ms] library:
# "60", "2h", "7d"...)
JWT_EXPIRES_IN=2h

Customize the newly created [.env] file to match your [MySQL] installation (username, password, port). Since authentication was added, this file also contains a key named JWT_SECRET: the example value is suitable for following this document, but must be replaced with a random string unique to you before any actual use.

3.3. Creating the Database

The [dbrdvmedecins] database mirrors the structure of the original document (2014, Spring 4 / [MySQL] 5): same tables—[doctors], [clients], [slots], [appointments]—same foreign keys, same uniqueness constraint. The only new addition, introduced by the “authentication” step of this port: the table [users]. The relationships between these tables are as follows: a doctor ([doctors]) has several time slots ([slots], foreign key [doctor_id]); an appointment ([appointments]) links a day, a time slot (and thus a doctor), and a client ([clients]); the table [users] is independent of the other four: it is used solely for authentication.

3.3.1. [database/dbrdvmedecins.sql]

Image

-- =========================================================================
-- Database [dbrdvmedecins] - MySQL 8
--
-- This script reproduces EXACTLY the relational schema of the original document
-- (2014, Spring 4 / Spring Data / MySQL 5): same tables, same columns,
-- same foreign keys, same uniqueness constraint. Only the database engine
-- changes its name (MySQL 5 -> MySQL 8), not the structure.
--
-- The [users] table now appears ("authentication" step of this
-- port). The original document used THREE tables (users, roles,
-- users_roles) to allow a user to have SEVERAL roles.
-- Since this application only knows two mutually exclusive roles
-- (ADMIN, USER), we simplify here to a single [role] column
-- on the [users] table rather than rebuilding the original many-to-
-- many relation - a deliberate teaching simplification, documented
-- in the "Authentication" chapter of the course.
-- =========================================================================


CREATE DATABASE IF NOT EXISTS dbrdvmedecins CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE dbrdvmedecins;

-- we start again from an empty database on each run of the script (handy in development)
DROP TABLE IF EXISTS appointments;
DROP TABLE IF EXISTS slots;
DROP TABLE IF EXISTS clients;
DROP TABLE IF EXISTS doctors;
DROP TABLE IF EXISTS users;

-- -------------------------------------------------------------------------
-- table [doctors]: the doctors of the practice
-- -------------------------------------------------------------------------
CREATE TABLE doctors (
  id         INT AUTO_INCREMENT PRIMARY KEY,   -- primary key (technical identifier)
  version    INT NOT NULL DEFAULT 0,            -- row version number (optimistic lock)
  title      VARCHAR(5)  NOT NULL,              -- Melle, Mme, Mr
  last_name  VARCHAR(20) NOT NULL,
  first_name VARCHAR(20) NOT NULL
);

-- -------------------------------------------------------------------------
-- table [clients]: the doctors' patients
-- -------------------------------------------------------------------------
CREATE TABLE clients (
  id         INT AUTO_INCREMENT PRIMARY KEY,
  version    INT NOT NULL DEFAULT 0,
  title      VARCHAR(5)  NOT NULL,
  last_name  VARCHAR(20) NOT NULL,
  first_name VARCHAR(20) NOT NULL
);

-- -------------------------------------------------------------------------
-- table [slots]: the time slots of each doctor
-- -------------------------------------------------------------------------
CREATE TABLE slots (
  id           INT AUTO_INCREMENT PRIMARY KEY,
  version      INT NOT NULL DEFAULT 0,
  doctor_id    INT NOT NULL,                     -- foreign key -> doctors(id)
  start_hour   INT NOT NULL,                     -- start hour of the slot
  start_minute INT NOT NULL,                     -- start minute of the slot
  end_hour     INT NOT NULL,                     -- end hour of the slot
  end_minute   INT NOT NULL,                     -- end minute of the slot
  CONSTRAINT fk_slots_doctor FOREIGN KEY (doctor_id) REFERENCES doctors(id)
);

-- -------------------------------------------------------------------------
-- table [appointments]: the appointments booked by the clients
-- -------------------------------------------------------------------------
CREATE TABLE appointments (
  id         INT AUTO_INCREMENT PRIMARY KEY,
  version    INT NOT NULL DEFAULT 0,
  day        DATE NOT NULL,                      -- day of the appointment
  slot_id    INT NOT NULL,                       -- foreign key -> slots(id)
  client_id  INT NOT NULL,                       -- foreign key -> clients(id)
  CONSTRAINT fk_appointments_slot   FOREIGN KEY (slot_id)   REFERENCES slots(id),
  CONSTRAINT fk_appointments_client FOREIGN KEY (client_id) REFERENCES clients(id),
  -- the same slot, on the same day, can be booked only once
  CONSTRAINT unq1_appointments UNIQUE (day, slot_id)
);

-- -------------------------------------------------------------------------
-- table [users]: the users allowed to log in to the application
-- -------------------------------------------------------------------------
CREATE TABLE users (
  id       INT AUTO_INCREMENT PRIMARY KEY,
  version  INT NOT NULL DEFAULT 0,
  login    VARCHAR(30) NOT NULL UNIQUE,          -- login identifier
  password VARCHAR(100) NOT NULL,                -- HASHED password (bcrypt) - never in clear
  name     VARCHAR(40) NOT NULL,                 -- name displayed once logged in
  role     VARCHAR(10) NOT NULL                  -- 'ADMIN' or 'USER'
);

-- =========================================================================
-- Demo data
-- =========================================================================

INSERT INTO doctors (title, last_name, first_name) VALUES
  ('Mme', 'PELISSIER', 'Marie'),
  ('Mr',  'TATOU',      'Paul'),
  ('Melle','DUPONT',    'Sophie');

INSERT INTO clients (title, last_name, first_name) VALUES
  ('Mr',   'MARTIN',   'Jean'),
  ('Mme',  'DURAND',   'Alice'),
  ('Melle','BISTROU',  'Brigitte'),
  ('Mr',   'GERMAN',   'Luc');

-- slots of doctor PELISSIER (doctor_id = 1): from 8:00 to 12:00 in 20-minute steps
INSERT INTO slots (doctor_id, start_hour, start_minute, end_hour, end_minute) VALUES
  (1, 8, 0, 8, 20), (1, 8, 20, 8, 40), (1, 8, 40, 9, 0),
  (1, 9, 0, 9, 20), (1, 9, 20, 9, 40), (1, 9, 40, 10, 0),
  (1, 10, 0, 10, 20), (1, 10, 20, 10, 40), (1, 10, 40, 11, 0),
  (1, 11, 0, 11, 20), (1, 11, 20, 11, 40), (1, 11, 40, 12, 0);

-- slots of doctor TATOU (doctor_id = 2): from 14:00 to 18:00 in 20-minute steps
INSERT INTO slots (doctor_id, start_hour, start_minute, end_hour, end_minute) VALUES
  (2, 14, 0, 14, 20), (2, 14, 20, 14, 40), (2, 14, 40, 15, 0),
  (2, 15, 0, 15, 20), (2, 15, 20, 15, 40), (2, 15, 40, 16, 0),
  (2, 16, 0, 16, 20), (2, 16, 20, 16, 40), (2, 16, 40, 17, 0),
  (2, 17, 0, 17, 20), (2, 17, 20, 17, 40), (2, 17, 40, 18, 0);

-- an appointment already booked, to see an occupied slot in the schedule right away
INSERT INTO appointments (day, slot_id, client_id) VALUES
  (CURDATE(), 2, 3);

-- two demo users: admin/admin (ADMIN role, all rights)
-- and user/user (USER role, read-only) - the passwords are hashed
-- with bcrypt (10 salt rounds), never stored in clear.
INSERT INTO users (login, password, name, role) VALUES
  ('admin', '$2b$10$YSNBJXXHX9mp13nPZb6YGu8M1jR6uvPwO1s2X73z0QAqBR7TOQjoq', 'Administrator', 'ADMIN'),
  ('user',  '$2b$10$6Cvoion7AgJkpYHw5N0jMuWPf8T90J28En56kWBL9PT0bXt7lwW7.', 'User',          'USER');

Let’s break down this code:

  • Lines 20–21: [CREATE DATABASE IF NOT EXISTS dbrdvmedecins …] followed by [USE dbrdvmedecins]—creates the database if it does not yet exist (character set [utf8mb4], which supports all Unicode characters, including accents), then selects it: all subsequent instructions apply to it. These two lines must absolutely come FIRST: if executed after [DROP TABLE …], they would fail because no database is selected (error [No database selected]);
  • Lines 24–28: [DROP TABLE IF EXISTS …] — the script starts from an empty database on each execution, in the reverse order of dependencies (starting with appointments, which references slots and clients, down to users, which is not referenced by anyone): [MySQL] would refuse to drop a table that is still referenced by a foreign key;
  • Line 58: [doctor_id INT NOT NULL], then line 63: [CONSTRAINT fk_slots_doctor FOREIGN KEY …] — a time slot always belongs to a doctor;
  • line 78: [CONSTRAINT unq1_appointments UNIQUE (day, slot_id)] — the application’s central business rule: a single time slot on a given day can be booked only once. It is this constraint, and this constraint alone, that prevents a double booking—neither the validator nor the business logic layer explicitly rechecks it;
  • lines 84–91: [CREATE TABLE users (…)] — the table added by this step. login is UNIQUE (two users cannot share the same login ID); password stores a password hashed using bcrypt (never in plaintext); role is either 'ADMIN' or 'USER'—a single text column, whereas the original document used three tables (users, roles, users_roles) to allow multiple roles per user. Since this application requires only two mutually exclusive roles, the original many-to-many relationship would have been more than necessary;
  • lines 129–131: [INSERT INTO users …] — two demo accounts, admin/admin (role ADMIN) and user/user (role USER), with their passwords already hashed using bcrypt (10 rounds of salting)—never in plaintext, including in this demo script.

3.3.2. Running the script with HeidiSQL

To create the [MySQL] database required by the [NestJS] server, we proceed as follows:

 
  • In [1], all services must be started;
  • in [2], the [Database] option allows you to manage the database;

The option [Database] launches a utility called HeidiSQL:

  • In [1], you can select various DBMS options. The default value provided is the MySQL DBMS. This is acceptable;
  • in [2], you log in to HeidiSQL;
  • In [1-2], run the SQL file, then select the [dbrdvmedecins.sql] file, which you’ll find in the [rdvmedecins-nestjs-server/database] folder:

Image

Running the [3] file will create the [dbrdvmedecins] database used by the [NestJS] server. To view it, run F5 within HeidiSQL to refresh the display:

Image

The database [dbrdvmedecins] has been created. You can view its contents:

Image

  • In [1-3], the table [clients];
    • [id] is the primary key of the table;
    • [version] is the row’s version number. This number increments each time the row is modified;
    • [title, last_name, first_name] identifies the person;

Image

  • in [1-3], the table [doctors];
    • [id] is the primary key of the table;
    • [version] is the version number of the row;
    • [title, last_name, first_name] identifies the person;

Image

  • In [1-3], the table of doctors’ appointment time slots:
    • [id]: primary key;
    • [version]: row version number;
    • [doctor_id]: foreign key referencing the doctor’s primary key;
    • [start_hour, start_minute]: hour and minutes of the appointment start time. Thus, [8,20] means 8:20 a.m.
    • [end_hour, end_minute]: hour and minutes of the end of the consultation.

Image

  • In [1-3], the [appointments] table of scheduled appointments;
    • [id]: primary key;
    • [version]: version number;
    • [day]: appointment date;
    • [slot_id]: foreign key referencing the primary key of the appointment time slot. Since a time slot is linked to a doctor, we know which doctor is handling this appointment;
    • [client_id]: foreign key to the number (primary key) of the customer making the inquiry;

Image

  • in [1-3], the table [users], which lists the users authorized to use the application [NestJS];
    • [id, version]: primary key, version number;
    • [login, password]: user credentials;
    • [name]: user name;
    • [role]: the user’s role. [ADMIN] has full access. [USER] has read-only access. This user can view a doctor’s schedule but cannot schedule or cancel an appointment;

3.4. Starting the [NestJS] server

Start all Laragon services ([Start All]), if you haven’t already. Then start the NestJS server in a VSCode terminal for the NestJS project:

npm install
npm run start:dev

The console displays, in order: the startup of the [NestJS] module, the list of routes exposed by the controller, and then confirmation of startup:

[Nest] RoutesResolver RdvMedecinsController {/}:
[Nest] RouterExplorer Mapped {/getAllDoctors, GET} route
[Nest] RouterExplorer Mapped {/getAllClients, GET} route
[Nest] RouterExplorer Mapped {/getAllSlots/:doctorId, GET} route
[Nest] RouterExplorer Mapped {/getDoctorAppointmentsOfDay/:doctorId/:day, GET} route
[Nest] RouterExplorer Mapped {/getDoctorScheduleOfDay/:doctorId/:day, GET} route
[Nest] RouterExplorer Mapped {/getDoctorById/:id, GET} route
[Nest] RouterExplorer Mapped {/getClientById/:id, GET} route
[Nest] RouterExplorer Mapped {/getSlotById/:id, GET} route
[Nest] RouterExplorer Mapped {/getAppointmentById/:id, GET} route
[Nest] RouterExplorer Mapped {/addAppointment, POST} route
[Nest] RouterExplorer Mapped {/deleteAppointment, POST} route
[Nest] RoutesResolver AuthController {/}:
[Nest] RouterExplorer Mapped {/login, POST} route
[Nest] NestApplication Nest application successfully started
[RdvMedecins] NestJS server started on http://localhost:8080

This console output confirms that the server is indeed listening on port 8080—exactly the same port as the Spring Boot server in the original document, which was deliberately chosen for this reason.

These routes constitute the API and JSON that the client [Kotlin] will consume in Chapter 4. [POST /login] is the only public route: it returns a token (JWT) that all other routes require in the header ([Authorization: Bearer <token>]). The read-only routes ([GET …]) are accessible to the roles ADMIN and USER; the two routes that modify data, [POST /addAppointment] and [POST /deleteAppointment], are reserved for the ADMIN role. Business responses are wrapped in a [{ status, data }] object, where [status] is set to 0 if the operation is successful.

3.5. Test the server with a browser

Since authentication was added, all routes under [RdvMedecinsController] require a valid JWT token; therefore, a browser alone can no longer be used to test them directly. For example:

http://localhost:8080/getAllDoctors

now returns a 401 Unauthorized error without a token:

Image

This is the expected behavior: this response confirms that the server is properly protecting its routes. Postman (next section) is required to obtain a token and then use it.

3.6. Testing the [NestJS] server with Postman

The Postman tool is available at URL [https://www.postman.com/downloads/]. You may need to create an account.

Start all Laragon services, then open the Postman desktop application:

  • In [1-2], create a request HTTP to query the doctors’ appointment management server;

3.6.1. Log in and obtain a token

  1. Create a new Postman request with the method POST, URL http://localhost:8080/login;
  1. In the [Body] tab, select "raw," then the JSON format, and enter: {"login": "admin", "password": "admin"} (or "user/user" to test the USER role);
  2. Submit the request.

Image

  • in [1-2], the request POST to the server [NestJS] running on port 8080;
  • to [3-5], the body of the request. This transmits to the server the string JSON containing the credentials of the user attempting to log in, in this case admin/admin;
  • in [6], the server’s response;
  • in [7], the authentication token that subsequent requests must send to the server to indicate “I have already authenticated”;

Copy the value from accessToken: this is the token that must now be included in every subsequent request. In the example above, the token to copy is “eyJ...zoM” (without the quotes).

3.6.2. Call a protected route using the token

For GET /getAllDoctors (or any other route of RdvMedecinsController), in Postman, on the Authorization tab, select the Bearer Token type and paste the token copied above (Postman will then automatically add the Authorization: Bearer <token> header).

Image

  • in [1-2], the request sent to the server. It requests the list of doctors in the database;
  • In [3-5], the request must send the authentication token. In [4], select [Bearer Token], and in [5], paste the authentication token you copied earlier;
  • in [7], the server’s response;

3.6.3. Test the routes POST (/addAppointment, /deleteAppointment)

Follow the same procedure as above (method POST, body JSON, Bearer Token), exactly as the original document used the Chrome extension “Advanced Rest [Client]” for the same routes.

Note: You can find the following Postman requests in the [rdvmedecins-nestJS.postman_collection.json] collection:

Image

In Postman, press Ctrl-0 to load the collection:

Image

  • Click on [1] to load the collection and select the collection file. Once the collection is loaded, it displays its requests:

Image

To test /addAppointment with the token from an account ADMIN:

  1. Postman request, method POST, URL http://localhost:8080/addAppointment;
  2. Authorization tab: Bearer Token, admin token;
  3. [Body] tab, raw, JSON: {"day": "2026-09-13", "clientId": 1, "slotId": 5};
  4. Submit the request.

Image

  • as [3-5], the request body;

Image

You can verify with HeidiSQL that an appointment has been added to the database:

Image

To test /deleteAppointment, follow the same procedure with the body {"appointmentId": 2} (the ID of the appointment to be canceled (8) above).

Image

  • in [4], where the number above is the appointment to be deleted;

Image

  • to [5-7], the authentication token;
  • to [9], the server’s response. [status=0] indicates that the operation was successful. You can verify this with HeidiSQL:

Image

  • in [3], appointment #2 no longer exists;

Try this: repeat the same request /addAppointment using the token for the USER account (user/user) instead of the admin token: the response becomes 403 Forbidden, with the message “role [USER] does not allow this action (required role: ADMIN)”—a concrete demonstration of the role-based access control implemented by the server.

3.7. Launch the Kotlin client for the first time

With the [NestJS] server running, open the [rdvmedecins-kotlin-client] project in Android Studio (see Chapter 1) and start the emulator. In the Android Studio toolbar, the device drop-down list should display the emulator (for example, [Pixel 8 API 37]); the [Run] button (the green triangle) next to it compiles the app, installs it on the emulator, and launches it.

Without Android Studio, the same steps can be performed from the command line: the Gradle task [installDebug] compiles and installs the app, and then [adb] launches it:

1
2
3
$env:JAVA_HOME = "C:\Program Files\Android\Android Studio\jbr"
.\gradlew.bat installDebug
adb shell am start -n fr.rdvmedecins.client/.MainActivity
  • [installDebug]: builds the app (like [assembleDebug]; see Chapter 1), then installs it on the connected device—emulator or phone;
  • [adb shell am start -n <application>/<activity>]: instructs the Android Activity Manager ([am], Activity Manager) to launch the [MainActivity] activity of the [fr.rdvmedecins.client] app—which is what the system does when you tap the app icon.

Image

The login screen appears first, just as it did for previous clients in this course: two demo accounts are provided with the server database, [admin]/[admin] (role [ADMIN], full access) and [user]/[user] (role [USER], read-only). The “FR”/“EN” buttons in the upper-right corner switch the interface to French, even before logging in.

The “Server URL” field contains [http://10.0.2.2:8080] by default: in the emulator, the special address [10.0.2.2] refers to the computer running the emulator, i.e., the server [NestJS]. If the connection fails after about ten seconds (message: “Incorrect login or password. ”), it is most often because this address is not responding: this happened on the computer used to write this document. The solution is described in Chapter 5: the command [adb reverse tcp:8080 tcp:8080], followed by URL and [http://localhost:8080].