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.
CRUD | VERB | URL | DONNEES | ROLE |
C | POST | /api/cars | body param : { name: « Clio », brand: « Renault », color: « Rouge » } | Création d’une voiture |
R | GET | /api/cars | – | Récupération de toutes les voitures |
R | GET | /api/cars/1 | route param : 1 | Récupération de la voiture portant l’identifiant 1 |
R | GET | /api/cars?color=red | query param : color=red | Récupération de toutes les voitures de couleur rouge |
U | UPDATE | /api/cars/1 | body param : { id: 1, name: « Clio », brand: « Renault », color: « Jaune » } | Mise à jour de la voiture portant l’identifiant 1 |
D | DELETE | /api/cars/1 | route 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('https://', '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..
Très bel article, tout est bien expliqué et clair. J’ai cependant une petite question, dans certains cas comme lamines, je dois récupérer des items mais en utilisant une pagination côté serveur car beaucoup trop d’items, du coup mon api retourne un json de ce type:
{
« data »: [
{
« id »: 1,
…
},
{
« id »: 2,
…
},
],
« meta »: {
« pagination »: {
« total »: 14,
« count »: 14,
« per_page »: 20,
« current_page »: 1,
« total_pages »: 1,
« links »: {}
}
}
}
Ce code ne permet pas de récupérer les metas (à moins que je sois passé à côté de qqchose).
Comment est ce que je peux l’adapter pour récupérer ces metas ?
Merci.
Hello, merci pour ce tutorial,
mon seule souci avec cette approche est la rédendance d’hydratation, là on gère seulement un model Car , imagine avec un projet ou il y’a une dizaine,
voici mon petit hack pour créer une hydratation générique:
static hydrate(entity: EntityResourceInterface, json: Object) {
let theEntity = EntityResourceService.createClass(entity.resourceName);
return Object.assign(
theEntity,
JSON.parse(JSON.stringify(json), (key, value) => {
if (
value &&
value[‘resourceName’] &&
!Array.isArray(value) &&
typeof value == ‘object’
) {
return Object.assign(
EntityResourceService.createClass(value[‘resourceName’], value),
value,
);
}
return value;
}),
);
}
static createClass(className: string, value?: any) {
let theObject: any;
switch (className) {
case ‘Quote’:
theObject = new Quote();
break;
case ‘WorkSituation’:
theObject = new WorkSituation();
break;
case ‘User’:
theObject = new User();
on effet, il fallait toujours envoyés le nom de la classe dans l’api, pour pouvoir l’hydrater correctement,
qu’est se qu evous en pensez ?
Salut, merci pour ton retour !
Mon code se prête à un cas relativement générique, d’où le titre. Je manipule des structures plutôt simples. Tu devrais à mon avis jouer avec le ApiResponseDto qui prend tout comme dans ton cas une propriété nbElements (=meta.count). Il te suffirait de modifier ApiResponseDto pour qu’elle réponde à ton besoin pour y intégrer ta propriété meta et agir en conséquence côté serveur pour les renseigner:
export interface ApiResponseDto {
isSuccess: boolean;
data: any; => ici le CardDto ou ta liste d’objets à toi
error: any;
meta: MetaDto;
}
export interface MetaDto {
paginatation: PaginationDto
}
export interface PaginationDto {
total: number;
count: number;
per_page: number;
current_page: number;
total_pages: number;
links: any;
}
Bonjour, est-ce en référence aux Serializers ? Si oui, le toJson ou le fromJson ? Si j’ai bien compris ce que fait ton code, ce que tu propose revient a pousser encore plus le côté générique et de ne faire qu’un serializer comme mon BaseApiDtoSerializer tout simplement et ça ne répond pas aux cas spécifiques de mapping de propriétés différentes entre les modèles de données serveur et front. Mon cas en exemple est simple mais on pourrait imaginer dans le toJson, comme dans le fromJson, d’appeler un service lambda pour traduire une donnée à affecter à telle ou telle propriété d’une façon X ou Y.