Official JavaScript / TypeScript SDK for Bonyan-API — a unified API for Quran, Azkar, Hadith, Tafsir, Prayer times, Hijri calendar and Qibla direction, with automatic fallback between multiple upstream sources.
import { BonyanClient } from '@bonyanoss/bonyan-api';
const client = new BonyanClient();
const reciters = await client.reciters.list();
const fatiha = await client.surah.getById(1);
const morning = await client.azkar.getByCategory('أذكار الصباح');- Install
- Quick start
- Client options
- Resources
- Error handling
- Validation
- Retry and rate limiting
- Custom fetch / environments
- TypeScript
- Contributing
- License
npm install @bonyanoss/bonyan-api
pnpm add @bonyanoss/bonyan-api
yarn add @bonyanoss/bonyan-apiRequirements: Node.js ≥ 20, or any modern browser. No runtime dependencies.
import { BonyanClient } from '@bonyanoss/bonyan-api';
const client = new BonyanClient();
// Surahs
const surahs = await client.surah.list(); // 114 chapters
const fatiha = await client.surah.getById(1);
// Ayat
const verse = await client.ayat.getBySurah(2, 255); // Ayat al-Kursi
const search = await client.ayat.search('الرحمن', { limit: 20 });
console.log(search.total, search.results.length);
// Reciters
const reciter = await client.reciters.getById(1);
const audio = await client.reciters.getSurah(1, 1);
// Prayer times
const times = await client.prayer.getTimes({ city: 'Mecca', country: 'SA' });
// Health / metadata
const health = await client.health();
const readiness = await client.ready();import { BonyanClient } from '@bonyanoss/bonyan-api';
const client = new BonyanClient({
baseUrl: 'https://api.bonyanoss.org', // default
timeoutMs: 10_000, // default 10 s
retry: 3, // default 3
headers: { 'X-App-Id': 'my-app' }, // optional
userAgent: 'my-app/1.0', // optional
fetch: customFetch, // optional custom fetch
});| Option | Type | Default | Description |
|---|---|---|---|
baseUrl |
string |
https://api.bonyanoss.org |
Base URL of the API |
timeoutMs |
number |
10_000 |
Per-request timeout in milliseconds |
retry |
number |
3 |
Retry attempts on 5xx / 429 / network errors |
headers |
Record<string, string> |
— | Extra headers applied to every request |
userAgent |
string |
— | Adds a User-Agent header |
fetch |
typeof fetch |
globalThis.fetch |
Override the fetch implementation |
Every resource is reachable from the client instance. All methods validate their arguments before hitting the network — invalid input throws ValidationError.
| Method | Endpoint |
|---|---|
client.reciters.list() |
GET /reciters |
client.reciters.getById(id) |
GET /reciters/:id |
client.reciters.search(name) |
GET /reciters/search?name=… |
client.reciters.getSurah(reciterId, surah) |
GET /reciters/:id/surah/:surah |
const all = await client.reciters.list();
const reciter = await client.reciters.getById(1);
const matches = await client.reciters.search('العفاسي');
const audio = await client.reciters.getSurah(1, 1);| Method | Endpoint |
|---|---|
client.surah.list() |
GET /surah |
client.surah.getById(id) |
GET /surah/:id |
client.surah.search(name) |
GET /surah/search?name=… |
| Method | Endpoint |
|---|---|
client.ayat.list() |
GET /ayat (heavy response — every ayah of every surah) |
client.ayat.getById(id) |
GET /ayat/:id — global id (1..6236) |
client.ayat.getBySurah(surah, aya) |
GET /ayat/:surah/aya/:aya — surah (1..114) + aya (1..286) |
client.ayat.search(text, { limit }) |
GET /ayat/search?text=…&limit=… (max 500) |
const { total, results } = await client.ayat.search('الرحمن', { limit: 50 });| Method | Endpoint |
|---|---|
client.azkar.listCategories() |
GET /azkar |
client.azkar.getByCategory(category) |
GET /azkar/:category |
client.azkar.search(text, { limit }) |
GET /azkar/search?text=…&limit=… |
client.azkar.random() |
GET /azkar/random |
| Method | Endpoint |
|---|---|
client.tafsir.listEditions() |
GET /tafsir |
client.tafsir.forSurah(edition, surah, { aya }) |
GET /tafsir/:edition/:surah |
client.tafsir.forAya(edition, surah, aya) |
GET /tafsir/:edition/:surah/:aya |
| Method | Endpoint |
|---|---|
client.hadith.listBooks() |
GET /hadith |
client.hadith.getBook(bookId, { from, to }) |
GET /hadith/:book |
client.hadith.getByNumber(bookId, number) |
GET /hadith/:book/:number |
client.hadith.random({ book }) |
GET /hadith/random |
The from/to range is capped at 300 items per request.
| Method | Endpoint |
|---|---|
client.prayer.getTimes(options) |
GET /prayer/times |
options requires either latitude + longitude, or city + country. Optional: date (DD-MM-YYYY), method (calculation method).
| Method | Endpoint |
|---|---|
client.hijri.today() |
GET /hijri/today |
client.hijri.fromGregorian(date?) |
GET /hijri/from-gregorian?date=DD-MM-YYYY |
client.hijri.toGregorian(date) |
GET /hijri/to-gregorian?date=DD-MM-YYYY |
| Method | Endpoint |
|---|---|
client.qibla.getDirection(latitude, longitude) |
GET /qibla?latitude=…&longitude=… |
| Method | Endpoint |
|---|---|
client.health() |
GET /health - returns { status, code, timestamp } (no envelope) |
client.ready() |
GET /ready - returns readiness plus cache stats |
client.routes() |
GET / - returns the API route catalogue |
client.metrics() |
GET /metrics - returns Prometheus metrics as text |
The SDK throws three distinct error classes — handle each one by instanceof or the exported type guards.
import {
BonyanClient,
BonyanApiError,
BonyanRequestError,
ValidationError,
} from '@bonyanoss/bonyan-api';
const client = new BonyanClient();
try {
await client.reciters.getById(999_999);
} catch (error) {
if (error instanceof ValidationError) {
console.error('Invalid input on field:', error.field, error.message);
} else if (error instanceof BonyanApiError) {
// The API responded with a non-2xx
console.error(error.status, error.code, error.requestId, error.message);
} else if (error instanceof BonyanRequestError) {
// Network failure, timeout, DNS, …
console.error('Network error:', error.message);
}
}| Class | When | Notable fields |
|---|---|---|
ValidationError |
Argument failed client-side validation (no network request was sent) | field |
BonyanApiError |
API returned a non-2xx response | status, code, requestId, retryAfterMs, body |
BonyanRequestError |
Network error, DNS, timeout, abort | cause |
Type guards: isBonyanApiError, isBonyanRequestError, isValidationError.
Every argument is checked before a request is sent:
surahmust be an integer in1..114- Global aya
id(used byayat.getById) must be in1..6236 - Per-surah
ayanumber (inayat.getBySurah,tafsir.forAya) must be in1..286 latitude∈[-90, 90],longitude∈[-180, 180]- Dates use the
DD-MM-YYYYformat - Search
limitis bounded per endpoint (200 forazkar, 500 forayat) - Hadith
from/torange is capped at 300
When validation fails, ValidationError is thrown synchronously — no HTTP request is made.
By default the SDK retries up to 3 times on:
5xxserver errors429 Too Many Requests- Network errors (DNS failure, socket reset, timeout)
Retry-After headers (in seconds or HTTP-date) are honored. Between attempts the SDK sleeps with exponential backoff + jitter (100ms * 2^attempt + random(0-100)).
Disable retries entirely:
const client = new BonyanClient({ retry: 0 });The SDK uses globalThis.fetch by default. To use it on a runtime without a global fetch, or with a custom HTTP stack:
import { BonyanClient } from '@bonyanoss/bonyan-api';
import { fetch as undiciFetch } from 'undici';
const client = new BonyanClient({ fetch: undiciFetch as typeof fetch });Works out of the box on:
- ✅ Node.js ≥ 20
- ✅ Browsers (Chrome, Firefox, Safari, Edge)
- ✅ Deno, Bun
- ✅ Cloudflare Workers, Vercel Edge, Netlify Edge
The SDK ships with strict type definitions — strict, exactOptionalPropertyTypes, noUncheckedIndexedAccess. Every public API has TSDoc comments visible in your editor.
import type {
Reciter,
Surah,
Aya,
AzkarItem,
HadithItem,
PrayerTimings,
HijriDate,
QiblaInfo,
} from '@bonyanoss/bonyan-api';We ❤️ pull requests! See CONTRIBUTING.md for the dev setup, code style, and how to add a new resource.
Quick start:
pnpm install
pnpm test
pnpm build