One provider or many, painlessly
Ask only for what you need; the library picks a suitable provider and falls back automatically on failure.
Verse text, audio, translation, and tafsir from multiple providers — with automatic provider selection and fallback, so your app never hard-couples to one provider.
npm i quran-api-unified
# or
pnpm add quran-api-unifiedimport { createQuranClient } from 'quran-api-unified'
const quran = createQuranClient()
const res = await quran.get({
ref: { surah: 1, ayah: 1 },
include: ['text', 'audio', 'translation', 'tafsir'],
})
if (res.ok) {
console.log(res.value.text?.value?.text) // بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ
console.log(res.value.audio?.value?.url) // the recitation URL
} else {
console.error(res.error.code, res.attempts) // a typed error, with the full attempt trail
}createQuranClient() needs no arguments — the keyless providers (Al-Quran Cloud, Quran API Edge, Quran Hub, Quran Explorer, spa5k tafsir) cover all four concerns out of the box.
ref — { surah, ayah }. Omit ayah to mean "the whole surah" (surah-scope support varies by concern; see each concern below).include — which concerns to fetch: any of 'text', 'audio', 'translation', 'tafsir'. get() fetches all of them in parallel, each with its own provider fallback chain.get() only ever throws for misuse — an empty include, or an explicitly named source that's unknown, doesn't serve the concern, or is missing required credentials. Every provider/network failure comes back as res.ok: false or as a failed Part — see Handling partial results.Each accepts an optional per-request identifier; when omitted, the provider's own default is used.
const res = await quran.get({
ref: { surah: 2, ayah: 255 },
include: ['text', 'audio', 'translation', 'tafsir'],
reciter: 'ar.alafasy', // audio: reciter id (provider-specific)
edition: 'en.sahih', // translation: edition id (provider-specific)
tafsirId: 'en-tafisr-ibn-kathir', // tafsir: edition id (provider-specific)
})| Concern | Shape (res.value.<concern>?.value) |
|---|---|
text | { key, surah, ayah, source, text, meta? } |
audio | { key, surah, ayah?, scope, source, reciter, url, format, meta? } |
translation | { key, surah, ayah, source, edition, language, text, meta? } |
tafsir | { key, surah, ayah, source, tafsirId, text, meta? } |
key is always the canonical "surah:ayah" string; source is the display name of the provider that actually served the value (e.g. "Al-Quran Cloud"). See each provider's reference page under Providers for exactly which id it expects for reciter, edition, or tafsirId.
res.ok covers the whole call: it's true as long as at least one requested concern succeeded. Each concern is independently inspectable, so one failure never takes down the others:
const res = await quran.get({ ref: { surah: 18, ayah: 10 }, include: ['text', 'tafsir'] })
if (res.ok) {
if (res.value.tafsir?.ok) {
console.log(res.value.tafsir.value.text)
} else {
// this edition may not cover this ayah — every provider tried is in .attempts
console.warn(res.value.tafsir?.error?.code, res.value.tafsir?.attempts)
}
}Every Part (res.value.text, .audio, .translation, .tafsir) carries:
ok / value / error — the outcome for that concern.source — the adapter id that served it, when ok is true.attempts — every provider tried for this concern, in order, each with its own adapterId, ok, error?, and durationMs?.By default each concern auto-selects across the registered adapters, in registration order, skipping any that need credentials you haven't supplied. To pin a concern to one provider (with your own fallback order), pass source:
const res = await quran.get({
ref: { surah: 1, ayah: 1 },
include: ['text'],
source: {
text: { id: 'quran_hub', fallback: ['quran_finder', 'alquran_cloud'] },
},
})An unknown id, an adapter that doesn't serve the requested concern, or one missing required credentials is treated as misuse here and throws — since you named it explicitly, a typo or a wrong assumption should surface immediately rather than fail silently.
Keyless providers need nothing. A credentialed provider (currently Quran Foundation, OAuth2 client-credentials) is configured per adapter id when you create the client:
const quran = createQuranClient({
credentials: {
quran_foundation: { clientId: '...', secret: '...' },
},
})The client exchanges clientId/secret for an access token at the adapter's token endpoint, caches it for its lifetime, and refreshes it automatically. Without credentials, a credentialed adapter is skipped during auto-selection — your keyless request still succeeds via the other providers — and only throws if you name it explicitly via source.
If you already have a token (e.g. minted elsewhere), you can supply it directly and skip the exchange:
const quran = createQuranClient({
credentials: { quran_foundation: { accessToken: 'eyJ...' } },
})Pass includeRaw: true to also get each provider's original, un-normalized response body alongside the unified value — useful for debugging a mapping or showing a raw-vs-unified comparison:
const res = await quran.get({
ref: { surah: 1, ayah: 1 },
include: ['text'],
includeRaw: true,
})
if (res.ok && res.value.text?.ok) {
console.log(res.value.text.value) // the unified UnifiedVerse
console.log(res.value.text.raw) // the provider's original response body (unknown)
}raw is only present on a successful Part when includeRaw was set — results stay lean by default.
An adapter is a declarative recipe — a buildUrl function plus a pure transform — not a class to extend. Register your own to add a provider or override a built-in one by id:
import { createQuranClient, type Adapter } from 'quran-api-unified'
const myProvider: Adapter = {
id: 'my_provider',
name: 'My Provider',
capabilities: ['text'],
auth: 'none',
text: {
buildUrl: (q) => `https://example.com/api/ayah/${q.surah}/${q.ayah ?? 1}`,
transform: (raw, q) => {
const data = raw as { text: string }
return {
key: `${q.surah}:${q.ayah ?? 1}`,
surah: q.surah,
ayah: q.ayah ?? 1,
source: 'My Provider',
text: data.text,
}
},
},
}
const quran = createQuranClient({ adapters: [myProvider] })
// or, to use *only* your own adapters, without the built-ins:
// createQuranClient({ adapters: [myProvider], useBuiltins: false })Once registered, myProvider participates in auto-selection alongside the built-ins (in registration order) and can be named in source.<concern>.id/.fallback like any other adapter.
An optional quran-api-unified/zod entry exports zod schemas for the unified shapes — zod is a peer dependency, not bundled into the core:
import { parseUnifiedVerse, unifiedVerseSchema } from 'quran-api-unified/zod'
const verse = parseUnifiedVerse(res.value.text?.value) // throws on shape mismatch
const result = unifiedVerseSchema.safeParse(res.value.text?.value) // { success, data | error }Matching parse/safeParse pairs exist for audio, translation, and tafsir (parseUnifiedAudio, parseUnifiedTranslation, parseUnifiedTafsir, and their safeParse… counterparts).
Some providers (Quran Hub, Quran Explorer) don't send CORS headers, so their adapters set useProxy: true and need a proxy configured to work from a browser:
const quran = createQuranClient({
proxy: 'https://your-cors-proxy.example.com/?url=',
// or a function: proxy: (url) => `https://your-proxy.example.com/${url}`
})Leave proxy unset outside the browser (Node, Deno, Bun) — those providers work directly without one.
Ships as dual ESM + CJS with full TypeScript types, for Node, browsers, Deno, and Bun:
// ESM
import { createQuranClient } from 'quran-api-unified'
// CJS
const { createQuranClient } = require('quran-api-unified')In an environment with no global fetch, pass one explicitly via createQuranClient({ fetch }).
Each built-in provider has a reference page with its upstream API and why the adapter maps it the way it does:
See the CHANGELOG for release history and CONTRIBUTING for the adapter contract if you'd like to contribute a new provider.