Custom headers between frontend and BFF
In the SHOPin storefront accelerator, the BFF client’s central wrapper already forwards x-data-source, Accept-Language, and cookies (see Communication between presentation and BFF). When you need additional headers (for example x-user-id, experiment bucket, tenant id), extend both the presentation client and the BFF so the value is sent, read per request, and injectable in NestJS services and data sources.
The steps below use x-user-id as a walkthrough. Paths, module names, and snippets are illustrative—match your branch’s apps/presentation/lib/bff and apps/bff/src layout.
Flow
- Presentation — Merge the header into the default headers wherever the BFF client builds requests (the same area used by
bff-fetch-server.ts/bff-fetch-client.tsand the central wrapper). - BFF — Middleware reads the header and stores it on the request object.
- BFF — A request-scoped provider exposes the value via an injection token.
- BFF — A module registers the provider;
app.module.ts(or equivalent) imports the module and applies the middleware on the right routes. - Integrations — Data source or client code injects the token and uses the value (outbound API calls, filtering, logging, etc.).
Only propagate headers you trust on the server side (e.g. set or normalised in your own backend/session layer); do not treat client-supplied identifiers as authentication by themselves.
1. Presentation: send the header
Extend the BFF fetch path so the header is included with existing defaults (Content-Type, language, data source, cookies).
// Illustrative: merge into the layer that builds default headers for BFF calls.
export async function bffFetch(
baseUrl: string,
path: string,
options?: BffFetchOptions,
locale?: string
): Promise<Response> {
// ... existing logic (build URL, accept-language, etc.) ...
const acceptLanguageHeader = buildAcceptLanguage(locale)
const defaultOptions: BffFetchOptions = {
...options,
headers: {
'Content-Type': 'application/json',
[LANGUAGE_HEADER]: acceptLanguageHeader,
...getDataSourceHeader(cookies),
'x-user-id': getUserIdFromContext(),
...(options?.headers as Record<string, string> | undefined),
},
}
return fetch(url, defaultOptions)
}
getUserIdFromContext() (or your helper) must be safe on server and client (session/request on the server; cookie or store in the browser). Omit the header when there is no value.
2. BFF: middleware
Read the header and attach it to the request for downstream code.
// apps/bff/src/user-id/user-id.middleware.ts (illustrative path)
import { Injectable, NestMiddleware } from '@nestjs/common'
import { Response, NextFunction } from 'express'
export interface UserIdRequest {
userId?: string
}
@Injectable()
export class UserIdMiddleware implements NestMiddleware {
use(req: UserIdRequest, res: Response, next: NextFunction) {
const userId = req.headers['x-user-id']?.toString()
req.userId = userId
next()
}
}
Extend Express Request typings if you attach custom fields (namespace augmentation or a dedicated interface).
3. BFF: request-scoped provider
// apps/bff/src/user-id/user-id.provider.ts
import { Injectable, Inject, Scope } from '@nestjs/common'
import { REQUEST } from '@nestjs/core'
import type { UserIdRequest } from './user-id.middleware'
export const USER_ID_TOKEN = 'USER_ID_TOKEN'
@Injectable({ scope: Scope.REQUEST })
export class UserIdProvider {
constructor(@Inject(REQUEST) private readonly request: UserIdRequest) {}
getUserId(): string | undefined {
return this.request.userId
}
}
Scope.REQUEST keeps one provider instance per HTTP request.
4. BFF: module
// apps/bff/src/user-id/user-id.module.ts
import { Global, Module } from '@nestjs/common'
import { UserIdProvider, USER_ID_TOKEN } from './user-id.provider'
import { UserIdMiddleware } from './user-id.middleware'
@Global()
@Module({
providers: [
UserIdMiddleware,
{
provide: USER_ID_TOKEN,
useClass: UserIdProvider,
},
],
exports: [USER_ID_TOKEN],
})
export class UserIdModule {}
Wire middleware in the app module (next step).
5. BFF: register in the app
// apps/bff/src/app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'
import { UserIdModule } from './user-id/user-id.module'
import { UserIdMiddleware } from './user-id/user-id.middleware'
@Module({
imports: [
// ... other modules
UserIdModule,
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(UserIdMiddleware)
.forRoutes('*')
}
}
Use a narrower forRoutes(...) if only some controllers need the header.
6. Data source: consume the token
Inject the provider where integration code runs (mock client, Commercetools client, etc.).
// Illustrative factory wiring (paths vary by integration)
import { Global, Module } from '@nestjs/common'
import { USER_ID_TOKEN } from '../user-id/user-id.provider'
import type { UserIdProvider } from '../user-id/user-id.provider'
class MockApi {
private readonly userId?: string
constructor(userIdProvider?: UserIdProvider) {
this.userId = userIdProvider?.getUserId()
}
getUserId(): string | undefined {
return this.userId
}
}
export const MOCK_API = 'MOCK_API'
@Global()
@Module({
providers: [
{
provide: MOCK_API,
inject: [USER_ID_TOKEN],
useFactory: (userIdProvider: UserIdProvider) =>
new MockApi(userIdProvider),
},
],
exports: [MOCK_API],
})
export class MockClientModule {}
Summary
| Step | Where | What |
|---|---|---|
| 1 | apps/presentation | Add header next to existing BFF default headers. |
| 2 | apps/bff | Middleware copies header → req. |
| 3 | BFF | Request-scoped provider + token. |
| 4 | BFF | Module exports token. |
| 5 | BFF | AppModule imports module, applies middleware. |
| 6 | integrations/* / BFF | Inject token; use value in clients or services. |
For built-in behaviour (language, data source, cookies), rely on the central wrapper. For new headers, keep presentation and BFF changes paired so behaviour stays testable and consistent.