DEV Community

peachstone
peachstone

Posted on

How I Built a Moon-Phase Tarot App with Zero External API Calls

The Problem: Most Tarot Apps Are Random

When I started building Luna Tarot — a web app that gives you a daily tarot reading tied to the actual moon phase — I faced a design decision that would shape the entire project.

Most tarot apps work like this: shuffle, deal, look up a card meaning from a table, display. The card you pull is random. The meaning never changes. There's no connection to anything external — no rhythm, no context.

I wanted something different. I wanted each reading to be grounded in something real: the actual phase of the moon.

So I needed a way to calculate the current moon phase — accurately, reliably, and without relying on third-party APIs.

The Solution: Astronomy Engine

I discovered** Astronomy Engine** — an open-source library for calculating positions of the Sun, Moon, and planets.

It's available as an npm package and supports both browser and Node.js environments. Here's why it stood out:

  • Accurate: Built on VSOP87 and NOVAS, validated against JPL Horizons data — precise to within ±1 arcminute.

  • Lightweight: The minified bundle is only ~120KB.

  • Fast: Moon-phase calculations run in under 5ms — zero impact on page load or UI responsiveness.

Here's what it can do:

  • Calculate the Moon's phase (expressed as an ecliptic longitude)

  • Find quarter moon phases (new, first quarter, full, third quarter)

  • Calculate rise, set, and culmination times

  • Predict lunar and solar eclipses

  • And much more

Why Zero API Calls?

Most developers would reach for a weather or astronomy API. Here's why I didn't:

  1. Privacy by default: For a spiritual practice app, users shouldn't have to wonder if their reading data is being sent to some third-party service. By calculating everything locally, the app never makes any external requests — no API keys, no network calls, no tracking.

  2. No geolocation needed: Moon phase and illumination percentage are global constants (unlike rise/set times, which depend on location). This means I never need to request the user's GPS data — eliminating another potential privacy leak at the source.

  3. No costs, no outages: No rate limits, no paywalls, no CORS proxy servers, and zero dependency on third-party uptime.

How I Implemented It

Here's the core TypeScript implementation I use in Luna Tarot:

import * as Astronomy from 'astronomy-engine';

// Static metadata for each lunar phase
const moonPhaseInfo = {
  new_moon: { nameKey: 'new-moon-name', descKey: 'new-moon-desc-full', icon: '🌑' },
  waxing_crescent: { nameKey: 'waxing-crescent-name', descKey: 'waxing-crescent-desc-full', icon: '🌒' },
  first_quarter: { nameKey: 'first-quarter-name', descKey: 'first-quarter-desc-full', icon: '🌓' },
  waxing_gibbous: { nameKey: 'waxing-gibbous-name', descKey: 'waxing-gibbous-desc-full', icon: '🌔' },
  full_moon: { nameKey: 'full-moon-name', descKey: 'full-moon-desc-full', icon: '🌕' },
  waning_gibbous: { nameKey: 'waning-gibbous-name', descKey: 'waning-gibbous-desc-full', icon: '🌖' },
  last_quarter: { nameKey: 'last-quarter-name', descKey: 'last-quarter-desc-full', icon: '🌗' },
  waning_crescent: { nameKey: 'waning-crescent-name', descKey: 'waning-crescent-desc-full', icon: '🌘' },
};

type MoonPhaseType = keyof typeof moonPhaseInfo;

/**
 * Calculate precise lunar phases using astronomy-engine
 * phaseValue: 0-360 degrees, 0 = new moon, 180 = full moon
 * illumination: percentage of moon surface visible from Earth
 */
function calculateMoonPhase(date: Date): { phase: MoonPhaseType; illumination: number } {
  const phaseValue = Astronomy.MoonPhase(date);

  // Standard illumination formula: (1 - cos(phaseAngle)) / 2
  // 0° → 0%, 90° → 50%, 180° → 100%, 270° → 50%
  const illumination = (1 - Math.cos(phaseValue * Math.PI / 180)) / 2 * 100;

  // Map 0-360° to 8 discrete phases (45° each)
  const phases: MoonPhaseType[] = [
    'new_moon', 'waxing_crescent', 'first_quarter', 'waxing_gibbous',
    'full_moon', 'waning_gibbous', 'last_quarter', 'waning_crescent'
  ];
  const index = Math.floor(((phaseValue + 22.5) % 360) / 45);
  const phase = phases[index] || 'new_moon';

  return { phase, illumination: Math.round(illumination * 10) / 10 };
}
Enter fullscreen mode Exit fullscreen mode

With just a few lines of code, I can determine exactly where the moon is in its cycle — and use that to change the tarot reading dynamically.

💡 Alternative approach: If you prefer explicit range checks for readability, the classic if/else if chain works perfectly too. The array-mapping version above is just a compact, functional alternative I ended up using.

The Result

Luna Tarot now delivers daily tarot readings that shift with the actual lunar cycle. The same card on a New Moon (beginnings, planting seeds) tells a different story than on a Full Moon (culmination, release).


Caption: The main dashboard blends daily tarot guidance with the current lunar energy — all computed client-side.


Caption: The calendar shows real-time phase, illumination percentage, and traditional almanac data, updated instantly for any date.

The full tech stack:

  • Next.js 14.2 + React 18 + TypeScript — frontend framework

  • Tailwind CSS — styling

  • astronomy-engine — moon phase calculations

  • lunar-javascript — Chinese lunar calendar conversion

  • Python + NumPy DSP — procedurally generated meditation music (pre‑rendered as audio assets)

Everything runs in the browser. No API calls. No server dependencies for the core functionality.

🧑‍💻 Development Note: I used GLM-5.2 as an AI coding assistant (similar to Copilot) during development for translating content and generating boilerplate. However, the production app itself makes zero external API calls — no AI models, no third-party services, no tracking.

Key Takeaways

  1. You don't always need an API. For many types of data (astronomy, math, calendars), local calculation is not only possible but often better — faster, cheaper, and more reliable.

  2. Open-source libraries are powerful. Astronomy Engine is a hidden gem — accurate, well-tested, and available in multiple languages.

  3. Privacy is a feature. When you eliminate external calls, you eliminate data sharing concerns by default.

  4. Start with the user experience. I chose local calculation not because it was easier, but because it created a better, more private experience for users.

Try It Yourself

Luna Tarot is live and completely free: https://www.lunatarotapp.com

No account required. Just you, the cards, and the moon. 🌙


If you're interested in the astronomy-engine library, check out the GitHub repository or the npm package.


📌 Suggested Tags for DEV.to:
nextjs, typescript, webdev, javascript, astronomy, opensource, tailwindcss, privacy, frontend, indiedev

📌 Discussion Prompt (Pin this in comments):

Have you built calendar, weather, or astronomy tools that avoid third-party APIs? What math libraries or local-computation tricks did you use? I'd love to compare approaches in the comments!

Top comments (0)