Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/unjs/ofetch/llms.txt

Use this file to discover all available pages before exploring further.

Overview

You can augment the FetchOptions interface to add custom properties with full TypeScript support. This is useful for adding domain-specific options that work throughout your application.

Basic Usage

Place this in any .ts or .d.ts file in your project:
// types.d.ts or custom.d.ts
declare module 'ofetch' {
  interface FetchOptions {
    // Add your custom properties
    requiresAuth?: boolean
  }
}

export {}
Ensure the file is included in your tsconfig.json “files” or covered by “include” patterns.

Using Augmented Types

Once augmented, your custom properties are available everywhere:
import { ofetch } from 'ofetch'

// TypeScript knows about requiresAuth
await ofetch('/api/protected', {
  requiresAuth: true  // ✅ Type-safe
})

Example from README

From README.md:356-384:
// Place this in any `.ts` or `.d.ts` file.
// Ensure it's included in the project's tsconfig.json "files".
declare module "ofetch" {
  interface FetchOptions {
    // Custom properties
    requiresAuth?: boolean;
  }
}

export {};
This lets you pass and use those properties with full type safety throughout ofetch calls:
const myFetch = ofetch.create({
  onRequest(context) {
    //      ^? { ..., options: {..., requiresAuth?: boolean }}
    console.log(context.options.requiresAuth);
  },
});

myFetch("/foo", { requiresAuth: true });

Common Use Cases

Authentication Requirements

// types.d.ts
declare module 'ofetch' {
  interface FetchOptions {
    requiresAuth?: boolean
    authType?: 'bearer' | 'basic' | 'apiKey'
  }
}

export {}
// api-client.ts
import { ofetch } from 'ofetch'

const api = ofetch.create({
  baseURL: '/api',
  async onRequest({ options }) {
    if (options.requiresAuth) {
      const token = await getAuthToken()
      
      if (options.authType === 'bearer') {
        options.headers.set('Authorization', `Bearer ${token}`)
      } else if (options.authType === 'apiKey') {
        options.headers.set('X-API-Key', token)
      }
    }
  }
})

// Usage with type safety
await api('/protected/resource', {
  requiresAuth: true,
  authType: 'bearer'
})

Cache Control

// types.d.ts
declare module 'ofetch' {
  interface FetchOptions {
    cacheStrategy?: 'no-cache' | 'force-cache' | 'revalidate'
    cacheTTL?: number
  }
}

export {}
const api = ofetch.create({
  async onRequest({ options }) {
    if (options.cacheStrategy === 'no-cache') {
      options.headers.set('Cache-Control', 'no-cache')
    } else if (options.cacheStrategy === 'force-cache') {
      options.headers.set('Cache-Control', `max-age=${options.cacheTTL || 3600}`)
    }
  }
})

await api('/api/data', {
  cacheStrategy: 'force-cache',
  cacheTTL: 7200
})

Rate Limiting

// types.d.ts
declare module 'ofetch' {
  interface FetchOptions {
    rateLimit?: {
      maxRequests: number
      perSeconds: number
    }
  }
}

export {}
const rateLimiters = new Map<string, RateLimiter>()

const api = ofetch.create({
  async onRequest({ request, options }) {
    if (options.rateLimit) {
      const limiter = getRateLimiter(
        request.toString(),
        options.rateLimit
      )
      await limiter.acquire()
    }
  }
})

await api('/api/endpoint', {
  rateLimit: { maxRequests: 10, perSeconds: 60 }
})

API Versioning

// types.d.ts
declare module 'ofetch' {
  interface FetchOptions {
    apiVersion?: 'v1' | 'v2' | 'v3'
  }
}

export {}
const api = ofetch.create({
  baseURL: '/api',
  async onRequest({ options }) {
    const version = options.apiVersion || 'v2'
    options.headers.set('X-API-Version', version)
  }
})

await api('/users', { apiVersion: 'v3' })

Request Tracking

// types.d.ts
declare module 'ofetch' {
  interface FetchOptions {
    trackingId?: string
    analytics?: boolean
  }
}

export {}
const api = ofetch.create({
  async onRequest({ options }) {
    if (options.analytics !== false) {
      const trackingId = options.trackingId || generateId()
      options.headers.set('X-Request-ID', trackingId)
      trackRequest(trackingId)
    }
  },
  async onResponse({ options, response }) {
    if (options.analytics !== false && options.trackingId) {
      trackResponse(options.trackingId, response.status)
    }
  }
})

await api('/api/action', {
  trackingId: 'user-action-123',
  analytics: true
})

Multiple Augmentations

You can augment the interface multiple times:
// auth.d.ts
declare module 'ofetch' {
  interface FetchOptions {
    requiresAuth?: boolean
  }
}

export {}
// cache.d.ts
declare module 'ofetch' {
  interface FetchOptions {
    cacheKey?: string
  }
}

export {}
Both properties will be available:
await ofetch('/api/data', {
  requiresAuth: true,
  cacheKey: 'my-data'
})

Context Types

From src/types.ts:94-99:
export interface FetchContext<T = any, R extends ResponseType = ResponseType> {
  request: FetchRequest;
  options: ResolvedFetchOptions<R>;
  response?: FetchResponse<T>;
  error?: Error;
}
Your augmented properties are available in interceptors through context.options:
declare module 'ofetch' {
  interface FetchOptions {
    customProperty?: string
  }
}

const api = ofetch.create({
  async onRequest(context) {
    // TypeScript knows about customProperty
    console.log(context.options.customProperty)
  }
})

Best Practices

1. Use Optional Properties

// ✅ Good - optional properties
declare module 'ofetch' {
  interface FetchOptions {
    myOption?: string
  }
}

// ❌ Avoid - required properties (breaks existing code)
declare module 'ofetch' {
  interface FetchOptions {
    myOption: string
  }
}

2. Document Your Properties

declare module 'ofetch' {
  interface FetchOptions {
    /**
     * Requires authentication for this request.
     * @default false
     */
    requiresAuth?: boolean
    
    /**
     * Custom timeout in milliseconds for this specific request.
     * Overrides the global timeout setting.
     */
    customTimeout?: number
  }
}

export {}

3. Keep Type Files Organized

// types/ofetch.d.ts
import 'ofetch'

declare module 'ofetch' {
  interface FetchOptions {
    // Group related properties
    
    // Authentication
    requiresAuth?: boolean
    authType?: 'bearer' | 'basic'
    
    // Caching
    cacheStrategy?: 'no-cache' | 'force-cache'
    cacheTTL?: number
    
    // Tracking
    trackingId?: string
    analytics?: boolean
  }
}

export {}

4. Export Empty Object

// ✅ Required - makes this a module
declare module 'ofetch' {
  interface FetchOptions {
    myOption?: string
  }
}

export {} // Don't forget this!

tsconfig.json Setup

Make sure your type declaration files are included:
{
  "compilerOptions": {
    "types": ["ofetch"]
  },
  "include": [
    "src/**/*",
    "types/**/*"  // Include your .d.ts files
  ]
}

Complete Example

// types/ofetch.d.ts
declare module 'ofetch' {
  interface FetchOptions {
    /** Require authentication for this request */
    requiresAuth?: boolean
    /** API version to use */
    apiVersion?: 'v1' | 'v2'
    /** Custom request ID for tracking */
    requestId?: string
  }
}

export {}
// api/client.ts
import { ofetch } from 'ofetch'

export const api = ofetch.create({
  baseURL: '/api',
  
  async onRequest({ options }) {
    // Add version header
    if (options.apiVersion) {
      options.headers.set('X-API-Version', options.apiVersion)
    }
    
    // Add auth token
    if (options.requiresAuth) {
      const token = await getAuthToken()
      options.headers.set('Authorization', `Bearer ${token}`)
    }
    
    // Add request ID
    const requestId = options.requestId || crypto.randomUUID()
    options.headers.set('X-Request-ID', requestId)
  }
})
// Usage with full type safety
const user = await api('/users/me', {
  requiresAuth: true,
  apiVersion: 'v2',
  requestId: 'custom-id-123'
})

Build docs developers (and LLMs) love