Skip to content

Database

Romain Guillemot edited this page Apr 16, 2025 · 5 revisions

StartER intègre une base de données MySQL pour le stockage persistant des données. Cette page explique comment configurer, utiliser et étendre la base de données dans votre application.

Configuration

Variables d'environnement

StartER utilise des variables d'environnement pour la configuration de la base de données. Vous pouvez définir ces variables dans le fichier .env à la racine de votre projet.

# Database Configuration
MYSQL_ROOT_PASSWORD=YOUR_MYSQL_ROOT_PASSWORD
MYSQL_DATABASE=YOUR_MYSQL_DATABASE_NAME

Ces variables sont utilisées par Docker pour configurer le conteneur MySQL et par l'application pour établir la connexion.

Client de base de données

Le fichier src/database/client.ts configure la connexion à la base de données :

// Get variables from .env file for database connection
const { MYSQL_ROOT_PASSWORD, MYSQL_DATABASE } = process.env;

// Create a connection pool to the database
import mysql from "mysql2/promise";

/* ************************************************************************ */

const client = mysql.createPool(
  `mysql://root:${MYSQL_ROOT_PASSWORD}@database:3306/${MYSQL_DATABASE}`,
);

/* ************************************************************************ */

// Ready to export
export default client;

// Types export
import type { Pool, ResultSetHeader, RowDataPacket } from "mysql2/promise";

type DatabaseClient = Pool;
type Result = ResultSetHeader;
type Rows = RowDataPacket[];

export type { DatabaseClient, Result, Rows };

Le client utilise mysql2 avec prise en charge des promesses pour une intégration fluide avec les fonctions asynchrones de JavaScript.

StartER vérifie au démarrage la connexion à la base de données via le fichier src/database/checkConnection.ts :

import client from "./client";

// Try to get a connection to the database
client
  .getConnection()
  .then((connection) => {
    console.info(`Using database ${process.env.MYSQL_DATABASE}`);
    connection.release();
  })
  .catch((error: Error) => {
    console.warn(
      "Warning:",
      "Failed to establish a database connection.",
      "Please check your database credentials in the .env file if you need a database access.",
    );
    console.warn(error.message);
  });

Importée dans server.ts, cette vérification permet de détecter rapidement les problèmes de configuration.

import fs from "node:fs";
import express, { type ErrorRequestHandler, type Express } from "express";
import { rateLimit } from "express-rate-limit";
import { createServer as createViteServer } from "vite";

/* ************************************************************************ */

import "./src/database/checkConnection";

/* ************************************************************************ */

const port = 5173;

createServer().then((server) => {
  server.listen(port, () => {
    console.info(`Listening on http://localhost:${port}`);
  });
});

// ...

Schéma de base de données

Le schéma de la base de données est défini dans le fichier src/database/schema.sql.

Dans le code de base proposé, le schéma déclare des tables user et item avec des premières lignes insérées :

create table user (
  id int unsigned primary key auto_increment not null,
  email varchar(255) not null unique,
  password varchar(255) not null,
  created_at datetime default current_timestamp,
  updated_at datetime default current_timestamp on update current_timestamp,
  deleted_at datetime default null
);

create table item (
  id int unsigned primary key auto_increment not null,
  title varchar(255) not null,
  created_at datetime default current_timestamp,
  updated_at datetime default current_timestamp on update current_timestamp,
  deleted_at datetime default null,
  user_id int unsigned not null,
  foreign key(user_id) references user(id) on delete cascade
);

insert into user(id, email, password)
values
  (1, "jdoe@mail.com", "$argon2id$v=19$m=19456,t=2,p=1$M6cNKyAnMbdydp1xs6voqA$BNdO1lV91bQBqzOpvkROZJKbSHqEW5PzFAp5C/bgvwY");

insert into item(id, title, user_id)
values
  (1, "Stuff", 1),
  (2, "Doodads", 1);

Ce fichier est exécuté automatiquement lors du premier démarrage du conteneur MySQL. Passé le premier démarrage, vous pouvez recharger le schéma avec la commande suivante :

docker compose run --build --rm server npm run database:sync

Attention : le script database:sync supprime la base de données existante pour en créer une nouvelle à jour par rapport au contenu du fichier src/database/schema.sql.

Une autre solution est d'importer le schéma depuis l'interface du service Adminer fourni dans StartER.

Adminer

StartER inclut Adminer, une interface web légère pour gérer votre base de données. Pour y accéder :

  1. Assurez-vous que votre application est en cours d'exécution (docker compose up)
  2. Ouvrez votre navigateur à l'adresse http://localhost:8080
  3. Connectez-vous avec les identifiants suivants :
    • Système : MySQL
    • Serveur : database
    • Utilisateur : root
    • Mot de passe : (valeur de MYSQL_ROOT_PASSWORD dans votre fichier .env)
    • Base de données : (valeur de MYSQL_DATABASE dans votre fichier .env)

Pour importer votre schéma de base de données, cliquez sur "Importer" (ou tentez ce lien) et importez le fichier src/database/schema.sql.

Bonnes pratiques

Nous vous recommandons d'inclure les champs suivants dans chaque table (quand le bon sens vous dicte que c'est pertinent) :

  • id : Identifiant unique et auto-incrémenté
  • created_at : Date et heure de création
  • updated_at : Date et heure de la dernière mise à jour
  • deleted_at : Date et heure de suppression (pour la suppression douce)

Utilisez des clés étrangères avec contraintes d'intégrité référentielle pour maintenir la cohérence des données :

foreign key(user_id) references user(id) on delete cascade

Repository pattern

Pour accéder à la base de données depuis votre application, nous vous recommandons d'utiliser le pattern Repository comme démontré dans src/express/modules/item/itemRepository.ts. Ce pattern encapsule la logique d'accès aux données et fournit une interface claire pour les opérations CRUD.

import databaseClient, {
  type Result,
  type Rows,
} from "../../../database/client";

class ItemRepository {
  // The C of CRUD - Create operation
  async create(item: Omit<Item, "id">) {
    const [result] = await databaseClient.query<Result>(
      "insert into item (title, user_id) values (?, ?)",
      [item.title, item.user_id],
    );

    return result.insertId;
  }

  // The Rs of CRUD - Read operations
  async read(id: number) {
    const [rows] = await databaseClient.query<Rows>(
      "select * from item where id = ? and deleted_at is null",
      [id],
    );

    return rows[0] as Item | null;
  }

  // ...
}

export default new ItemRepository();

Un mot sur TypeScript : la méthode databaseClient.query est générique pour toutes les requêtes SQL. Pour permettre à TypeScript d'inférer le type du retour, vous devez préciser si votre requête produit des lignes (requête avec select : utilisez databaseClient.query<Rows>) ou un résultat d'opération (requête avec insert, update ou delete : utilisez databaseClient.query<Result>).

Voir le code complet pour les détails :