Next.js Bible API Example: Verse of the Day

Secure server-side Scripture retrieval with layered caching and clean App Router architecture.

Last updated: February 2026


What You Will Build


Prerequisites


Step 1: Create a Next.js Project

Initialize Project
npx create-next-app@latest verse-of-the-day
cd verse-of-the-day

Step 2: Set Environment Variable

Never expose API keys to the browser — only use them in Server Components or route handlers.

Mac / Linux
export BIBLEBRIDGE_API_KEY=your_api_key_here

Restart the development server after setting environment variables.


Step 3: Scripture Data Layer with Layered Caching

This example fetches a curated set of popular verses once and caches them:

Create lib/getVerses.ts:

lib/getVerses.ts
export type VerseType = {
  book: { name: string };
  chapter: number;
  data: {
    verse: number;
    text: string;
  };
};

// Top 10 popular verses used for the example demo.
// These are fetched once and cached for 24 hours.
const VERSES = [
  { bookID: "19", chapter: "23", verse: "1" },   // Psalm 23:1
  { bookID: "43", chapter: "3", verse: "16" },   // John 3:16
  { bookID: "20", chapter: "3", verse: "5" },    // Proverbs 3:5
  { bookID: "45", chapter: "8", verse: "28" },   // Romans 8:28
  { bookID: "23", chapter: "41", verse: "10" },  // Isaiah 41:10
  { bookID: "19", chapter: "46", verse: "1" },   // Psalm 46:1
  { bookID: "50", chapter: "4", verse: "13" },   // Philippians 4:13
  { bookID: "24", chapter: "29", verse: "11" },  // Jeremiah 29:11
  { bookID: "40", chapter: "5", verse: "16" },   // Matthew 5:16
  { bookID: "58", chapter: "11", verse: "1" },   // Hebrews 11:1
];

// In-memory cache (per server instance)
// Prevents repeated fetch calls during the same runtime session.
let cached: VerseType[] | null = null;

export async function getCachedVerses(): Promise {
  // Serve from memory if already fetched
  if (cached) return cached;

  const key = process.env.BIBLEBRIDGE_API_KEY?.trim();

  if (!key) {
    throw new Error("Missing BIBLEBRIDGE_API_KEY");
  }

  const results: VerseType[] = [];

  for (const v of VERSES) {
    const url =
      `https://holybible.dev/api/scripture` +
      `?bookID=${v.bookID}` +
      `&chapter=${v.chapter}` +
      `&verse=${v.verse}` +
      `&version=KJV`;

    const res = await fetch(url, {
      method: "GET",
      headers: {
        // Bearer auth keeps API key server-side only
        Authorization: `Bearer ${key}`,
      },
      // Cache response for 24 hours at the Next.js layer
      next: { revalidate: 86400 },
    });

    if (!res.ok) {
      const body = await res.text();
      console.error("URL:", url);
      console.error("Status:", res.status);
      console.error("Body:", body);
      throw new Error(
        `Failed to fetch ${v.bookID}-${v.chapter}-${v.verse}`
      );
    }

    results.push(await res.json());
  }

  // Store results in memory cache
  cached = results;

  return results;
}

Scripture retrieval works with a free API key. Caching reduces upstream API calls.


Step 4: Server Component (app/page.tsx)

Server Components fetch Scripture securely and choose an initial verse index on the server — this prevents hydration mismatch.

app/page.tsx
import { getCachedVerses } from "@/lib/getVerses";
import VerseActions from "@/components/VerseActions";

export default async function Home() {
  // Fetch verses server-side (cached for 24 hours)
  const verses = await getCachedVerses();

  // Select initial verse on the server to avoid hydration mismatch
  const initialIndex = Math.floor(Math.random() * verses.length);

  return (
    <main className="min-h-screen flex flex-col items-center justify-center p-8">
      <VerseActions
        verses={verses}
        initialIndex={initialIndex}
      />
    </main>
  );
}

Step 5: Client Component (VerseActions.tsx)

Client Components handle interaction — cycling verses, copy-to-clipboard, and social sharing. The shuffle logic ensures each verse is shown once per cycle before reshuffling.

components/VerseActions.tsx
"use client";

import { useState, useMemo } from "react";

type VerseType = {
  book: { name: string };
  chapter: number;
  data: { verse: number; text: string };
};

type Props = {
  verses: VerseType[];
  initialIndex: number;
};

function shuffle(array: number[]) {
  const arr = [...array];
  for (let i = arr.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }
  return arr;
}

export default function VerseActions({ verses, initialIndex }: Props) {
  const safeInitialIndex =
    typeof initialIndex === "number" &&
    initialIndex >= 0 &&
    initialIndex < verses.length
      ? initialIndex
      : 0;

  const initialOrder = useMemo(() => {
    const indexes = verses.map((_, i) => i);
    const shuffled = shuffle(indexes);

    const startIndex = shuffled.indexOf(safeInitialIndex);
    if (startIndex > 0) {
      [shuffled[0], shuffled[startIndex]] = [
        shuffled[startIndex],
        shuffled[0],
      ];
    }

    return shuffled;
  }, [verses, safeInitialIndex]);

  const [order, setOrder] = useState(initialOrder);
  const [position, setPosition] = useState(0);

  const verse = verses[order[position]];
  if (!verse) return null;

  function getNewVerse() {
    if (position < order.length - 1) {
      setPosition(position + 1);
    } else {
      const newOrder = shuffle(verses.map((_, i) => i));
      setOrder(newOrder);
      setPosition(0);
    }
  }

  return (
    <div>
      <h1>{verse.book.name} {verse.chapter}:{verse.data.verse}</h1>
      <p>{verse.data.text}</p>

      <button onClick={getNewVerse}>Get Another Verse</button>
    </div>
  );
}

Client-side logic is purely interactive — API access remains server-side.


Step 6: Optional API Route (app/api/verse/route.ts)

This route returns a single random verse using the cached dataset — no additional upstream API calls are required.

app/api/verse/route.ts
import { NextResponse } from "next/server";
import { getCachedVerses } from "@/lib/getVerses";

export async function GET() {
  try {
    const verses = await getCachedVerses();

    if (!Array.isArray(verses) || verses.length === 0) {
      return NextResponse.json(
        { error: "Invalid verse data" },
        { status: 500 }
      );
    }

    const random =
      verses[Math.floor(Math.random() * verses.length)];

    return NextResponse.json(random);
  } catch (error) {
    console.error("Verse route error:", error);

    return NextResponse.json(
      { error: "Failed to load verse" },
      { status: 500 }
    );
  }
}

Deployment (Vercel)

Deploy this example using Vercel’s one-click flow.

Deploy with Vercel

After clicking the button:

  1. Import the repository into your Vercel account
  2. Add the environment variable BIBLEBRIDGE_API_KEY
  3. Deploy
Required Environment Variable (Vercel)
BIBLEBRIDGE_API_KEY=your_api_key_here

Why This Pattern Matters

This example demonstrates production-safe Scripture integration — server-only API keys, layered caching, hydration-safe rendering, and reusable server logic.

BibleBridge provides canonical Scripture data while your application controls presentation, interaction, and deployment.


Common Errors & Fixes


Full Source Code

Official GitHub repository:
https://github.com/ZeroCoolZiemer/biblebridge-nextjs-verse-of-the-day


Related Tutorials


For full endpoint details, see the BibleBridge API documentation.