CORS & browser integration
KhaleejiAPI is a server-to-server API. Browsers cannot (and should not) call it directly. This guide shows the proxy patterns we recommend for Next.js, Express, and Rails so your end users get the data they need without leaking your API key.
NEXT_PUBLIC_ /VITE_ /REACT_APP_ - is public. A leaked key can be revoked from the dashboard, but quota and any spend are yours.Why direct browser calls fail
KhaleejiAPI does not send Access-Control-Allow-Origin on successful responses, which is intentional. CORS is what stops a malicious page in another tab from calling our API on your behalf using a leaked key. If your fetch from the browser succeeds today, it is almost certainly hitting a same-origin proxy you (or a framework) already set up - and you should keep it that way.
// What happens if you call us directly from the browser:fetch("https://khaleejiapi.dev/api/v1/iban/AE070331234567890123456", { headers: { "x-api-key": "kh_live_..." } // BAD: key is in your bundle})// Browser console:// Access to fetch at 'https://khaleejiapi.dev/api/v1/iban/...' from origin// 'https://your-app.com' has been blocked by CORS policy: No// 'Access-Control-Allow-Origin' header is present on the requested resource. The specific error wording differs slightly between Chromium, Firefox, and Safari, but every browser blocks the response. There is no configuration on our side that will make this work.
The proxy pattern
Put a small route on the same origin as your web app. The route forwards the request to https://khaleejiapi.dev, attaches the x-api-key header from a server-only environment variable, and streams the response back. Three benefits:
- The key never reaches the browser.
- CORS is no longer your problem - the browser only talks to your own origin.
- You can add caching, request validation, and per-end-user rate limiting on top.
Next.js (App Router)
Drop this route once and every endpoint becomes available at /api/khaleeji/v1/<...> on your own origin.
// app/api/khaleeji/[...path]/route.ts (Next.js App Router)import { NextRequest, NextResponse } from "next/server" export const runtime = "nodejs" // Edge runtime works too if you don't need Node APIs const UPSTREAM = "https://khaleejiapi.dev/api"const KHALEEJI_API_KEY = process.env.KHALEEJI_API_KEY! async function proxy(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) { const { path } = await ctx.params const url = new URL(`${UPSTREAM}/${path.join("/")}`) url.search = req.nextUrl.search // Forward only the methods + headers your app actually needs. const upstream = await fetch(url, { method: req.method, headers: { "x-api-key": KHALEEJI_API_KEY, "content-type": req.headers.get("content-type") ?? "application/json", // Optional: forward end-user IP for accurate rate-limit attribution "x-forwarded-for": req.headers.get("x-forwarded-for") ?? "", }, body: ["GET", "HEAD"].includes(req.method) ? undefined : await req.text(), }) // Pass through the upstream JSON so the browser can show error codes verbatim. const body = await upstream.text() return new NextResponse(body, { status: upstream.status, headers: { "content-type": upstream.headers.get("content-type") ?? "application/json", // CORS is handled by your own origin now - the upstream call is server-to-server. }, })} export const GET = proxyexport const POST = proxyexport const PUT = proxyexport const DELETE = proxy Then call it from a client component without thinking about keys:
// app/components/iban-check.tsx (browser code)"use client"import { useState } from "react" export function IbanCheck() { const [result, setResult] = useState<unknown>(null) async function check(iban: string) { // We hit OUR origin, not khaleejiapi.dev directly. // The proxy above injects the API key on the server. const res = await fetch(`/api/khaleeji/v1/iban/${iban}`) setResult(await res.json()) } return ( <button onClick={() => check("AE070331234567890123456")}> Validate IBAN </button> )} Express / Node.js
// server.js (Express / Node.js)import express from "express"import { createProxyMiddleware } from "http-proxy-middleware" const app = express()const KHALEEJI_API_KEY = process.env.KHALEEJI_API_KEY app.use( "/api/khaleeji", createProxyMiddleware({ target: "https://khaleejiapi.dev", changeOrigin: true, pathRewrite: { "^/api/khaleeji": "/api" }, onProxyReq: (proxyReq) => { proxyReq.setHeader("x-api-key", KHALEEJI_API_KEY) }, }),) app.listen(3000) Ruby on Rails
# config/routes.rb + app/controllers/khaleeji_proxy_controller.rb (Rails)require "net/http" class KhaleejiProxyController < ApplicationController UPSTREAM = URI("https://khaleejiapi.dev") def proxy upstream_path = "/api/" + params[:path] uri = UPSTREAM.dup uri.path = upstream_path uri.query = request.query_string.presence req = Net::HTTP.const_get(request.method.capitalize).new(uri) req["x-api-key"] = ENV.fetch("KHALEEJI_API_KEY") req["content-type"] = request.content_type if request.content_type req.body = request.raw_post if %w[POST PUT PATCH].include?(request.method) Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| res = http.request(req) render plain: res.body, status: res.code, content_type: res.content_type end endend Hardening checklist
- Allowlist routes. Don't blanket-proxy every path - only forward the endpoints your app actually needs. This limits blast radius if someone abuses your proxy.
- Rate-limit per session, not per IP. Your origin sees the user's session; KhaleejiAPI sees your origin's IP. Apply your own rate limiter (e.g.
upstash/ratelimit) before the proxy call. - Forward
x-forwarded-forif you want our analytics dashboard to show end-user geography, but never forward client-supplied auth headers verbatim. - Cache idempotent GETs. Reference data (Prayer Times, Hijri date, IBAN format) is safe to cache 1-15 minutes at the edge.
- Rotate keys on incident. See the leaked API key playbook.
Mobile apps
Mobile binaries can be decompiled. The same rule applies: ship a backend that holds the key and exposes only the endpoints your app uses, ideally behind your existing user authentication. iOS/Android App Attest or Firebase App Check can help authenticate that requests come from a real install of your app, but that is layered on top of the proxy - not a replacement for it.