Secure KhaleejiAPI with the App Router
Keep your API key on the server and use the official TypeScript SDK from Server Components, Server Actions, or Route Handlers.
KHALEEJI_API_KEY in a Client Component or a NEXT_PUBLIC_* variable. Anything compiled into browser JavaScript can be inspected and copied.1. Quickstart: install the SDK and configure a private key
pnpm add @khaleejiapi/sdkAdd the key to your local .env.local and your hosting provider's server environment. Do not commit the file or prefix the variable with NEXT_PUBLIC_. Create a key with the validation:phone scope for the first example below. The SDK sends it in the Authorization: Bearer header from the server.
# .env.local — keep this file out of source controlKHALEEJI_API_KEY=replace_with_your_server_keyUse a server-only module so an accidental Client Component import fails during development:
// lib/khaleejiapi.tsimport "server-only";import { KhaleejiAPI } from "@khaleejiapi/sdk"; export function getKhaleejiAPI() { const apiKey = process.env.KHALEEJI_API_KEY; if (!apiKey) throw new Error("Missing KHALEEJI_API_KEY"); return new KhaleejiAPI({ apiKey });}2. Call the SDK from a Server Component
When data is needed to render a page, call the SDK on the server instead of adding a proxy endpoint. noStore() opts the request out of Next.js caching, which is useful for user-specific validation results. Authenticate and authorize before loading user-specific data.
// app/phone-check/page.tsx — Server Componentimport { unstable_noStore as noStore } from "next/cache";import { getKhaleejiAPI } from "@/lib/khaleejiapi"; export default async function PhoneCheckPage() { noStore(); const result = await getKhaleejiAPI().validation.validatePhone({ phone: "+971501234567", country: "AE", }); return <p>{result.valid ? "Valid format" : "Invalid format"}</p>;}Why direct browser calls fail
A browser request to khaleejiapi.dev is cross-origin. Browsers enforce CORS, and an Authorization header can trigger a preflight request; a policy or network condition may block the call. Whether or not a particular request currently succeeds, putting the API key in client code exposes it in the bundle and browser developer tools. CORS is not a credential-protection mechanism.
Server Components can call the SDK directly. For browser-driven requests, send data to your own same-origin Route Handler or invoke a Server Action. The API key and upstream request then stay server-side.
3. Use a Route Handler for browser requests
Expose a narrow same-origin endpoint, validate its input, and return only the result fields your UI needs. This example calls the UAE phone-validation API with the SDK.
// app/api/validate/phone/route.tsimport { unstable_noStore as noStore } from "next/cache";import { NextResponse } from "next/server"; import { getKhaleejiAPI } from "@/lib/khaleejiapi"; export async function POST(request: Request) { noStore(); const body: unknown = await request.json().catch(() => null); if ( typeof body !== "object" || body === null || !("phone" in body) || typeof body.phone !== "string" || body.phone.trim().length === 0 || body.phone.length > 32 ) { return NextResponse.json( { error: "Provide a phone number of at most 32 characters." }, { status: 400, headers: { "Cache-Control": "no-store" } }, ); } // Authenticate the user and apply your per-user rate limit here. try { const result = await getKhaleejiAPI().validation.validatePhone({ phone: body.phone, country: "AE", }); return NextResponse.json( { valid: result.valid, type: result.type, country: result.country?.code }, { headers: { "Cache-Control": "no-store" } }, ); } catch { return NextResponse.json( { error: "Phone validation is temporarily unavailable." }, { status: 502, headers: { "Cache-Control": "no-store" } }, ); }}A Client Component can call this route without ever receiving the KhaleejiAPI key:
// In a Client Component: call your own same-origin Route Handler.const response = await fetch("/api/validate/phone", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phone: "+971501234567" }),});const result: unknown = await response.json();4. Use a Server Action for form submissions
Server Actions are server code, but they are still callable by clients. Validate their arguments and perform authorization inside each action; hiding a button is not access control. This action uses the official Emirates ID SDK method.
"use server"; import { unstable_noStore as noStore } from "next/cache"; import { getKhaleejiAPI } from "@/lib/khaleejiapi"; export type EmiratesIdResult = | { valid: boolean; message: string } | { error: string }; export async function validateEmiratesId(id: string): Promise<EmiratesIdResult> { noStore(); // Authenticate the user and authorize this operation on every invocation. if (typeof id !== "string" || id.trim().length === 0 || id.length > 32) { return { error: "Enter an Emirates ID of at most 32 characters." }; } try { const result = await getKhaleejiAPI().validation.validateEmiratesId({ id }); return { valid: result.valid, message: result.message }; } catch { return { error: "Emirates ID validation is temporarily unavailable." }; }}Import the exported action into a Client Component to submit a form. Keep all API-key access in the server-only module:
"use client"; import { useState, type FormEvent } from "react"; import { validateEmiratesId, type EmiratesIdResult,} from "@/app/actions/validate-emirates-id"; export function EmiratesIdForm() { const [result, setResult] = useState<EmiratesIdResult | null>(null); const [pending, setPending] = useState(false); function submit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); const id = new FormData(event.currentTarget).get("id"); if (typeof id !== "string") return; setPending(true); void validateEmiratesId(id) .then(setResult) .catch(() => setResult({ error: "Validation could not be completed." })) .finally(() => setPending(false)); } return ( <form onSubmit={submit}> <label> Emirates ID <input name="id" autoComplete="off" required /> </label> <button type="submit" disabled={pending}> {pending ? "Checking…" : "Validate"} </button> {result && ( <p role="status"> {"error" in result ? result.error : result.message} </p> )} </form> );}validation:phone and/or validation:emirates-id scope it needs. Do not log submitted identifiers or cache personalized results.Validation is not identity proof
Emirates ID validation checks format and its check digit; it is not a government registry lookup. Phone validation reports number format and regional metadata, not ownership or possession. Treat these responses as validation signals, not identity verification.
For a deployable Next.js starter, see the KhaleejiAPI Next.js starter template. See the TypeScript SDK reference, browser and CORS integration guide, Emirates ID API reference, and phone API reference.