<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vaibhav Govind</title>
    <description>The latest articles on DEV Community by Vaibhav Govind (@vaibhavgovind).</description>
    <link>https://dev.to/vaibhavgovind</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4101160%2F8bab5571-4a33-4c20-92aa-8d53052de253.png</url>
      <title>DEV Community: Vaibhav Govind</title>
      <link>https://dev.to/vaibhavgovind</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vaibhavgovind"/>
    <language>en</language>
    <item>
      <title>Building a Vedic Astrology API: thread-local bugs, 1,500-year-old test fixtures, and a 429 disguised as CORS</title>
      <dc:creator>Vaibhav Govind</dc:creator>
      <pubDate>Sun, 30 Aug 2026 09:25:12 +0000</pubDate>
      <link>https://dev.to/vaibhavgovind/building-a-vedic-astrology-api-thread-local-bugs-1500-year-old-test-fixtures-and-a-429-5ge8</link>
      <guid>https://dev.to/vaibhavgovind/building-a-vedic-astrology-api-thread-local-bugs-1500-year-old-test-fixtures-and-a-429-5ge8</guid>
      <description>&lt;p&gt;Astrology apps are one of India's quietest huge markets — panchang widgets, kundli generators, matrimonial matching, muhurta pickers. Under every one of them sits the same unforgiving requirement: &lt;strong&gt;the astronomy has to be exactly right&lt;/strong&gt;, because your user's grandmother has a printed panchang on her wall and she &lt;em&gt;will&lt;/em&gt; check.&lt;/p&gt;

&lt;p&gt;I spent the last few months building &lt;a href="https://grahaapi.com" rel="noopener noreferrer"&gt;GrahaAPI&lt;/a&gt; — 237 REST endpoints across 23 modules of Vedic astrology, Hindi + English in every response. This post isn't a feature tour. It's the four engineering problems I didn't expect, because I think they're interesting even if you never touch astrology.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, 60 seconds of domain: what the computer actually calculates
&lt;/h2&gt;

&lt;p&gt;Strip away the mysticism and Vedic astrology is a coordinate system plus 1,500 years of lookup tables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tithi&lt;/strong&gt; (the "lunar date"): the Moon-Sun angular separation, divided into 12° slices. 30 per lunar month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nakshatra&lt;/strong&gt;: which of 27 equal 13°20′ segments of the ecliptic the Moon occupies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dasha&lt;/strong&gt;: a 120-year planetary period cycle, seeded entirely by the Moon's exact position at birth — a birth-time error of minutes shifts period boundaries by &lt;em&gt;months&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;The whole thing runs on the &lt;strong&gt;sidereal&lt;/strong&gt; zodiac, offset from the tropical zodiac by ~24° (the &lt;em&gt;ayanamsa&lt;/em&gt; — we use Lahiri, the Indian government standard).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So: an ephemeris gives you planetary longitudes, and everything else is careful classical bookkeeping. Which brings me to the first bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #1: the thread-local zodiac
&lt;/h2&gt;

&lt;p&gt;Our ephemeris core is a C library with Python bindings, and it holds "which zodiac mode are you in" as &lt;strong&gt;global state — per thread&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;FastAPI runs sync endpoints on a threadpool. First request warms up thread A: sidereal mode set, positions correct. Then a request lands on freshly-spawned thread B: mode silently defaults to &lt;strong&gt;tropical&lt;/strong&gt;, every longitude comes back ~24° off, and — because 24° is almost exactly one nakshatra-and-a-bit — the Moon lands in a &lt;em&gt;plausible but wrong&lt;/em&gt; nakshatra. Which seeds the dasha. Which means the API happily returned dasha periods &lt;strong&gt;shifted by years&lt;/strong&gt;, no exception, HTTP 200.&lt;/p&gt;

&lt;p&gt;That's the nastiest class of bug: wrong answers that look right. The fix is boring — re-assert the sidereal mode at the top of every function that touches the ephemeris — but the lesson generalizes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you bind to a C library, find out where it keeps its state &lt;strong&gt;before&lt;/strong&gt; you put it behind a threadpool. &lt;code&gt;grep&lt;/code&gt; for anything that looks like &lt;code&gt;set_mode&lt;/code&gt;, and assume it's thread-local until proven otherwise.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Test fixtures written 1,500 years ago
&lt;/h2&gt;

&lt;p&gt;How do you write regression tests for astrology? You steal your fixtures from the classics. Our accuracy suite encodes invariants no code change is allowed to break:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vimshottari dasha periods must sum to &lt;strong&gt;exactly 120 years&lt;/strong&gt; — the canonical cycle length.&lt;/li&gt;
&lt;li&gt;Two &lt;em&gt;identical&lt;/em&gt; birth charts run through 36-point marriage matching must score &lt;strong&gt;28/36, not 36/36&lt;/strong&gt; — identical charts share a &lt;em&gt;nadi&lt;/em&gt;, which is a dosha. (Great interview question for astrology-app devs, by the way.)&lt;/li&gt;
&lt;li&gt;Friday's first choghadiya period must be &lt;strong&gt;Char&lt;/strong&gt; — the sequences are fixed tables, and any drift means an off-by-one in the day-slicing.&lt;/li&gt;
&lt;li&gt;The Ashtakavarga total for our reference chart must equal the classical published value, point for point.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Standing rule in the repo: &lt;strong&gt;if an accuracy test fails, the code is wrong — never the test.&lt;/strong&gt; It's surprisingly clarifying to work on a system where the spec was frozen centuries before software existed.&lt;/p&gt;

&lt;p&gt;My favorite bug this suite's philosophy caught: sunrise at &lt;code&gt;07:59:34&lt;/code&gt; was displayed as &lt;code&gt;07:00&lt;/code&gt;. Classic — we rounded seconds into minutes (&lt;code&gt;59.57 → 60&lt;/code&gt;) without carrying into the hour. Every "time of day" formatter you've ever written has flirted with this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 429 that cosplayed as CORS
&lt;/h2&gt;

&lt;p&gt;One morning, every free tool on our consumer site died at once. The browser console said:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Access to fetch at 'https://api...' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CORS config? Untouched for weeks. &lt;code&gt;curl&lt;/code&gt; from a terminal: everything 200. Classic "works in curl, dies in browser."&lt;/p&gt;

&lt;p&gt;The real story: the site's API key had blown through its monthly quota, so the quota middleware was returning &lt;code&gt;429&lt;/code&gt;. But that middleware was registered &lt;strong&gt;outside&lt;/strong&gt; the CORS middleware — its short-circuited response never passed through the layer that attaches &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;. A response without that header is, to a browser, indistinguishable from a CORS violation. The &lt;em&gt;actual&lt;/em&gt; error — a perfectly informative bilingual 429 with reset dates — was invisible.&lt;/p&gt;

&lt;p&gt;Two fixes, both worth stealing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every early-return path in outer middleware must attach CORS headers itself.&lt;/strong&gt; Your error responses need CORS more than your success responses — that's when the client is trying to tell the user what went wrong.&lt;/li&gt;
&lt;li&gt;When you're debugging "CORS errors" that appear out of nowhere: it's very often &lt;strong&gt;not CORS&lt;/strong&gt;. It's an error response from something (a proxy, a rate limiter, a WAF) that sits outside your CORS layer.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Honesty as an API feature
&lt;/h2&gt;

&lt;p&gt;The design decision I'd defend hardest: most astrology software silently assumes &lt;strong&gt;noon&lt;/strong&gt; when the birth time is unknown, then confidently prints house placements that are pure fiction.&lt;/p&gt;

&lt;p&gt;We made &lt;code&gt;tob&lt;/code&gt; (time of birth) optional — and when it's missing, the response is computed from a sunrise chart &lt;strong&gt;plus an explicit &lt;code&gt;accuracy&lt;/code&gt; object&lt;/strong&gt; stating which parts are reliable (panchang-level facts) and which aren't (house placements, dasha boundaries). The client app decides what to show. Astrology output is probabilistic interpretation stacked on deterministic astronomy; the API's job is to keep the boundary between the two visible.&lt;/p&gt;

&lt;p&gt;Same philosophy in quota handling: the free tier gets a clear 429 with its reset date; &lt;strong&gt;paid plans are throttled (0.5–2s), never blocked&lt;/strong&gt; — an astrology app going down during Diwali because of a quota edge case is not an acceptable failure mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test mode I wish every API had
&lt;/h2&gt;

&lt;p&gt;The feature I'm proudest of has nothing to do with astrology. &lt;code&gt;sk-test-&lt;/code&gt; keys are free, unlimited, no card — and instead of hand-written mock JSON, a test request is &lt;strong&gt;replayed through the real endpoint code&lt;/strong&gt; against a fixed sample chart, then cached:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.grahaapi.com/v1/kundli/full &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer sk-test-anything"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"dob":"1990-06-15","tob":"10:30","lat":28.6139,"lon":77.209,"tz":5.5}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hand-written mocks drift — every team eventually ships a mock with a field the real endpoint renamed two sprints ago. Replayed-through-production mocks &lt;em&gt;can't&lt;/em&gt; drift, because they aren't mocks. Every response is stamped &lt;code&gt;meta.mode: "test"&lt;/code&gt; so nobody accidentally ships sample data. New endpoints get test mode for free, automatically.&lt;/p&gt;

&lt;p&gt;You can integrate all 237 endpoints before paying a rupee, and the structure is guaranteed identical to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to poke at it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Docs for every endpoint: &lt;a href="https://grahaapi.com/docs" rel="noopener noreferrer"&gt;grahaapi.com/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Node SDK: &lt;a href="https://github.com/Vaigo/grahaapi-node" rel="noopener noreferrer"&gt;github.com/Vaigo/grahaapi-node&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;GrahaAPI&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grahaapi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;graha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GrahaAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sk-test-hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;graha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rahuKaal&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-08-30&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;19.076&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;72.8777&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tz&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;5.5&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// → today's Rahu Kaal for Mumbai, from the city's actual sunrise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Free tier is 1,000 calls/month — enough for a real side project. If you're building anything astrology-adjacent for the Indian market (or you've fought your own thread-local C library), I'd genuinely love to hear about it in the comments.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>api</category>
      <category>python</category>
      <category>india</category>
    </item>
  </channel>
</rss>
