Créer ses premières entités TypeORM dans NestJS

Créer ses premières entités TypeORM dans NestJS

Dans une application NestJS connectée à une base SQL, la question revient vite : comment représenter mes données côté TypeScript tout en gardant un schéma SQL propre et maintenable ? Avec TypeORM, la réponse passe par les entités : des classes décorées qui décrivent le mapping entre votre code et vos tables.

Dans ce tutoriel, on va créer et structurer ses premières entités TypeORM dans NestJS, en suivant des conventions simples (snake_case en base, camelCase en TypeScript), en factorisant les champs communs dans une BaseEntity, et en mettant en place un auto-chargement des entités via le pattern *.entity.ts.

Prérequis : connaître les bases de NestJS (modules/providers), avoir une application NestJS prête à se connecter à une base SQL avec TypeORM.

1) Qu’est-ce qu’une entité TypeORM ? (et le mapping SQL)

Une entité est une classe TypeScript qui représente une table SQL. Chaque propriété de la classe est généralement mappée sur une colonne de la table. TypeORM utilise des décorateurs (annotations) pour décrire ce mapping.

On peut voir une entité comme un plan : elle dit à TypeORM comment lire/écrire des lignes SQL sous forme d’objets JavaScript/TypeScript. Concrètement :

  • une classe User ↔ une table users
  • une propriété email ↔ une colonne email
  • un id auto-généré ↔ une clé primaire

Ce mapping est essentiel pour :

  • garder un modèle de données cohérent dans le code,
  • centraliser les contraintes (unique, nullable, etc.),
  • faciliter les migrations et les évolutions du schéma.

2) Où placer les entités dans un projet NestJS ?

Pour rester organisé, on adopte une convention claire :

  • Entités : src/database/entities/
  • Nom de fichier : *.entity.ts (ex. user.entity.ts)

Cette convention permet ensuite d’auto-charger toutes les entités avec un simple glob.

Arborescence recommandée

src/
  database/
    entities/
      base.entity.ts
      user.entity.ts
      guidance-service.entity.ts
      oracle.entity.ts

3) Les décorateurs de base : @Entity, @Column, @PrimaryGeneratedColumn

TypeORM s’appuie sur trois décorateurs fondamentaux :

  • @Entity() : déclare la classe comme entité et définit (optionnellement) le nom de table
  • @PrimaryGeneratedColumn() : déclare une clé primaire auto-incrémentée (ou UUID selon config)
  • @Column() : déclare une colonne (type + options)

Exemple minimal :

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  email: string;
}

Ici :

  • la table est explicitement nommée users,
  • id est une clé primaire auto-générée,
  • email est une colonne texte (TypeORM infère souvent le type SQL à partir du type TS, mais on va voir comment le préciser).

4) Types de colonnes courants (string, number, boolean, date, enum, json)

TypeORM propose deux approches :

  • laisser TypeORM inférer le type SQL à partir du type TypeScript,
  • ou déclarer explicitement le type (recommandé pour les cas ambigus : dates, json, enums, etc.).

4.1 string

Pour une chaîne, on utilise généralement varchar (avec une longueur) ou text (sans longueur fixe).

@Column({ type: 'varchar', length: 255 })
email: string;

@Column({ type: 'text' })
bio: string;

4.2 number

Pour un nombre, le type SQL dépend de la base (PostgreSQL, MySQL…). Le plus courant : int, float, numeric.

@Column({ type: 'int' })
age: number;

4.3 boolean

@Column({ type: 'boolean', default: true })
isActive: boolean;

4.4 date / datetime

Pour les dates, on privilégie des types explicites :

  • date : date sans heure
  • timestamp / timestamptz (PostgreSQL) : date + heure
@Column({ type: 'date', nullable: true })
birthDate: Date | null;

@Column({ type: 'timestamp' })
lastLoginAt: Date;

4.5 enum

Un enum permet de limiter les valeurs possibles. TypeORM sait mapper un enum TypeScript vers un type SQL (selon la base).

export enum UserRole {
  USER = 'USER',
  ADMIN = 'ADMIN',
}

@Column({ type: 'enum', enum: UserRole, default: UserRole.USER })
role: UserRole;

4.6 json

Pour stocker des données semi-structurées (préférences, configuration), on peut utiliser json / jsonb (PostgreSQL).

type UserPreferences = {
  theme: 'light' | 'dark';
  newsletter: boolean;
};

@Column({ type: 'json', nullable: true })
preferences: UserPreferences | null;

Piège courant : le JSON n’est pas un substitut à un modèle relationnel. Utilisez-le pour des préférences ou des structures qui changent souvent, pas pour des relations métier.

5) Options de colonnes : nullable, unique, default, length

Les options de colonnes sont un point clé : elles documentent et imposent des règles au niveau de la base.

nullable

Autorise la valeur NULL. Côté TypeScript, reflétez-le avec | null pour éviter les surprises.

@Column({ type: 'varchar', length: 255, nullable: true })
displayName: string | null;

unique

Ajoute une contrainte d’unicité (souvent sur email, slug, etc.).

@Column({ type: 'varchar', length: 255, unique: true })
email: string;

Piège courant : une validation applicative ne remplace pas une contrainte SQL. Gardez le unique en base, sinon vous aurez des doublons en cas de concurrence.

default

Définit une valeur par défaut en base.

@Column({ type: 'boolean', default: false })
emailVerified: boolean;

length

Contraint la taille d’un varchar.

@Column({ type: 'varchar', length: 50 })
username: string;

6) Une BaseEntity réutilisable (id, createdAt, updatedAt)

Dans la plupart des projets, toutes les tables partagent des champs communs :

  • id : identifiant
  • createdAt : date de création
  • updatedAt : date de mise à jour

Pour éviter de répéter ces colonnes dans chaque entité, on crée une entité de base abstraite dans src/database/entities/base.entity.ts.

import {
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
} from 'typeorm';

export abstract class BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;

  // Remplie automatiquement à l'insertion
  @CreateDateColumn({ name: 'created_at', type: 'timestamp' })
  createdAt: Date;

  // Mise à jour automatiquement à chaque modification
  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
  updatedAt: Date;
}

Deux points importants :

  • On utilise CreateDateColumn et UpdateDateColumn : TypeORM gère la valeur automatiquement.
  • On force le nom SQL en snake_case via name: 'created_at' et name: 'updated_at', tout en gardant createdAt/updatedAt en TypeScript.

7) Convention de nommage : snake_case en BDD, camelCase en TypeScript

Une convention très répandue est :

  • camelCase dans le code (TypeScript) : createdAt, isActive
  • snake_case en base : created_at, is_active

Pourquoi ? Parce que chaque écosystème a ses standards : JavaScript/TypeScript privilégie camelCase, SQL est souvent plus lisible en snake_case.

Pour appliquer cette convention, vous avez deux stratégies :

  1. Nommer chaque colonne via l’option name (simple, explicite, un peu verbeux).
  2. Utiliser une stratégie globale de naming (plus avancé, dépend de votre configuration).

Dans ce tutoriel, on reste sur l’approche explicite (facile à relire et à maintenir).

8) Exemples d’entités : User, GuidanceService, Oracle

On va maintenant créer trois entités typiques, en s’appuyant sur les concepts précédents. L’objectif est de montrer :

  • les décorateurs de base,
  • les types (enum, json, dates),
  • les options (nullable, unique, default, length),
  • le naming snake_case en base.

8.1 User entity

Créons src/database/entities/user.entity.ts :

import { Column, Entity } from 'typeorm';
import { BaseEntity } from './base.entity';

export enum UserRole {
  USER = 'USER',
  ADMIN = 'ADMIN',
}

type UserPreferences = {
  theme: 'light' | 'dark';
  newsletter: boolean;
};

@Entity('users')
export class User extends BaseEntity {
  @Column({ type: 'varchar', length: 255, unique: true })
  email: string;

  @Column({ name: 'password_hash', type: 'varchar', length: 255 })
  passwordHash: string;

  @Column({ name: 'display_name', type: 'varchar', length: 120, nullable: true })
  displayName: string | null;

  @Column({ type: 'boolean', default: true, name: 'is_active' })
  isActive: boolean;

  @Column({ type: 'enum', enum: UserRole, default: UserRole.USER })
  role: UserRole;

  @Column({ type: 'json', nullable: true })
  preferences: UserPreferences | null;
}

À noter :

  • passwordHash est stocké en base sous password_hash.
  • displayName est nullable, donc typé string | null.
  • preferences illustre une colonne JSON.

8.2 GuidanceService entity

Une entité GuidanceService peut représenter un service de production (tarification, durée, état actif…). Créons src/database/entities/guidance-service.entity.ts :

import { Column, Entity } from 'typeorm';
import { BaseEntity } from './base.entity';

export enum GuidanceServiceType {
  CHAT = 'CHAT',
  CALL = 'CALL',
  EMAIL = 'EMAIL',
}

@Entity('guidance_services')
export class GuidanceService extends BaseEntity {
  @Column({ type: 'varchar', length: 120 })
  name: string;

  @Column({ type: 'enum', enum: GuidanceServiceType })
  type: GuidanceServiceType;

  @Column({ name: 'price_cents', type: 'int', default: 0 })
  priceCents: number;

  @Column({ name: 'duration_minutes', type: 'int', nullable: true })
  durationMinutes: number | null;

  @Column({ name: 'is_enabled', type: 'boolean', default: true })
  isEnabled: boolean;
}

Ici, on illustre :

  • un enum métier (GuidanceServiceType),
  • des champs numériques stockés en entier (ex. price_cents),
  • des options default et nullable.

8.3 Oracle entity

Une entité Oracle peut représenter un contenu (jeu, oracle, support) avec une configuration JSON et une date de publication. Créons src/database/entities/oracle.entity.ts :

import { Column, Entity } from 'typeorm';
import { BaseEntity } from './base.entity';

export enum OracleStatus {
  DRAFT = 'DRAFT',
  PUBLISHED = 'PUBLISHED',
  ARCHIVED = 'ARCHIVED',
}

type OracleConfig = {
  locale: string;
  version: number;
};

@Entity('oracles')
export class Oracle extends BaseEntity {
  @Column({ type: 'varchar', length: 160, unique: true })
  slug: string;

  @Column({ type: 'varchar', length: 255 })
  title: string;

  @Column({ type: 'text', nullable: true })
  description: string | null;

  @Column({ type: 'enum', enum: OracleStatus, default: OracleStatus.DRAFT })
  status: OracleStatus;

  @Column({ name: 'published_at', type: 'timestamp', nullable: true })
  publishedAt: Date | null;

  @Column({ type: 'json', nullable: true })
  config: OracleConfig | null;
}

Ce modèle montre un cas très fréquent : un slug unique, un status enum, une date nullable, et une config JSON.

9) Auto-chargement des entités via le pattern *.entity.ts

Une fois vos entités rangées dans src/database/entities et nommées *.entity.ts, vous pouvez les charger automatiquement dans la configuration TypeORM.

Le principe : au lieu d’importer et lister chaque entité manuellement, vous utilisez un glob sur les fichiers *.entity.ts (ou *.entity.js en build).

Exemple de configuration TypeORM (pattern glob)

Dans votre module database (ou dans la config TypeORM), vous pouvez utiliser :

import { join } from 'path';

export const typeOrmConfig = {
  // ... host, port, username, password, database, etc.
  entities: [join(__dirname, 'entities', '*.entity.{ts,js}')],
};

Pourquoi {ts,js} ? Parce qu’en dev vous exécutez du TypeScript, et en production vous exécutez souvent le code compilé en JavaScript.

Piège courant : si vos entités ne sont pas trouvées, vérifiez le chemin réel (selon l’emplacement du fichier de config) et le fait que vos fichiers compilés conservent la même arborescence.

10) Bonnes pratiques et points d’attention

  • Typage strict : si une colonne est nullable, reflétez-le en TypeScript (| null).
  • Contraintes en base : unique, default, nullable doivent vivre aussi en SQL, pas seulement dans vos DTO.
  • Snake_case explicite : utilisez name pour les colonnes dont le nom SQL doit différer.
  • JSON avec parcimonie : utile pour de la configuration, pas pour modéliser des relations.
  • Factorisation : une BaseEntity réduit la duplication et standardise les timestamps.

Pour aller plus loin sur TypeORM : consultez la documentation officielle https://typeorm.io/.

Conclusion

Vous avez maintenant une base solide pour créer vos premières entités TypeORM dans NestJS : définition d’une entité et son mapping SQL, décorateurs essentiels, types de colonnes courants, options de colonnes, convention snake_case/camelCase, et mise en place d’une BaseEntity réutilisable. Vous avez aussi une structure de projet claire (src/database/entities) et un auto-chargement pratique via *.entity.ts.

Pour approfondir, vous pouvez ensuite aborder :

  • les relations (OneToMany, ManyToOne, ManyToMany) et les jointures,
  • les migrations (génération, exécution, stratégie en CI/CD),
  • la validation des données via DTO + pipes (côté NestJS) en complément des contraintes SQL.

Commentaires

Soyez le premier à laisser un commentaire !

Laisser un commentaire

Les champs obligatoires sont indiqués avec *