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 tableusers - une propriété
email↔ une colonneemail - un
idauto-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, idest une clé primaire auto-générée,emailest 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 heuretimestamp/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
uniqueen 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: identifiantcreatedAt: date de créationupdatedAt: 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
CreateDateColumnetUpdateDateColumn: TypeORM gère la valeur automatiquement. - On force le nom SQL en snake_case via
name: 'created_at'etname: 'updated_at', tout en gardantcreatedAt/updatedAten 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 :
- Nommer chaque colonne via l’option
name(simple, explicite, un peu verbeux). - 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 :
passwordHashest stocké en base souspassword_hash.displayNameest nullable, donc typéstring | null.preferencesillustre 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
enummétier (GuidanceServiceType), - des champs numériques stockés en entier (ex.
price_cents), - des options
defaultetnullable.
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,nullabledoivent vivre aussi en SQL, pas seulement dans vos DTO. - Snake_case explicite : utilisez
namepour 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
BaseEntityré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