API & Networking

Twinbloc provides a robust networking layer built on Axios, TanStack Query, and React Query Kit. It handles authentication, error parsing, and offline persistence out of the box.

The Client Wrapper

We use a custom wrapper executeRest around Axios to standardize request handling. This function automatically:

  • Injects the Authorization header with the current access token.
  • Handles 401 Unauthorized responses by silently rotating the access token and retrying the request once.
  • Signs the user out only when the session is unrecoverable (refresh fails or the retried request is also rejected).
  • Unwraps the response data.
  • Provides typed error handling via ApiError.
1import { executeRest } from '@/api/common';
2
3// Basic GET request
4const data = await executeRest<User>('/users/me', 'GET');
5
6// POST request with body
7await executeRest('/auth/login', 'POST', {
8 email: 'user@example.com',
9 password: 'password'
10});
11
12// Request with custom options (headers, params)
13await executeRest('/search', 'GET', undefined, {
14 params: { q: 'react' },
15 ignore401: true // Skip refresh + sign-out for this call
16});

Refresh-Token Rotation

When a request comes back 401, the client posts the stored refresh token to EXPO_PUBLIC_REFRESH_URL (see src/api/common/refresh-auth.ts), persists the rotated token pair via the auth store, and retries the original request once with the fresh access token.

  • Single-flight: concurrent 401s share one in-flight refresh request, so parallel queries don't hammer the refresh endpoint.
  • Graceful failure: a malformed response, missing session, or network error returns without throwing — the request simply fails with a typed ApiError.
  • Sign-out: only when the refresh fails or the rotated token is itself rejected, the session is cleared and the query cache reset.
  • Set ignore401: true on a request to skip both refresh and sign-out for that call.

The refresh endpoint is backend-configurable per environment via EXPO_PUBLIC_REFRESH_URL (configured in each .env.* file and validated in root-env.js).

Reusable Query Hooks

We use react-query-kit to create reusable query hooks. This keeps your API logic co-located and type-safe.

Here is an example of the useGetUser hook located in src/api/user/use-get-user.ts:

1import { createQuery } from "react-query-kit";
2import { executeRest, QueryKey } from "../common";
3import { type TUser } from "./types";
4
5// 1. Create the base query
6const _useGetUser = createQuery<TUser, void, AxiosError>({
7 queryKey: [QueryKey.USER], // unique key for caching
8 fetcher: async () => {
9 const response = await executeRest<{ data: TUser }>("auth/me", "GET");
10 return response?.data;
11 },
12});
13
14// 2. Export a custom hook wrapper for better DX
15export const useGetUser = (userId?: string) => {
16 const { auth_data } = useAuth();
17
18 // Only run query if user is logged in
19 return _useGetUser({
20 enabled: !!auth_data?.access,
21 });
22};

Pagination & Infinite Lists

For infinite scrolling lists (like feeds), we provide utilities in api-utils.ts to normalize pages and handle cursors.

1import { createInfiniteQuery } from "react-query-kit";
2import { getNextPageParam, normalizePages } from "@/api/common";
3
4const usePosts = createInfiniteQuery<Post[]>({
5 queryKey: ['posts'],
6 fetcher: async ({ pageParam }) => {
7 return executeRest('/posts', 'GET', undefined, {
8 params: { cursor: pageParam }
9 });
10 },
11 getNextPageParam, // Automatically extracts 'next', 'cursor', etc.
12});
13
14// Usage in component
15const { data } = usePosts();
16const allPosts = normalizePages(data?.pages); // Flattens pages into single array

Persistence & Provider

The APIProvider in src/api/common/api-provider.tsx wraps the app with the QueryClient and configures persistence using MMKV.

1import { APIProvider } from '@/api/common';
2
3export default function App() {
4 return (
5 <APIProvider>
6 <RootNavigator />
7 </APIProvider>
8 );
9}

Note: Queries are persisted to MMKV storage by default. You can control this per-query using gcTime and staleTime.

Last updated on 10/4/2026

Edit this page on GitHub