Create serverless API proxy endpoints that hide API keys and provide a unified backend for the dashboard frontend. Designed for Vercel deployment.
External APIs often require API keys that must not be exposed in frontend code. Create serverless proxy endpoints that:
/api/* namespace for the frontendEach API endpoint is a file in the api/ directory:
api/
├── stocks.ts # Stock market data proxy
├── news.ts # News API proxy
├── calendar.ts # Calendar events proxy
└── _cors.ts # Shared CORS helper
// api/_cors.ts
export function corsHeaders() {
return {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
}
export function handleCors(req: Request): Response | null {
if (req.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: corsHeaders() });
}
return null;
}
// api/stocks.ts
import type { VercelRequest, VercelResponse } from '@vercel/node';
export default async function handler(req: VercelRequest, res: VercelResponse) {
// CORS
res.setHeader('Access-Control-Allow-Origin', '*');
if (req.method === 'OPTIONS') return res.status(204).end();
const symbols = (req.query.symbols as string || '').split(',').filter(Boolean);
if (symbols.length === 0) {
return res.status(400).json({ error: 'Missing symbols parameter' });
}
const apiKey = process.env.FINNHUB_API_KEY;
if (!apiKey) {
return res.status(500).json({ error: 'API key not configured' });
}
try {
const quotes = await Promise.all(
symbols.map(async (sym) => {
const resp = await fetch(
`https://finnhub.io/api/v1/quote?symbol=${sym}&token=${apiKey}`
);
if (!resp.ok) throw new Error(`Finnhub ${resp.status}`);
const data = await resp.json();
return {
symbol: sym,
price: data.c, // current price
change: data.dp, // percent change
high: data.h,
low: data.l,
open: data.o,
prevClose: data.pc,
};
})
);
res.setHeader('Cache-Control', 's-maxage=30, stale-while-revalidate=60');
return res.json({ quotes });
} catch (err) {
console.error('Stock API error:', err);
return res.status(502).json({ error: 'Upstream API failed' });
}
}
// api/news.ts
import type { VercelRequest, VercelResponse } from '@vercel/node';
export default async function handler(req: VercelRequest, res: VercelResponse) {
res.setHeader('Access-Control-Allow-Origin', '*');
if (req.method === 'OPTIONS') return res.status(204).end();
const apiKey = process.env.NEWS_API_KEY;
if (!apiKey) return res.status(500).json({ error: 'API key not configured' });
const query = req.query.q as string || '';
const category = req.query.category as string || 'general';
const lang = req.query.lang as string || 'en';
try {
const url = query
? `https://gnews.io/api/v4/search?q=${encodeURIComponent(query)}&lang=${lang}&max=20&token=${apiKey}`
: `https://gnews.io/api/v4/top-headlines?category=${category}&lang=${lang}&max=20&token=${apiKey}`;
const resp = await fetch(url);
if (!resp.ok) throw new Error(`GNews ${resp.status}`);
const data = await resp.json();
res.setHeader('Cache-Control', 's-maxage=120, stale-while-revalidate=300');
return res.json({
articles: (data.articles || []).map((a: any) => ({
title: a.title,
description: a.description,
url: a.url,
source: a.source?.name || '',
publishedAt: a.publishedAt,
image: a.image,
})),
});
} catch (err) {
console.error('News API error:', err);
return res.status(502).json({ error: 'Upstream API failed' });
}
}
api/ directoryprocess.env.FINNHUB_API_KEY)During vite dev, configure a proxy in vite.config.ts:
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
},
});
Or use vercel dev to run serverless functions locally.