nfx-network
v1.1.1
Published
Modular, multi-platform network layer for Vite, Next.js, and React Native
Readme
@nickelfox/network
A modular, type-safe, multi-platform network layer for Vite, Next.js, and React Native. Built on Ky and TanStack Query v5.
Features
- Single package, three platforms — Vite SPA, Next.js (client + SSR), React Native (bare + Expo)
- TanStack Query v5 — cache, deduplication, background refetch, offline persistence
- Cookie-based auth for web — httpOnly cookies +
credentials: include, zero JS token handling - Bearer token auth for React Native — MMKV with device-ID-derived encryption key
- Automatic token refresh — shared promise prevents concurrent refresh storms
- Proactive token expiry check — refreshes before expiry, no wasted round-trips
- Typed errors — discriminated
ApiResult<T>union, canonicalNetworkErrorCoderegistry - Retry with jitter — exponential backoff,
Retry-Afterheader respected - In-flight deduplication — concurrent identical GETs share one request
- Content-Type negotiation — JSON, blob, text, and 204 No Content handled automatically
- Structured logging — correlation IDs, pluggable
Loggeradapter, automatic redaction - Strict TypeScript —
strict,noUncheckedIndexedAccess,exactOptionalPropertyTypes - Tree-shakeable — platform adapters on separate entry points,
sideEffects: false
Installation
# In your app (workspace or file reference)
npm install @nickelfox/network
# Required peer dependencies (all platforms)
npm install ky @tanstack/react-query
# Web only
npm install react
# React Native only
npm install react-native-mmkv react-native-device-info
# Next.js only — next is already in your projectQuick Start
Vite SPA
// main.tsx
import { networkLayerSetup, createNetworkConfig } from '@nickelfox/network'
import { LocalStorageAdapter } from '@nickelfox/network/adapters/web'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: { networkMode: 'offlineFirst', staleTime: 30_000 }
}
})
networkLayerSetup({
config: createNetworkConfig({
baseURL: import.meta.env.VITE_API_URL,
authMode: 'cookie',
}),
onLogout: () => {
queryClient.clear()
window.location.href = '/login'
},
})
export function App() {
return (
<QueryClientProvider client={queryClient}>
<Router />
</QueryClientProvider>
)
}Next.js
// app/providers.tsx (client component)
'use client'
import { networkLayerSetup, createNetworkConfig } from '@nickelfox/network'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useRouter } from 'next/navigation'
import { useRef } from 'react'
export function Providers({ children }: { children: React.ReactNode }) {
const router = useRouter()
const queryClientRef = useRef(new QueryClient({
defaultOptions: { queries: { networkMode: 'offlineFirst', staleTime: 30_000 } }
}))
useRef(() => {
networkLayerSetup({
config: createNetworkConfig({
baseURL: process.env.NEXT_PUBLIC_API_URL!,
serverBaseURL: process.env.INTERNAL_API_URL,
authMode: 'cookie',
}),
onLogout: () => {
queryClientRef.current.clear()
router.push('/login')
},
})
})
return (
<QueryClientProvider client={queryClientRef.current}>
{children}
</QueryClientProvider>
)
}React Native (bare + Expo)
// App.tsx
import { networkLayerSetup, createNetworkConfig } from '@nickelfox/network'
import { MMKVAdapter } from '@nickelfox/network/adapters/react-native'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { navigationRef } from './navigation/navigationRef'
const queryClient = new QueryClient({
defaultOptions: {
queries: { networkMode: 'offlineFirst', staleTime: 30_000 }
}
})
networkLayerSetup({
config: createNetworkConfig({
baseURL: process.env.API_URL!,
authMode: 'bearer',
tokenKeys: {
access: 'user-token',
refresh: 'refresh-token',
refreshEndpoint: '/auth/refresh-token/',
},
}),
storage: new MMKVAdapter(),
onLogout: () => {
queryClient.clear()
navigationRef.current?.navigate('Login')
},
})
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<RootNavigator />
</QueryClientProvider>
)
}Usage at a Glance
// Define endpoints once
import { RouteDefinition } from '@nickelfox/network'
export const API = {
DEALS: {
LIST: { endpoint: '/deals/', method: 'GET' } satisfies RouteDefinition,
CREATE: { endpoint: '/deals/', method: 'POST' } satisfies RouteDefinition,
DETAIL: (id: string) => ({ endpoint: `/deals/${id}/`, method: 'GET' }) satisfies RouteDefinition,
UPDATE: (id: string) => ({ endpoint: `/deals/${id}/`, method: 'PATCH' }) satisfies RouteDefinition,
}
}
// Query
const { data, isLoading, error } = useApiQuery<Deal[]>(API.DEALS.LIST)
// Query with params
const { data } = useApiQuery<Deal>(
API.DEALS.DETAIL(dealId),
{ pathParams: { id: dealId } }
)
// Filtered query
const { data } = useApiQuery<Deal[]>(
API.DEALS.LIST,
{ queryParams: { status: 'active', page: 1 } }
)
// Mutation
const { mutate, isPending } = useApiMutation<Deal, CreateDealPayload>(
API.DEALS.CREATE,
{},
{
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/deals/'] })
}
}
)
// Mutation with dynamic route
const { mutate } = useApiMutation<Deal, UpdateDealPayload>(
(vars) => API.DEALS.UPDATE(vars.id),
)Package Entry Points
| Import path | Contents | Use in |
|---|---|---|
| @nickelfox/network | Hooks, types, createNetworkConfig, networkLayerSetup | All platforms |
| @nickelfox/network/adapters/web | LocalStorageAdapter, SessionStorageAdapter | Vite |
| @nickelfox/network/adapters/next | NextServerCookieAdapter, LocalStorageAdapter | Next.js |
| @nickelfox/network/adapters/react-native | MMKVAdapter | React Native |
| @nickelfox/network/mocks | http, HttpResponse, delay from MSW | Tests / dev |
Adapters are on separate entry points so platform-specific code (MMKV, next/headers) never enters the wrong bundle.
Documentation
- Usage Guide — setup, hooks, uploads, errors, offline, MSW
- Technical Reference — architecture, internals, design decisions
