mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-07-31 17:36:54 -04:00
5.5 KiB
5.5 KiB
API Layer Architecture
Overview
Wrapper-based API abstraction built on inaturalistjs, combined with React Query for state management. All API responses are transformed to match Realm schemas before storage. Custom error handling with retry strategies and Grafana logging.
Key Files
| File | Purpose |
|---|---|
src/api/observations.js |
Observation CRUD, search, faves, subscriptions (see its export block for the full list) |
src/api/taxa.js |
Taxon fetch and search with field optimization |
src/api/users.js |
User profiles, blocking/muting |
src/api/projects.js |
Project search, membership |
src/api/search.ts |
Cross-resource search |
src/api/error.ts |
Custom error classes (INatApiError, 401, 429) and handleError |
src/api/types.d.ts |
TypeScript interfaces for API responses |
src/api/log/index.ts |
iNatLogstashTransport — posts logs to the iNaturalist API log endpoint (surfaced downstream in Grafana) |
src/sharedHooks/useAuthenticatedQuery.ts |
React Query wrapper with JWT injection |
src/sharedHooks/useAuthenticatedMutation.ts |
Mutation wrapper with JWT + 401/429 handling |
src/sharedHooks/useAuthenticatedInfiniteQuery.ts |
Infinite query wrapper for pagination |
src/sharedHooks/useQuery.ts |
Non-authenticated query wrapper |
src/sharedHelpers/logging.js |
Retry logic and delay strategies |
Standard API Wrapper Pattern
Every API module follows this pattern:
import inatjs from "inaturalistjs";
import handleError from "./error";
const searchObservations = async ( params = {}, opts = {} ) => {
try {
const response = await inatjs.observations.search( params, opts );
response.results = response.results.map( mapToLocalSchema );
return response;
} catch ( e ) {
return handleError( e, { context: { functionName: "searchObservations" } } );
}
};
Rules:
- ALL calls go through wrapper functions (never call inaturalistjs directly from components)
- Every call wrapped in try-catch with
handleError - Responses transformed to Realm-compatible schema before returning
params= query/body data,opts= options includingapi_token
React Query Hooks
useAuthenticatedQuery
const { data, error, isLoading, refetch } = useAuthenticatedQuery(
["fetchTaxon", taxonId],
optsWithAuth => fetchTaxon( taxonId, params, optsWithAuth ),
{ enabled },
);
- Waits for auth state before executing
- Injects JWT via
getJWT(allowAnonymousJWT) - Includes auth state in query key for cache separation
- Custom retry logic per status code
useAuthenticatedMutation
const { mutate } = useAuthenticatedMutation(
( faveParams, optsWithAuth ) => faveObservation( faveParams, optsWithAuth ),
{ onSuccess: () => { /* ... */ }, onError: ( error ) => { /* ... */ } },
);
mutate( { uuid } );
- Auto-injects JWT
- Smart 401 handling: logs context, attempts token refresh
- Smart 429 handling: logs the error (no automatic retry/backoff on mutations — exponential backoff applies to the query path only; see
logging.js)
useAuthenticatedInfiniteQuery
const { data, fetchNextPage, isFetchingNextPage } = useAuthenticatedInfiniteQuery(
queryKey,
async ( params, optsWithAuth ) => searchObservations( { ...baseParams, ...params }, optsWithAuth ),
{ getNextPageParam, enabled },
);
const observations = flatten( data?.pages?.map( r => r.results ) ) || [];
useNonAuthenticatedQuery (useQuery.ts)
For public endpoints that don't need JWT.
Query Key Conventions
Format: [domain, ...identifiers]
["fetchTaxon", taxonId]
["fetchQualityMetrics", observationUUID]
["useInfiniteExploreScroll", { ...filterParams }]
["fetchSearchResults", locationName]
Auth state appended automatically by hooks: [...queryKey, allowAnonymousJWT, userLoggedIn]
Response Transformation
Two-stage process:
- API wrapper normalizes common fields (e.g.,
license_code→licenseCode) - Realm model mapper applies domain logic (e.g.,
Taxon.mapApiToRealm())
Error Handling
Error Classes (src/api/error.ts)
INatApiError— Base class with json, status, contextINatApiUnauthorizedError— 401 errorsINatApiTooManyRequestsError— 429 errors
Retry Strategy (src/sharedHelpers/logging.js)
| Status | Behavior |
|---|---|
| 429 (Rate limit) | Exponential backoff + jitter, retry 3x |
| 401/403 (Auth) | Token refresh attempt, retry 2x |
| 404 (Not found) | No retry |
| 408 / Network error | Retry 3x |
| Others | Retry 2x |
Retry Delay
- 429:
1000 * 2^failureCount + random(0-100)ms - Others:
min(1000 * 2^failureCount, 30000ms)
Field Filtering
API calls specify exact fields to reduce payload:
const FIELDS = {
ancestor_ids: true,
default_photo: { url: true },
name: true,
preferred_common_name: true,
rank: true,
rank_level: true,
};
Common Operations
Adding a new API endpoint
- Create wrapper function in appropriate
src/api/*.jsfile following the standard pattern - Create React Query hook using
useAuthenticatedQueryoruseAuthenticatedMutation - Transform response to match Realm schema if persisting locally
- Add TypeScript types to
src/api/types.d.tsif needed
Adding a new React Query hook
- Choose the right base hook:
useAuthenticatedQuery(read),useAuthenticatedMutation(write),useAuthenticatedInfiniteQuery(paginated list) - Define a descriptive query key including relevant identifiers
- Pass the API wrapper function with auth options
- Handle errors in
onErrorcallback for mutations