Skip to content

Commit 6508868

Browse files
authored
Merge pull request #227 from Y33t-dev/backend
feat: add /api/markets, /api/notifications routes, and OpenAPI spec
2 parents 8e9240d + 6b8f510 commit 6508868

14 files changed

Lines changed: 1590 additions & 3 deletions

File tree

‎README.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,30 @@ npm run svg
195195

196196
This will automatically convert SVGs to React components in `components/shared/ui/icons/`.
197197

198+
## 🗄️ Backend & API
199+
200+
The server-side API surface is documented in two places:
201+
202+
| Resource | Description |
203+
|---|---|
204+
| [`docs/backend-architecture.md`](docs/backend-architecture.md) | Architecture overview — lib/ modules, caching model, security, and how to add a new route |
205+
| [`openapi.yaml`](openapi.yaml) | OpenAPI 3.1 spec for all `app/api/*` routes, params, and response shapes |
206+
207+
### Available API Routes
208+
209+
| Method | Path | Auth | Description |
210+
|---|---|---|---|
211+
| `GET` | `/api/health` | Public | Platform & Stellar network health |
212+
| `POST/GET/DELETE` | `/api/auth/session` | — | Session lifecycle |
213+
| `GET` | `/api/prices` | Public | Asset spot prices (cached 5 s) |
214+
| `GET` | `/api/markets` | Public | Per-asset supply/borrow APR & utilization (cached 30 s) |
215+
| `GET` | `/api/positions` | Optional | User lending/borrowing positions |
216+
| `GET/POST` | `/api/transactions` | Public | Transaction history and creation |
217+
| `GET` | `/api/transactions/export` | Public | Transactions CSV export |
218+
| `POST` | `/api/quote` | Public | Lending/borrowing quote calculation |
219+
| `GET` | `/api/notifications` | Required | List in-app notifications |
220+
| `PATCH` | `/api/notifications/:id` | Required | Mark notification as read |
221+
198222
## 🔗 Helpful Links
199223

200224
### Documentation

‎app/api/markets/route.ts‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { getUser } from '@/lib/auth';
3+
import { markNotificationRead } from '@/lib/notifications/repository';
4+
5+
export const runtime = 'nodejs';
6+
7+
/** PATCH /api/notifications/:id
8+
*
9+
* Marks a notification as read for the authenticated user.
10+
* Requires an authenticated session (session cookie).
11+
*
12+
* Route params:
13+
* id – notification ID (string)
14+
*
15+
* Response shape:
16+
* { notification: Notification }
17+
*
18+
* Errors:
19+
* 400 – missing or blank id
20+
* 401 – no valid session
21+
* 404 – notification not found for this user
22+
*/
23+
export async function PATCH(
24+
_req: NextRequest,
25+
{ params }: { params: Promise<{ id: string }> },
26+
) {
27+
const user = await getUser();
28+
if (!user) {
29+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
30+
}
31+
32+
const { id } = await params;
33+
34+
if (!id || typeof id !== 'string' || id.trim() === '') {
35+
return NextResponse.json({ error: 'Invalid notification id' }, { status: 400 });
36+
}
37+
38+
const notification = markNotificationRead(user.id, id.trim());
39+
if (!notification) {
40+
return NextResponse.json({ error: 'Notification not found' }, { status: 404 });
41+
}
42+
43+
return NextResponse.json({ notification });
44+
}

‎app/api/notifications/route.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { NextResponse } from 'next/server';
2+
import { getUser } from '@/lib/auth';
3+
import { getNotifications } from '@/lib/notifications/repository';
4+
5+
export const runtime = 'nodejs';
6+
7+
/** GET /api/notifications
8+
*
9+
* Requires an authenticated session (session cookie).
10+
* Returns the caller's notifications list and unread count.
11+
*
12+
* Response shape:
13+
* { notifications: Notification[], unreadCount: number }
14+
*
15+
* Errors:
16+
* 401 – no valid session
17+
*/
18+
export async function GET() {
19+
const user = await getUser();
20+
if (!user) {
21+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
22+
}
23+
24+
const notifications = getNotifications(user.id);
25+
const unreadCount = notifications.filter((n) => !n.read).length;
26+
27+
return NextResponse.json({ notifications, unreadCount });
28+
}

‎app/lending/page.tsx‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useState } from 'react';
3+
import { useState, useEffect } from 'react';
44
import LendingForm from '@/components/features/lending/components/LendingForm';
55
import BorrowingForm from '@/components/features/lending/components/BorrowingForm';
66
import InterestCalculator from '@/components/features/lending/components/InterestCalculator';
@@ -30,6 +30,23 @@ export default function LendingPage() {
3030
const [calculationResult, setCalculationResult] = useState<CalculationResult | null>(null);
3131
const [showConfirmModal, setShowConfirmModal] = useState(false);
3232

33+
// Hydrate default interest rates from the live /api/markets endpoint.
34+
// Falls back silently to the hardcoded values above if the fetch fails.
35+
useEffect(() => {
36+
const controller = new AbortController();
37+
fetch('/api/markets?asset=XLM', { signal: controller.signal })
38+
.then((res) => (res.ok ? res.json() : null))
39+
.then((data: { markets?: Array<{ asset: string; supplyApr: number; borrowApr: number }> } | null) => {
40+
if (!data?.markets) return;
41+
const xlm = data.markets.find((m) => m.asset === 'XLM');
42+
if (!xlm) return;
43+
setLendingData((prev) => ({ ...prev, interestRate: xlm.supplyApr }));
44+
setBorrowingData((prev) => ({ ...prev, interestRate: xlm.borrowApr }));
45+
})
46+
.catch(() => { /* keep hardcoded fallback */ });
47+
return () => controller.abort();
48+
}, []);
49+
3350
const handleLendingSubmit = (data: LendingData) => {
3451
setLendingData(data);
3552
setShowConfirmModal(true);

0 commit comments

Comments
 (0)