This document is the developer reference for src/lib/api.ts,
the HTTP client that connects the frontend to the Heliobond backend REST API.
Every function falls back to bundled fixture data when NEXT_PUBLIC_API_URL is
not set, so the app works out of the box without a running backend.
- Environment configuration
- Response types
- Endpoints
- Error handling
- Demo / fixture fallback
- Usage examples
| Variable | Required | Example | Purpose |
|---|---|---|---|
NEXT_PUBLIC_API_URL |
No | http://localhost:3001 |
Backend base URL. When absent every call uses local fixture data and no HTTP request is made. |
Copy .env.example to .env.local and set NEXT_PUBLIC_API_URL to point at
your local or staging backend before running bun run dev.
All types are exported from src/lib/api.ts and can be imported directly.
Defined in src/data.ts. Core bond project record: id, name, yield, term,
rating, funded status, and pool-related fields.
Defined in src/data/projectDetails.ts. Extended record with oracle score
history, funding timeline, price/yield history, and creator attribution.
interface ProjectWithDetail {
project: Project
detail: ProjectDetail
}Returned by getProject.
interface Investment {
id: number // Server-assigned investment ID
projectId: number // The project invested in
amount: number // USDC amount
projectUrl: string // Canonical URL, e.g. "/projects/42"
}Returned by createInvestment.
interface PaginatedProjectsResponse {
projects: Project[]
total: number // Total matching records (not just this page)
page: number // 1-indexed current page
pageSize: number // Items per page
hasMore: boolean // Whether another page exists
}Returned by getProjectsPaginated.
interface PricePoint {
date: string // ISO 8601 date, e.g. "2025-03-14"
price: number // Bond price in USDC
yield?: number // Yield at that date (percentage)
}Returned (as an array) by getPriceHistory.
async function getProjects(): Promise<Project[]>Fetches all bond projects.
Backend endpoint: GET /projects
Returns: Array of Project objects. Empty array on network error (falls back
to fixture data instead, see fallback behavior).
Demo fallback: Returns all projects from src/data.ts via
selectProjects().
Example:
import { getProjects } from '@/lib/api'
const projects = await getProjects()
// [{ id: 1, name: 'Solaris Alpha', yield: 6.5, ... }, ...]async function getProjectsPaginated(
page?: number, // default: 1
pageSize?: number, // default: 12
): Promise<PaginatedProjectsResponse>Fetches a paginated slice of bond projects. Use this for the Explore screen initial load — it reduces time-to-interactive from 3–5 s down to sub-second by deferring off-screen projects.
Backend endpoint: GET /projects?page={page}&limit={pageSize}
Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
page |
number |
1 |
1-indexed page number |
pageSize |
number |
12 |
Items per page |
Returns: PaginatedProjectsResponse
Demo fallback: Slices selectProjects() with the same pagination math.
Handles both paginated API responses ({ projects, total, page, ... }) and
legacy flat-array responses from older backend versions.
Example:
import { getProjectsPaginated } from '@/lib/api'
// First page
const page1 = await getProjectsPaginated(1, 12)
// { projects: [...], total: 34, page: 1, pageSize: 12, hasMore: true }
// Next page
const page2 = await getProjectsPaginated(2, 12)
// { projects: [...], total: 34, page: 2, pageSize: 12, hasMore: true }async function getProject(id: number): Promise<ProjectWithDetail | null>Fetches a single project with its full detail record.
Backend endpoint: GET /projects/:id
Parameters:
| Param | Type | Description |
|---|---|---|
id |
number |
Must be a positive integer (id >= 1). Non-integer, negative, zero, or NaN ids return null immediately without a network call. |
Returns: ProjectWithDetail or null when the project is not found.
Demo fallback: Looks up selectProjectById(id) and selectProjectDetail(id)
from fixture data. Returns null if either is missing.
Example:
import { getProject } from '@/lib/api'
const result = await getProject(1)
if (result) {
const { project, detail } = result
console.log(project.name, detail.scoreHistory.credit)
}
// Invalid ids return null immediately — no request fired:
await getProject(-1) // null
await getProject(NaN) // null
await getProject(1.5) // nullasync function createInvestment(input: {
projectId: number
amount: number
}): Promise<Investment>Creates a new investment record on the backend.
Backend endpoint: POST /investments
Request body:
{
"projectId": 1,
"amount": 100
}Parameters:
| Field | Type | Constraints |
|---|---|---|
projectId |
number |
Must be a positive integer (>= 1). Throws on invalid input. |
amount |
number |
Must be a positive finite number (> 0). Throws on invalid input. |
Returns: Investment
Throws: Error('Invalid investment input') for any of:
- Non-integer
projectId projectId < 1- Non-finite
amount amount <= 0
Input is validated before any network call, so invalid inputs never reach the backend.
Demo fallback: Returns a mock Investment with a random id, preserving
projectId and amount. The projectUrl field is always /projects/:id.
Example:
import { createInvestment } from '@/lib/api'
// Valid investment
const investment = await createInvestment({ projectId: 3, amount: 250 })
// { id: 84712, projectId: 3, amount: 250, projectUrl: '/projects/3' }
// Invalid — throws before any HTTP call
await createInvestment({ projectId: 0, amount: 100 }) // throws
await createInvestment({ projectId: 1, amount: -50 }) // throws
await createInvestment({ projectId: 1.5, amount: 100 }) // throwsasync function getPriceHistory(projectId: number): Promise<PricePoint[]>Fetches 30-day bond price and yield history for a project.
Backend endpoint: GET /projects/:projectId/price-history
Parameters:
| Param | Type | Description |
|---|---|---|
projectId |
number |
Project ID |
Returns: Array of PricePoint objects sorted in ascending chronological
order (oldest first). The backend response is re-sorted if necessary.
Demo fallback: Generates a deterministic 30-day mock series using
projectId as a seed so the sparkline shape is consistent per project across
renders.
Example:
import { getPriceHistory } from '@/lib/api'
const history = await getPriceHistory(2)
// [
// { date: '2025-08-06', price: 103.42, yield: 5.12 },
// { date: '2025-08-07', price: 103.61, yield: 5.09 },
// ...
// ]
// Feed directly into PriceHistoryChart:
<PriceHistoryChart data={history} />async function biometricLogin(): Promise<boolean>Triggers a WebAuthn biometric prompt (Face ID / Touch ID) and returns whether the user authenticated successfully.
No backend endpoint — this is a pure client-side WebAuthn call. The challenge is generated locally (random 32-byte array). In production, replace the local challenge with a server-issued one before verifying the assertion.
Returns: true if the biometric credential was successfully retrieved,
false if:
- WebAuthn is not supported (
window.PublicKeyCredentialis absent) - The user cancelled
- An error occurred (logged as a warning)
Environment: Only works in a browser context with a registered
authenticator. Always returns false in SSR (window === undefined).
Example:
import { biometricLogin } from '@/lib/api'
const ok = await biometricLogin()
if (ok) {
// proceed with authenticated session
} else {
// show fallback (password, wallet sign-in, etc.)
}All network functions follow the same pattern:
- Optimistic attempt — call the backend, parse the response.
- On failure — log a
console.warnwith the endpoint name and fall back to local fixture data without surfacing an error to the user. - Never throw for network errors — only
createInvestmentthrows, and only for invalid input, never for network failures.
The one input-validation exception is createInvestment, which throws
Error('Invalid investment input') synchronously for bad projectId or
amount. Callers should catch this and show a validation message.
For mapping backend error codes to user-facing strings, see
src/lib/errorMessages.ts and
ERROR_CODES.md.
When NEXT_PUBLIC_API_URL is not set (or is empty), no HTTP requests are
made. Every function returns deterministic fixture data from:
src/data.ts— pool summary, projects list, investor position, activity feedsrc/data/projectDetails.ts— per-project oracle history, creator info, funding timeline, price historysrc/state/selectors.ts— flat accessor functions over the above
This means the full click-through works without a backend, including the Explore, Project Detail, and Deposit screens.
When NEXT_PUBLIC_API_URL is set and a request fails, the same fixture
fallback kicks in silently. The app never shows a blank screen due to a missing
or slow backend.
import { getProjectsPaginated } from '@/lib/api'
// Initial load — first 12 projects only
const { projects, hasMore, total } = await getProjectsPaginated(1, 12)
// Infinite scroll — fetch the next page when the sentinel enters the viewport
if (hasMore) {
const next = await getProjectsPaginated(2, 12)
}// src/app/project/[id]/page.tsx
import { getProject } from '@/lib/api'
export default async function Page({ params }: { params: { id: string } }) {
const id = Number(params.id)
const data = await getProject(id)
if (!data) notFound()
return <ProjectDetail project={data.project} detail={data.detail} />
}import { createInvestment } from '@/lib/api'
async function handleDeposit(projectId: number, amount: number) {
try {
const investment = await createInvestment({ projectId, amount })
router.push(investment.projectUrl)
} catch (err) {
if (err instanceof Error && err.message === 'Invalid investment input') {
setFormError('Please enter a valid amount.')
}
}
}import { getPriceHistory } from '@/lib/api'
import { PriceHistoryChart } from '@/components'
const history = await getPriceHistory(projectId)
return <PriceHistoryChart data={history} />