NestJS Backend Integration Guide
Build a secure NestJS proxy for KhaleejiAPI so your web or mobile app never exposes its API key client-side. This guide covers secure proxy controllers, standardAuthorization: Bearer headers, high-performance HttpServicewiring, and production-safe handling for401, 403, 429, and upstream 5xx responses.
Authorization: Bearer header from an environment variable.Recommended: reuse the official NestJS SDK
Prefer @khaleejiapi/nestjs over maintaining your own client, filter, and regional DTOs. It exports KhaleejiApiModule, KhaleejiApiService,KhaleejiApiExceptionFilter, and EmiratesIdDto,SaudiIdDto, and GccPhoneDto. Install the packages below and save the documented environment file. For this path, use these module and controller files instead of the advanced custom examples that follow.
This SDK requires NestJS 12: its @nestjs/common and@nestjs/core peer dependencies are ^12.1.0, and@nestjs/axios must satisfy ^12.0.1. Use compatible@nestjs/config versions too. For NestJS 10 or 11 applications, use the custom HttpService alternative below instead; do not bypass peer-dependency checks to install the SDK.
# Run in your existing NestJS applicationpnpm add @khaleejiapi/nestjs @nestjs/axios @nestjs/config axios rxjs class-validator class-transformer# .env (SDK path: requests already include /v1)KHALEEJI_API_KEY=your_api_key_hereKHALEEJI_API_BASE_URL=https://khaleejiapi.dev/apiKHALEEJI_API_TIMEOUT_MS=5000The SDK base URL ends in /api because its request paths include/v1. Do not use the custom client's /api/v1 base URL here.
// src/app.module.ts (recommended SDK path)import { Module } from "@nestjs/common"import { ConfigModule } from "@nestjs/config"import { KhaleejiIntegrationModule } from "./khaleeji/khaleeji.module" @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: ".env" }), KhaleejiIntegrationModule, ],})export class AppModule {}// src/khaleeji/khaleeji.module.ts (recommended SDK path)import { Module } from "@nestjs/common"import { ConfigModule, ConfigService } from "@nestjs/config"import { KhaleejiApiModule } from "@khaleejiapi/nestjs" import { AuthModule } from "../auth/auth.module"import { KhaleejiApiController } from "./khaleeji.controller" @Module({ imports: [ AuthModule, KhaleejiApiModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: (...args: unknown[]) => { const config = args[0] if (!(config instanceof ConfigService)) throw new Error("ConfigService is required") return { apiKey: config.getOrThrow<string>("KHALEEJI_API_KEY"), baseUrl: config.get<string>("KHALEEJI_API_BASE_URL", "https://khaleejiapi.dev/api"), timeout: Number(config.get("KHALEEJI_API_TIMEOUT_MS", "5000")), keepAlive: true, maxRateLimitRetries: 0, } }, }), ], controllers: [KhaleejiApiController],})export class KhaleejiIntegrationModule {}Supply your application's authentication/authorization and per-user throttle guards as described below; SDK authentication protects the upstream key, not your public proxy. The SDK handles connection pooling and Authorization headers. Its request() returns the envelope's data only, so these controllers wrap it in{ data }; upstream metadata is not retained.
// src/khaleeji/khaleeji.controller.ts (recommended SDK path)import { Body, Controller, Post, UseFilters, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common"import { IsEmail } from "class-validator"import { EmiratesIdDto, KhaleejiApiExceptionFilter, KhaleejiApiService } from "@khaleejiapi/nestjs" import { AppAuthGuard } from "../auth/app-auth.guard"import { PerUserThrottleGuard } from "../auth/per-user-throttle.guard" class ValidateEmailDto { @IsEmail() email!: string} @Controller("api/khaleeji")@UseGuards(AppAuthGuard, PerUserThrottleGuard)@UsePipes(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }))@UseFilters(KhaleejiApiExceptionFilter)export class KhaleejiApiController { constructor(private readonly client: KhaleejiApiService) {} @Post("email/validate") async validateEmail(@Body() body: ValidateEmailDto) { return { data: await this.client.request<unknown>({ method: "POST", url: "/v1/email/validate", data: { email: body.email }, }) } } @Post("emirates-id/validate") async validateEmiratesId(@Body() body: EmiratesIdDto) { return { data: await this.client.request<unknown>({ method: "POST", url: "/v1/emirates-id/validate", data: { ids: [body.id] }, }) } }}Use JSON POST bodies for personal validation values, not query strings that can enter access logs, tracing URLs, or cache keys. Email accepts { email }, phone accepts { phone }, and the Emirates ID POST alias accepts the batch body { ids: [id] } and returns results plus a summary. The SDK convenience validators currently use GET; use request() as shown instead. Disable its automatic 429 retries here to avoid unbounded waits or replaying arbitrary POST writes. The SDK filter preserves Nest HTTP exceptions and upstream status codes; use the advanced filter below if you need explicit timeout mapping, rate-limit header forwarding, and sanitized network-error messages. POST alone does not prevent body logging: redact request/response bodies, credentials, and Axios error configs in application logs and APM, and disable caching for these routes.
Advanced alternative: 1. Store the API key in NestJS config
In an existing NestJS application, install the packages used by the examples below. Choose @nestjs/axios and @nestjs/config versions compatible with your NestJS major version. The remaining examples are an optional custom HttpService implementation for teams needing a different retry/error policy; do not register it alongside the SDK path. The SDK package is not needed for this alternative.
# Run in your existing NestJS applicationpnpm add @nestjs/axios @nestjs/config axios rxjs class-validator class-transformerDo not send the key from React, Flutter, React Native, or any browser bundle. Put it in your NestJS environment and inject it into a singleton HttpModule so every upstream request automatically uses the standard Authorization: Bearer format.
# .env (custom HttpService path)KHALEEJI_API_KEY=your_api_key_hereKHALEEJI_API_BASE_URL=https://khaleejiapi.dev/api/v1KHALEEJI_API_TIMEOUT_MS=5000This custom client uses a base URL ending in /api/v1 and appends service paths such as /email/validate. Use only this path's environment example, not the SDK configuration above.
Save .env in the NestJS project root, keep it in .gitignore, and never commit a real API key. Initialize ConfigModule.forRoot() once in your root AppModule to load this file and make ConfigServiceavailable to the HTTP client. Merge these imports into your existing module rather than replacing its other imports, controllers, or providers. If config is already initialized, reuse that registration. Production can inject the same variables through its secret manager instead of a file.
// src/app.module.tsimport { Module } from "@nestjs/common"import { ConfigModule } from "@nestjs/config" import { KhaleejiApiModule } from "./khaleeji/khaleeji.module" @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: ".env" }), KhaleejiApiModule, ],})export class AppModule {}2. Configure HttpService for low-latency GCC traffic
NestJS wraps Axios through @nestjs/axios. Reuse that singleton, enablekeepAlive, and set a short timeout so validation requests to Mumbai stay fast for GCC users behind your own backend. In this setup, Axios' timeout option is the request deadline that application teams should tune.
// src/khaleeji/khaleeji.module.tsimport * as http from "node:http"import * as https from "node:https" import { HttpModule } from "@nestjs/axios"import { Module } from "@nestjs/common"import { ConfigModule, ConfigService } from "@nestjs/config" import { AuthModule } from "../auth/auth.module"import { KhaleejiApiController } from "./khaleeji.controller"import { KhaleejiApiService } from "./khaleeji.service"import { KhaleejiApiExceptionFilter } from "./khaleeji-exception.filter" @Module({ imports: [ AuthModule, ConfigModule, HttpModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: (configService: ConfigService) => ({ baseURL: configService.get<string>( "KHALEEJI_API_BASE_URL", "https://khaleejiapi.dev/api/v1", ), timeout: Number(configService.get("KHALEEJI_API_TIMEOUT_MS", "5000")), headers: { Authorization: "Bearer " + configService.getOrThrow<string>("KHALEEJI_API_KEY"), Accept: "application/json", "Content-Type": "application/json", }, httpAgent: new http.Agent({ keepAlive: true, maxSockets: 100, timeout: 30_000, }), httpsAgent: new https.Agent({ keepAlive: true, maxSockets: 100, timeout: 30_000, }), }), }), ], controllers: [KhaleejiApiController], providers: [KhaleejiApiService, KhaleejiApiExceptionFilter], exports: [KhaleejiApiService],})export class KhaleejiApiModule {}Keep this module server-only. The browser should never know the value ofKHALEEJI_API_KEY.
3. Wrap KhaleejiAPI in a reusable service and proxy controller
A dedicated service keeps controller code thin and makes it easy to reuse one upstream client across email, phone, IBAN, and ID validation routes.
Service
// src/khaleeji/khaleeji.service.tsimport { setTimeout as sleep } from "node:timers/promises"import { HttpService } from "@nestjs/axios"import { Injectable } from "@nestjs/common"import { isAxiosError } from "axios"import { firstValueFrom } from "rxjs" // At most two retries. Never shorten an upstream Retry-After.export function readRetryDelay(error: unknown, retry: number): number | null { if (retry > 2 || !isAxiosError(error)) return null if (![429, 502, 503, 504].includes(error.response?.status ?? 0)) return null const header = error.response?.headers["retry-after"] let delay = 250 * 2 ** (retry - 1) if (header !== undefined && header !== null) { const value = String(header).trim() const seconds = /^[0-9]+$/.test(value) ? Number(value) : null const date = seconds === null ? Date.parse(value) : NaN const requested = seconds !== null ? seconds * 1000 : date - Date.now() if (!Number.isFinite(requested)) return null delay = Math.max(delay, requested) } return delay <= 2000 ? delay : null} interface EmailValidationResponse { data: { email: string valid: boolean deliverabilityScore: number reason: string } meta: { timestamp: string }} @Injectable()export class KhaleejiApiService { constructor(private readonly http: HttpService) {} async validateEmail(email: string): Promise<EmailValidationResponse> { return this.postValidation<EmailValidationResponse>("/email/validate", { email }) } async validatePhone(phone: string) { return this.postValidation<unknown>("/phone/validate", { phone }) } async validateEmiratesId(emiratesId: string) { return this.postValidation<unknown>("/emirates-id/validate", { ids: [emiratesId] }) } private async postValidation<T>(path: string, body: Record<string, string | string[]>): Promise<T> { for (let attempt = 0; ; attempt++) { try { const response = await firstValueFrom(this.http.post<T>(path, body)) return response.data } catch (error) { const delay = readRetryDelay(error, attempt + 1) if (delay === null) throw error await sleep(delay) } } }}Proxy controller
Prerequisite: import your existing AuthModule, which exportsAppAuthGuard and PerUserThrottleGuard. These names are application-specific, not built-in NestJS guards: supply implementations that verify the caller's session/token, authorize access, and enforce a per-authenticated-user quota (for example, 10 requests/minute in a shared store). Do not remove the guards to make the sample run. For cookie sessions, also enforce CSRF protection on POST routes. Expose only the routes your app needs.
// src/khaleeji/khaleeji.controller.tsimport { Body, Controller, Post, UseFilters, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common"import { Transform } from "class-transformer"import { IsEmail, IsString, Matches } from "class-validator" // Your application's existing guards, exported by AuthModule.import { AppAuthGuard } from "../auth/app-auth.guard"import { PerUserThrottleGuard } from "../auth/per-user-throttle.guard"import { KhaleejiApiExceptionFilter } from "./khaleeji-exception.filter"import { KhaleejiApiService } from "./khaleeji.service" class ValidateEmailDto { @IsEmail() email!: string} class ValidateEmiratesIdDto { @Transform(({ value }: { value: unknown }) => typeof value === "string" ? value.replace(/[-\s]/g, "") : value, ) @IsString() @Matches(/^784[0-9]{12}$/, { message: "Use a 15-digit Emirates ID starting with 784; spaces and hyphens are allowed", }) emiratesId!: string} @Controller("api/khaleeji")@UseGuards(AppAuthGuard, PerUserThrottleGuard)@UsePipes(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }))@UseFilters(KhaleejiApiExceptionFilter)export class KhaleejiApiController { constructor(private readonly khaleejiApi: KhaleejiApiService) {} @Post("email/validate") validateEmail(@Body() body: ValidateEmailDto) { return this.khaleejiApi.validateEmail(body.email) } @Post("emirates-id/validate") validateEmiratesId(@Body() body: ValidateEmiratesIdDto) { return this.khaleejiApi.validateEmiratesId(body.emiratesId) }}The controller-scoped ValidationPipe activates the DTO decorators. Emirates IDs accept compact, hyphenated, or space-separated input, matching upstream normalization; the local check validates shape only, not checksum or identity. The upstream alias remains /emirates-id/validate, now using a JSON{ ids: [id] } batch body rather than personal values in a URL.
Your frontend or mobile app now talks only to your own origin:
// Browser or mobile clientawait fetch("/api/khaleeji/email/validate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: "[email protected]" }),}) // Your NestJS backend adds Authorization: ******// before forwarding to https://khaleejiapi.dev/api/v1/email/validate4. Handle 401, 403, 429, and 5xx responses explicitly
The most common NestJS production mistake is collapsing every Axios error into a generic500. Instead, preserve authentication and rate-limit signals from KhaleejiAPI, pass through Retry-After when present, and map transient upstream failures to502 Bad Gateway so your logs stay actionable. Intentional NestJS HttpException responses (including validation and guard failures) keep their original status and body; both Axios timeout codes map to 504.
// src/khaleeji/khaleeji-exception.filter.tsimport { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from "@nestjs/common"import { isAxiosError } from "axios" interface HttpErrorResponse { status: (code: number) => { json: (body: unknown) => void } setHeader: (name: string, value: string | number | readonly string[]) => void} interface KhaleejiErrorPayload { error?: { code?: string message?: string messageEn?: string messageAr?: string }} function normalizeHeaderValue(value: unknown): string | null { if (Array.isArray(value)) return normalizeHeaderValue(value[0]) if (typeof value === "string" || typeof value === "number") return String(value) return null} @Catch()export class KhaleejiApiExceptionFilter implements ExceptionFilter { catch(error: unknown, host: ArgumentsHost) { const response = host.switchToHttp().getResponse<HttpErrorResponse>() if (error instanceof HttpException) { response.status(error.getStatus()).json(error.getResponse()) return } if (!isAxiosError<KhaleejiErrorPayload>(error)) { response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ error: { code: "UNEXPECTED_PROXY_ERROR", message: "Unexpected NestJS proxy error.", }, }) return } if (!error.response) { if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT") { response.status(HttpStatus.GATEWAY_TIMEOUT).json({ error: { code: "KHALEEJI_API_TIMEOUT", message: "KhaleejiAPI timed out before it returned a response.", }, }) return } response.status(HttpStatus.BAD_GATEWAY).json({ error: { code: "KHALEEJI_API_UNREACHABLE", message: "KhaleejiAPI could not be reached from the NestJS proxy.", }, }) return } const status = error.response.status const payload = ( typeof error.response.data === "object" && error.response.data !== null ? error.response.data : {} ) as KhaleejiErrorPayload const headers = error.response.headers ?? {} const retryAfter = normalizeHeaderValue(headers["retry-after"]) const rateLimitLimit = normalizeHeaderValue(headers["x-ratelimit-limit"]) const rateLimitRemaining = normalizeHeaderValue(headers["x-ratelimit-remaining"]) const rateLimitReset = normalizeHeaderValue(headers["x-ratelimit-reset"]) if (retryAfter) response.setHeader("Retry-After", retryAfter) if (rateLimitLimit) response.setHeader("X-RateLimit-Limit", rateLimitLimit) if (rateLimitRemaining) response.setHeader("X-RateLimit-Remaining", rateLimitRemaining) if (rateLimitReset) response.setHeader("X-RateLimit-Reset", rateLimitReset) if (status === HttpStatus.TOO_MANY_REQUESTS) { response.status(status).json({ error: { code: payload.error?.code ?? "KHALEEJIAPI_RATE_LIMITED", message: payload.error?.message ?? "KhaleejiAPI rate limit exceeded.", messageEn: payload.error?.messageEn, messageAr: payload.error?.messageAr, }, }) return } if (status === HttpStatus.UNAUTHORIZED || status === HttpStatus.FORBIDDEN) { response.status(status).json({ error: { code: payload.error?.code ?? "KHALEEJIAPI_REQUEST_REJECTED", message: payload.error?.message ?? "KhaleejiAPI rejected the request.", messageEn: payload.error?.messageEn, messageAr: payload.error?.messageAr, }, }) return } if (status >= HttpStatus.INTERNAL_SERVER_ERROR) { response.status(HttpStatus.BAD_GATEWAY).json({ error: { code: payload.error?.code ?? "KHALEEJIAPI_UPSTREAM_ERROR", message: payload.error?.message ?? "KhaleejiAPI returned a transient upstream error.", messageEn: payload.error?.messageEn, messageAr: payload.error?.messageAr, }, }) return } response.status(status).json({ error: { code: payload.error?.code ?? "KHALEEJIAPI_BAD_REQUEST", message: payload.error?.message ?? "KhaleejiAPI rejected the request payload.", messageEn: payload.error?.messageEn, messageAr: payload.error?.messageAr, }, }) }}The custom service retries only these side-effect-free validation reads, transported as POST for privacy, for 429, 502, 503, and 504: at most two retries (three attempts), with 250/500 ms backoff. It honors Retry-Afteras seconds or an HTTP date. Invalid headers or delays over two seconds stop retries and pass the failure to the filter; never cap an upstream delay and retry early. At the configured five-second timeout, this allows up to 19 seconds across attempts and waits, so choose an appropriate caller deadline. Do not reuse this policy for writes, or retry 400/401/403 and local validation/guard failures.
Error handling checklist
401 Unauthorized- Missing Authorization header- Forgot the Bearer scheme in the Authorization header- Wrong or revoked API key 403 Forbidden- API key lacks the required scope: validation:email for email, validation:phone for phone, or validation:emirates-id for Emirates ID- Project is blocked by organization IP allowlist 429 Too Many Requests- Respect Retry-After- Log X-RateLimit-Limit / Remaining / Reset for dashboards- Retry only idempotent reads with backoff 5xx upstream error- Treat as transient- Return 502 Bad Gateway from your own API- Add a circuit breaker if the same route spikes repeatedlyRegional performance checklist
- Checklist item:Reuse one
HttpServiceinstance per NestJS process instead of creating ad-hoc Axios clients. - Checklist item:Enable
keepAliveon both HTTP and HTTPS agents to reduce repeated TCP/TLS setup cost. - Checklist item:Keep request timeouts around 3-5 seconds for validation endpoints so stuck upstream calls do not exhaust worker threads.
- Checklist item:Retry only safe idempotent reads after
429or transient5xxresponses — not client validation errors. - Checklist item:Log the upstream
X-RateLimit-Limit,X-RateLimit-Remaining, andX-RateLimit-Resetheaders for dashboards and alerting.