Database en US - rocambille/start-express-react GitHub Wiki

StartER relies on a MySQL database to provide persistent storage of application data. This page details its configuration, usage, and best practices for extending it to suit your needs.

Configuration

Environment variables

StartER uses environment variables for database configuration. These settings are defined in the .env file at the root of the project.

# Database Configuration
MYSQL_ROOT_PASSWORD=YOUR_MYSQL_ROOT_PASSWORD
MYSQL_DATABASE=YOUR_MYSQL_DATABASE_NAME

These variables are used by Docker to configure the MySQL container and by the application to establish the connection.

Database client

The src/database/client.ts file configures the database connection:

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

The client is based on the mysql2 package, whose promise-based API integrates naturally with async/await.

StarTER checks the database connection via the src/database/checkConnection.ts file at startup:

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);
  });

This check, imported into server.ts, allows any configuration error to be detected upon startup.

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 = +(process.env.APP_PORT ?? 5173);

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

// ...

Build the database

Schema

The database schema is defined in the src/database/schema.sql file.

Here is an example of a minimal schema provided with StartER:

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

This schema defines two tables (user and item) with their relationships.

The file is automatically executed when the MySQL container is first started. After the first startup, you can reload the schema with the following command:

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

This command is useful during development to easily reset your database to a clean state.

Warning: The database:sync script deletes the existing database and creates a new one.

Another solution is to import the schema from the Adminer interface provided as a service in StartER.

Seeder

The database content can be defined in the file src/database/seeder.sql.

Here is an example of a minimal seeder provided with StartER:

insert into user(id, email, password)
values
  (1, "[email protected]", "$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);

This seeder inserts some example records into the tables user and item.

You can use the seeder when synchronizing with the schema by adding the --use-seeder option to the command:

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

Warning: The database:sync script deletes the existing database and creates a new one.

As with the schema, another solution is to import the seeder from the Adminer interface provided as a service in StartER.

Adminer

StartER includes Adminer, a lightweight web interface for managing your database. To access it:

  1. Make sure your application is running (docker compose up)
  2. Open your browser at http://localhost:8080
  3. Log in with the following credentials:
    • System: MySQL
    • Server: database
    • User: root
    • Password: (value of MYSQL_ROOT_PASSWORD in your .env file)
    • Database: (value of MYSQL_DATABASE in your .env file)

To import the schema or the seeder, click "Import", or use this direct link), and import the file ( src/database/schema.sql or src/database/seeder.sql).

Best practices

We recommend including the following fields in each table, where relevant:

  • id: Unique, auto-incrementing identifier
  • created_at: Creation date and time
  • updated_at: Last updated date and time
  • deleted_at: Deleted date and time (for soft delete)

These conventions ensure the consistency and traceability of your data throughout the application.

Use foreign keys with referential integrity constraints to maintain data consistency:

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

Repository pattern

To access the database, StartER adopts the Repository pattern. This pattern encapsulates SQL queries in dedicated classes and provides a clear interface for performing CRUD operations. This separates the SQL code from the rest of the application and facilitates maintenance, testing, and model evolution.

A sample implementation is provided in src/express/modules/item/itemRepository.ts.

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(byId: number): Promise<Item | null> {
    const [rows] = await databaseClient.query<Rows>(
      "select id, title, user_id from item where id = ? and deleted_at is null",
      [byId],
    );

    if (rows[0] == null) {
      return null;
    }

    const { id, title, user_id } = rows[0];

    return { id, title, user_id };
  }

  // ...
}

export default new ItemRepository();

This way, each Express module can have its own repository, ensuring a clear and extensible organization of the code.

A TypeScript word: The databaseClient.query method is generic for all SQL queries. To allow TypeScript to infer the return type, you must specify whether your query produces rows (select query: use databaseClient.query<Rows>) or an operation result (insert, update, or delete query: use databaseClient.query<Result>).

See the full code for details:


Your database is now ready and usable in Express. In the next page, we will see how to expose this data through structured and typed API routes.

⚠️ **GitHub.com Fallback** ⚠️