# 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 `revalidate` caching
- 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](/docs/mcp) and the Vercel AI SDK

## What you need, 30 seconds

1. A Roxy API key. Get one on the [pricing page](/pricing).
2. 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.
3. Five minutes if you already have a Next.js project. Ten if you are starting from scratch.

**Tip: Our TypeScript SDK ([`@roxyapi/sdk`](/docs/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.

```bash
# .env.local
ROXY_API_KEY=your_roxyapi_key_here
```

**Warning: 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:

1. Vercel dashboard, project, **Settings, Environment Variables**.
2. Add `ROXY_API_KEY` to **Production** (and **Preview** if you want preview deploys to work).
3. 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.


### curl

Confirm the key works before you wire anything:

```bash
curl "https://roxyapi.com/api/v2/astrology/horoscope/aries/daily" \
  -H "X-API-Key: $ROXY_API_KEY"
```

### Server Component with fetch

```tsx
// 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>
  );
}
```

### TypeScript SDK

```tsx
// 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>
  );
}
```

### Server Action for a form

```tsx
// 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() };
}
```

### Route Handler for client widgets

```ts
// 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](/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](/docs/sdk)** gives typed methods on every endpoint.
- **Domain guides** for which endpoints to call in what order:
  - [Western Astrology](/docs/guides/astrology), [Vedic Astrology](/docs/guides/vedic-astrology), [Tarot](/docs/guides/tarot), [Numerology](/docs/guides/numerology), [I Ching](/docs/guides/iching), [Dreams](/docs/guides/dreams)
- **Starter apps:** [AI Astrology Chatbot](https://github.com/RoxyAPI/astrology-ai-chatbot) (Next.js 16, Vercel AI SDK v6, MCP) and [Vedic Kundli app](https://github.com/RoxyAPI/jyotish-vedic-astrology-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:

1. **Server Components** (any `page.tsx` or `layout.tsx` without `'use client'`)
2. **Server Actions** (functions marked with `'use server'`)
3. **Route Handlers** (`app/api/.../route.ts`)
4. **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.

```tsx
// 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();
}
```

**Warning: **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.

```tsx
// 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() };
}
```

```tsx
// 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`](https://react.dev/reference/react/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.

```ts
// 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:

```tsx
// 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>
    </>
  );
}
```

**Tip: 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`](/ui), 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.

```bash
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.

```tsx
// 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} />;
}
```

```tsx
// 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} />;
}
```

**Warning: 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:

- `RoxyNatalChart` for `POST /astrology/natal-chart` (Western birth chart wheel)
- `RoxyVedicKundli` for `POST /vedic-astrology/birth-chart` (set `chartStyle="south"`, `"north"`, or `"east"`)
- `RoxyHoroscopeCard` for `GET /astrology/horoscope/{sign}/{daily,weekly,monthly}` (set `period`)
- `RoxyNumerologyCard`, `RoxyTarotSpread`, `RoxyPanchangTable`, and more in the [Roxy UI reference](/ui).

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`](/docs/sdk) for cleaner code, full IDE autocomplete, and exact response types on every method.

```bash
npm install @roxyapi/sdk
# or: bun add @roxyapi/sdk
```

```tsx
// 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](/docs/mcp) 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](https://github.com/RoxyAPI/astrology-ai-chatbot) (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](/docs/guides/function-calling).

## 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

```bash
# 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

```ts
// 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

```ts
// 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

```ts
// 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_KEY` in a `'use client'` file or prefix it with `NEXT_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 `-5` are 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](/docs/sdk) covers every typed method for cleaner server code.
- The [AI chatbot tutorial](/docs/tutorials/ai-chatbot) wires multiple domains into a conversational app.
- The [MCP server](/docs/mcp) is the right primitive for AI agents and chatbots.
- Browse the [API reference](/api-reference) for every endpoint across 12 domains, or the [starter apps](/docs/starters) for ready-to-clone projects.
