Server-side API Calls in Astro
Astro can do API/SSR, but keep “static first, minimal runtime”. Only put what must run at runtime on the server.
API routes
- Place under
src/pages/api/*.ts(or.js), exportGET/POSThandlers. - Return a
Responseobject; you can use thejson()helper. - Edge/Node runtimes depend on hosting; avoid Node-only modules if targeting Edge.
// src/pages/api/hello.ts
export async function GET() {
return new Response(JSON.stringify({ msg: "hello" }), {
headers: { "Content-Type": "application/json" },
});
}
SSR pages
- Export
prerender = falsein the page to enable SSR. - Use
Astro.requestfor headers/query, andfetchyour backend. - Good for user-context data, real-time, personalization; keep static when possible.
Middle-tier purposes
- Wrap backend APIs: auth, rate-limit, signing—protect upstream.
- Edge rendering: return personalized content close to users.
- Progressive dynamic: list static, detail or user bits via API.
Deployment notes
- For Cloudflare Pages Functions/Workers, stick to Fetch API; avoid Node-specific modules.
- Configure routing so
/api/*is not shadowed by static assets.
🌐
浏览器
↓ API 请求
🖥️
后端服务
↓ 转发请求
🔌
第三方 API
↑ 返回数据
🖥️
后端服务
↑ 返回数据
🌐
浏览器
Pain points of traditional FE/BE split
Calling third-party APIs in a classic split setup often hits:
- CORS limits: browser same-origin blocks direct calls
- Security: API keys may leak in frontend bundles
- Complexity: need a separate backend to proxy
Example with Laravel + Vue.js:
// Laravel backend
public function getIpInfo(Request $request) {
$response = Http::get('http://ip-api.com/json/');
return response()->json($response->json());
}
// Vue.js frontend
async function fetchIpInfo() {
const response = await fetch('/api/ip-info');
const data = await response.json();
this.ipInfo = data;
}
🌐
浏览器
↓ API 请求
🖥️
后端服务
↓ 转发请求
🔌
第三方 API
↑ 返回数据
🖥️
后端服务
↑ 返回数据
🌐
浏览器
Astro’s elegant solution
Astro gives a full-stack path: frontend UI + backend API together.
// src/pages/api/ip-info.ts
import type { APIRoute } from 'astro';
interface IpInfo {
query: string;
country: string;
city: string;
isp: string;
regionName: string;
}
export const GET: APIRoute = async () => {
try {
const response = await fetch('<http://ip-api.com/json/>');
if (!response.ok) {
return new Response(
JSON.stringify({ error: 'Failed to fetch IP info' }),
{
status: 500,
headers: { 'Content-Type': 'application/json' }
}
);
}
const data: IpInfo = await response.json();
return new Response(
JSON.stringify(data),
{
status: 200,
headers: { 'Content-Type': 'application/json' }
}
);
} catch (e) {
return new Response(
JSON.stringify({ error: e instanceof Error ? e.message : 'Unknown error' }),
{
status: 500,
headers: { 'Content-Type': 'application/json' }
}
);
}
};
<!-- src/components/IpInfoDemo.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue';
interface IpInfo {
query: string;
country: string;
city: string;
isp: string;
regionName: string;
}
const ipInfo = ref<IpInfo | null>(null);
const error = ref<string | null>(null);
const loading = ref(false);
const fetchIpInfo = async () => {
loading.value = true;
error.value = null;
try {
const response = await fetch('/api/ip-info');
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to fetch IP info');
}
ipInfo.value = await response.json();
} catch (e) {
error.value = e instanceof Error ? e.message : 'Unknown error';
} finally {
loading.value = false;
}
};
onMounted(fetchIpInfo);
</script>
<template>
<div class="not-content p-6 bg-white rounded-xl shadow-md">
<div class="flex items-center justify-between mb-6">
<h3 class="text-xl font-semibold">IP Info</h3>
<button
@click="fetchIpInfo"
:disabled="loading"
class="px-4 py-2 bg-indigo-600 text-white rounded-md">
{{ loading ? 'Loading...' : 'Refresh' }}
</button>
</div>
<div v-if="ipInfo" class="grid grid-cols-2 gap-4">
<div class="p-4 bg-gray-50 rounded-lg">
<div class="text-sm text-gray-500">IP Address</div>
<div class="text-lg">{{ ipInfo.query }}</div>
</div>
<!-- more fields ... -->
</div>
</div>
</template>
Now a concrete demo:
IP 信息展示
Flow
- Frontend component calls internal
/api/ip-info - Astro API route requests external IP service
- API route returns the result
- Frontend updates UI
Benefits:
- Secrets and sensitive steps stay server-side
- No extra backend service—simpler deploy
- Full typing support for better DX
- Unified error/loading handling
Advantages recap
- Simpler code: no separate backend API needed
- Developer efficiency: full-stack in one file
- Security: API calls server-side, keys stay hidden
- Performance: supports static and incremental generation
Best practices
- Cache smartly: build-time fetch for rarely changing data
- Error handling: add proper error/loading states
- Type safety: define response types with TypeScript
---
interface IpInfo {
query: string;
country: string;
city: string;
isp: string;
}
try {
const response = await fetch('<http://ip-api.com/json/>');
const ipInfo: IpInfo = await response.json();
} catch (error) {
console.error('Failed to fetch IP info:', error);
}
---
Conclusion
With server components and API routes, Astro neatly solves API-call pain points in traditional FE/BE splits. You can ship full-stack features with concise code while keeping security and performance in check—better developer velocity, better user experience.