Archives Wild Code School
NestJS

3. NestJS et TypeOrm

Quête 1 sur 2

Tu vas apprendre à utiliser l'ORM intégrée à NestJS. Elle te permettra d'interagir avec la base de données directement par l'intermédiaire d'objets d'entités.

Objectifs

  • Initialiser TypeORM
  • Créer une entité
  • Gérer les migrations
  • Créer le service qui gère cette entité
  • Binder le controller de l'API de l'entité

Initialiser Nest avec TypeORM

Installer TypeORM

Tu vas utiliser l'ORM TypeORM dans ton projet cependant elle n'est pas directement incluse dans un projet NestJS lors de sa création. Tu dois l'ajouter :

shell
npm install --save @nestjs/typeorm typeorm mysql2

Tu viens d'installer TypeORM ainsi que le driver mysql2.

Configurer TypeORM

Qui dit ORM dit base de données et donc credentials pour se connecter à la base de données. Comme tu le sais, tu ne dois jamais exposer un mot de passe. Pour faire les choses correctement, tu vas faire un peu de configuration pour ne pas mettre le nom de base ainsi que l'utilisateur et le mot de passe de connexion en dure.

Tu dois au préalable créer la base de données sur mariadb ou mysql

Tout d'abord, crée un fichier .env à la racine de ton projet et complète les variables :

DATABASE_HOST=
DATABASE_PORT=
DATABASE_USERNAME=
DATABASE_PASSWORD=
DATABASE_DB=

Ensuite, dans un dossier config de ton répertoire src crée un fichier database.config.ts. Il contiendra la configuration de ta base de données dans un format compréhensif par TypeORM.

typescript
// src/config/database.config.ts

import { DataSourceOptions } from 'typeorm';
import 'dotenv/config';

export const databaseConfig: DataSourceOptions = {
  type: 'mariadb',
  host: process.env.DATABASE_HOST,
  port: parseInt(process.env.DATABASE_PORT || '3000'),
  username: process.env.DATABASE_USERNAME,
  password: process.env.DATABASE_PASSWORD,
  database: process.env.DATABASE_DB,
  entities: [__dirname + '/../**/*.entity{.ts,.js}'],
  synchronize: false,
};

Tu observeras 2 points importants dans cette configuration :

  • ligne 13 : tous les fichiers se terminant par .entity.ts sont référencés en tant que entities. Nous en parlerons un peu plus loin.
  • ligne 14 : si vrai, toutes modifications dans les entités s'appliqueront directement dans la base de données (ajout de colonnes, création/suppression de tables...). En développement ce flag peut resté sur true mais jamais en production. Si tu travailles à plusieurs sur un projet, il est recommandé de mettre le flag à false et de gérer les modifications de la structure de ta base de données avec des migrations que nous verrons ensuite.

Tu dois maintenant utiliser la configuration que tu viens de déclarer. Pour cela, tu dois ajouter le module d'import de configuration à ton projet :

shell
npm install @nestjs/config --save

Il faut dire à Nest quels seront les modules qui utiliseront une connexion avec la base de données. Dans notre cas, tu vas définir la connexion sur le AppModule; elle sera ainsi disponible dans tous les modules de l'application.

typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { databaseConfig } from './config/database.config';

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: () => databaseConfig,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Tu as importé le module TypeOrmModule et tu lui as fourni la configuration de ta base de données databaseConfig. Tu importes dans ce module TypeOrmModule le module ConfigModule car c'est lui qui permet de lire dans le fichier .env

Scripts TypeORM

TypeORM s'utilise avec un CLI. Si tu veux, tu peux ajouter quelques raccourcis dans la section scripts de ton package.json :

json
// package.json
{
    ...
    "scripts": {
        ...
        "migration:create": "./node_modules/.bin/ts-node ./node_modules/.bin/typeorm migration:create",
        "migration:generate": "./node_modules/.bin/ts-node ./node_modules/.bin/typeorm migration:generate -d src/config/migration.config.ts",
        "migration:up": "./node_modules/.bin/ts-node ./node_modules/.bin/typeorm migration:run -d src/config/migration.config.ts",
        "migration:down": "./node_modules/.bin/ts-node ./node_modules/.bin/typeorm migration:revert -d src/config/migration.config.ts"
    },
    ...
}

Tu as maintenant accès à ces lignes de commandes :

  • npm run migration:create pour créer une nouvelle migration vide

  • npm run migration:generate pour génerer une nouvelle migration

  • npm run migration:up pour exécuter les migrations non exécutées

  • npm run migration:down pour rolllback la dernière migration

Tu vas voir comment utiliser toutes ces lignes de commandes par la suite.

Entité de base de données

Une entité de base de données est une classe qui représente une table. Les champs de la table correspondent à des propriétés de la classe. Tu dois définir quelle propriété correspond à la clé primaire de ta table, ainsi que les caractéristiques de chaque champs.

Tout d'abord, tu vas créer un nouveau module pour gérer les utilisateurs de ton application. A terme, ce module contiendra ton entité User, le UserService ainsi que le UserController mais tu vas procéder par étape.

Nest fournit une CLI qui permet de générer les classes dont tu as besoin et de les lier au bon endroit.

Module utilisateur

Les utilisateurs sont stockés dans une table user de ta base de données. Cette table est reliée à une classe Entity. Tu auras besoin d'un service UserService pour manipuler les entités User et donc les données de la base de données.

Tu dois donc créer ce module, ce service et le controller associé.

shell
nest g module users
nest g service users
nest g controller users

Tu viens de générer :

  • un module users : il contiendra la gestion des utilisateurs

  • un UserService : il contiendra les méthodes d'interaction avec la base de données

  • un UsersController : il contiendra les méthodes correspondant aux routes de l'api /users

Entité

Tu vas créer une entité User que tu mettras dans un fichier user.entity.ts.

shell
nest g class users/user.entity

Tu renommeras la classe UserEntity en User pour plus de simplicité (renomme dans le fichier .spec.ts aussi). Le décorateur @Entity() permet de définir une classe comme une entité de base de données.

Un User est composé de :

  • id: autogénéré

  • email: string 50 caractères

  • password: string 50 caractères

  • firstname: string 80 caractères

  • lastname: string 80 caractères

typescript
// app/src/users/user.entity.ts

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

@Entity()
export class User {

    @PrimaryGeneratedColumn()
    id: number;

    @Column({ length: 50 })
    email:string;

    @Column({ length: 255 })
    password:string;

    @Column({ length: 80 })
    firstname:string;

    @Column({ length: 80 })
    lastname: string;
}

Tu dois spécifier à TypeORM que tu utilises une entité dans ce module :

typescript
// app/src/users/users.module.ts

import { User } from './user.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
...

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

Migration

Tu as créé la classe d'entité mais la table user correspondante n'existe pas encore dans mysql. Tu vas devoir créer une migration.

Les migrations sont des fichiers javascript auto générés. Lors de la création d'une nouvelle migration, TypeORM traduit en SQL les modifications que tu as apporté à tes entités afin de les appliquer dans la base de données. Tu auras donc une multitude de fichiers de migrations qui correspondront à toutes les modifications successives que tu as faites sur ta base de données.

Lors d'une nouvelle mise en production, tu n'auras qu'à exécuter les migrations sur ta base de production avec une simple ligne de commande.

De plus, un fichier de migration possède 2 fonctions : up() et down() qui respectivement applique les modifications ou les enlève. Il est donc possible de revenir facilement à une version antérieure de la base de données. Toutes les données de migrations appliquées à la base se trouvent dans une table nommée migrations qui est directement gérée par TypeOrm.

Pour utiliser correctement les migrations, tu dois définir une configuration. Crée un fichier migration.config.ts dans le répertoire src/config/

typescript
// src/config/migration.config.ts

import { DataSource } from 'typeorm';
import { databaseConfig } from './database.config';

export default new DataSource({
  ...databaseConfig,
  migrations: [__dirname + '/../migrations/*.ts'],
});

Dans ce fichier, tu définis que la connexion à la base de données et la même que celle de ton application. Tu ajoutes le chemin de l'emplacement du dossier de migrations que tu dois créer dans src/migrations Dans ce fichier, tu définis que la connexion à la base de données est la même que celle de ton application. Tu ajoutes le chemin de l'emplacement du dossier de migrations que tu dois créer dans src/migrations Tu dois d'abord lancer ton serveur car les migrations seront générées en TS, mais devront être transpilées en JS.

shell
npm run start:dev

Pour créer la migration correspondante à la création de la table user :

shell
npm run migration:generate src/migrations/CreateUserTable

Tu dois spécifier dans quel dossier tu veux que ta migration soit générée.

Tu peux regarder le contenu du fichier de migration.

Maintenant que tu as généré une migration, tu l'appliques :

shell
npm run migration:up

TypeORM va récupérer toutes les migrations de ton dossier de migrations et va appliquer celles qu'il n'a pas encore appliqué. A la fin, TypeORM renseigne dans sa table migrations tous les fichiers de migrations qu'il a exécuté. Tu verras sur le terminal les requêtes SQL qui ont été exécutées. Tu peux vérifier dans ta base de données que tes modifications ont bien été appliquées.

A chaque fois que tu modifieras ta base de données, tu devras générer une migration et l'appliquer. Tes camarades récupèreront les migrations via git et pourront les appliquer à leur tour.

Il te reste 2 étapes :

  • Créer les méthodes de manipulation des entités User dans le UserService
  • Créer les méthodes de l'api dans le UserController. Elles utiliseront le UserService

Manipulation des entités

Les interactions avec la base de données se font exclusivement dans les services. Pour cela, tu dois injecter le Repository correspondant à ton entité User. Un Repository est une classe qui contient toutes les méthodes dont tu auras besoin pour interagir avec la base. Il se charge de traduire tes demandes en SQL.

Un Repository s'injecte dans le constructeur de ton service presque comme un service lambda.

typescript
    constructor(@InjectRepository(User) private usersRepository: Repository<User>) { }

Comme tu le vois, tu n'as pas besoin de créer une classe de Repository par entité. Il te suffit de le typer à l'aide des chevrons <User>

Ta classe UsersService devrait ressembler à cela :

typescript
// src/app/users/users.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User) private usersRepository: Repository<User>,
  ) {}

  async getUsers(): Promise<User[]> {
    return await this.usersRepository.find();
  }

  async getUser(id: number): Promise<User[]> {
    return await this.usersRepository.find({
      // Properties to return. We don't want the password property.
      select: ['firstname', 'lastname', 'email'],
      where: [{ id: id }],
    });
  }

  saveUser(user: User): Promise<User> {
    return this.usersRepository.save(user);
  }

  deleteUser(user: User): void {
    this.usersRepository.delete(user);
  }
}

Création de l'API

UsersController

Les controllers gèrent les routes suivant une méthode/verbe HTTP. Ce sont eux qui constitueront l'API de ton application. Le décorateur @Controller('users') sera chargé de gérer la route http://localhost:3000/users peu importe la méthode.

Tu créeras ensuite une méthode par appel d'API que tu veux gérer. Tu devras préciser quelle méthode HTTP est gérée par quelle méthode de la classe avec les décorateurs : @Get(), @Post(), @Put(), @Delete().

Tu peux également définir des paramètres : @Get(':id').

N'oublie pas d'injecter le UsersService dans ton constructeur. Tu vas en avoir besoin pour interagir avec ta base de données.

Ton UsersController devrait ressembler à cela :

typescript
// src/app/users/users.controller.ts

import {
  Controller,
  Post,
  Body,
  Get,
  Put,
  Delete,
  Param,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { User } from './user.entity';

@Controller('users')
export class UsersController {
  constructor(private service: UsersService) {}

  @Get(':id')
  get(@Param() params) {
    return this.service.getUser(params.id);
  }

  @Post()
  create(@Body() user: User) {
    return this.service.saveUser(user);
  }

  @Put()
  update(@Body() user: User) {
    return this.service.saveUser(user);
  }

  @Delete(':id')
  deleteUser(@Param() params) {
    this.service.deleteUser(params.id);
    return;
  }
}


Tu peux récupérer le body ou/et les paramètres en paramétrant les arguments de tes méthodes avec @Param() et @Body().

Tu peux maintenant ouvrir Postman et lancer cette requête :

shell
POST http://localhost:3000/users
{
    "firstname": "Pierre",
    "lastname": "Dupont",
    "email": "dupont@example.com",
    "password": "azerty"
}

Amuse-toi à tester toutes les requêtes avec Postman ou curl.

Ressources partagées sur cette quête