Build a kundli page in Next.js

You will build a page that takes a birth date, time and place, and shows the lagna, the planets and a chart image. The browser talks only to your own route, and the route holds the API key.

You need Node.js 20 or later and a test key. Test keys run the real engine and are never charged.

1. Create the app

npx create-next-app@latest kundli-demo --ts --app --no-tailwind --no-eslint --no-src-dir --yes
cd kundli-demo

2. Install the client

npm install https://developers.astrologics.in/downloads/sdks/astrologics-typescript-0.1.0.tgz

3. Keep the key on the server

Create .env.local. create-next-app already keeps .env* files out of git.

ASTROLOGICS_API_KEY=al_test_YOUR_KEY

Do not give this variable a NEXT_PUBLIC_ prefix. That prefix sends a value to the browser, and a secret key must never reach it.

4. Add the route

Create app/api/kundli/route.ts. Birth details travel in the POST body, never in a URL.

import { AstroLogics, ApiError } from "astrologics";

export async function POST(request: Request) {
  const { date, time, place, placeId } = await request.json().catch(() => ({}));
  const birth = { date, time, place: placeId ? { place_id: placeId } : { query: place } };
  const client = new AstroLogics({ apiKey: process.env.ASTROLOGICS_API_KEY! });

  try {
    const [kundli, chart] = await Promise.all([
      client.getKundli({ birth }),
      client.getChartSvg({ varga: "D1", birth }),
    ]);
    return Response.json({ lagna: kundli.data.lagna, planets: kundli.data.planets, chart });
  } catch (e) {
    if (!(e instanceof ApiError)) throw e;
    if (e.status === 400 || e.status === 422) {
      return Response.json({ error: e.message, candidates: e.candidates }, { status: 422 });
    }
    console.error(e.code, e.requestId);
    return Response.json({ error: "The chart could not be calculated. Try again." }, { status: 502 });
  }
}

The route makes two calls: POST /v1/kundli (2 credits) and POST /v1/charts/{varga}.svg (2 credits). When a place name matches more than one place, the API answers place_ambiguous with candidates. It never guesses, so the page asks the user to choose.

5. Add the page

Replace app/page.tsx. The chart goes in an <img> as a data URL, so the page never inserts markup from a response into the document.

"use client";
import { useState, type FormEvent } from "react";

type Candidate = { place_id: string; name: string; state?: string; country?: string };
type Planet = { id: string; label: string; sign: string; house?: number; retrograde: boolean };
type Result = { lagna: { sign_label: string }; planets: Planet[]; chart: string };

export default function Home() {
  const [form, setForm] = useState({ date: "1990-08-13", time: "06:30", place: "New Delhi" });
  const [result, setResult] = useState<Result | null>(null);
  const [candidates, setCandidates] = useState<Candidate[]>([]);
  const [error, setError] = useState("");
  const [busy, setBusy] = useState(false);

  async function load(placeId?: string) {
    setBusy(true); setError(""); setCandidates([]); setResult(null);
    try {
      const res = await fetch("/api/kundli", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ ...form, placeId }),
      });
      const body = await res.json();
      if (res.ok) setResult(body);
      else { setError(body.error); setCandidates(body.candidates ?? []); }
    } catch {
      setError("Could not reach the server. Try again.");
    } finally {
      setBusy(false);
    }
  }
  function submit(e: FormEvent) { e.preventDefault(); load(); }
  const field = (name: keyof typeof form) => ({
    value: form[name],
    onChange: (e: { target: { value: string } }) => setForm({ ...form, [name]: e.target.value }),
  });

  return (
    <main style={{ maxWidth: 640, margin: "2rem auto", padding: "0 1rem" }}>
      <h1>Kundli</h1>
      <form onSubmit={submit} style={{ display: "grid", gap: "0.75rem" }}>
        <label>Date <input type="date" required {...field("date")} /></label>
        <label>Time <input type="time" required {...field("time")} /></label>
        <label>Place <input required {...field("place")} /></label>
        <button disabled={busy}>{busy ? "Calculating…" : "Show chart"}</button>
      </form>

      {error && <p role="alert">{error}</p>}
      {candidates.map((c) => (
        <p key={c.place_id}>
          <button onClick={() => load(c.place_id)}>
            {[c.name, c.state, c.country].filter(Boolean).join(", ")}
          </button>
        </p>
      ))}

      {result && (
        <section>
          <h2>Lagna: {result.lagna.sign_label}</h2>
          <img
            src={`data:image/svg+xml;charset=utf-8,${encodeURIComponent(result.chart)}`}
            alt="Birth chart"
            width={300}
            height={300}
          />
          <table>
            <thead><tr><th>Planet</th><th>Sign</th><th>House</th></tr></thead>
            <tbody>
              {result.planets.map((p) => (
                <tr key={p.id}>
                  <td>{p.label}{p.retrograde ? " (R)" : ""}</td>
                  <td style={{ textTransform: "capitalize" }}>{p.sign}</td>
                  <td>{p.house ?? "–"}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </section>
      )}
    </main>
  );
}

6. Run it

npm run dev

Open http://localhost:3000 and press Show chart. To see the place choices, enter a name that many places share.

Before you go live

  • Protect the route. Anyone who can reach /api/kundli can spend your credits. Require sign-in or add a rate limit of your own first.
  • Use a live key. Set ASTROLOGICS_API_KEY to your al_live_ key in your host's settings. Only successful calls are charged.
  • Take care with birth details. They are personal data. This route stores nothing and does not log them, so keep it that way unless you need to, and tell your users if you do. We never write them to our database or logs.
  • Unknown birth times. Add a way to send time_accuracy, rather than filling in a default time. See the birth object.

Next: TypeScript quickstart · errors · Flutter tutorial