Build an Astrology App with Vue.js & Nuxt & Vedika API

Build an Astrology App with Vue.js, Nuxt 3, and Vedika API
Published: March 13, 2026 | By Vedika Intelligence | Reading time: 13 minutes

Nuxt 3 brings the best of Vue's Composition API together with universal rendering, auto-imports, and a zero-config production server. For astrology app developers, this combination means SEO-friendly birth chart pages, reactive horoscope components, and a clean pattern for keeping API keys server-side — all without boilerplate.

In this tutorial, we'll build a production-ready astrology app using Nuxt 3 and Vedika API — the only B2B astrology API with AI-powered conversational capabilities, 140+ endpoints, and Swiss Ephemeris precision. You'll implement composables for API calls, Pinia for birth data state, server/api/ routes for key security, and useHead for dynamic SEO.

What You'll Build: A Nuxt 3 astrology app with server-rendered birth chart and horoscope pages, a Pinia store for birth data, composables that wrap Vedika API calls, a secure server/api/ proxy, and an AI chatbot component with real-time responses.

Why Nuxt 3 for Astrology Apps?

Universal Rendering

SSR, SSG, and SPA modes in one project. Birth chart pages SSR for SEO; chat interface runs as SPA for real-time updates.

Auto-Imports

Composables, components, and Pinia stores are auto-imported. No import statements for useBirthStore() or useVedika().

Nitro Server

Built-in server/api/ routes run in Node.js or edge environments. Proxy Vedika API with your key hidden from the client.

useHead for SEO

Dynamic title, description, and Open Graph tags per page. Birth chart pages get unique meta tags with the user's ascendant sign.

Prerequisites

Step 1: Create the Nuxt 3 Project

# Create Nuxt 3 project npx nuxi@latest init astrology-app cd astrology-app # Install Pinia for state management npm install @pinia/nuxt pinia # Install Nuxt UI (optional, for polished components) npm install @nuxt/ui

Configure nuxt.config.ts

// nuxt.config.ts export default defineNuxtConfig({ modules: [ '@pinia/nuxt', '@nuxt/ui' ], runtimeConfig: { // Private: only available on the server vedikaApiKey: process.env.VEDIKA_API_KEY, // Public: exposed to the client (safe values only) public: { vedikaBaseUrl: 'https://api.vedika.io' } }, nitro: { preset: 'node-server' } });

Create a .env file:

# .env — never commit this VEDIKA_API_KEY=vk_live_your_key_here

Step 2: Server API Routes (Secure Proxy)

Nuxt 3 server routes run on Nitro and have access to private runtime config. Create routes that proxy Vedika API calls:

// server/api/birth-chart.post.ts export default defineEventHandler(async (event) => { const config = useRuntimeConfig(); const body = await readBody(event); const { datetime, latitude, longitude, timezone = '+05:30' } = body; if (!datetime || latitude == null || longitude == null) { throw createError({ statusCode: 400, message: 'Missing birth details' }); } const res = await $fetch('https://api.vedika.io/v2/astrology/birth-chart', { method: 'POST', headers: { 'X-API-Key': config.vedikaApiKey, 'Content-Type': 'application/json' }, body: { datetime, latitude, longitude, timezone } }); return res; });
// server/api/horoscope.post.ts export default defineEventHandler(async (event) => { const config = useRuntimeConfig(); const { sign, language = 'en' } = await readBody(event); return $fetch('https://api.vedika.io/v2/astrology/horoscope/daily', { method: 'POST', headers: { 'X-API-Key': config.vedikaApiKey }, body: { sign, language } }); });
// server/api/chat.post.ts export default defineEventHandler(async (event) => { const config = useRuntimeConfig(); const { question, birth, language = 'en' } = await readBody(event); return $fetch('https://api.vedika.io/api/v1/astrology/query', { method: 'POST', headers: { 'X-API-Key': config.vedikaApiKey }, body: { question, language, birthDetails: { datetime: birth.datetime, latitude: birth.latitude, longitude: birth.longitude, timezone: birth.timezone || '+05:30' } } }); });

Step 3: Pinia Store for Birth Data

Create a Pinia store to hold birth details across all pages. In Nuxt 3, this is auto-imported everywhere:

// stores/useBirthStore.ts import { defineStore } from 'pinia'; export interface BirthDetails { datetime: string; latitude: number; longitude: number; timezone: string; locationName: string; } export const useBirthStore = defineStore('birth', { state: (): { data: BirthDetails | null; chart: any | null } => ({ data: null, chart: null }), getters: { isReady: (state) => !!state.data?.datetime && state.data?.latitude != null && state.data?.longitude != null, ascendant: (state) => state.chart?.data?.ascendant?.sign || null, planets: (state) => state.chart?.data?.planets || [] }, actions: { async setBirthAndFetchChart(details: BirthDetails) { this.data = details; this.chart = await $fetch('/api/birth-chart', { method: 'POST', body: details }); }, clear() { this.data = null; this.chart = null; } }, persist: true // requires @pinia-plugin-persistedstate });

Step 4: Composables for Vedika API

Auto-imported composables wrap API calls in clean, reusable functions:

// composables/useVedika.ts export function useVedika() { const birthStore = useBirthStore(); async function getDailyHoroscope(sign: string, language = 'en') { return $fetch('/api/horoscope', { method: 'POST', body: { sign, language } }); } async function askQuestion(question: string, language = 'en') { if (!birthStore.isReady) throw new Error('No birth details'); return $fetch('/api/chat', { method: 'POST', body: { question, birth: birthStore.data, language } }); } async function getKundliMatch(birth1: any, birth2: any) { return $fetch('/api/kundli-match', { method: 'POST', body: { birth1, birth2 } }); } return { getDailyHoroscope, askQuestion, getKundliMatch }; }

Step 5: Birth Chart Page with useAsyncData

The birth chart page uses useAsyncData for server-side data fetching with a loading state:

<!-- pages/chart.vue --> <script setup lang="ts"> const birthStore = useBirthStore(); const router = useRouter(); // Form state const form = reactive({ datetime: '', latitude: null as number | null, longitude: null as number | null, timezone: '+05:30', locationName: '' }); const submitting = ref(false); const error = ref(''); async function submitChart() { if (!form.datetime || form.latitude == null || form.longitude == null) { error.value = 'Please fill all birth details'; return; } submitting.value = true; error.value = ''; try { await birthStore.setBirthAndFetchChart({ ...form } as any); } catch (e: any) { error.value = e.message || 'Failed to fetch birth chart'; } finally { submitting.value = false; } } // Dynamic SEO based on chart data useHead({ title: computed(() => birthStore.ascendant ? `${birthStore.ascendant} Ascendant Birth Chart | Vedika` : 'Vedic Birth Chart Calculator | Vedika' ), meta: [ { name: 'description', content: computed(() => birthStore.ascendant ? `Your Vedic birth chart with ${birthStore.ascendant} rising. Computed with Swiss Ephemeris precision.` : 'Generate your Vedic birth chart with astronomical precision.' ) } ] }); </script> <template> <div class="chart-page"> <h1>Vedic Birth Chart Calculator</h1> <form @submit.prevent="submitChart"> <div class="form-group"> <label>Date & Time of Birth</label> <input v-model="form.datetime" type="datetime-local" required /> </div> <div class="form-group"> <label>Latitude</label> <input v-model.number="form.latitude" type="number" step="0.0001" placeholder="28.6139 (New Delhi)" required /> </div> <div class="form-group"> <label>Longitude</label> <input v-model.number="form.longitude" type="number" step="0.0001" placeholder="77.2090" required /> </div> <div class="form-group"> <label>Timezone (UTC offset)</label> <input v-model="form.timezone" type="text" placeholder="+05:30" /> </div> <p v-if="error" class="error">{{ error }}</p> <button type="submit" :disabled="submitting"> {{ submitting ? 'Calculating...' : 'Generate Birth Chart' }} </button> </form> <div v-if="birthStore.chart" class="chart-result"> <h2>Ascendant: {{ birthStore.ascendant }}</h2> <table> <thead> <tr><th>Planet</th><th>Sign</th><th>House</th><th>Degree</th></tr> </thead> <tbody> <tr v-for="planet in birthStore.planets" :key="planet.name"> <td>{{ planet.name }}</td> <td>{{ planet.sign }}</td> <td>{{ planet.house }}</td> <td>{{ planet.degree?.toFixed(2) }}°</td> </tr> </tbody> </table> </div> </div> </template>

Step 6: Daily Horoscope Page with useFetch

useFetch is the idiomatic Nuxt 3 pattern for data that's fetched on page load and needs SSR:

<!-- pages/horoscope/[sign].vue --> <script setup lang="ts"> const route = useRoute(); const sign = computed(() => route.params.sign as string); // useFetch runs on server during SSR — fully SEO-indexed const { data: horoscope, pending, error, refresh } = await useFetch( '/api/horoscope', { method: 'POST', body: computed(() => ({ sign: sign.value, language: 'en' })), watch: [sign] // refetch when sign changes } ); useHead({ title: computed(() => `${capitalize(sign.value)} Daily Horoscope 2026 | Vedika`), meta: [ { name: 'description', content: computed(() => `Today's ${capitalize(sign.value)} horoscope powered by Vedika AI. ` + 'Swiss Ephemeris precision, updated daily.' ) }, { property: 'og:type', content: 'article' } ] }); function capitalize(s: string) { return s.charAt(0).toUpperCase() + s.slice(1); } </script> <template> <article> <h1>{{ capitalize(sign) }} Daily Horoscope</h1> <p v-if="pending">Loading today's prediction...</p> <p v-else-if="error">Failed to load horoscope.</p> <div v-else-if="horoscope" class="horoscope-content"> <p>{{ horoscope.data?.prediction || horoscope.data?.horoscope }}</p> <button @click="refresh">Refresh</button> </div> </article> </template>

Ready to Build Your Nuxt Astrology App?

Try the FREE Sandbox for development — 65 mock endpoints, no credit card required. Production plans from $12/month.

Get Your API Key

Step 7: AI Chatbot Component

A reactive Vue component for the AI chatbot that reads birth data from the Pinia store:

<!-- components/AstroChat.vue --> <script setup lang="ts"> const birthStore = useBirthStore(); const { askQuestion } = useVedika(); interface Message { role: 'user' | 'ai'; text: string } const messages = ref<Message[]>([]); const input = ref(''); const loading = ref(false); async function sendMessage() { if (!input.value.trim() || loading.value || !birthStore.isReady) return; const question = input.value; input.value = ''; messages.value.push({ role: 'user', text: question }); loading.value = true; try { const result: any = await askQuestion(question); const answer = result?.data?.answer || result?.answer || 'No response'; messages.value.push({ role: 'ai', text: answer }); } catch (e: any) { messages.value.push({ role: 'ai', text: `Error: ${e.message}` }); } finally { loading.value = false; } } </script> <template> <div class="chat-container"> <div v-if="!birthStore.isReady" class="no-birth"> <NuxtLink to="/chart">Enter birth details to chat with Vedika AI</NuxtLink> </div> <template v-else> <div class="messages"> <div v-for="(msg, i) in messages" :key="i" :class="['message', msg.role]">{{ msg.text }}</div> <div v-if="loading" class="message ai thinking"> Vedika AI is thinking... </div> </div> <form @submit.prevent="sendMessage" class="chat-form"> <input v-model="input" type="text" placeholder="Ask about your birth chart..." :disabled="loading" /> <button type="submit" :disabled="loading">Send</button> </form> </template> </div> </template>

Step 8: Multi-Language Support

Vedika API responds in 30 languages. Detect the user's locale in Nuxt and pass it to the API:

// composables/useLocale.ts export function useLocale() { const supportedLangs = [ 'en', 'hi', 'ta', 'te', 'bn', 'gu', 'mr', 'kn', 'ml', 'pa', 'ar', 'fr' ]; const detectedLang = computed(() => { if (process.server) return 'en'; const browserLang = navigator.language.split('-')[0]; return supportedLangs.includes(browserLang) ? browserLang : 'en'; }); return { detectedLang }; }

Production Deployment

# Build for production npm run build # Run production server node .output/server/index.mjs # Deploy to Vercel (zero config) npx vercel # Deploy to Google Cloud Run gcloud run deploy astrology-nuxt \ --source . \ --set-env-vars VEDIKA_API_KEY=vk_live_xxx \ --region asia-south1 \ --allow-unauthenticated # Generate static site (for CDN deployment) npm run generate

Why Choose Vedika API?

AI-Powered

The only astrology API with a built-in AI engine. Natural language questions — no complex calculation code.

140+ Endpoints

Birth charts, yogas, doshas, dashas, Ashtakavarga, transits, Kundali matching, numerology, and more.

30 Languages

Hindi, Tamil, Telugu, Bengali, Gujarati, Marathi, Kannada, Arabic, and 22 more languages supported.

Swiss Ephemeris

Astronomical-grade planetary accuracy. The same precision used by professional astrologers and research applications.

MCP Server

World's first astrology MCP server. AI agents and automation tools can call Vedika natively.

From $12/month

Starter covers indie developers. Business plan at $120/month for production Nuxt apps with traffic.

Pricing

Vedika API offers transparent pricing starting at $12/month:

All plans include 140+ endpoints, AI chatbot, and 30-language support. View detailed pricing

Conclusion

Nuxt 3 and Vedika API together deliver a production-ready astrology app with minimal boilerplate. Nuxt handles SSR, SEO, and the full-stack architecture — Vedika handles Swiss Ephemeris calculations, AI predictions, and 30 languages.

Next steps:

  1. Get your Vedika API key at vedika.io/signup
  2. Read the full API documentation
  3. Try the free sandbox at vedika.io/sandbox
  4. Add Kundali matching and transit predictions to your app

About Vedika Intelligence: Vedika is the only B2B astrology API with AI-powered chatbot capabilities, serving production apps worldwide. Our AI engine enables natural language astrology queries with Swiss Ephemeris verified precision, supporting both Vedic and Western astrology systems across 30 languages.

Try the #1 Vedic Astrology API

140+ endpoints, 30 languages, Swiss Ephemeris precision. Free sandbox included — no credit card required.

Get Free API Key