HTTP layer and services
Service conventions
A service is a thin @Injectable wrapper over one backend concern. The canonical
shape:
@Injectable()
export class SupplierEmailPreferencesService {
private readonly apiUrl: string;
constructor(private appConfig: AppConfigService, private http: HttpClient) {
this.apiUrl = appConfig.env.internalApiUrl; // never hardcoded
}
getPreferences(supplierKey: string): Observable<EmailPreference[]> {
return this.http
.get<EmailPreferenceDto[]>(`${this.apiUrl}suppliers/${supplierKey}/email_preferences`)
.pipe(map((dto) => dto.map(toEmailPreference))); // shape mapping in the service
}
}
Rules:
-
Base URLs come from
AppConfigService.appConfig.env.<someApiUrl>— one of the ~15 keys inIEnvironment. Never a literal host. -
Return
Observable<T>. No promises, with one exception:FeatureFlagsServiceusesasync/awaitand.toPromise()internally because flag initialisation is awaited by a guard. -
Map the response shape inside the service. Components should receive models, not raw API payloads.
-
One concern per service.
SupplierServiceis the counter-example — it has grown to cover details, tokens, reports, balances, advances, health and statements; do not extend it further.
Base URL keys and their meaning
env key |
Backend |
|---|---|
|
The primary internal API ( |
|
The CLI service ( |
|
Supplier widget endpoints used by onboarding. |
|
Supplier auth tokens and SMS number provisioning. |
|
CRM activity (notes, tasks, files) and global settings. |
|
Domain event stream and the Elasticsearch changelog. |
|
Event rules v1 plus its Elasticsearch index. |
|
Outbound email/SMS. |
|
B2B task manager — long-running jobs and the SSE progress stream. |
|
Four document/file backends, used by different features. |
|
Marketplace service (supplier-marketplace data). |
|
Automatic underwriting tool — real-time validation and UW data. |
|
The EVA micro-front-end origin (iframe |
|
Supplier self-service site (login-as-supplier) and referral link base. |
Which service uses which is enumerated in services and endpoints; the wider picture is in the integration landscape.
The interceptor chain
Two interceptors are registered in AppModule, in this order:
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: FetchedRequestsInterceptor, multi: true },
and a third is contributed by NgHttpLoaderModule via useExisting.
AuthInterceptor
src/app/auth/auth-interceptor.ts
-
Reads the
authslice withtake(1)on every request. -
If
hasCorrectToken(authState), clones the request with theX-Session-Token: <jwt>header. Header name is exported asAUTH_HEADER_NAME. -
Catches
HttpErrorResponse; on401with a token present it logs and dispatchesLogout({ message: 'Auth token expired, please try to login again.' }), then rethrows. -
If there is no valid token, the request passes through unmodified — which is how the login call itself works (it sends its own
Authorization: Basicheader).
|
Because the interceptor selects the store per request, an in-flight token change is
picked up by subsequent requests but not by requests already dispatched. Nothing
refreshes a token: expiry is detected by |
FetchedRequestsInterceptor
src/app/shared/interceptors/fetched-requests.interceptor.ts dispatches
FetchRequest(req) for every request and forwards it untouched. The errors
reducer keeps the last 50 requests, JSON.stringify-ed, in state.
This is purely a debugging aid — it is what the error-handling UI shows as recent request context. It also means every request body passes through the store, so avoid dispatching sensitive payloads through paths you would not want in a DevTools snapshot.
PendingInterceptorService
src/app/shared/http-loader/pending-interceptor.service.ts (a vendored
ng-http-loader):
-
Counts in-flight requests and pushes
true/falsetopendingRequestsStatus, which drivesSpinnerComponent/PreloaderDirective. -
Additionally toasts any response with
status >= 500as"<status>: <message>", then rethrows.
So a 5xx produces a toast here, and any error the calling code passes to
ErrorVisualizationService produces a second one. Duplicate toasts on server
errors are expected behaviour, not a bug to chase.
CachingInterceptor — present but not registered
src/app/shared/cache/caching-interceptor.ts implements read-through caching
for GET requests against HttpCacheAbstract. It is not in AppModule’s
`HTTP_INTERCEPTORS list, so it is inactive globally; HttpCacheAbstract →
HttpCacheLocalStorage is still provided and can be injected directly.
If you enable it, note the 5-minute TTL and MD5 cache key in
HttpCacheLocalStorage, and that it caches all GETs including permission- and
supplier-scoped ones. See
caching and persistence.
Service abstractions and mocks
Eleven abstract base classes define seams that specs and alternative implementations plug into:
| Abstract class | Live implementation |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Alongside these, ~30 services ship a *-mock.service.ts sibling
(supplier-mock.service.ts, crm-mock.service.ts, tasks-mock.service.ts,
payment-config-mock.service.ts, b2b-document-mock.service.ts, …). They are the
test doubles; specs provide them by token. They must never be imported from
production code paths — see testing.
The ServiceUpdatePartialInterface
(shared/interfaces/service-update-partial-interface/) declares
saveItemPart(itemKey, itemPart, …params). Services that back the supplier
workspace’s card-by-card editing implement it, which is what lets the
UPDATE_MODEL_PART effect call any of them uniformly. See
partial editing.
Error mapping
Services do not surface raw HttpErrorResponse objects to components. Three
patterns are in use, oldest first:
| Pattern | Where |
|---|---|
|
Older services ( |
|
Repository services ( |
Return an |
Store-backed flows. The component reads the error out of state. |
For user-facing presentation, everything funnels through
ErrorParserService → ErrorVisualizationService, which understands the internal
API’s error envelope (error.meta.errors[].message) as well as plain
{ message } shapes. Details in error handling.
Request shaping conventions
- Query parameters
-
Built with
HttpParamsand.set()/.append()chaining, never string concatenation. Grid-driven endpoints takelimit/offsetplus arbitrary filter keys (BalanceApprovalService.getDatacopies aDataTableQueryParamsobject into params, skippinglimit/offset). - Cache-busting
-
refreshTimestamp=Date.now()is appended when a caller asks for a refresh (SupplierService.fetchSupplierDetails). - Custom headers
-
Several backends need extra headers, set per service rather than in an interceptor:
X-InstanceId(event rules, global settings — value fromenv.instanceId, 8001),X-EventLogger-InstanceId,X-EventLogger-Type,X-EventLogger-TargetUUID,X-Actions-TargetUUID(CRM).CrmServicekeeps the target-UUID header in sync by subscribing to the store in its constructor. - File uploads
-
FormDatabuilt by a shared helper (BatchImportService.getFormData), posted to*_csv_uploadendpoints. - Downloads
-
responseType: 'text'for report dumps,file-saverfor writing the file. SeeCsvExportServiceandPaymentBatchDownloaderService.
|
|