Secure pattern for proxying AI APIs (OpenAI, Gemini, Grok) through Nuxt server routes. Comprehensive Guide Jan 2026 — model names refreshed mid-2026.
Why Server Routes (Not Direct Client Calls)?
| Reason | Explanation |
|---|
| Security | API keys stay on the server — never exposed to the browser |
| Flexibility | Handle streaming, retries, and errors in one place |
| SSR compatibility | Works with Nitro; no CORS issues |
| Cost control | Add rate limiting, usage tracking, and guards |
Step 1: Environment Variables
# .env
OPENAI_API_KEY=sk-your-openai-key
GEMINI_API_KEY=your-google-gemini-key
GROK_API_KEY=your-xai-grok-key
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
openaiApiKey: process.env.OPENAI_API_KEY,
geminiApiKey: process.env.GEMINI_API_KEY,
grokApiKey: process.env.GROK_API_KEY,
}
})
Step 2: Server Routes
OpenAI — server/api/openai/chat.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const config = useRuntimeConfig(event)
const response = await $fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${config.openaiApiKey}`,
'Content-Type': 'application/json',
},
body: {
model: body.model || 'gpt-5-mini',
messages: body.messages,
stream: body.stream || false,
},
})
return response
})
Google Gemini — server/api/gemini/generate.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const config = useRuntimeConfig(event)
const response = await $fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:generateContent?key=${config.geminiApiKey}`,
{
method: 'POST',
body: { contents: body.contents },
}
)
return response
})
xAI Grok — server/api/grok/chat.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const config = useRuntimeConfig(event)
return await $fetch('https://api.x.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${config.grokApiKey}`,
'Content-Type': 'application/json',
},
body: {
model: 'grok-4.3',
messages: body.messages,
},
})
})
Step 3: Frontend Usage
<script setup lang="ts">
const messages = ref<{ role: string; content: string }[]>([])
const userInput = ref('')
const loading = ref(false)
async function sendMessage() {
if (!userInput.value) return
const text = userInput.value
messages.value.push({ role: 'user', content: text })
userInput.value = ''
loading.value = true
const res = await $fetch('/api/openai/chat', {
method: 'POST',
body: {
messages: messages.value,
model: 'gpt-5-mini',
},
})
messages.value.push({ role: 'assistant', content: res.choices[0].message.content })
loading.value = false
}
</script>
<template>
<div class="chat">
<div v-for="msg in messages" :key="msg.content" :class="msg.role">
{{ msg.content }}
</div>
<input v-model="userInput" @keyup.enter="sendMessage" placeholder="Ask AI..." />
<button @click="sendMessage" :disabled="loading">Send</button>
</div>
</template>
Security Checklist
- Never use AI API keys in
useRuntimeConfig().public — only in private runtimeConfig - Validate and sanitize all input before forwarding to the API
- Add rate limiting per user/session (e.g.,
h3 middleware or Redis) - Set spending limits on provider dashboards
- Sanitize AI output before rendering (avoid XSS if rendering as HTML)
- Monitor for unusual usage patterns
Cost-Saving Strategies
| Strategy | How |
|---|
| Caching | Store common responses in Redis/KV; skip API call on cache hit |
| Prompt optimization | Shorter system prompts = fewer tokens per request |
| Model routing | Use cheap/fast model first, escalate only if needed |
| Batch processing | Use batch APIs for non-real-time tasks (often 50% cheaper) |
| Streaming | Use streaming for perceived speed; cancel early if user navigates away |
Prompt Engineering Checklist
- Clear objective stated upfront
- Specific constraints (length, format, tone)
- Relevant context provided
- Examples included for complex tasks
- Output format specified
- Edge cases addressed