The Next.js app lives at the repository root (package.json, next.config.*, src/app/). Quran endpoints are App Router handlers under src/app/api/quran/.
| Method | Path | Purpose |
|---|---|---|
GET |
/api/quran/chapters |
Surah index (normalized for mobile/web). |
GET |
/api/quran/chapters/:id/verses |
Ayāt for surah :id (1–114). |
GET |
/api/quran/audio |
Resolved CDN mp3 URL for one ayah. |
OPTIONS |
Same as above | CORS preflight (204, Access-Control-*). |
Serving priority (chapters & audio):
MOCK_QURAN_API=true→ offline scaffold dataset (development / QA).- Quran Foundation OAuth configured (
QURAN_CLIENT_ID+QURAN_CLIENT_SECRETor legacy alias env keys) →@quranjs/apiContent API. - Public Quran.com v4 bridge (default when OAuth absent) → read-only Quran.com API v4 from the server (no OAuth).
- If
QURAN_DISABLE_PUBLIC_HTTP_BRIDGE=trueand credentials are absent → graceful mock scaffold whenQURAN_GRACEFUL_MOCK_FALLBACKis enabled, else503QURAN_BLOCKED.
/api/quran/chapters/:id/verses: Unless MOCK_QURAN_API=true, verses are fetched only via Quran.com v4 HTTPS (not @quranjs/api). This avoids OAuth/SDK payloads that lacked Arabic text. Primary source: /verses/by_chapter with per_page=50 (max server allows) repeated through pagination.total_pages. If needed: merge GET /v4/quran/verses/uthmani?chapter_number=:id by verse_key, and GET /v4/quran/translations/:id?chapter_number=:id zipped by verse order for missing translation lines.
| Step | Path |
|---|---|
| Primary (paginated) | /verses/by_chapter/:id?language=en&words=false&fields=text_uthmani,text_imlaei,text_uthmani_simple,chapter_id&translations={id}&translation_fields=resource_name,language_name&per_page=50&page=N |
| Arabic fallback | /quran/verses/uthmani?chapter_number=:id |
| Translation fallback | /quran/translations/:resourceId?chapter_number=:id |
| DeenNotes route | Quran.com v4 upstream |
|---|---|
GET /api/quran/chapters |
GET /chapters |
GET /api/quran/audio |
GET /recitations/:reciterId/by_ayah/:verseKey → path joined with https://verses.quran.com/ |
| Variable | Required | Notes |
|---|---|---|
QURAN_PUBLIC_TRANSLATION_ID |
No | Quran.com translation resource id for the public verses bridge. Defaults to 85. |
QURAN_DEFAULT_RECITER_ID |
No | Default reciter for /api/quran/audio when reciter is omitted. Defaults to 7. |
QURAN_DISABLE_PUBLIC_HTTP_BRIDGE |
No | Set to true to disable Quran.com HTTP from the server (falls back to mock/blocked logic). |
QURAN_CLIENT_ID, QURAN_CLIENT_SECRET |
For OAuth path only | Quran Foundation Content API credentials. |
Mobile (Expo / EAS): no secrets required for Quran text. Ensure production builds expose only the public site origin:
EXPO_PUBLIC_NEXT_ORIGIN=https://deennotesai.netlify.app
(Optionally mirror translation id client-side via EXPO_PUBLIC_QURAN_PUBLIC_TRANSLATION_ID for offline/public fallbacks inside the app — see apps/mobile/src/services/quranFoundationClient.ts.)
Successful JSON bodies include a _quran meta field (servingMode, offlineReflectionDataset). Errors use { error, code, retryable, hint?, _quran }. Upstream Quran.com failures are never passed through verbatim.
- Responses include permissive
Access-Control-Allow-Origin: *forGET/OPTIONS. - Chapter and verse responses use short-to-medium CDN cache hints; audio uses
no-store.
npm run devcurl -sS "http://localhost:3000/api/quran/chapters" | head -c 600
# Expect 7 verses, chapterId 1, Arabic + ≥1 translation for 1:1
curl -sS "http://localhost:3000/api/quran/chapters/1/verses" | python3 -c "import json,sys; r=json.load(sys.stdin); v=r['verses'][0]; print(len(r['verses']), v['chapterId'], v['verseKey'], len(v.get('textUthmani','')), len(v.get('translations') or []))"
curl -sS "http://localhost:3000/api/quran/audio?verseKey=1:1"
curl -sS "http://localhost:3000/api/quran/audio?chapter=1&verse=1&reciter=7"Replace the host with your deploy URL, for example:
curl -sS "https://deennotesai.netlify.app/api/quran/chapters" | head -c 400
curl -sS "https://deennotesai.netlify.app/api/quran/chapters/1/verses" | head -c 400
curl -sS "https://deennotesai.netlify.app/api/quran/audio?verseKey=1:1"Chapters (each item includes chapterNumber equal to id for mobile convenience):
{
"chapters": [
{
"id": 1,
"chapterNumber": 1,
"versesCount": 7,
"revelationPlace": "makkah",
"nameSimple": "Al-Fatihah",
"nameArabic": "الفاتحة"
}
],
"_quran": { "servingMode": "public_http", "offlineReflectionDataset": false }
}Verses:
{
"verses": [
{
"id": 1,
"verseNumber": 1,
"verseKey": "1:1",
"chapterId": 1,
"textUthmani": "…",
"textImlaei": "…",
"translations": [{ "text": "…", "resourceName": "M.A.S. Abdel Haleem", "resourceId": 85, "languageName": "english" }]
}
],
"_quran": { "servingMode": "public_http", "offlineReflectionDataset": false }
}Audio:
{
"verseKey": "1:1",
"reciterId": "7",
"audioUrl": "https://verses.quran.com/Alafasy/mp3/001001.mp3",
"format": "mp3",
"_quran": { "servingMode": "public_http", "offlineReflectionDataset": false }
}Audio unavailable (404):
{
"error": "Audio is temporarily unavailable for this verse.",
"code": "QURAN_NOT_FOUND",
"retryable": false,
"_quran": { "servingMode": "public_http", "offlineReflectionDataset": false }
}src/app/api/quran/chapters/route.tssrc/app/api/quran/chapters/[id]/verses/route.tssrc/app/api/quran/audio/route.tssrc/lib/quran/quranDotComPublicFetch.ts(verse paging + Arabic/translation fallbacks)src/lib/quran/quranDotComVerses.merge.ts+quranDotComVerses.merge.test.ts(pure merge helpers —npx vitest run src/lib/quran/quranDotComVerses.merge.test.ts)src/lib/quran/env.ts(serving mode +canServeQuranApiRoutes)
- Quran.com availability / rate limits — verse payloads depend on
api.quran.comwheneverMOCK_QURAN_APIis unset/false. - Translation ZIP fallback —
/quran/translations/:id?chapter_number=…returns lines in ascending verse order with no verse key; mismatched lengths vs/verses/by_chapterwould skip attaches (detected safely).