La verification des données d’un formulaire avec NestJS

Nous allons créer une route pour recevoir les messages de contact. Elle rejettera automatiquement les e-mails incorrects ou les messages trop courts. Par la suite, cette route pourra être connectée à un formulaire Angular 20+. (Niveau : Débutant)

Préparer le projet

Dans le terminal du projet NestJS :

npm install class-validator class-transformer
npx nest generate controller contact

La première commande installe les outils de validation. La seconde crée un contrôleur et l’enregistre dans le module du projet.

Définir les données attendues

Crée le fichier src/contact/contact.dto.ts

import { IsEmail, IsString, MinLength } from 'class-validator';

// Un DTO décrit les données que notre API accepte.
export class ContactDto {
  @IsEmail({}, { message: 'L’adresse email est incorrecte.' })
  email!: string;

  @IsString({ message: 'Le message doit être du texte.' })
  @MinLength(10, {
    message: 'Le message doit contenir au moins 10 caractères.',
  })
  message!: string;
}

Les éléments qui commencent par @ sont des décorateurs : ici, ils ajoutent des règles aux champs.

Activer la validation

Dans src/main.ts, configure le démarrage ainsi :


import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Vérifie les données avant de les transmettre au contrôleur.
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true, // Refuse les champs non prévus.
    }),
  );

  await app.listen(3000);
}

bootstrap();

NestJS utilise ValidationPipe pour appliquer les règles du DTO aux données reçues.

Recevoir le formulaire

Nous allons remplacer le contenu de src/contact/contact.controller.ts :

import { Body, Controller, Post } from '@nestjs/common';
import { ContactDto } from './contact.dto';

@Controller('contact')
export class ContactController {
  @Post()
  recevoir(@Body() donnees: ContactDto) {
    // Cette méthode s’exécute uniquement si les données sont valides.
    return {
      confirmation: 'Votre message a bien été reçu.',
      email: donnees.email,
    };
  }
}

Cette route valide la demande et répond ; elle n’envoie pas encore d’email et ne sauvegarde rien.

Lance le serveur :

npm run start:dev

Tester avec Postman

Postman est comme une application de messagerie pour les ordinateurs, mais au lieu d’envoyer un SMS à un ami, vous envoyez un message à un site web ou à un serveur.

http://localhost:3000/contact

Résultat attendu : 201 Created, avec cette réponse :

{
  "confirmation": "Votre message a bien été reçu.",
  "email": "yves@example.com"
}

Effectue ensuite ces trois essais :

  • Remplacer l’email par "bonjour"Erreur 400 : email incorrect
  • Remplacer le message par "Salut"Erreur 400 : message trop court
  • Ajouter "statut": "admin"Erreur 400 : champ non autorisé

Petit défi : ajoute un champ nom, obligatoire, de type texte et contenant au moins deux caractères. Teste une demande valide, puis une demande sans nom.

N.B. : Même avec des vérifications dans Angular, le serveur doit contrôler les données : on peut appeler ton API directement depuis Postman.

Nous allons concevoir un formulaire Angular avec des signaux, connecté à notre route NestJS POST http://localhost:3000/contact

Il affichera la confirmation, les erreurs de validation et un indicateur pendant l’envoi. On reprend les deux champs de l’exercice : email et message.

Nous allons autoriser Angular à communiquer avec NestJS

Dans notre projet NestJS, nous allons ajoute ceci dans src/main.ts, après la création de app et avant app.listen() :

// Autorise les requêtes provenant de notre application Angular.
app.enableCors({
  origin: 'http://localhost:4200',
});

Nous allons créer le composant de contact

Dans le terminal du projet Angular :

ng g c contact

Nous allons remplacer le contenu de src/app/contact/contact.component.ts :

import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { finalize } from 'rxjs';

// Structure de la réponse envoyée par notre contrôleur NestJS.
type ReponseContact = {
  confirmation: string;
  email: string;
};

@Component({
  selector: 'app-contact',
  imports: [
    FormsModule
  ],
  templateUrl: './contact.html',
  styleUrl: './contact.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class Contact {
  private readonly http = inject(HttpClient);

  // Les valeurs saisies dans le formulaire.
  readonly email = signal('');
  readonly message = signal('');

  // Les informations affichées à l’utilisateur.
  readonly envoiEnCours = signal(false);
  readonly confirmation = signal('');
  readonly erreurs = signal<string[]>([]);

  // Se recalcule automatiquement quand le message change.
  readonly nombreCaracteres = computed(() => this.message().length);

  envoyer():void{
    // Empêche de lancer plusieurs envois en même temps.
    if (this.envoiEnCours()) return;

    this.confirmation.set('');
    this.erreurs.set([]);
    this.envoiEnCours.set(true);

    // Lire un signal : email(). Modifier un signal : email.set(...).
    const donnees = {
      email: this.email(),
      message: this.message(),
    };

    this.http
      .post<ReponseContact>('http://localhost:3000/contact', donnees)
      .pipe(
        // Réactive le bouton après une réussite ou une erreur.
        finalize(() => this.envoiEnCours.set(false)),
      )
      .subscribe({
        next: (reponse) => {
          this.confirmation.set(reponse.confirmation);
        },

        error: (erreur: HttpErrorResponse) => {
          if (erreur.status === 0) {
            this.erreurs.set([
              'Impossible de joindre le serveur. Vérifie que NestJS fonctionne et que CORS est configuré.',
            ]);
            return;
          }

          // NestJS renvoie généralement un tableau pour les erreurs du DTO.
          const messages: unknown = erreur.error?.message;

          if (Array.isArray(messages)) {
            this.erreurs.set(messages.map((message) => String(message)));
          } else if (typeof messages === 'string') {
            this.erreurs.set([messages]);
          } else {
            this.erreurs.set(['Une erreur est survenue pendant l’envoi.']);
          }
        },
      });
  }
}

Nous allons ajouter le formulaire HTML

Nous allons remplacer le contenu de src/app/contact/contact.component.html :

<form (ngSubmit)="envoyer()" novalidate>
  <fieldset [disabled]="envoiEnCours()">
    <legend>Votre message</legend>

    <div>
      <label for="email">Adresse email</label>

      <input
        id="email"
        name="email"
        type="email"
        autocomplete="email"
        [ngModel]="email()"
        (ngModelChange)="email.set($event)"
      />
    </div>

    <div>
      <label for="message">Message</label>

      <textarea
        id="message"
        name="message"
        rows="5"
        [ngModel]="message()"
        (ngModelChange)="message.set($event)"
      ></textarea>

      <p>{{ nombreCaracteres() }} caractère(s) — minimum attendu : 10.</p>
    </div>

    <button type="submit">
      {{ envoiEnCours() ? 'Envoi en cours…' : 'Envoyer' }}
    </button>
  </fieldset>
</form>

@if (confirmation()) {
  <p role="status" style="color: green;">
    {{ confirmation() }}
  </p>
}

@if (erreurs().length > 0) {
  <div role="alert" style="color: red;">
    <p>La demande n’a pas abouti :</p>

    <ul>
      @for (erreur of erreurs(); track $index) {
        <li>{{ erreur }}</li>
      }
    </ul>
  </div>
}

La liaison du champ fonctionne en deux temps :

[ngModel]="email()"
(ngModelChange)="email.set($event)"
  • [ngModel] affiche la valeur du signal dans le champ.
  • (ngModelChange) met le signal à jour quand tu écris.

FormsModule fournit la directive ngModel utilisée ici, disponible dans Angular 20.

Afficher le composant

Dans le composant principal, généralement src/app/app.ts avec Angular, ajoute cet import :

import { ContactComponent } from './contact/contact.component';

Puis ajoute ContactComponent au tableau imports du décorateur, en conservant les éléments existants :

imports: [ContactComponent],

Dans le fichier HTML, généralement src/app/app.html, ajoute :

<app-contact></app-contact>

Testons dans le navigateur

il faut laisser fonctionner les deux projets dans deux terminaux séparés :

# Terminal du projet NestJS
npm run start:dev
# Terminal du projet Angular
npx ng serve

Ouvre http://localhost:4200

N.B. : les signaux mettent à jour l’affichage Angular ; HttpClient envoie les données ; le DTO NestJS les vérifie. La confirmation signifie que la route a accepté le message : l’exercice ne prévoit pas encore sa sauvegarde ni son envoi par email.