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 contactLa 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:devTester 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"Erreur400: email incorrect - Remplacer le message par
"Salut"Erreur400: message trop court - Ajouter
"statut": "admin"Erreur400: 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 contactNous 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 serveOuvre 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.