Fetch Utilities
Type-safe fetch wrapper for Nevr API calls with middleware and interceptor support.
Overview
The fetch utilities provide a powerful HTTP client with:
- Type-safe requests and responses
- Middleware chain for request modification
- Response interceptors for global error handling
- Automatic JSON parsing and error formatting
createNevrFetch()
Create a configured fetch function:
typescript
import { createNevrFetch } from "nevr/client"
const $fetch = createNevrFetch({
baseURL: "http://localhost:3000",
basePath: "/api",
middleware: [authMiddleware, logMiddleware],
interceptors: [refreshTokenInterceptor],
})
// Use it
const { data, error } = await $fetch<User[]>("/users")CreateFetchOptions
typescript
interface CreateFetchOptions {
/** Base URL (e.g., "http://localhost:3000") */
baseURL: string
/** API path prefix (e.g., "/api") */
basePath: string
/** Default options for all requests */
defaultOptions?: NevrFetchOptions
/** Legacy fetch plugins */
plugins?: NevrFetchPlugin[]
/** Client middleware chain */
middleware?: ClientMiddleware[]
/** Response interceptors */
interceptors?: ResponseInterceptor[]
}NevrFetch Response
All fetch calls return a consistent response format:
typescript
interface NevrFetchResponse<T> {
/** Response data (null if error) */
data: T | null
/** Error details (null if success) */
error: NevrFetchError | null
}
interface NevrFetchError {
status: number
statusText: string
message: string
code?: string
details?: any
}Usage Pattern
typescript
const { data, error } = await $fetch<User>("/users/123")
if (error) {
console.error(`Error ${error.status}: ${error.message}`)
return
}
// data is typed as User
console.log(data.email)NevrFetchOptions
Options for individual requests:
typescript
interface NevrFetchOptions {
/** HTTP method (default: "GET") */
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
/** Request headers */
headers?: Record<string, string>
/** Request body (auto-serialized to JSON) */
body?: any
/** Query parameters */
query?: Record<string, any>
/** Credentials mode (default: "include") */
credentials?: "include" | "omit" | "same-origin"
/** Disable signal triggers on success */
disableSignal?: boolean
/** Custom fetch function */
customFetch?: typeof fetch
/** Lifecycle callbacks */
onRequest?: (ctx: { url: string; options: RequestInit }) => void | Promise<void>
onResponse?: (ctx: { response: Response; data: any }) => void | Promise<void>
onSuccess?: (ctx: { data: any }) => void | Promise<void>
onError?: (ctx: { error: NevrFetchError }) => void | Promise<void>
}Examples
typescript
// GET with query params
const { data } = await $fetch<User[]>("/users", {
query: { role: "admin", limit: 10 },
})
// Calls: GET /api/users?role=admin&limit=10
// POST with body
const { data } = await $fetch<User>("/users", {
method: "POST",
body: { email: "new@example.com", name: "New User" },
})
// With custom headers
const { data } = await $fetch("/protected", {
headers: { "X-Custom-Header": "value" },
})
// With lifecycle hooks
const { data } = await $fetch("/users", {
onRequest: ({ url }) => console.log(`Fetching ${url}`),
onSuccess: ({ data }) => console.log(`Got ${data.length} users`),
onError: ({ error }) => console.error(`Failed: ${error.message}`),
})Client Middleware
Middleware intercepts requests before they're sent. Use for:
- Adding auth tokens
- Logging
- Retry logic
- Request modification
ClientMiddleware Type
typescript
type ClientMiddleware = (
ctx: MiddlewareRequestContext,
next: () => Promise<MiddlewareResponseContext>
) => Promise<MiddlewareResponseContext>
interface MiddlewareRequestContext {
url: string
path: string
method: string
headers: Record<string, string> // Mutable
body?: unknown // Mutable
query?: Record<string, any>
abort: (response: NevrFetchResponse<any>) => void
aborted: boolean
}
interface MiddlewareResponseContext {
request: MiddlewareRequestContext
data: any
error: NevrFetchError | null
status: number
ok: boolean
}Middleware Examples
Auth Middleware
typescript
const authMiddleware: ClientMiddleware = async (ctx, next) => {
const token = localStorage.getItem("token")
if (token) {
ctx.headers["Authorization"] = `Bearer ${token}`
}
return next()
}Logging Middleware
typescript
const logMiddleware: ClientMiddleware = async (ctx, next) => {
const start = Date.now()
console.log(`[Request] ${ctx.method} ${ctx.path}`)
const result = await next()
const duration = Date.now() - start
console.log(`[Response] ${result.status} (${duration}ms)`)
return result
}Retry Middleware
typescript
const retryMiddleware: ClientMiddleware = async (ctx, next) => {
let result = await next()
// Retry once on 5xx errors
if (result.error && result.status >= 500) {
console.log(`Retrying ${ctx.path}...`)
result = await next()
}
return result
}Abort Middleware
typescript
const cacheMiddleware: ClientMiddleware = async (ctx, next) => {
const cached = cache.get(ctx.url)
if (cached) {
// Return cached response without making request
ctx.abort({ data: cached, error: null })
return { request: ctx, data: cached, error: null, status: 200, ok: true }
}
const result = await next()
if (result.ok) {
cache.set(ctx.url, result.data)
}
return result
}Request Body Transform
typescript
const transformMiddleware: ClientMiddleware = async (ctx, next) => {
// Add timestamps to all POST/PUT requests
if (ctx.body && ["POST", "PUT"].includes(ctx.method)) {
ctx.body = {
...ctx.body,
_timestamp: Date.now(),
}
}
return next()
}Response Interceptors
Interceptors run after receiving a response. Use for:
- Token refresh on 401
- Global error handling
- Data transformation
ResponseInterceptor Type
typescript
type ResponseInterceptor = (
ctx: MiddlewareResponseContext
) => Promise<MiddlewareResponseContext | { retry: true }>Interceptor Examples
Token Refresh
typescript
const refreshInterceptor: ResponseInterceptor = async (ctx) => {
if (ctx.error?.status === 401) {
// Try to refresh token
const refreshed = await refreshToken()
if (refreshed) {
// Signal to retry the original request
return { retry: true }
}
// Refresh failed, redirect to login
window.location.href = "/login"
}
return ctx
}Date Transform
typescript
const dateInterceptor: ResponseInterceptor = async (ctx) => {
if (ctx.data) {
ctx.data = transformDates(ctx.data)
}
return ctx
}
function transformDates(obj: any): any {
if (!obj) return obj
if (typeof obj === "string" && isISODate(obj)) {
return new Date(obj)
}
if (Array.isArray(obj)) {
return obj.map(transformDates)
}
if (typeof obj === "object") {
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [k, transformDates(v)])
)
}
return obj
}Error Normalization
typescript
const errorInterceptor: ResponseInterceptor = async (ctx) => {
if (ctx.error) {
// Normalize error format
ctx.error = {
...ctx.error,
message: ctx.error.message || "An unexpected error occurred",
code: ctx.error.code || "UNKNOWN_ERROR",
}
// Log errors
console.error(`[API Error] ${ctx.error.code}: ${ctx.error.message}`)
}
return ctx
}Full Client Setup
typescript
import { createTypedClient, entityClient } from "nevr/client"
// ... (middleware defined above)
// Create client
const client = createTypedClient<API>({
baseURL: "http://localhost:3000",
basePath: "/api",
middleware: [authMiddleware, logMiddleware],
interceptors: [refreshInterceptor],
plugins: [
entityClient({ entities: ["user", "post", "product"] }),
],
})
// Use the client
const { data, error } = await client.users.list()
// Or use $fetch directly for custom endpoints
const { data: stats } = await client.$fetch<Stats>("/stats")Middleware Execution Order
Middleware executes in order, wrapping each subsequent middleware:
typescript
const client = createTypedClient<API>({
middleware: [first, second, third],
})
// Execution:
// 1. first (before)
// 2. second (before)
// 3. third (before)
// 4. actual fetch
// 5. third (after)
// 6. second (after)
// 7. first (after)Request → first → second → third → fetch() → third → second → first → ResponseQuery String Building
Query parameters are automatically serialized:
typescript
$fetch("/users", {
query: {
role: "admin",
status: ["active", "pending"], // Arrays supported
page: 1,
limit: 10,
},
})
// URL: /api/users?role=admin&status=active&status=pending&page=1&limit=10Error Handling Patterns
Pattern 1: Per-Request
typescript
const { data, error } = await $fetch("/users")
if (error) {
if (error.status === 404) {
console.log("Not found")
} else if (error.status === 401) {
console.log("Unauthorized")
} else {
console.log(`Error: ${error.message}`)
}
return
}Pattern 2: Global Interceptor
typescript
const errorInterceptor: ResponseInterceptor = async (ctx) => {
if (ctx.error) {
switch (ctx.error.status) {
case 401:
window.location.href = "/login"
break
case 403:
toast.error("Access denied")
break
case 500:
toast.error("Server error, please try again")
break
}
}
return ctx
}Pattern 3: onError Callback
typescript
$fetch("/users", {
onError: ({ error }) => {
Sentry.captureException(new Error(error.message))
},
})TypeScript Types
typescript
import type {
NevrFetch,
NevrFetchOptions,
NevrFetchResponse,
NevrFetchError,
ClientMiddleware,
ResponseInterceptor,
MiddlewareRequestContext,
MiddlewareResponseContext,
} from "nevr/client"Next Steps
- Client Overview - Full client setup
- Entity Client - Auto-generated CRUD
- React Integration - React hooks
