Connecter PostgreSQL à NestJS avec TypeORM

Connecter PostgreSQL à NestJS avec TypeORM

Connecter une base PostgreSQL à une application NestJS est une étape structurante : c’est elle qui conditionne la persistance des données, la stabilité au démarrage et, très vite, la qualité de votre architecture (modules, configuration, gestion d’erreurs, etc.). Dans ce tutoriel, nous allons mettre en place une connexion PostgreSQL via TypeORM en suivant une approche “projet réel” : module dédié (DatabaseModule), configuration typée (configuration.ts), variables d’environnement (ConfigModule + .env), et tests de connexion au démarrage avec gestion des erreurs.

Pré-requis :

  • Node.js 18+ (recommandé) et npm/pnpm/yarn
  • NestJS (projet existant ou nouveau)
  • Une instance PostgreSQL disponible (locale via Docker ou installation native)

Versions utilisées (indicatif) : NestJS 10.x, TypeORM 0.3.x, pg 8.x.

Objectif : obtenir une configuration prête à l’emploi, testable, et alignée avec une structure modulaire type “backend” (ex : un projet NestJS de production) : un DatabaseModule autonome et une config centralisée.

1) Installer les dépendances nécessaires

TypeORM s’intègre à NestJS via le package @nestjs/typeorm. PostgreSQL est piloté par le driver pg.

# npm
npm i @nestjs/typeorm typeorm pg

# (optionnel) types pour Node si besoin
npm i -D @types/node

Si vous n’avez pas encore le module de configuration NestJS :

npm i @nestjs/config

2) Créer un fichier .env pour PostgreSQL

À la racine du projet (ou dans le dossier attendu par votre tooling), créez un fichier .env contenant les variables suivantes :

DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_NAME=app_db

Piège courant : DB_PORT est une chaîne de caractères dans l’environnement. On le convertira en nombre dans notre configuration typée (ou via une validation).

3) Mettre en place une configuration typée (configuration.ts)

L’idée est de centraliser la lecture de l’environnement et de produire un objet de configuration typé. C’est l’équivalent d’un “contrat” : votre code consomme une config structurée au lieu d’appeler process.env partout.

Créez par exemple : src/config/configuration.ts.

// src/config/configuration.ts

export type AppConfig = {
  database: {
    host: string;
    port: number;
    username: string;
    password: string;
    name: string;
    // En général on garde synchronize en dev uniquement
    synchronize: boolean;
  };
};

export default (): AppConfig => {
  const dbPort = Number(process.env.DB_PORT ?? 5432);

  return {
    database: {
      host: process.env.DB_HOST ?? 'localhost',
      port: Number.isNaN(dbPort) ? 5432 : dbPort,
      username: process.env.DB_USERNAME ?? 'postgres',
      password: process.env.DB_PASSWORD ?? 'postgres',
      name: process.env.DB_NAME ?? 'app_db',
      synchronize: process.env.NODE_ENV !== 'production',
    },
  };
};

Pourquoi “synchronize” dépend de l’environnement ? Parce que synchronize: true demande à TypeORM de créer/mettre à jour automatiquement le schéma à partir de vos entités. En développement, c’est pratique. En production, c’est risqué : une évolution d’entité peut provoquer des changements destructeurs ou non maîtrisés. En prod, on privilégie des migrations.

Documentation officielle : NestJS Database et TypeORM.

4) Activer ConfigModule (variables d’environnement + config typée)

Dans une structure modulaire, on configure généralement ConfigModule au niveau du AppModule et on le rend global pour éviter de l’importer partout.

Exemple : src/app.module.ts.

// src/app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import configuration from './config/configuration';
import { DatabaseModule } from './database/database.module';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      // charge configuration.ts
      load: [configuration],
      // vous pouvez aussi préciser envFilePath: ['.env'] si nécessaire
    }),
    DatabaseModule,
  ],
})
export class AppModule {}

Bonnes pratiques :

  • isGlobal: true évite de ré-importer ConfigModule dans chaque module
  • load permet de construire une config structurée (ici database.*)

5) Créer un DatabaseModule avec TypeOrmModule.forRootAsync

TypeOrmModule.forRootAsync est la méthode recommandée dès que votre configuration dépend de services Nest (comme ConfigService) ou de calculs asynchrones. Cela colle très bien à une architecture “backend” : un module isolé, importé une seule fois.

Créez : src/database/database.module.ts.

// src/database/database.module.ts

import { Module, Logger } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigService } from '@nestjs/config';

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => {
        // On récupère les valeurs depuis configuration.ts
        const host = configService.get<string>('database.host');
        const port = configService.get<number>('database.port');
        const username = configService.get<string>('database.username');
        const password = configService.get<string>('database.password');
        const database = configService.get<string>('database.name');
        const synchronize = configService.get<boolean>('database.synchronize');

        // Log minimal (évitez de logger le password)
        Logger.log(
          `TypeORM config: ${username}@${host}:${port}/${database} (synchronize=${synchronize})`,
          'DatabaseModule',
        );

        return {
          type: 'postgres',
          host,
          port,
          username,
          password,
          database,

          // Important : en dev OK, en prod on désactive
          synchronize,

          // À adapter selon votre arborescence
          // autoLoadEntities permet de ne pas déclarer toutes les entités ici
          autoLoadEntities: true,

          // logging peut aider en dev, à limiter en prod
          logging: configService.get('NODE_ENV') !== 'production',
        };
      },
    }),
  ],
})
export class DatabaseModule {}

Pourquoi autoLoadEntities ? Cela permet à TypeORM de charger automatiquement les entités déclarées via TypeOrmModule.forFeature([...]) dans vos modules métier. C’est souvent plus maintenable que de maintenir une liste centrale.

6) Tester la connexion au démarrage

TypeORM va tenter la connexion au bootstrap. Mais dans un backend, on veut souvent :

  • Valider explicitement que la connexion est opérationnelle
  • Remonter une erreur claire (et éventuellement arrêter l’app)

Pour cela, on peut ajouter un petit service de “health check” au démarrage, basé sur DataSource (TypeORM 0.3+). Créez : src/database/database-health.service.ts.

// src/database/database-health.service.ts

import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
import { DataSource } from 'typeorm';

@Injectable()
export class DatabaseHealthService implements OnApplicationBootstrap {
  private readonly logger = new Logger(DatabaseHealthService.name);

  constructor(private readonly dataSource: DataSource) {}

  async onApplicationBootstrap(): Promise<void> {
    try {
      // Vérifie que TypeORM est bien initialisé
      if (!this.dataSource.isInitialized) {
        this.logger.warn('DataSource not initialized yet. Initializing...');
        await this.dataSource.initialize();
      }

      // Test simple : ping DB
      await this.dataSource.query('SELECT 1');
      this.logger.log('PostgreSQL connection OK');
    } catch (error) {
      // Ici, on log clairement et on relance l'erreur pour stopper le démarrage
      this.logger.error('PostgreSQL connection FAILED', error instanceof Error ? error.stack : undefined);
      throw error;
    }
  }
}

Ensuite, déclarez ce provider dans votre DatabaseModule :

// src/database/database.module.ts

import { Module, Logger } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigService } from '@nestjs/config';
import { DatabaseHealthService } from './database-health.service';

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => {
        const host = configService.get<string>('database.host');
        const port = configService.get<number>('database.port');
        const username = configService.get<string>('database.username');
        const password = configService.get<string>('database.password');
        const database = configService.get<string>('database.name');
        const synchronize = configService.get<boolean>('database.synchronize');

        Logger.log(
          `TypeORM config: ${username}@${host}:${port}/${database} (synchronize=${synchronize})`,
          'DatabaseModule',
        );

        return {
          type: 'postgres',
          host,
          port,
          username,
          password,
          database,
          synchronize,
          autoLoadEntities: true,
          logging: configService.get('NODE_ENV') !== 'production',
        };
      },
    }),
  ],
  providers: [DatabaseHealthService],
})
export class DatabaseModule {}

Analogie : considérez la DB comme un “service externe” dont dépend votre API. Le test au démarrage, c’est comme vérifier qu’une prise réseau est branchée avant d’ouvrir un bureau au public.

7) Gérer les erreurs de connexion proprement

Deux niveaux sont utiles :

  1. Au démarrage : si la DB est indispensable, on préfère souvent échouer vite (fail fast) plutôt que démarrer une API inutilisable.
  2. En runtime : si la DB peut être temporairement indisponible, vous pouvez mettre en place une stratégie de retry (à manier avec prudence).

7.1 Fail fast (recommandé pour beaucoup d’APIs)

Le code du DatabaseHealthService ci-dessus relance l’erreur : NestJS stoppera le bootstrap, ce qui est généralement souhaitable en production (Kubernetes / systemd / Docker redémarrera le conteneur, et vous aurez des logs explicites).

7.2 Activer une stratégie de retry

TypeORM côté Nest supporte des options de retry via TypeOrmModule (selon versions). Une approche courante est d’ajouter :

  • retryAttempts
  • retryDelay

Vous pouvez les ajouter dans l’objet retourné par useFactory si votre version les supporte. Exemple :

return {
  type: 'postgres',
  host,
  port,
  username,
  password,
  database,
  synchronize,
  autoLoadEntities: true,
  logging: configService.get('NODE_ENV') !== 'production',

  // Optionnel : utile en Docker Compose (DB pas encore prête)
  retryAttempts: 5,
  retryDelay: 3000,
};

Piège courant : un retry trop agressif peut masquer un mauvais paramétrage (mauvais mot de passe, mauvais host). Gardez des logs clairs et un nombre de tentatives raisonnable.

8) Vérifier que tout fonctionne

Démarrez votre application :

npm run start:dev

Vous devriez voir :

  • Un log de configuration TypeORM (sans mot de passe)
  • Un log PostgreSQL connection OK

Pour tester la gestion d’erreur, changez temporairement DB_PASSWORD dans .env et relancez : l’application doit échouer au démarrage avec un message explicite.

9) (Optionnel) Exemple minimal d’entité pour valider synchronize

Pour vérifier que synchronize: true crée bien des tables en développement, vous pouvez ajouter une entité simple.

Créez par exemple : src/users/user.entity.ts.

// src/users/user.entity.ts

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

@Entity({ name: 'users' })
export class UserEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

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

Ensuite, dans un module UsersModule, vous déclareriez :

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([UserEntity])],
})
export class UsersModule {}

Avec autoLoadEntities: true, TypeORM prendra en compte l’entité et créera la table users en dev. En production, vous désactivez synchronize et vous passez par des migrations.

Conclusion

Vous avez maintenant une connexion PostgreSQL robuste dans NestJS via TypeORM, basée sur :

  • ConfigModule + .env pour gérer l’environnement
  • Une config typée via configuration.ts
  • Un DatabaseModule isolé avec TypeOrmModule.forRootAsync
  • Un test de connexion au démarrage et une gestion d’erreurs explicite
  • Une explication claire de synchronize: true en développement et pourquoi l’éviter en production

Pour aller plus loin :

  • Mettre en place des migrations TypeORM pour la production
  • Ajouter une validation de configuration (schéma) pour échouer si une variable manque
  • Implémenter un endpoint de health check (ex : /health) si vous déployez sur Kubernetes

Commentaires

Soyez le premier à laisser un commentaire !

Laisser un commentaire

Les champs obligatoires sont indiqués avec *