OpenSpace api-proxy-endpoint
Create serverless API proxy endpoints that hide API keys and provide a unified backend for the dashboard frontend. Designed for Vercel deployment.
install
source · Clone the upstream repo
git clone https://github.com/HKUDS/OpenSpace
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/HKUDS/OpenSpace "$T" && mkdir -p ~/.claude/skills && cp -r "$T/showcase/skills/api-proxy-endpoint" ~/.claude/skills/hkuds-openspace-api-proxy-endpoint && rm -rf "$T"
manifest:
showcase/skills/api-proxy-endpoint/SKILL.mdsource content
API Proxy Endpoint Pattern
External APIs often require API keys that must not be exposed in frontend code. Create serverless proxy endpoints that:
- Hide API keys on the server side
- Provide a unified
namespace for the frontend/api/* - Handle CORS, rate limiting, and error wrapping
Endpoint Structure
Each 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
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; }
Example: Stock Proxy Endpoint
// 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' }); } }
Example: News Proxy Endpoint
// 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' }); } }
Key Patterns
- One file per API domain in the
directoryapi/ - Always set CORS headers — frontend runs on different origin during dev
- Environment variables for API keys (
)process.env.FINNHUB_API_KEY - Cache-Control headers for edge caching (Vercel CDN)
- Error wrapping — return structured JSON errors, never raw upstream errors
- Input validation — validate query parameters before calling upstream
- Typed responses — keep response shapes consistent for frontend consumption
Local Development
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.