Enregistrer des données hors connexion et les synchroniser automatiquement

Nous allons développer une petite application permettant d’enregistrer des signalements. Il est préférable d’avoir un niveau intermédiaire en Angular et NestJs.

Le fonctionnement sera le suivant :

  • L’utilisateur saisit un signalement dans Angular.
  • Si Internet fonctionne, le signalement est envoyé à NestJS.
  • Si Internet est coupé, le signalement est conservé dans localStorage.
  • Lorsque la connexion revient, Angular envoie automatiquement le signalement à NestJS.
  • NestJS enregistre les informations dans MySQL.

Création de ‘entité MySQL

Dans notre projet NestJS, nous allons créer le fichier signalement.entity.ts.

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

@Entity('signalements')
export class Signalement {

  @PrimaryGeneratedColumn()
  id: number;

  // Contenu du signalement
  @Column('text')
  contenu: string;

  // Date de création automatique
  @CreateDateColumn()
  dateCreation: Date;
}

Assurez-vous que votre projet NestJS est connecté à MySQL et que l’entité est enregistrée dans TypeORM.

Pour cet exercice, nous allons utiliser synchronize: true en développement afin de créer la table automatiquement. A n’est pas utilise en production.

Création du service NestJS

Fichier signalements.service.ts :

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

@Injectable()
export class SignalementsService {

  constructor(
    @InjectRepository(Signalement)
    private readonly repository: Repository<Signalement>
  ) {}

  // Enregistrer un nouveau signalement
  async creer(contenu: string) {

    // Création de l'objet
    const signalement = this.repository.create({
      contenu
    });

    // Enregistrement dans MySQL
    return this.repository.save(signalement);
  }
}

Dans signalements.controller.ts :

import {
  Controller,
  Post,
  Body
} from '@nestjs/common';

import { SignalementsService } from './signalements.service';

@Controller('signalements')
export class SignalementsController {

  constructor(
    private readonly service: SignalementsService
  ) {}

  @Post()
  creer(@Body('contenu') contenu: string) {

    // Transmettre le contenu au service
    return this.service.creer(contenu);
  }
}

Pour que notre route fonctionne, nous allons déclarer le contrôleur, le service et TypeOrmModule.forFeature([Signalement]) dans notre module NestJS. Et activer également CORS pour l’application Angular.

Test avec Postman

Méthode : POST

Adresse :

http://localhost:3000/signalements

Body → raw → JSON :

{
  "contenu": "Une panne informatique a été constatée."
}

Résultat attendu :

{
  "id": 1,
  "contenu": "Une panne informatique a été constatée.",
  "dateCreation": "2026-10-10T08:00:00.000Z"
}

Angular : détecter la connexion Internet

Nous allons utiliser deux Signals :

readonly enLigne = signal(navigator.onLine);

readonly enAttente = signal(
  localStorage.getItem('signalement') ?? ''
);

Le premier indique l’état de la connexion. Le deuxième contient le signalement qui attend une synchronisation.

Dans le composant app.ts :

import {
  Component,
  HostListener,
  inject,
  signal
} from '@angular/core';

import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-root',
  standalone: true,
  templateUrl: './app.html'
})
export class App {

  private readonly http = inject(HttpClient);

  private readonly apiUrl =
    'http://localhost:3000/signalements';

  // État de la connexion Internet
  readonly enLigne = signal(navigator.onLine);

  // Signalement conservé localement
  readonly enAttente = signal(
    localStorage.getItem('signalement') ?? ''
  );

  // Évite plusieurs envois simultanés
  readonly sauvegardeEnCours = signal(false);

  constructor() {

    // Synchroniser les données au démarrage
    this.synchroniser();
  }

  // Détection du retour d'Internet
  @HostListener('window:online')
  retourInternet() {

    this.enLigne.set(true);

    // Envoyer les données en attente
    this.synchroniser();
  }

  // Détection de la coupure Internet
  @HostListener('window:offline')
  perteInternet() {

    this.enLigne.set(false);
  }

  // Enregistrement du signalement
  enregistrer(contenu: string) {

    const texte = contenu.trim();

    if (!texte || this.enAttente()) {
      return;
    }

    // Sauvegarde locale avant tout envoi
    localStorage.setItem('signalement', texte);

    this.enAttente.set(texte);

    // Tentative d'envoi vers NestJS
    this.synchroniser();
  }

  // Synchronisation avec le serveur
  synchroniser() {

    if (
      !this.enLigne() ||
      !this.enAttente() ||
      this.sauvegardeEnCours()
    ) {
      return;
    }

    this.sauvegardeEnCours.set(true);

    this.http.post(this.apiUrl, {
      contenu: this.enAttente()
    }).subscribe({

      next: () => {

        // Supprimer uniquement après succès
        localStorage.removeItem('signalement');

        this.enAttente.set('');

        this.sauvegardeEnCours.set(false);
      },

      error: (erreur) => {

        console.error('Erreur de synchronisation', erreur);

        // Les données restent dans localStorage
        this.sauvegardeEnCours.set(false);
      }
    });
  }
}

Point important : navigator.onLine indique si le navigateur est connecté à un réseau. Il ne garantit pas que le serveur NestJS est accessible. C’est pourquoi nous conservons le signalement lorsqu’une requête HTTP échoue.

Cette première version gère un seul signalement en attente. Pour gérer plusieurs signalements, il faudra ensuite utiliser un tableau ou une file d’attente.

Création de l’interface HTML

<h2>Gestion des signalements</h2>

<!-- Message affiché hors connexion -->
@if (!enLigne()) {

  <p style="color: red;">
    ⚠️ Connexion Internet indisponible
  </p>

}

<!-- Formulaire -->
<textarea
  #contenu
  placeholder="Décrivez votre signalement">
</textarea>

<br><br>

<button
  [disabled]="!!enAttente() || sauvegardeEnCours()"
  (click)="enregistrer(contenu.value); contenu.value = ''">

  Enregistrer

</button>

<!-- Information sur la synchronisation -->
@if (enAttente()) {

  <p>
    Signalement en attente de synchronisation.
  </p>

  <p>{{ enAttente() }}</p>

}

@if (sauvegardeEnCours()) {

  <p>Synchronisation en cours...</p>

}

Transformation d’Angular en PWA

Dans notre projet Angular, il faut exécute :

ng add @angular/pwa

Cette commande ajoute notamment un service worker et un manifeste permettant de rendre l’application installable.

A retenir

Nous avons travaillé la communication entre Angular et NestJS, l’utilisation des Signals, le stockage local, la détection des changements de connexion et l’enregistrement des données avec TypeORM et MySQL.

Vous pouvez télécharger le code source de l’exemple avec un design amélioré.