Next.js 14 / 15 Guide

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.

1. Quickstart: install the SDK and configure a private key

bash
pnpm add @khaleejiapi/sdk

Add 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.

bash
# .env.local — keep this file out of source control
KHALEEJI_API_KEY=replace_with_your_server_key

Use a server-only module so an accidental Client Component import fails during development:

typescript
// lib/khaleejiapi.ts
import "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.

tsx
// app/phone-check/page.tsx — Server Component
import { 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.

typescript
// app/api/validate/phone/route.ts
import { 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:

typescript
// 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.

typescript
"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:

tsx
"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 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.