- Docs
- Integrations
- Next.js
Next.js astrology API with the App Router
Ship a server-rendered horoscope page, a natal chart form, or an embeddable widget from a Next.js app without the API key ever reaching the browser. Works on Next.js 14, 15 and 16.
Everything below is the part of a RoxyAPI integration that is specific to the App Router: where the key is allowed to live, which of the four server surfaces fits which feature, how the location lookup feeds a chart, how to render the result, and what caching actually does since the Next.js 15 default changed. The endpoints, fields and error contract are not repeated here, because your agent should read them from the source rather than from a page that can go stale.
Point your agent at the source, not at this page
Drop this into the AGENTS.md or CLAUDE.md at your repo root, so every coding agent that opens the project starts with it:
RoxyAPI: one REST API for astrology, Vedic astrology, forecasting, human design, numerology, tarot and 18+ insight domains on one key. Base URL https://roxyapi.com/api/v2. Auth is the X-API-Key header, read from process.env.ROXY_API_KEY, server side only.
Where the truth lives, in this order:
1. The docs MCP server at https://roxyapi.com/mcp/docs. Streamable HTTP, no key, one tool: search_docs. Search it before every endpoint, field, SDK method and integration step.
2. https://roxyapi.com/AGENTS.md, read in full before any code. Auth rules, the location-first rule, request body shapes, the error contract, field formats, domain gotchas.
3. The OpenAPI spec at https://roxyapi.com/api/v2/openapi.json, every domain in one document: query it with the jq recipe in AGENTS.md, never read it whole. Generate types for one domain from that domain spec, never hand-write a response interface:
npx openapi-typescript https://roxyapi.com/api/v2/astrology/openapi.json -o src/api/schema.ts
4. No MCP available? Fetch https://roxyapi.com/llms.txt instead.
Rules: npm install @roxyapi/sdk and use its typed methods rather than hand-rolled fetch. TypeScript strict, no any. Never call RoxyAPI from a 'use client' file. Resolve a birthplace with GET /location/search?q={city} before any chart call and pass its timezone through.
Your editor probably has its own way to register an MCP server, one page each: Claude Code, Cursor, Windsurf, GitHub Copilot, Codex, Gemini CLI. For a whole app in one paste rather than one feature, copy a prompt from AI prompts.
Set the key
# .env.local
ROXY_API_KEY=your_key_here
On Vercel: Settings, Environment Variables, add ROXY_API_KEY to Production and Preview, then redeploy. Values are baked in at build time for serverless functions, so a new key without a redeploy is a 401 in production and a working page locally.
Never prefix it NEXT_PUBLIC_. That inlines the value into the client bundle at build time, where DevTools can read it. If a feature genuinely needs to call from the browser, the answer is a publishable pk_ key locked to your origin, minted at your account, and the widgets, never a secret sk_ key behind a public prefix.
The one rule: never from a client component
Anything in a file marked 'use client' runs in the browser. There are exactly four safe places to hold the key:
- Server Components, any
page.tsxorlayout.tsxwithout'use client' - Server Actions, functions marked
'use server' - Route Handlers,
app/api/.../route.ts - Middleware and Edge functions
For modules that must never be imported client side, add import 'server-only' at the top. The build then fails with a clear error instead of shipping the key.
Pattern 1, Server Component for stable content
Best for horoscopes, dream symbols, reference data. The server fetches, renders HTML, and the browser never sees the 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 },
},
);
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>
);
}
Pattern 2, Server Action for the birth form
Best for natal charts and anything else that belongs to one person. Charts need latitude, longitude and timezone, so the form collects a city and the action resolves it. Nobody should be typing coordinates.
// app/birth-chart/actions.ts
'use server';
import { createRoxy } from '@roxyapi/sdk';
const roxy = createRoxy(process.env.ROXY_API_KEY!);
export async function generateBirthChart(formData: FormData) {
const { data: places } = await roxy.location.searchCities({
query: { q: String(formData.get('city')) },
});
const city = places?.cities[0];
if (!city) return { error: 'Location not found' };
const { data, error } = await roxy.astrology.generateNatalChart({
body: {
date: String(formData.get('date')), // YYYY-MM-DD
time: `${formData.get('time')}:00`, // HH:MM:SS
latitude: city.latitude,
longitude: city.longitude,
timezone: city.timezone, // IANA, DST correct for that date
},
});
if (error) return { error: error.error, code: error.code };
return { chart: data };
}
searchCities returns { total, limit, offset, cities }, and each city carries city, province, country, iso2, latitude, longitude, timezone and utcOffset. Pass the IANA timezone string straight through: the server resolves it to the daylight-saving-correct offset for the birth date, which a hardcoded -5 cannot do.
For field-level validation and pending state, wrap the action with useActionState. A 400 comes back with issues[] listing every field problem at once, which maps onto form errors directly.
Pattern 3, Route Handler as a proxy
Best for widgets, polling, and anything a client component has to call. The handler holds the key.
// 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 then calls /api/horoscope/aries, never roxyapi.com. Add Access-Control-Allow-Origin to the response and the same route serves an embeddable widget on other sites, on your quota and your brand.
Pattern 4, render it with the component library
Patterns 1 to 3 fetch the data and leave you hand-building the markup. @roxyapi/ui-react draws it instead: chart wheels, kundli grids, spreads, panchang tables, dasha timelines.
npm install @roxyapi/ui-react
The components mount custom elements and need the DOM, so any file that imports them declares 'use client'. The Server Component stays key side and streams the data down.
// app/birth-chart/chart-view.tsx
'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. The SDK returns { data, error, response }; destructure on the server and hand the component only data. Passing the whole envelope renders [object Object].
Take the type from the SDK too (NatalChartResponse here). A locally declared interface drifts the day the API grows a field, and the component then silently renders nothing.
For a live city picker inside the client component, mount RoxyLocationSearch and read latitude, longitude and timezone off its roxy-location-select event, then call your Route Handler. The picker runs in the browser, the key stays in the route.
Caching, and the default that changed
In Next.js 14, fetch cached by default. In 15 and 16 the default is auto no cache: a statically prerendered route still fetches once at build, but the moment the route reads a request-time API it fetches on every request. That is the single biggest reason a Next.js project burns a month of quota in a day.
| Data | Setting |
|---|---|
| Daily content: horoscopes, panchang, transits | next: { revalidate: 3600 } |
| Permanent content: dream symbols, hexagrams, angel numbers, zodiac reference | cache: 'force-cache' |
| Per person: natal charts, synastry, custom readings | cache: 'no-store' |
The SDK does not go through the Next.js fetch cache. Two ways to cache it anyway: set export const revalidate = 3600 at the page level, or on Next.js 16 set cacheComponents: true in next.config.ts and wrap the call in a function marked 'use cache' with cacheLife('hours').
Troubleshooting
| Symptom | Cause |
|---|---|
| 401 in production, fine locally | ROXY_API_KEY missing on Vercel, or set but not redeployed. |
401 with code: "api_key_required" | The file is not reading the env var: a 'use client' boundary, a name mismatch, or a dev server that was not restarted. |
| Quota gone in hours | A high traffic page with no revalidate or force-cache. |
process.env.ROXY_API_KEY undefined in a Route Handler | You are on the Edge runtime. Drop export const runtime = 'edge' or expose the variable to edge functions. |
| Hydration error after a call | A non-serializable value (Date, Map, undefined) crossed from a Server Component into a client component. Convert it on the server. |
| Stale page after a deploy | Lower revalidate, or call revalidatePath() from a Server Action. |
What to build next
- Domain guides, for which endpoints to call in what order:
- Templates: MIT apps to clone, including the AI astrology chatbot and the Vedic kundli app, both Next.js.
- Remote MCP: the right primitive for a chatbot, where the model calls calculations as tools. Wiring one into a chat UI is the AI chatbot tutorial; rendering the tool result as a chart is AI chat widgets. Without MCP, the function calling guide.
- TypeScript SDK: every typed method, and the error codes worth branching on.
- API reference: the human playground, where you can try any endpoint in the browser.