Introduction

Dans cet article, nous allons apprendre à mettre en place une solution simple et générique pour nos requêtes HTTP sous Angular. Nous créerons une carcasse de client HTTP réutilisable que nous couplerons avec un intercepteur HTTP. Enfin, nous terminerons par l’implémentation d’une solution de mise en cache de nos requêtes.

Si vous avez eu l’occasion de lire mon article Generic Repository, Unit Of Work et Entity Framework, vous ne serez pas étonnés de remarquer que j’aime optimiser mon code, le rendre le plus réutilisable possible et faire en sorte qu’il couvre un maximum de besoins sans avoir à réinventer la roue. Je vous propose donc de créer un service générique, c’est-à-dire qu’il conviendra à une grande majorité de situations.

Objectif

J’aimerais pouvoir créer un service HTTP respectant les normes imposées par la spécification REST, qui soit simple, réutilisable et adaptable. Il me permettra d’effectuer des requêtes CRUD (Create, Read, Update, Delete) sur des APIs afin de manipuler des DTOs correspondant à mon modèle de données. Dans mon exemple, il me faudra être en mesure de créer, récupérer, mettre à jour et supprimer des voitures. La force de mon service résidera en partie dans le fait qu’il pourra aussi s’adapter à n’importe quelles données (pizzas, films etc…), sous réserve de respecter certaines conditions.

Ce service permettra d’interroger un serveur exposant une API car (voiture pour les non anglophones ;-)) en effectuant des requêtes POST, GET, PUT, DELETE sur un point de terminaison donné. Ce faisant, nous respecterons la convention REST en utilisant les VERBES HTTP à bon escient et nos ressources seront accessibles par une propriété discriminante, un identifiant, sur un point de terminaison unique et propre à la ressource visée (https://mon-site.fr/api/cars).

Le tableau ci-dessous présente nos requêtes CRUD.

CRUDVERBURLDONNEESROLE
CPOST/api/carsbody param : {
name: « Clio »,
brand: « Renault »,
color: « Rouge »
}
Création d’une voiture
RGET/api/carsRécupération de toutes les voitures
RGET /api/cars/1route param : 1 Récupération de la voiture portant l’identifiant 1
RGET /api/cars?color=redquery param : color=red Récupération de toutes les voitures de couleur rouge
UUPDATE/api/cars/1body param : {
id: 1,
name: « Clio »,
brand: « Renault »,
color: « Jaune »
}
Mise à jour de la voiture portant l’identifiant 1
DDELETE/api/cars/1route param : 1 Suppression de la voiture portant l’identifiant 1

DTO (Data Transfert Object)

Il s’agit d’un objet représentant un modèle d’entité, parfois plus souple que l’entité elle même, c’est celui qui est généralement transmis du serveur au client. Il peut être plus léger (propriétés inutiles masquées côté client) et différent du modèle de base de données par exemple (les propriétés peuvent ne pas porter exactement le même nom).

Création d’un DTO générique

Dans le souci de bien faire et de respecter les principes fondamentaux du REST, chaque ressource est accessible via une propriété discriminante, nous allons créer une interface IBaseDto ayant pour seule propriété un identifiant.

ibase.dto.ts

export interface IBaseDto {
  id?: number | string;
}

Création du DTO Car

Le DTO représentant ma voiture doit se conformer, à défaut de l’implémenter, à l’interface IBaseDto afin de certifier la présence dans ses propriétés d’un identifiant le reconnaissant parmi ses congénères.

2 façons de procéder :

car.dto.ts

export class CarDto implements IBaseDto {
  id: number;
  name: string;
  brand: string;
  color: string;
}

Ou bien :

car.dto.ts

export interface CarDto {
  id: number;
  name: string;
  brand: string;
  color: string;
}

« Attention ! » me direz-vous, « Tu crées une interface sans I« . En effet, j’ai choisi de créer une structure et d’en vérifier sa conformité, son type. Ce ne sera donc pas une classe. Là ou TypeScript peut porter à confusion, c’est au niveau des interfaces.

Les interfaces sont utilisées dans TypeScript pour effectuer la vérification de type, elles sont présentes jusqu’à la transpilation et disparaissent en production. Les interfaces ne peuvent pas non plus être utilisées pour instancier ; elles font aussi office d’interface au sens interface du modèle objet.

Les classes issues d’ES6 sont également utilisées pour la vérification de type, mais elles restent après la transpilation et génèrent du code en production. En outre, ils sont utilisées pour instancier.

Attention tout de même : dans certains cas, il est nécessaire d’utiliser des classes (ex: objet instancié). C’est un confort que je m’accorde et vous présente car cela peut être utile, et dans un souci d’optimisation de tout, c’est toujours ça de gagné. C’est une approche réfléchie.

Définition du type de réponse

Dans mon cas, et parce que j’aime bien faire les choses, je voudrais que mes APIs me retournent un peu plus qu’un simple objet. Je le wrappe donc dans une structure me permettant d’en savoir un peu plus sur ma réponse. Pensez à implémenter ce genre de choses dans vos développements. Ce sera utile pour vous et pour les consommateurs de vos APIs. De même, la plupart des APIs publiques proposent ce genre d’informations. Il faudra donc implémenter ce mécanisme avec des réponses customisées côté serveur.

/i\ Au passage, petit coup de pub pour AspNetBoilerPlate qui est un Framework de développement Web tout en un. Il implémente des designs et best practices en tout genre, dont ce genre de réponses custom de façon magique pour ses Applications Services et Controllers. /i\

api-response.dto.ts

export interface ApiResponseDto {
  isSuccess: boolean;
  result: any;
  error: any;
  nbElements: number;
}

Les choses sérieuses maintenant !

Création du service HTTP générique – generic

Générique car il conviendra à la plupart de nos besoins et generic car il fait intervenir la mécanique des generics du modèle objet avec les fameux type T. Cela permet de rendre la classe utilisable par tous les DTOs implémentant l’interface IBaseDto et sa propriété id. Les autres objets ne sont pas acceptés.

base-dto-api.service.ts

@Injectable({
  providedIn: 'root'
})
// Le service accepte uniquement les objets implémentant l'interface IBaseDto
export abstract class BaseDtoApiService<T extends IBaseDto> { 
  // Propriété sous forme de getter pour permettre plus de souplesse et ne pas brider nos URLs à une seule et unique URL au moment de la transpilation. Elle peut être redéfinie au runtime
  protected get _baseUrl(): string {
    // Le remoteServiceBaseUrl est défini dans mon fichier de configuration
    return `${AppConfig.appSettings.apis.remoteServiceBaseUrl}/api/${this._endpoint}`;
  }

  constructor(
    // Le client HTTP fourni par Angular
    protected readonly _httpClient: HttpClient,
    // Le serializer de données
    protected readonly _apiSerializer: BaseDtoApiSerializer<T>,
    // Le service de mise en cache
    protected readonly _httpCachingService: HttpCachingService,
    // Le point d'entrée de l'API ciblée
    private readonly _endpoint: string
  ) {}

  // Génère une query string en fonction des paramètres passés sous la forme de ?color=red
  private getQueryString(queryParams: any): string {
    // https://www.npmjs.com/package/query-string
    return queryParams ? `?${queryString.stringify(queryParams)}` : '';
  }

  //#region CONVERT
  // Extrait le résultat de l'objet ApiResponseDto et le convertit en type T via le serializer
  protected convertData(response: ApiResponseDto): T {
    return this._apiSerializer.fromJson(response.result) as T;
  }

  // Extrait les résultats de l'objet ApiResponseDto et les convertit en tableau de type T via le serializer
  protected convertDataList(response: ApiResponseDto): T[] {
    return response.result.map((result: any) => this._apiSerializer.fromJson(result) as T);
  }

  //#endregion

  //#region CREATE
  public add(item: T): Observable<T> {
    return this._httpClient.post(this._baseUrl, this._apiSerializer.toJson(item)).pipe(map((data: any) => this.convertData(data)));
  }

  //#endregion

  //#region READ
  // Récupère une ressource par son identifiant
  public get(id: number | string, isInCache = false): Observable<T> {
    const url = `${this._baseUrl}/${id}`;

    if (isInCache) this._httpCachingService.addCachingUrl(url);

    return this._httpClient.get<T>(url).pipe(map((data: any) => this.convertData(data)));
  }

  // Récupère une ressources en fonction des paramètres passés
  public getWithParams(queryParams: any, isInCache = false): Observable<T> {
    const url = `${this._baseUrl}${this.getQueryString(queryParams)}`;

    if (isInCache) this._httpCachingService.addCachingUrl(url);

    return this._httpClient.get<T>(url).pipe(map((data: any) => this.convertData(data)));
  }

  // Récupère une ou plusieurs ressources en fonction des paramètres passés
  public getManyWithParams(queryParams: any, isInCache = false): Observable<T[]> {
    const url = `${this._baseUrl}${this.getQueryString(queryParams)}`;

    if (isInCache) this._httpCachingService.addCachingUrl(url);

    return this._httpClient.get<T[]>(url).pipe(map((data: any) => this.convertDataList(data)));
  }

  public getAll(isInCache = false): Observable<T[]> {
    if (isInCache) this._httpCachingService.addCachingUrl(this._baseUrl);

    return this._httpClient.get<T[]>(this._baseUrl).pipe(map((data: any) => this.convertDataList(data)));
  }

  //#endregion

  //#region UPDATE
  public update(id: number | string, item: T): Observable<T> {
    return this._httpClient
      .put(`${this._baseUrl}/${id}`, this._apiSerializer.toJson(item))
      .pipe(map((data: any) => this.convertData(data)));
  }

  //#endregion

  //#region DELETE
  public delete(id: number | string): Observable<any> {
    return this._httpClient.delete(`${this._baseUrl}/${id}`);
  }

  //#endregion
}

Les serializers

Dans le meilleur des mondes, les objets retournés par les APIs correspondent strictement à nos DTOs. Mais nous ne vivons pas dans le meilleur des mondes (ce n’est pas moi qui vous l’apprends hein ;-)). Imaginons que la propriété correspondant à CarDto.name se nomme theName, et bien il faudra s’assurer que mes données soient bien renseignées dans les bonnes propriétés de mon DTO. Il va falloir prévoir ce mapping. J’introduis donc une couche supplémentaire avec les serializers.

Comme pour le BaseDtoApiService, nous allons créer un BaseDtoApiSerializer. De base, mon serializer retourne et transforme les objets entrants et sortants en faisant correspondre les propriétés.

base-dto-api.serializer.ts

export abstract class BaseApiDtoSerializer<T extends IBaseDto> {
  // Convertit l'objet provenant du serveur en objet de type T
  public fromJson(object: any): T {
    return object as T;
  }

  // Convertit l'objet de type T en objet json correspondant à ce que le serveur attend
  public toJson(object: T): any {
    return object;
  }
}

On en a fini avec les generics ? Car ma voiture, elle chauffe pendant ce temps…

Ça vient, ça vient ! Pour utiliser nos Bases avec nos DTOs, rien de plus simple. Il nous suffit de créer un CarApiService et un CarApiSerializer, puis, de les étendre avec leurs Bases. Pour le cas du serializer, c’est ici que nous allons implémenter la gestion particulière du mapping propre à mon CarDto en redéfinissant les méthodes de la classe abstraite parente.

car-api.service.ts

@Injectable({
  providedIn: 'root'
})
export class CarApiService extends BaseDtoApiService<CarDto> {
  constructor(httpClient: HttpClient, apiSerializer: CarApiSerializer, httpCachingService: HttpCachingService) {
    super(httpClient, apiSerializer, httpCachingService, 'cars'); // Définition du endpoint => api/cars
  }
}

car-api.serializer.ts

@Injectable({
  providedIn: 'root'
})
export class CarApiSerializer extends BaseDtoApiSerializer<CarDto> {
  constructor() {
    super();
  }

  // Convertit l'objet provenant du serveur en objet de type T
  public fromJson(object: any): CarDto {
    return {
      id: object.id,
      name: object.theName, // Le fameux mapping
      brand: object.brand,
      color: object.color
    } as CarDto;
  }

  // Convertit l'objet de type T en objet json correspondant à ce que le serveur attend
  public toJson(object: CarDto ): any {
    return {
      id: object.id,
      theName: object.name, // Le fameux mapping
      brand: object.brand,
      color: object.color
    };
  }
}

Utilisation de l’HTTP Interceptor

A quoi sert un interceptor ?

Un interceptor permet d’intercepter les requêtes HTTP entrantes et sortantes afin de les modifier ou d’implémenter une mécanique logicielle particulière. Dans notre cas, vous avez pu constater que tous mes appels HTTP ne sont jamais décorés du Header Content-Type = application/json. Nous aurions pu le faire pour chaque méthode mais l’interceptor est là pour nous simplifier la vie. J’intercepte donc ce qui sort, je rajoute mes Headers HTTP, je peux très bien rajouter des identifiants, une « api key », pour gérer l’authentification et les autorisations etc… Je peux aussi manipuler le corps de ma requête, modifier mon URL, forcer le https et bien plus encore.

Les interceptors sont très puissants. De plus, ils peuvent être chaînés. Vous pouvez donc définir des interceptors ayant leur propre logique et les faire s’exécuter dans l’ordre qu’il vous plaira. Attention cependant, si vous chaînez les interceptors A – B – C, les requêtes suivrons l’ordre A – B – C mais à l’inverse, les réponses suivront l’ordre C – B – A.

J’en profiterais pour implémenter la mise en cache de mes requêtes HTTP GET. Nous verrons plus en détail cette facette dans la section suivante.

custom-http.interceptor.ts

@Injectable({
  providedIn: 'root'
})
export class CustomHttpInterceptor implements HttpInterceptor {
  constructor(
    // Service de routing
    private readonly _router: Router,
    // Service de mise en cache
    private readonly _httpCachingService: HttpCachingService,
    // Service gérant l'animation de chargement (spinner par exemple)
    private readonly _loaderService: LoaderService
  ) {}

  // Créer une réponse HTTP avec un statut particulier à partir d'une erreur
  private createHttpResponseFromHttpErrorResponse(httpErrorResponse: HttpErrorResponse, status: number = null): Observable<HttpEvent<any>> {
    return of(
      new HttpResponse({
        headers: httpErrorResponse.headers,
        status: status ?? httpErrorResponse.status,
        statusText: httpErrorResponse.statusText,
        url: httpErrorResponse.url,
        body: httpErrorResponse.error
      })
    );
  }

  // Le point d'entrée de l'intercepteur
  public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Ajout des headers dont le Content-Type
    const newRequest = request.clone({
      url: request.url.replace('http://', 'https://'),
      setHeaders: { 'Content-Type': 'application/json' }
    });

    // Si la requête n'a pas besoin d'être mise en cache (c'est le cas des requêtes non GET ou des requêtes GET que nous ne voulons pas mettre en cache), nous gérons la requête
    if (newRequest.method !== 'GET' || !this._httpCachingService.existsCachingUrl(newRequest.url)) {
      return this.requestHandler(newRequest, next);
    }

    // Dans le cas contraire, nous renvoyons, si elle existe, la réponse de la requête qui se trouve dans le cache
    const cacheEntry = this._httpCachingService.getCacheEntry(newRequest.urlWithParams);

    if (cacheEntry) {
      console.log(
        `HTTP ${newRequest.method} ${newRequest.url} ${newRequest.body ? 'BODY: ' + JSON.stringify(newRequest.body) : ''} (CACHE)`
      );
    }

    // Gestion du cas des requêtes parallèles sur la même URL. Nous renvoyons la requête en cours sinon celle mise en cache
    return cacheEntry ? (cacheEntry instanceof Observable ? cacheEntry : of(cacheEntry.clone())) : this.requestHandler(newRequest, next);
  }

  // Gestion de la requête
  private requestHandler(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const startRequestTime = Date.now();
    let isRequestInSucces = true;

    // Affichons le loader
    this._loaderService.show();

    return next.handle(request).pipe(
      tap((event: HttpEvent<any>) => {
        // Dans le cas d'une réponse sans erreur, nous en profitons pour recycler le cache et mettre en cache si besoin la réponse courante
        if (event instanceof HttpResponse) {
          this._httpCachingService.deleteExpiredCacheEntries();

          if (this._httpCachingService.existsCachingUrl(request.url)) {
            this._httpCachingService.setCacheEntry(request.urlWithParams, event);
          }
        }
      }),
      catchError(
        // Dans le cas d'une réponse en erreur
        (httpErrorResponse: HttpErrorResponse): Observable<HttpEvent<any>> => {
          isRequestInSucces = false;
          let errorMessage: string;

          if (httpErrorResponse.error instanceof ErrorEvent) {
            // Erreur côté client
            errorMessage = httpErrorResponse.error.message;
          } else {
            // Erreur côté serveur
            switch (httpErrorResponse.status) {
              case 401:
                console.error('(CustomHttpInterceptor)', 'Unauthorized request');
                this._router.navigate(['/error/unauthorized']);
                break;
              case 403:
                conosole.error('(CustomHttpInterceptor)', 'Forbidden request');
                this._router.navigate(['/error/forbidden']);
                break;
              case 404:
                // Dans le cas d'une erreur 404, j'ai retourné en plus du code 404 un objet null ou une collection vide. Pour autant, si j'utilise un resolver par exemple, je peux vouloir quand même afficher la page d'information d'une voiture mais indiquer que le modèle n'existe plus plutôt que de rediriger vers une page 404 Not Found ou avoir une page blanche. Je ne veux pas forcément provoquer une erreur parce que la 404 n'est ici, en tant que telle, pas vraiment une erreur, comme le serait une route donnant sur une page non gérée ou inexistante par exemple. Je souhaite donc que mon code continue.
                return this.createHttpResponseFromHttpErrorResponse(httpErrorResponse);
              default:
                errorMessage = httpErrorResponse.message || httpErrorResponse.statusText;
            }
          }
          
          // Retour de l'erreur à l'appelant pour qu'il la gère à sa manière
          return throwError(errorMessage);
        }
      ),
      finalize(() => {
      // Cachons le loader
      this._loaderService.hide();

        // Le petit plus : nous affichons dans la console les informations de la requête et son temps d'exécution
        const elapsedRequestTime = Date.now() - startRequestTime;

        console.log(
          `HTTP ${request.method} ${request.url} ${request.body ? 'BODY: ' + JSON.stringify(request.body) : ''} (${
            isRequestInSucces ? 'SUCCESS' : 'ECHEC'
          } ${elapsedRequestTime}ms)`
        );
      })
    );
  }
}

Déclaration de l’HTTP Interceptor dans App.module.ts

Pour que notre interceptor fonctionne comme il faut, il faut le déclarer dans les providers du fichier app.module.ts. Si vous souhaitez les chaîner, il faudra les déclarer dans l’ordre que vous visez.

providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: (appInitializer: AppInitializerService) => appInitializer.init(),
      deps: [AppInitializerService],
      multi: true
    },
    { provide: ErrorHandler, useClass: ErrorHandlerService },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: CustomHttpInterceptor, // A déclarer ici
      multi: true
    },
    { provide: LOCALE_ID, useValue: 'fr-FR' }
  ]

La mise en cache des requêtes HTTP

La mise en cache des requêtes HTTP permet de gagner de nombreuses secondes, et ainsi rendre l’application plus légère et réactive, mais aussi d’alléger la charge serveur, notamment lorsque l’on appelle régulièrement la même URL et que la réponse est toujours la même.

Nous pouvons imaginer vouloir mettre la requête de récupération de la fiche d’information d’une voiture en cache. En effet, les spécifications d’une voiture mise sur le marché ne changent pas tous les 4 matins. Ce n’est qu’un exemple. Le plus flagrant serait le cas des pizzas. Les recettes millénaires ne changent pas, la margarita reste la margarita, on est d’accord ? ;-D.

Les mécaniques de cache peuvent être implémentées côté serveur mais nous pouvons aussi le faire côté client et agir plus finement.

Comment ça marche ?

Rappelez-vous, les méthodes HTTP GET de mon BaseDtoApiService permettent la mise en cache grâce au paramètre isInCache. Ce paramètre, s’il est à true, inscrit l’URL dans la liste des URLs à suivre via le HttpCachingService.

  public get(id: number, queryParams: any = null, isInCache = false): Observable<T> {
    const url = `${this._baseUrl}/${id}${this.getQueryString(queryParams)}`;

    if (isInCache) this._httpCachingService.addCachingUrl(url); // Ici

    return this._httpClient.get<T>(url).pipe(map((data: any) => this.convertData(data)));
  }

Nous allons maintenant créer ce service.

http-caching.service.ts

@Injectable({
  providedIn: 'root'
})
export class HttpCachingService {
  private _cacheEntries = new Map<string, CacheEntry>(); // La liste des réponses mises en cache
  private _cachingUrls: string[] = []; // La liste des URLs à suivre

  constructor() {}

  //#region CACHING URLS
  // Vérification si l'URL à suivre est déjà enregistrée
  public existsCachingUrl(url: string): boolean {
    return this._cachingUrls.indexOf(url) > -1;
  }

  // Ajout d'une URL à suivre
  public addCachingUrl(url: string): void {
    if (!this.existsCachingUrl(url)) {
      this._cachingUrls.push(url);
    }
  }

  //#endregion

  //#region CACHE ENTRIES
  // Vérification du TTL (Time To Live) des réponses de requêtes mises en cache
  private hasCacheEntryExpired(cacheEntry: CacheEntry): boolean {
    return Date.now() > cacheEntry.expirationTime;
  }

  // Nettoyage du cache. Suppression des données expirées
  public deleteExpiredCacheEntries(): void {
    this._cacheEntries.forEach((cacheEntry: CacheEntry) => {
      if (this.hasCacheEntryExpired(cacheEntry)) {
        this._cacheEntries.delete(cacheEntry.url);
      }
    });
  }

  // Nettoyage du cache complet sans compromis
  public deleteCacheEntries(): void {
    this._cacheEntries.clear();
  }

  // Récupération de la réponse d'une requête mise en cache en fonction de son URL
  public getCacheEntry(urlWithParams: string): HttpResponse<any> | null {
    const cacheEntry = this._cacheEntries.get(urlWithParams);

    return cacheEntry ? (this.hasCacheEntryExpired(cacheEntry) ? null : cacheEntry.response) : null;
  }

  // Mise en cache de la réponse d'une requête à partir de son URL
  public setCacheEntry(urlWithParams: string, response: HttpResponse<any>): void {
    const cacheEntry: CacheEntry = {
      url: urlWithParams,
      response,
      expirationTime: Date.now() + AppConfig.appSettings.apis.cacheAge // Configuration du TTL
    };

    this._cacheEntries.set(urlWithParams, cacheEntry);
  }

  //#endregion
}

// Définition d'une entrée dans le cache
export interface CacheEntry {
  url: string;
  response: HttpResponse<any>;
  expirationTime: number;
}

Cycle de vie

Tant que que l’application Angular vit et que la page courante n’est pas rafraîchie, les requêtes HTTP mises en cache ne se déclenchent qu’une seule fois. Les appels suivants iront chercher la donnée dans le cache, sous réserve que lors de l’appel à la fonction, le paramètre isInCache soit à true et que le TTL (Time To Live) de l’entrée située dans le cache n’ait pas expiré.

Vous pouvez aller plus loin en gérant la mise en cache dans le sessionStorage / localStorage du navigateur pour plus de persistance des données en supportant le rafraîchissement de la page ou la fermeture du navigateur par exemple. Dans mon cas, je pars sur une mise en cache simple. Rafraîchir ma page, et donc l’application Angular, remet les compteurs à zéro.

Exemple

export class Test {
  constructor(private readonly _carApiService: CarApiService)
  {
    this.test(); // Test 1
    this.test(); // Test 2
  }

  private test(): void {
    const idCar = 1;

    this._carApiService.get(1, true).pipe(
      map((car: CarDto) => console.log(`La ${car.brand} ${car.name} est de couleur ${car.color}`)),
      catchError((err) => {
        console.error(`An error occured when retrieving the car with id: ${idCar}`;
      })
    );
  }
}
Console 1er appel :
HTTP GET https://mon-site.fr/api/cars/1 (SUCCESS) 300ms
La Renault Clio est de couleur Rouge

Console 2ème appel :
HTTP GET https://mon-site.fr/api/cars/1 (CACHE)
HTTP GET https://mon-site.fr/api/cars/1 (SUCCESS) 0ms
La Renault Clio est de couleur Rouge  

Débrief

C’est top tout ça, ça m’aide vraiment beaucoup, je peux gérer des pizzas et des voitures correctement sans me prendre la tête et de façon efficace. Ça couvre tous mes cas d’utilisation, SAUF, les cas où je ne veux pas travailler avec des entités mais faire des requêtes HTTP simple, ou retourner autre chose que des DTOs par exemple. On s’éloigne cependant un peu du REST.

Dans ce cas je vous propose 3 solutions.

Enrichir votre CarApiService

Si vous travaillez sur une entité ou en rapport avec le domaine de l’entité, vous pouvez définir une nouvelle méthode dans votre CarApiService. Par exemple, si vous voulez connaitre le nombre de voitures rouges et ne recevoir que le nombre de voitures rouges mais pas la liste des DTOs des voitures rouges.

@Injectable({
  providedIn: 'root'
})
export class CarApiService extends BaseDtoApiService<CarDto> {
  constructor(httpClient: HttpClient, apiSerializer: CarApiSerializer, httpCachingService: HttpCachingService) {
    super(httpClient, apiSerializer, httpCachingService, 'cars');
  }

  public getNbRedCars(): Observable<number> {
    return this._httpClient.get<boolean>(`${this._baseUrl}/nbRedCars`).subscribe(data => data);
  }
}

Faire vous-même les requêtes

À l’ancienne quoi.

this._httpClient.get<boolean>(url, params).subscribe(
  data => {
    // doSomethingWithData
  }
);

Créer un BaseApiService

Ici, ce n’est plus un BaseDtoApiService mais un BaseApiService. Il en reprend cependant quelques principes comme la construction de l’URL à appeler et le HttpCaching mais vous devrez gérer vous-même la sérialisation et il n’y aura plus de dépendance forte avec un DTO en particulier.

@Injectable({
  providedIn: 'root'
})
export abstract class BaseApiService {
  // Propriété sous forme de getter pour permettre plus de souplesse et ne pas brider nos URLs à une seule et unique URL au moment de la transpilation. Elle peut être redéfinie au runtime
  protected get _baseUrl(): string {
    return `${AppConfig.appSettings.apis.remoteServiceBaseUrl}/api/${this._endpoint}`;
  }

  constructor(
    // Le client HTTP fourni par Angular
    protected readonly _httpClient: HttpClient,
    // Le service de mise en cache
    protected readonly _httpCachingService: HttpCachingService,
    // Le point d'entrée de l'API ciblée
    private readonly _endpoint: string
  ) {}
}

Il ne vous restera plus qu’à implémenter ce service dans un service enfant et déclarer vous-même vos fonctions tout en profitant de l’URL auto-générée et du service de mise en cache.

Le mot de la fin

Vous avez maintenant de quoi gérer efficacement vos appels HTTP et faire du CRUD en mode REST pour les entités que vous souhaitez gérer. Vous allez gagner en productivité et robustesse..

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.