- Docs
- Integrations
- Next.js
Use RoxyAPI with Next.js
Ship a server-rendered daily horoscope page, a natal chart form, a tarot reading widget, or an AI astrology chatbot from a Next.js app in under 20 minutes without leaking the API key to the browser.
Next.js is the fastest path from "I have an idea" to a deployed Roxy-backed app on Vercel. This page opens with the quick vibecoder-friendly flow and then drops into full App Router reference: route handlers, server actions, caching, Edge runtime, and the MCP chatbot path. Works with Next.js 14, 15, and 16.
What you can build on Next.js
- Server-rendered daily horoscope pages with
revalidatecaching - Birth chart generator backed by a Server Action and form state
- Embeddable horoscope widget exposed via a Route Handler with CORS
- Tarot reading page that renders three-card or Celtic Cross spreads
- Numerology Life Path lookup with master-number detection
- Vedic kundli generator with nakshatra, dasha, and panchang
- AI astrology chatbot powered by our MCP server and the Vercel AI SDK
What you need, 30 seconds
- A Roxy API key. Get one on the pricing page.
- A Next.js 14, 15, or 16 project using the App Router. The examples below use the App Router. Pages Router patterns are in Advanced patterns.
- Five minutes if you already have a Next.js project. Ten if you are starting from scratch.
Our TypeScript SDK (@roxyapi/sdk) gives full type safety and exact response types on every endpoint. Use raw fetch for one or two endpoints, the SDK for anything more.
Step 1, connect your first endpoint
Create .env.local at the project root. Restart the dev server.
# .env.local
ROXY_API_KEY=your_roxyapi_key_here
Do not prefix the variable with NEXT_PUBLIC_. NEXT_PUBLIC_* values are inlined into the client bundle at build time and become readable in DevTools. Always use a plain name like ROXY_API_KEY.
For Vercel production:
- Vercel dashboard, project, Settings, Environment Variables.
- Add
ROXY_API_KEYto Production (and Preview if you want preview deploys to work). - Redeploy. Env vars are baked in at build time for serverless functions.
Step 2, ship a useful feature
Here is the simplest working pattern: a server-rendered daily horoscope page, cached for an hour.
Confirm the key works before you wire anything:
curl "https://roxyapi.com/api/v2/astrology/horoscope/aries/daily" \
-H "X-API-Key: $ROXY_API_KEY"
// app/horoscope/[sign]/page.tsx
type Props = { params: Promise<{ sign: string }> };
async function getHoroscope(sign: string) {
const res = await fetch(
`https://roxyapi.com/api/v2/astrology/horoscope/${sign}/daily`,
{
headers: { 'X-API-Key': process.env.ROXY_API_KEY! },
next: { revalidate: 3600 }, // cache 1 hour
},
);
if (!res.ok) throw new Error(`RoxyAPI ${res.status}`);
return res.json();
}
export default async function HoroscopePage({ params }: Props) {
const { sign } = await params;
const data = await getHoroscope(sign);
return (
<main>
<h1>{data.sign} daily</h1>
<p>{data.overview}</p>
<small>Lucky {data.luckyNumber}, color {data.luckyColor}</small>
</main>
);
}
// app/horoscope/[sign]/page.tsx
import { notFound } from 'next/navigation';
import { createRoxy } from '@roxyapi/sdk';
const roxy = createRoxy(process.env.ROXY_API_KEY!);
const ZODIAC_SIGNS = [
'aries', 'taurus', 'gemini', 'cancer',
'leo', 'virgo', 'libra', 'scorpio',
'sagittarius', 'capricorn', 'aquarius', 'pisces',
] as const;
type ZodiacSign = (typeof ZODIAC_SIGNS)[number];
export default async function HoroscopePage({
params,
}: {
params: Promise<{ sign: string }>;
}) {
const { sign } = await params;
if (!(ZODIAC_SIGNS as readonly string[]).includes(sign)) notFound();
const { data, error } = await roxy.astrology.getDailyHoroscope({
path: { sign: sign as ZodiacSign },
});
if (error) return <p>Could not load horoscope</p>;
return (
<main>
<h1>{data.sign}</h1>
<p>{data.overview}</p>
<small>Lucky {data.luckyNumber}</small>
</main>
);
}
// app/birth-chart/actions.ts
'use server';
export async function generateBirthChart(formData: FormData) {
const payload = {
date: String(formData.get('date')),
time: `${formData.get('time')}:00`,
latitude: Number(formData.get('latitude')),
longitude: Number(formData.get('longitude')),
timezone: String(formData.get('timezone') || 'UTC'),
};
const res = await fetch('https://roxyapi.com/api/v2/astrology/natal-chart', {
method: 'POST',
headers: {
'X-API-Key': process.env.ROXY_API_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
cache: 'no-store', // user specific, never cache
});
if (!res.ok) return { error: `Could not generate chart (${res.status})` };
return { chart: await res.json() };
}
// app/api/horoscope/[sign]/route.ts
import { NextResponse } from 'next/server';
export async function GET(
_request: Request,
{ params }: { params: Promise<{ sign: string }> },
) {
const { sign } = await params;
const res = await fetch(
`https://roxyapi.com/api/v2/astrology/horoscope/${sign}/daily`,
{
headers: { 'X-API-Key': process.env.ROXY_API_KEY! },
next: { revalidate: 3600 },
},
);
if (!res.ok) {
return NextResponse.json({ error: 'Upstream' }, { status: res.status });
}
return NextResponse.json(await res.json());
}
Step 3, scale to the full surface
Adding the next endpoint is the same pattern: another server component, another route handler, another server action. The env var, the auth header, and the caching strategy stay the same.
- API reference has a pre-filled test key. Try a call in the browser, copy the curl, paste into a new route handler.
- TypeScript SDK gives typed methods on every endpoint.
- Domain guides for which endpoints to call in what order:
- Starter apps: AI Astrology Chatbot (Next.js 16, Vercel AI SDK v6, MCP) and Vedic Kundli app.
Advanced patterns
The section below is the deep reference for anyone building production Roxy apps on Next.js. Patterns, caching rules, Edge runtime, MCP chatbots, and the full set of vibecoder gotchas.
The one rule: never call RoxyAPI from a client component
If you put your API key in a file that runs in the browser, the key ends up in your JavaScript bundle. Anyone with DevTools can copy it and burn through your quota. There are exactly four safe places to call Roxy from a Next.js app:
- Server Components (any
page.tsxorlayout.tsxwithout'use client') - Server Actions (functions marked with
'use server') - Route Handlers (
app/api/.../route.ts) - Middleware and Edge functions
Anything inside a file marked 'use client' runs in the browser. Never call fetch("https://roxyapi.com/api/v2/...", { headers: { "X-API-Key": ... } }) from there. Never read process.env.ROXY_API_KEY from there either.
Pattern 1: Server Component with cached fetch
Best for daily horoscopes, dream lookups, anything stable. The server fetches Roxy, renders HTML, the browser never sees the key.
// app/horoscope/[sign]/page.tsx
async function getHoroscope(sign: string) {
const res = await fetch(
`https://roxyapi.com/api/v2/astrology/horoscope/${sign}/daily`,
{
headers: { 'X-API-Key': process.env.ROXY_API_KEY! },
next: { revalidate: 3600 },
},
);
if (!res.ok) throw new Error(`RoxyAPI ${res.status}`);
return res.json();
}
Next.js 15 changed the fetch caching default. In Next.js 14, fetch() cached by default (force-cache). In Next.js 15 and 16, fetch() is uncached by default. Without next: { revalidate: N } or cache: 'force-cache', every page render hits Roxy.
For daily data use next: { revalidate: 86400 }. For permanent data (dream symbols, hexagram meanings, angel number lookups) use cache: 'force-cache'.
Pattern 2: Server Action for form submissions
Best for natal charts, custom readings, anything user specific.
// app/birth-chart/actions.ts
'use server';
export async function generateBirthChart(formData: FormData) {
const payload = {
date: String(formData.get('date')),
time: `${formData.get('time')}:00`,
latitude: Number(formData.get('latitude')),
longitude: Number(formData.get('longitude')),
timezone: String(formData.get('timezone') || 'UTC'),
};
const res = await fetch('https://roxyapi.com/api/v2/astrology/natal-chart', {
method: 'POST',
headers: {
'X-API-Key': process.env.ROXY_API_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
cache: 'no-store',
});
if (!res.ok) return { error: `Could not generate chart (${res.status})` };
return { chart: await res.json() };
}
// app/birth-chart/page.tsx
import { generateBirthChart } from './actions';
export default function BirthChartPage() {
return (
<form action={generateBirthChart}>
<input name="date" type="date" required />
<input name="time" type="time" required />
<input name="latitude" type="number" step="any" required />
<input name="longitude" type="number" step="any" required />
<input name="timezone" placeholder="America/New_York" required />
<button type="submit">Generate chart</button>
</form>
);
}
User-specific results must use cache: 'no-store'. Caching them would serve one chart to the next visitor. For richer form UX with field validation and pending states, wrap the action with useActionState from React 19.
Pattern 3: Route Handler for client widgets
Best for embeddable widgets, polling, AJAX. The Route Handler holds the key and proxies the request.
// app/api/horoscope/[sign]/route.ts
import { NextResponse } from 'next/server';
export async function GET(
_request: Request,
{ params }: { params: Promise<{ sign: string }> },
) {
const { sign } = await params;
const res = await fetch(
`https://roxyapi.com/api/v2/astrology/horoscope/${sign}/daily`,
{
headers: { 'X-API-Key': process.env.ROXY_API_KEY! },
next: { revalidate: 3600 },
},
);
if (!res.ok) {
return NextResponse.json({ error: 'Upstream error' }, { status: res.status });
}
return NextResponse.json(await res.json());
}
The client component calls your own /api/horoscope/aries endpoint, not roxyapi.com:
// app/components/HoroscopeWidget.tsx
'use client';
import { useEffect, useState } from 'react';
export default function HoroscopeWidget({ sign }: { sign: string }) {
const [data, setData] = useState<{ overview: string; luckyNumber: number } | null>(null);
useEffect(() => {
fetch(`/api/horoscope/${sign}`).then((r) => r.json()).then(setData);
}, [sign]);
if (!data) return <p>Loading...</p>;
return (
<>
<p>{data.overview}</p>
<small>Lucky number {data.luckyNumber}</small>
</>
);
}
This is also how you build an embeddable widget. Add CORS headers (Access-Control-Allow-Origin: *) to the Route Handler response and other websites can iframe or fetch it. They use your quota, you control the brand.
Pattern 4: render the response with Roxy UI
Patterns 1 to 3 fetch the data. They leave you hand-building the markup. For charts, tables, and reading cards, render the response with @roxyapi/ui-react, the official component library. The Server Component fetches with the SDK and the secret key, then passes the unwrapped data to a small client component that renders the matching component. The key never crosses the network.
npm install @roxyapi/ui-react
# or: bun add @roxyapi/ui-react
The components mount Custom Elements, which need the DOM, so any file that imports them must declare 'use client'. Server Components stay key-side and stream the data down. This is the same backend-only-key rule from above: the fetch is on the server, only the response reaches the browser.
// app/birth-chart/page.tsx (Server Component, holds the key)
import { createRoxy } from '@roxyapi/sdk';
import ChartView from './chart-view';
const roxy = createRoxy(process.env.ROXY_API_KEY!);
export default async function BirthChartPage() {
// Geocode first. Chart endpoints need latitude, longitude, timezone.
const { data: places } = await roxy.location.searchCities({
query: { q: 'Mumbai' },
});
const city = places?.cities[0];
if (!city) return <p>Location not found</p>;
const { data, error } = await roxy.astrology.generateNatalChart({
body: {
date: '1990-01-15',
time: '14:30:00',
latitude: city.latitude,
longitude: city.longitude,
timezone: city.timezone,
},
});
if (error || !data) return <p>Could not generate chart</p>;
return <ChartView data={data} />;
}
// app/birth-chart/chart-view.tsx (Client Component, renders the chart)
'use client';
import { RoxyNatalChart } from '@roxyapi/ui-react';
import type { NatalChartResponse } from '@roxyapi/sdk';
export default function ChartView({ data }: { data: NatalChartResponse }) {
return <RoxyNatalChart data={data} />;
}
Pass the unwrapped data, not the SDK envelope. The SDK returns { data, error }. Destructure data on the server and pass only that. Handing the whole envelope to a component renders [object Object].
The type comes straight from the SDK (NatalChartResponse), which is generated from the OpenAPI spec. Never declare a local interface for a Roxy response. It drifts the moment the spec changes and the component silently renders nothing.
The component you reach for matches the endpoint:
RoxyNatalChartforPOST /astrology/natal-chart(Western birth chart wheel)RoxyVedicKundliforPOST /vedic-astrology/birth-chart(setchartStyle="south","north", or"east")RoxyHoroscopeCardforGET /astrology/horoscope/{sign}/{daily,weekly,monthly}(setperiod)RoxyNumerologyCard,RoxyTarotSpread,RoxyPanchangTable, and more in the Roxy UI reference.
For a chart with a live city picker instead of a hardcoded location, mount RoxyLocationSearch in the client component, call your own Route Handler (Pattern 3) on select, and set the returned data on RoxyNatalChart. The picker runs in the browser, the key stays in the route.
The TypeScript SDK
Once you have more than two endpoints, switch to @roxyapi/sdk for cleaner code, full IDE autocomplete, and exact response types on every method.
npm install @roxyapi/sdk
# or: bun add @roxyapi/sdk
// app/horoscope/[sign]/page.tsx
import { notFound } from 'next/navigation';
import { createRoxy } from '@roxyapi/sdk';
const roxy = createRoxy(process.env.ROXY_API_KEY!);
const ZODIAC_SIGNS = [
'aries', 'taurus', 'gemini', 'cancer',
'leo', 'virgo', 'libra', 'scorpio',
'sagittarius', 'capricorn', 'aquarius', 'pisces',
] as const;
type ZodiacSign = (typeof ZODIAC_SIGNS)[number];
function isZodiacSign(value: string): value is ZodiacSign {
return (ZODIAC_SIGNS as readonly string[]).includes(value);
}
export default async function HoroscopePage({
params,
}: {
params: Promise<{ sign: string }>;
}) {
const { sign } = await params;
if (!isZodiacSign(sign)) notFound();
const { data, error } = await roxy.astrology.getDailyHoroscope({
path: { sign },
});
if (error) return <p>Could not load horoscope</p>;
return (
<main>
<h1>{data.sign}</h1>
<p>{data.overview}</p>
<small>Lucky number {data.luckyNumber}</small>
</main>
);
}
The SDK does not bypass Next.js fetch caching. Configure caching globally with export const revalidate = 3600 at the page level instead of per-call.
AI astrology chatbots with MCP
Building an AI chatbot that calls Roxy tools at the right moment is a separate problem from "make one HTTP call". For chatbots, do not write tool definitions by hand. Use our MCP server which exposes every endpoint as a tool to any MCP-compatible client (Vercel AI SDK v6, OpenAI Agents SDK, Claude Desktop, Cursor, Claude Code).
We maintain the full reference implementation: the AI Astrology Chatbot starter (Next.js 16, Vercel AI SDK v6, MCP, supports Gemini, Claude, GPT). Clone, set ROXY_API_KEY, deploy.
For the manual function-calling pattern without MCP, see the function calling guide.
Common mistakes (vibecoder gotchas)
AI code generators make these failures every single time. Check each one before you ship.
Mistake 1: NEXT_PUBLIC_ROXY_API_KEY in .env.local
# WRONG, exposes your key to every visitor
NEXT_PUBLIC_ROXY_API_KEY=...
# RIGHT
ROXY_API_KEY=...
The NEXT_PUBLIC_ prefix inlines the value into the client bundle. Always use a plain name.
Mistake 2: calling RoxyAPI from a 'use client' component
Move the fetch into a Server Component, Server Action, or Route Handler. If you need the result in a client component, fetch it in the parent Server Component and pass it down as a prop, or call your own /api/... Route Handler from the client.
Mistake 3: forgetting Next.js 15+ fetch caching defaults
// In Next.js 16, this hits RoxyAPI on EVERY request
const res = await fetch(url, { headers });
// Add caching
const res = await fetch(url, { headers, next: { revalidate: 3600 } });
Next.js 14 caches by default. Next.js 15 and 16 do not. This is the single biggest reason vibecoder Next.js projects burn through their Roxy quota in a day.
Mistake 4: hardcoding the API key
// WRONG
const KEY = 'abc123.def456...';
// RIGHT
const KEY = process.env.ROXY_API_KEY!;
Hardcoding ends up in git, screenshots, error logs, CI artifacts.
Mistake 5: forgetting to set the env var on Vercel
Local works, production breaks. Vercel dashboard, Settings, Environment Variables, add ROXY_API_KEY, redeploy.
Mistake 6: cache no-store on stable data
// Wastes quota on permanent data
await fetch(dreamSymbolsUrl, { cache: 'no-store' });
// Better
await fetch(dreamSymbolsUrl, { cache: 'force-cache' });
Use no-store only for user-specific data (personal natal charts, custom transit reports). Use force-cache or next: { revalidate: N } for everything else.
Troubleshooting
401 in production but works locally
You forgot to set ROXY_API_KEY in the Vercel dashboard, or set it but did not redeploy. Vercel bakes env vars in at build time.
401 with code api_key_required
Your file is not reading the env var. Check it is a Server Component (no 'use client'), the variable name matches .env.local, and you restarted the dev server.
Quota burned in a few hours
Missing cache: 'force-cache' or next: { revalidate: N } on a fetch in a high-traffic page. See Mistake 3.
process.env.ROXY_API_KEY is undefined in a Route Handler
You are on the Edge runtime. Remove export const runtime = 'edge' to use the Node.js default, or expose the env var to edge functions.
Hydration error after calling RoxyAPI
A non-serializable value (Date, Map, undefined) passed from a Server Component to a client component. Convert on the server.
Stale data after deploy
Next.js cached the page at build time. Lower revalidate or call revalidatePath() from a Server Action.
Gotchas
- Backend-only key. Never put
ROXY_API_KEYin a'use client'file or prefix it withNEXT_PUBLIC_. The four safe places are Server Components, Server Actions, Route Handlers, and Middleware. - Timezone. Prefer IANA strings (
"America/New_York","Asia/Kolkata"). Decimal offsets like-5are accepted but do not handle daylight saving. The server resolves IANA to the DST-correct offset for the request date. - Rate limits. Every Roxy plan has daily and monthly caps. A busy daily horoscope page without caching will eat a month of quota in hours. Default to
next: { revalidate: 3600 }for any endpoint that can tolerate an hour of staleness. - Env vars and Vercel. Redeploy after adding or changing
ROXY_API_KEY. Values are baked at build time for serverless. - server-only package. For modules that must never reach the client, add
import 'server-only'at the top. The build fails with a clear error if a client file imports it. - Fetch caching default flipped in Next.js 15. If you are on 15 or 16 and quota is burning fast, missing revalidate is the most likely cause.
What to build next
- The SDK guide covers every typed method for cleaner server code.
- The AI chatbot tutorial wires multiple domains into a conversational app.
- The MCP server is the right primitive for AI agents and chatbots.
- Browse the API reference for every endpoint across 12 domains, or the starter apps for ready-to-clone projects.