Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/api-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,55 @@ The shared HTTP client lives in `src/lib/apiClient.ts`.
Requests are sent to `${getApiBase()}${path}` where `getApiBase()` reads
`NEXT_PUBLIC_STABLEROUTE_API_BASE` (see `src/lib/config.ts`).

## API Response Types

All API response type definitions are centralized in `src/lib/types.ts` to
maintain a single source of truth and prevent type drift between pages.

### Available Types

- **`Pair`** — Routing pair response: `{ source, destination }`
- **`Quote`** — Quote response: `{ source_asset, dest_asset, amount, estimated_rate, route[] }`
- **`AppEvent`** — Raw event from API: `{ id, ts, type, payload }`
- **`DisplayEvent`** — Rendered event with serialized payloads: `{ id, ts, type, payloadPreview, fullPayload }`
- **`ApiKey`** — API key metadata: `{ prefix, label, createdAt }`
- **`CreateApiKeyResponse`** — API key creation response: `{ key, prefix? }`
- **`Webhook`** — Webhook subscription: `{ id, url, events[], createdAt }`

### Importing Types

Types are exported directly from `src/lib/types.ts`:

```ts
import type { Quote, Pair, ApiKey } from '@/lib/types';
```

For backward compatibility, types are also re-exported from their validation/utility modules:

```ts
// Both work:
import type { Quote } from '@/lib/types';
import type { Quote } from '@/lib/quote';

// Both work:
import type { AppEvent, DisplayEvent } from '@/lib/types';
import type { AppEvent, DisplayEvent } from '@/lib/events';

// Both work:
import type { Pair } from '@/lib/types';
import { type Pair } from '@/app/pairs/pairsUtils';
```

### Validation Functions

Validation logic remains in their respective modules:

- `quote.ts` — `isValidAmount()`, `assetsDiffer()`, `normalizeAsset()`
- `events.ts` — `parseEventsResponse()`, `escapeCsvCell()`, `buildEventsCsv()`
- `webhookEvents.ts` — `isWebhookEventType()`

These validators continue to use the centralized types, ensuring all validation is type-safe and consistent.

## Error shape

Failed responses parse JSON bodies matching:
Expand Down
14 changes: 7 additions & 7 deletions src/app/api-keys/Client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import { apiDelete, apiGet, apiPost } from '@/lib/apiClient';
import { useList } from '@/lib/useList';
import { writeToClipboard } from '@/lib/clipboard';
import { useToast } from '@/components/ToastProvider';

type Item = { prefix: string; label: string; createdAt: number };
import type { ApiKey, CreateApiKeyResponse } from '@/lib/types';

export default function ApiKeysClient() {
const loadItems = useCallback(
() =>
apiGet<{ items: Item[] }>('/api/v1/api-keys').then((body) => body.items),
apiGet<{ items: ApiKey[] }>('/api/v1/api-keys').then(
(body) => body.items
),
[]
);
const itemsResult = useList(loadItems);
Expand All @@ -36,10 +37,9 @@ export default function ApiKeysClient() {
event.preventDefault();
setSubmitting(true);
try {
const response = await apiPost<{ key: string; prefix?: string }>(
'/api/v1/api-keys',
{ label }
);
const response = await apiPost<CreateApiKeyResponse>('/api/v1/api-keys', {
label,
});
setCreated(response.key);
setCopyFailed(false);
setRecentPrefix(response.prefix ?? response.key.slice(0, 8));
Expand Down
3 changes: 2 additions & 1 deletion src/app/pairs/Client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { PageHeading } from '@/components/PageHeading';
import { Spinner } from '@/components/Spinner';
import { apiDelete } from '@/lib/apiClient';
import { useApi } from '@/lib/useApi';
import { filterPairs, groupBySource, type Pair } from './pairsUtils';
import { filterPairs, groupBySource } from './pairsUtils';
import { type Pair } from '@/lib/types';

export default function PairsClient() {
const api = useApi<{ pairs: Pair[] }>('/api/v1/pairs');
Expand Down
2 changes: 1 addition & 1 deletion src/app/pairs/pairsUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type Pair = { source: string; destination: string };
import type { Pair } from '@/lib/types';

/**
* Filters pairs whose source or destination contains the query text
Expand Down
9 changes: 1 addition & 8 deletions src/app/quote/Client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,7 @@ import { TextField } from '@/components/TextField';
import { apiFetch, type ApiError } from '@/lib/apiClient';
import { formatQuoteAmountDisplay, formatQuoteRateDisplay } from '@/lib/format';
import { useLocalStorage } from '@/lib/useLocalStorage';

type Quote = {
source_asset: string;
dest_asset: string;
amount: string;
estimated_rate: string;
route: string[];
};
import type { Quote } from '@/lib/types';

type FieldErrors = {
source?: string;
Expand Down
7 changes: 4 additions & 3 deletions src/app/webhooks/Client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import { TimeAgo } from '@/components/TimeAgo';
import { apiDelete, apiGet, apiPost } from '@/lib/apiClient';
import { useList } from '@/lib/useList';
import { WEBHOOK_EVENT_OPTIONS } from '@/lib/webhookEvents';

type Hook = { id: string; url: string; events: string[]; createdAt: number };
import type { Webhook } from '@/lib/types';

function isHttpsUrl(value: string): boolean {
try {
Expand All @@ -24,7 +23,9 @@ function isHttpsUrl(value: string): boolean {
export default function WebhooksClient() {
const loadHooks = useCallback(
() =>
apiGet<{ items: Hook[] }>('/api/v1/webhooks').then((body) => body.items),
apiGet<{ items: Webhook[] }>('/api/v1/webhooks').then(
(body) => body.items
),
[]
);
const hooks = useList(loadHooks);
Expand Down
Loading
Loading