|
| 1 | +import { NextRequest, NextResponse } from 'next/server'; |
| 2 | +import { globalCache } from '@/lib/cache'; |
| 3 | +import { ASSET_SYMBOLS, isAssetSymbol, type AssetSymbol } from '@/types/enums'; |
| 4 | +import { fetchMarkets } from '@/lib/markets/repository'; |
| 5 | + |
| 6 | +export const runtime = 'nodejs'; |
| 7 | + |
| 8 | +/** GET /api/markets |
| 9 | + * |
| 10 | + * Query params: |
| 11 | + * asset – optional, comma-separated AssetSymbol list (e.g. ?asset=XLM,USDC). |
| 12 | + * Omit to return all supported assets. |
| 13 | + * |
| 14 | + * Response shape: MarketsResponse (see lib/markets/types.ts) |
| 15 | + * { markets: AssetMarket[], timestamp: string, source: string } |
| 16 | + * |
| 17 | + * Caching: public, TTL 30 s / SWR 60 s. |
| 18 | + * Bypassed when Authorization header, session cookie, or x-user-id |
| 19 | + * header is present (returns X-Cache: BYPASS). |
| 20 | + * |
| 21 | + * Errors: |
| 22 | + * 400 – unknown asset symbol(s) in the ?asset param |
| 23 | + * 500 – upstream fetch failure |
| 24 | + */ |
| 25 | +export async function GET(request: NextRequest) { |
| 26 | + try { |
| 27 | + const { searchParams } = new URL(request.url); |
| 28 | + const assetParam = searchParams.get('asset') || ''; |
| 29 | + |
| 30 | + // Parse and validate requested assets |
| 31 | + let assets: AssetSymbol[]; |
| 32 | + if (assetParam) { |
| 33 | + const requested = assetParam.split(',').map((a) => a.trim().toUpperCase()); |
| 34 | + const invalid = requested.filter((a) => !isAssetSymbol(a)); |
| 35 | + if (invalid.length > 0) { |
| 36 | + return NextResponse.json( |
| 37 | + { error: `Unknown asset(s): ${invalid.join(', ')}. Supported: ${ASSET_SYMBOLS.join(', ')}` }, |
| 38 | + { status: 400 }, |
| 39 | + ); |
| 40 | + } |
| 41 | + assets = requested as AssetSymbol[]; |
| 42 | + } else { |
| 43 | + assets = [...ASSET_SYMBOLS]; |
| 44 | + } |
| 45 | + |
| 46 | + // Cache bypass for authenticated requests |
| 47 | + const authHeader = request.headers.get('Authorization'); |
| 48 | + const hasAuthCookie = request.cookies.has('session') || request.cookies.has('token'); |
| 49 | + const hasUserHeader = request.headers.has('x-user-id'); |
| 50 | + const bypassCache = !!(authHeader || hasAuthCookie || hasUserHeader); |
| 51 | + |
| 52 | + if (bypassCache) { |
| 53 | + const data = await fetchMarkets(assets); |
| 54 | + return NextResponse.json(data, { |
| 55 | + status: 200, |
| 56 | + headers: { |
| 57 | + 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate', |
| 58 | + 'Pragma': 'no-cache', |
| 59 | + 'Expires': '0', |
| 60 | + 'X-Cache': 'BYPASS', |
| 61 | + }, |
| 62 | + }); |
| 63 | + } |
| 64 | + |
| 65 | + // Sort to make the cache key order-invariant (?asset=USDC,XLM == ?asset=XLM,USDC) |
| 66 | + const cacheKey = `markets:assets:${[...assets].sort().join(',')}`; |
| 67 | + const cacheOptions = { ttl: 30 * 1000, swr: 60 * 1000 }; |
| 68 | + |
| 69 | + const { value, status } = await globalCache.getOrFetch( |
| 70 | + cacheKey, |
| 71 | + () => fetchMarkets(assets), |
| 72 | + cacheOptions, |
| 73 | + ); |
| 74 | + |
| 75 | + return NextResponse.json(value, { |
| 76 | + status: 200, |
| 77 | + headers: { |
| 78 | + 'Cache-Control': 'public, max-age=30, stale-while-revalidate=60', |
| 79 | + 'X-Cache': status, |
| 80 | + }, |
| 81 | + }); |
| 82 | + } catch (error) { |
| 83 | + console.error('Markets route error:', error); |
| 84 | + return NextResponse.json( |
| 85 | + { error: 'Failed to fetch market data' }, |
| 86 | + { status: 500 }, |
| 87 | + ); |
| 88 | + } |
| 89 | +} |
0 commit comments