Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

unjs/fetchdts

Open more actions menu

Repository files navigation

fetchdts

npm version npm downloads Github Actions Codecov

A suite of type utilities for building strongly-typed APIs

🚧 Under active development

Features

  • 💪 Strongly-typed query, body, headers, response.
  • 🗺️ Static path segments, as well as dynamic and wildcard parameters.
  • 📦 Exposes core utilities for building typed fetch functions.

Usage

Install package:

# npm
npm install fetchdts

# pnpm
pnpm install fetchdts

Quick Start

Define your API schema and create a strongly-typed fetch function:

import type { DynamicParam, Endpoint, TypedFetchInput, TypedFetchRequestInit, TypedFetchResponseBody, TypedResponse } from 'fetchdts'

// Define your API schema
interface APISchema {
  '/users': {
    [Endpoint]: {
      GET: {
        response: { id: number, name: string }[]
      }
      POST: {
        body: { name: string, email: string }
        response: { id: number, name: string, email: string }
      }
    }
    [DynamicParam]: { // /users/:id
      [Endpoint]: {
        GET: {
          response: { id: number, name: string, email: string }
        }
        DELETE: {
          response: { success: boolean }
        }
      }
    }
  }
}

// Create your typed fetch function
async function api<T extends TypedFetchInput<APISchema>>(
  input: T,
  init?: TypedFetchRequestInit<APISchema, T>,
) {
  return fetch(input, init as RequestInit) as unknown as Promise<TypedResponse<TypedFetchResponseBody<APISchema, T>>>
}

// Use with full type safety
const users = await api('/users').then(r => r.json()) // Type: { id: number; name: string }[]
const user = await api('/users/123').then(r => r.json()) // Type: { id: number; name: string; email: string }

Core Concepts

Schema Definition

Your API schema describes the structure of your endpoints using TypeScript interfaces:

interface Schema {
  '/path': {
    [Endpoint]: {
      [HTTPMethod]: {
        query?: { param: string } // Query parameters
        body?: { data: any } // Request body
        headers?: { auth: string } // Required headers
        response: { result: any } // Response data
        responseHeaders?: { 'x-rate-limit': string } // Response headers
      }
    }
  }
}

Path Types

Static Paths: Exact string matches

interface Schema {
  '/api/users': {
    [Endpoint]: {
      GET: { response: User[] }
    }
  }
}

Dynamic Parameters: Single path segments

interface Schema {
  '/api/users': {
    [DynamicParam]: { // matches /api/users/123, /api/users/abc, etc.
      [Endpoint]: {
        GET: { response: User }
      }
    }
  }
}

Wildcard Parameters: Multiple path segments

interface Schema {
  '/api': {
    [WildcardParam]: { // matches /api/anything/nested/deep
      [Endpoint]: {
        GET: { response: any }
      }
    }
  }
}

Symbols Reference

fetchdts uses special symbols to define different types of route matching:

import { DynamicParam, Endpoint, WildcardParam } from 'fetchdts'

interface Schema {
  // Endpoint: Marks where HTTP methods are defined
  [Endpoint]: {
    GET: { response: Data }
    POST: { body: Input, response: Data }
  }

  // DynamicParam: Matches single path segments (e.g., /users/:id)
  [DynamicParam]: {
    [Endpoint]: {
      GET: { response: User }
    }
  }

  // WildcardParam: Matches multiple path segments (e.g., /files/*)
  [WildcardParam]: {
    [Endpoint]: {
      GET: { response: File }
    }
  }
}

HTTP Methods

All standard HTTP methods are supported:

  • GET, POST, PUT, DELETE, PATCH
  • OPTIONS, HEAD, CONNECT, TRACE
interface RESTSchema {
  '/api/users': {
    [Endpoint]: {
      GET: { response: User[] }
      POST: { body: CreateUser, response: User }
    }
    [DynamicParam]: {
      [Endpoint]: {
        GET: { response: User }
        PUT: { body: UpdateUser, response: User }
        PATCH: { body: Partial<UpdateUser>, response: User }
        DELETE: { response: { success: boolean } }
      }
    }
  }
}

API Reference

Core Types

TypedFetchInput<Schema>

Extracts valid URL paths from your schema:

type ValidPaths = TypedFetchInput<APISchema>
// Result: '/users' | '/users/${string}'

TypedFetchRequestInit<Schema, Path>

Provides typed request options for a specific path:

// For paths requiring body/headers/query parameters
await api('/users', {
  method: 'POST',
  body: { name: 'John' }, // ✅ Typed based on schema
  headers: { authorization: 'Bearer token' }
})

TypedFetchResponseBody<Schema, Path>

Returns the typed response body for a given path:

const response = await api('/users/123')
// Type automatically inferred from schema

TypedHeaders<HeaderMap>

Provides typed header access:

const contentType = response.headers.get('content-type') // string | null
const customHeader = response.headers.get('x-custom') // Typed based on schema

Utilities

serializeRoutes(name, routes, options?)

Generate TypeScript schema from route definitions:

import { serializeRoutes } from 'fetchdts'

const schema = serializeRoutes('APISchema', [
  {
    path: '/users',
    metadata: {
      GET: {
        responseType: 'User[]'
      },
      POST: {
        bodyType: '{ name: string }',
        responseType: 'User'
      }
    }
  },
  {
    path: '/users/:id',
    type: 'dynamic',
    metadata: {
      GET: {
        responseType: 'User'
      }
    }
  }
])

console.log(schema)
// Outputs TypeScript interface definition

Advanced Examples

Typed Headers and Query Parameters

interface APISchema {
  '/search': {
    [Endpoint]: {
      GET: {
        query: { q: string, limit?: number }
        headers: { 'x-api-key': string }
        response: { results: string[], total: number }
        responseHeaders: { 'x-rate-limit-remaining': string }
      }
    }
  }
}

// Usage with required query and headers
const results = await api('/search', {
  query: { q: 'typescript', limit: 10 },
  headers: { 'x-api-key': 'your-key' }
})

// Access typed response headers
const rateLimit = results.headers.get('x-rate-limit-remaining') // string | null

Mixed Static and Dynamic Routes

interface APISchema {
  '/api': {
    '/health': {
      [Endpoint]: {
        GET: { response: { status: 'ok' | 'error' } }
      }
    }
    '/users': {
      [Endpoint]: {
        GET: { response: User[] }
        POST: { body: CreateUser, response: User }
      }
      [DynamicParam]: {
        [Endpoint]: {
          GET: { response: User }
          PUT: { body: UpdateUser, response: User }
          DELETE: { response: { deleted: boolean } }
        }
        '/posts': {
          [Endpoint]: {
            GET: { response: Post[] }
          }
          [DynamicParam]: {
            [Endpoint]: {
              GET: { response: Post }
            }
          }
        }
      }
    }
  }
}

// All of these are now typed:
await api('/api/health') // { status: 'ok' | 'error' }
await api('/api/users') // User[]
await api('/api/users/123') // User
await api('/api/users/123/posts') // Post[]
await api('/api/users/123/posts/456') // Post

Cross-Domain API Support

interface Schema {
  'https://api.github.com': {
    '/users': {
      [DynamicParam]: {
        [Endpoint]: {
          GET: { response: GitHubUser }
        }
        '/repos': {
          [Endpoint]: {
            GET: { response: Repository[] }
          }
        }
      }
    }
  }
}

// Works with full URLs
const user = await api('https://api.github.com/users/octocat')
const repos = await api('https://api.github.com/users/octocat/repos')

Error Handling with Types

interface APISchema {
  '/api/users': {
    [DynamicParam]: {
      [Endpoint]: {
        GET: {
          response: User | { error: string, code: number }
        }
      }
    }
  }
}

const result = await api('/api/users/123')
// result is typed as: User | { error: string; code: number }

if ('error' in result) {
  console.error(`Error ${result.code}: ${result.error}`)
}
else {
  console.log(`User: ${result.name}`)
}

Best Practices

Schema Organization

For large APIs, consider organizing your schemas into modules:

// types/api.ts
interface UserAPI {
  '/api/users': {
    [Endpoint]: {
      GET: { response: User[] }
      POST: { body: CreateUser, response: User }
    }
    [DynamicParam]: {
      [Endpoint]: {
        GET: { response: User }
        PUT: { body: UpdateUser, response: User }
        DELETE: { response: { success: boolean } }
      }
    }
  }
}

interface PostAPI {
  '/api/posts': {
    [Endpoint]: {
      GET: { query?: { limit?: number }, response: Post[] }
      POST: { body: CreatePost, response: Post }
    }
    [DynamicParam]: {
      [Endpoint]: {
        GET: { response: Post }
        PUT: { body: UpdatePost, response: Post }
        DELETE: { response: { success: boolean } }
      }
    }
  }
}

// Combine them
type APISchema = UserAPI & PostAPI

Runtime Validation

While fetchdts provides compile-time type safety, consider adding runtime validation:

import { z } from 'zod'

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email()
})

async function api<T extends TypedFetchInput<APISchema>>(
  input: T,
  init?: TypedFetchRequestInit<APISchema, T>
): Promise<TypedFetchResponseBody<APISchema, T>> {
  const response = await fetch(input, init as RequestInit)
  const data = await response.json()

  // Runtime validation for critical endpoints
  if (input.startsWith('/api/users/') && init?.method !== 'DELETE') {
    return UserSchema.parse(data) // Throws if invalid
  }

  return data
}

Error Handling

Design your schemas with error handling in mind:

interface APISchema {
  '/api/users': {
    [DynamicParam]: {
      [Endpoint]: {
        GET: {
          response:
            | { success: true, data: User }
            | { success: false, error: string, code: number }
        }
      }
    }
  }
}

// Usage
const result = await api('/api/users/123')
if (result.success) {
  console.log(result.data.name) // ✅ Type-safe access
}
else {
  console.error(`Error ${result.code}: ${result.error}`)
}

Troubleshooting

TypeScript Configuration

For the best experience, ensure your tsconfig.json includes:

{
  "compilerOptions": {
    "strict": true,
    "exactOptionalPropertyTypes": true,
    "noUncheckedIndexedAccess": true
  }
}

💻 Development

I would welcome contributions! Please see the Code of Conduct.

  • Clone this repository
  • Enable Corepack using corepack enable
  • Install dependencies using pnpm install
  • Run interactive tests using pnpm dev and type tests with pnpm test:types

License

Made with ❤️

Published under MIT License.

About

A suite of type utilities for building strongly-typed APIs

Resources

License

Code of conduct

Stars

Watchers

Forks

Sponsor this project

 

Packages

 
 
 

Contributors

Morty Proxy This is a proxified and sanitized view of the page, visit original site.