Integration Guide

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.

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.

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

ts
// 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 = proxy
export const POST = proxy
export const PUT = proxy
export const DELETE = proxy

Then call it from a client component without thinking about keys:

tsx
// 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

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

ruby
# 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
end
end

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-for if 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.