TypeORM : la base de données en TypeScript sans écrire de SQL
Entities, relations, migrations, QueryBuilder — TypeORM mappe vos classes TypeScript sur vos tables SQL et prend en charge le cycle de vie complet du schéma.
Un ORM (Object-Relational Mapper) fait le pont entre vos objets TypeScript et vos tables SQL. TypeORM est l'ORM de référence pour TypeScript — il tourne sur Node.js, supporte PostgreSQL, MySQL, SQLite, et une douzaine d'autres bases. Ses décorateurs transforment une classe ordinaire en entité persistée, ses migrations versionnent le schéma, et son QueryBuilder génère du SQL complexe tout en restant typé.
Installation et configuration
npm install typeorm reflect-metadata
npm install pg # pour PostgreSQL
# Pilote selon la base
npm install mysql2 # MySQL
npm install better-sqlite3 # SQLiteTypeORM utilise les décorateurs TypeScript — activez-les dans tsconfig.json :
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictPropertyInitialization": false
}
}Configuration de la connexion :
import { DataSource } from 'typeorm'
export const AppDataSource = new DataSource({
type: 'postgres',
host: process.env.DB_HOST ?? 'localhost',
port: Number(process.env.DB_PORT) || 5432,
username: process.env.DB_USER ?? 'postgres',
password: process.env.DB_PASS ?? '',
database: process.env.DB_NAME ?? 'myapp',
entities: ['src/entities/**/*.ts'],
migrations: ['src/migrations/**/*.ts'],
synchronize: false, // JAMAIS true en production
logging: process.env.NODE_ENV === 'development',
})synchronize: true modifie automatiquement le schéma à chaque démarrage pour correspondre aux entités. Pratique en développement, catastrophique en production — il peut supprimer des colonnes. Utilisez les migrations.
Entités
Une entité est une classe décorée avec @Entity(). Chaque propriété décorée devient une colonne :
import {
Entity, PrimaryGeneratedColumn, Column,
CreateDateColumn, UpdateDateColumn, Index
} from 'typeorm'
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string
@Index()
@Column({ unique: true })
email: string
@Column()
name: string
@Column({ select: false }) // jamais retourné par défaut
password: string
@Column({ default: 'user' })
role: 'user' | 'admin'
@Column({ nullable: true })
avatar?: string
@CreateDateColumn()
createdAt: Date
@UpdateDateColumn()
updatedAt: Date
}@PrimaryGeneratedColumn('uuid') génère un UUID automatiquement. select: false exclut le champ des requêtes par défaut — utile pour les mots de passe hashés.
Relations
TypeORM gère les quatre types de relations SQL :
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm'
import { User } from './user.entity'
import { Comment } from './comment.entity'
@Entity('articles')
export class Article {
@PrimaryGeneratedColumn('uuid')
id: string
@Column()
title: string
@Column('text')
content: string
@ManyToOne(() => User, user => user.articles, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'author_id' })
author: User
@Column()
authorId: string
@OneToMany(() => Comment, comment => comment.article)
comments: Comment[]
}@OneToMany(() => Article, article => article.author)
articles: Article[]ManyToMany avec table de jonction :
@Entity('tags')
export class Tag {
@PrimaryGeneratedColumn()
id: number
@Column({ unique: true })
name: string
@ManyToMany(() => Article, article => article.tags)
articles: Article[]
}
// Dans Article :
@ManyToMany(() => Tag, tag => tag.articles)
@JoinTable({
name: 'article_tags',
joinColumn: { name: 'article_id' },
inverseJoinColumn: { name: 'tag_id' },
})
tags: Tag[]Repository — les opérations CRUD
import { AppDataSource } from '../data-source'
import { User } from '../entities/user.entity'
const userRepo = AppDataSource.getRepository(User)
// Créer
const user = userRepo.create({ email: 'alice@example.com', name: 'Alice' })
await userRepo.save(user)
// Lire
const all = await userRepo.find()
const one = await userRepo.findOneBy({ id: 'uuid...' })
const byEmail = await userRepo.findOneBy({ email: 'alice@example.com' })
// Avec relations
const withArticles = await userRepo.findOne({
where: { id: 'uuid...' },
relations: { articles: true },
})
// Mettre à jour
await userRepo.update({ id: 'uuid...' }, { name: 'Alice B.' })
// Supprimer
await userRepo.delete({ id: 'uuid...' })
// Soft delete (nécessite @DeleteDateColumn sur l'entité)
await userRepo.softDelete({ id: 'uuid...' })
await userRepo.restore({ id: 'uuid...' })QueryBuilder — requêtes complexes
Pour les requêtes que find() ne peut pas exprimer simplement :
const articles = await AppDataSource
.getRepository(Article)
.createQueryBuilder('article')
.leftJoinAndSelect('article.author', 'author')
.leftJoinAndSelect('article.tags', 'tag')
.where('article.published = :published', { published: true })
.andWhere('author.role = :role', { role: 'admin' })
.orderBy('article.createdAt', 'DESC')
.take(10)
.skip(0)
.getMany()Pagination avec comptage :
const [articles, total] = await repo
.createQueryBuilder('article')
.where('article.published = true')
.orderBy('article.createdAt', 'DESC')
.take(pageSize)
.skip((page - 1) * pageSize)
.getManyAndCount()Agrégations :
const stats = await AppDataSource
.getRepository(Article)
.createQueryBuilder('article')
.select('article.authorId', 'authorId')
.addSelect('COUNT(article.id)', 'articleCount')
.addSelect('AVG(article.views)', 'avgViews')
.groupBy('article.authorId')
.having('COUNT(article.id) > :min', { min: 5 })
.getRawMany()Migrations
Les migrations versionnent les changements de schéma. Chaque migration est un fichier TypeScript avec deux méthodes : up (appliquer) et down (annuler).
Générer automatiquement depuis les entités :
npx typeorm migration:generate src/migrations/CreateUsers -d src/data-source.tsTypeORM compare l'état actuel de la base avec les entités et génère le diff SQL :
import { MigrationInterface, QueryRunner } from 'typeorm'
export class CreateUsers1719000000000 implements MigrationInterface {
name = 'CreateUsers1719000000000'
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE "users" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"email" character varying NOT NULL,
"name" character varying NOT NULL,
"password" character varying NOT NULL,
"role" character varying NOT NULL DEFAULT 'user',
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT "UQ_users_email" UNIQUE ("email"),
CONSTRAINT "PK_users" PRIMARY KEY ("id")
)
`)
await queryRunner.query(`CREATE INDEX "IDX_users_email" ON "users" ("email")`)
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_users_email"`)
await queryRunner.query(`DROP TABLE "users"`)
}
}Exécuter les migrations :
# Appliquer toutes les migrations en attente
npx typeorm migration:run -d src/data-source.ts
# Annuler la dernière migration
npx typeorm migration:revert -d src/data-source.ts
# Voir quelles migrations ont été appliquées
npx typeorm migration:show -d src/data-source.tsDans une app Express ou NestJS, lancez les migrations au démarrage :
import 'reflect-metadata'
import { AppDataSource } from './data-source'
AppDataSource.initialize()
.then(async () => {
await AppDataSource.runMigrations()
console.log('DB connectée, migrations appliquées')
// démarrer le serveur...
})
.catch(err => {
console.error('Erreur initialisation DB', err)
process.exit(1)
})Transactions
await AppDataSource.transaction(async manager => {
const user = manager.create(User, { email: 'bob@example.com', name: 'Bob' })
await manager.save(user)
const article = manager.create(Article, {
title: 'Premier article',
authorId: user.id,
})
await manager.save(article)
// Si une des deux save() échoue, les deux sont annulées
})TypeORM accélère le développement en éliminant le SQL boilerplate pour les opérations courantes. Les migrations garantissent que votre schéma de production évolue de façon reproductible et réversible — indispensable dès qu'une équipe touche la base de données.
Pour la base de données sous-jacente, PostgreSQL : les fondamentaux couvre les index, les types de données et les bonnes pratiques de schéma qui s'appliquent que vous utilisiez un ORM ou du SQL brut.
Déployer votre app Node.js avec TypeORM en production passe par héberger une app Node.js sur un VPS — PM2, Nginx, variables d'environnement et migrations au démarrage.