A first integration can be one script. A maintainable integration needs boundaries that let developers add endpoints, networks and storage without risking existing behavior. This guide shows a practical structure for a team project.
Start with clear modules
src/
config.ts
fomo-client.ts
schemas.ts
normalizers.ts
repositories.ts
poller.ts
signals.ts
risk.ts
index.ts
tests/
fixtures/
client.test.ts
normalizers.test.ts
The API client should understand HTTP but not your database. Normalizers should be pure functions. Repositories should know persistence but not request headers. Signal and risk modules should consume normalized domain records.
Validate configuration once
Fail during process startup rather than during the first scheduled poll:
function required(name: string) {
const value = process.env[name]?.trim()
if (!value) throw new Error(`${name} is required`)
return value
}
export const config = {
fomoApiKey: required("FOMO_API_KEY"),
fomoBaseUrl: process.env.FOMO_BASE_URL ?? "https://getfomoapi.fun/api",
requestTimeoutMs: 30_000,
}
Never accept an API key as a request parameter from frontend users. Keep it in the server environment and redact headers from logs.
Add runtime schemas
TypeScript types disappear at runtime. Validate responses before inserting them into trusted tables. A small schema library can distinguish required identifiers from optional display fields.
type Trader = {
id: string
userHandle: string
solana: string | null
evm: string | null
}
function isTrader(value: unknown): value is Trader {
if (!value || typeof value !== "object") return false
const item = value as Record<string, unknown>
return typeof item.id === "string" && typeof item.userHandle === "string"
}
For a larger client, use a maintained schema validator and preserve unknown fields in raw storage.
Design endpoint methods consistently
Every endpoint method should:
- Validate caller inputs.
- Construct the path and query safely.
- Use the shared authenticated request function.
- Validate the response envelope.
- Return endpoint data, not transport details, unless callers need headers.
async function getSwaps(userId: string, limit = 20) {
if (!userId) throw new Error("userId is required")
if (limit < 1 || limit > 150) throw new Error("limit must be 1–150")
return fomoGet(`/users/${encodeURIComponent(userId)}/swaps?limit=${limit}`)
}
Test with recorded fixtures
Store sanitized JSON fixtures for success, nullable fields, empty arrays, pagination, rate limiting and malformed responses. Never commit real API keys or private customer data.
Pure normalizer tests should cover:
| Case | Expected behavior |
|---|---|
| Missing profile image | Preserve null; do not reject trader |
| Unknown network | Store raw event; exclude from execution |
| Duplicate swap ID | No second state transition |
| Decimal price string | Parse with decimal-safe library |
| Empty leaderboard | Return an empty list, not an exception |
Integration tests can run against a mock HTTP server. Keep live API tests separate so CI is deterministic and does not consume rate limits.
Add observability before features
Record structured fields such as endpoint, duration, status, attempt, user ID and checkpoint. Do not log authentication headers or full wallet profiles by default.
Useful metrics include:
- Requests and errors by endpoint
- Rate-limit responses
- Retry count and total retry delay
- Poll checkpoint age
- New and duplicate swap count
- Normalization failures
- Signal rejection reason
- Execution confirmation time
Alert on stale checkpoints, not only server errors. A poller can return 200 while repeatedly processing no new data because its cursor is broken.
Contribution workflow
Before opening a change:
- Create a focused branch.
- Add or update sanitized fixtures.
- Implement the smallest endpoint or normalizer change.
- Run formatting, lint, type checks and tests.
- Document new configuration and migration steps.
- Explain security and compatibility implications in the review.
Avoid combining an endpoint addition with an unrelated framework migration. Small changes are easier to review and roll back.
Adding a new network
Create an explicit network registry containing the chain ID, address validator, native asset, RPC adapter and execution support level. “Readable” and “executable” should be different states.
const networks = {
1399811149: { name: "Solana", execution: "enabled" },
8453: { name: "Base", execution: "review" },
} as const
An unknown network should never fall through to a default executor.
Compatibility and migrations
If normalized database records change, version the transformation or write a migration. Keep raw responses so you can backfill. Add compatibility logic only for data or consumers that actually exist; speculative compatibility increases ambiguity.
Definition of done
A safe extension includes code, runtime validation, fixtures, unit tests, observability, documentation and a rollout plan. If the feature can affect transactions, it also needs risk-engine rules, dry-run behavior and a kill switch.