mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-08-01 01:47:43 -04:00
157 lines
5.5 KiB
Markdown
157 lines
5.5 KiB
Markdown
# 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:
|
|
|
|
```javascript
|
|
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 including `api_token`
|
|
|
|
## React Query Hooks
|
|
|
|
### useAuthenticatedQuery
|
|
```typescript
|
|
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
|
|
```typescript
|
|
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
|
|
```typescript
|
|
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]`
|
|
|
|
```javascript
|
|
["fetchTaxon", taxonId]
|
|
["fetchQualityMetrics", observationUUID]
|
|
["useInfiniteExploreScroll", { ...filterParams }]
|
|
["fetchSearchResults", locationName]
|
|
```
|
|
|
|
Auth state appended automatically by hooks: `[...queryKey, allowAnonymousJWT, userLoggedIn]`
|
|
|
|
## Response Transformation
|
|
|
|
Two-stage process:
|
|
1. **API wrapper** normalizes common fields (e.g., `license_code` → `licenseCode`)
|
|
2. **Realm model mapper** applies domain logic (e.g., `Taxon.mapApiToRealm()`)
|
|
|
|
## Error Handling
|
|
|
|
### Error Classes (`src/api/error.ts`)
|
|
- `INatApiError` — Base class with json, status, context
|
|
- `INatApiUnauthorizedError` — 401 errors
|
|
- `INatApiTooManyRequestsError` — 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:
|
|
```javascript
|
|
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
|
|
1. Create wrapper function in appropriate `src/api/*.js` file following the standard pattern
|
|
2. Create React Query hook using `useAuthenticatedQuery` or `useAuthenticatedMutation`
|
|
3. Transform response to match Realm schema if persisting locally
|
|
4. Add TypeScript types to `src/api/types.d.ts` if needed
|
|
|
|
### Adding a new React Query hook
|
|
1. Choose the right base hook: `useAuthenticatedQuery` (read), `useAuthenticatedMutation` (write), `useAuthenticatedInfiniteQuery` (paginated list)
|
|
2. Define a descriptive query key including relevant identifiers
|
|
3. Pass the API wrapper function with auth options
|
|
4. Handle errors in `onError` callback for mutations
|