# API Reference

> Complete API documentation for what-the-fetch with detailed examples and type signatures

## Main Function

### `createFetch()`

Creates a type-safe fetch function for your API.

```typescript
function createFetch<Schema extends ApiSchema>(
  apis: Schema,
  baseUrl: string,
  sharedInit?: RequestInit
): <Path extends ApiPath<Schema>>(
  path: Path,
  options?: FetchOptions<Schema, Path>,
  init?: RequestInit
) => Promise<ApiData<Schema, Path, 'response'>>
```

**Parameters:**

- `apis` - An object mapping API paths to their schema definitions
- `baseUrl` - The base URL for all API requests
- `sharedInit` (optional) - Shared [RequestInit](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit) options that will be merged with per-request options

**Returns:** A typed fetch function that accepts:
- `path` - The API path (must be a key from your schema)
- `options` (optional) - Request options (params, query, body) based on the path's schema
- `init` (optional) - Per-request [RequestInit](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit) to customize the fetch request (merged with sharedInit)

**Example:**

```typescript
import { createFetch } from 'what-the-fetch';
import { z } from 'zod';

const api = {
  '/users/:id': {
    params: z.object({ id: z.number() }),
    query: z.object({ fields: z.string().optional() }),
    response: z.object({
      id: z.number(),
      name: z.string(),
      email: z.string()
    })
  }
};

const apiFetch = createFetch(api, 'https://api.example.com');

const user = await apiFetch('/users/:id', {
  params: { id: 123 },
  query: { fields: 'name,email' }
});
// user is typed as { id: number; name: string; email: string }
```

---

## Type Definitions

### `ApiSchema`

Schema definition for an API. Maps API paths to their schema definitions.

```typescript
type ApiSchema = Record<
  string,
  {
    params?: StandardSchemaV1<Record<string, unknown>>
    query?: StandardSchemaV1<Record<string, unknown>>
    body?: StandardSchemaV1<Record<string, unknown>>
    response?: StandardSchemaV1<Record<string, unknown>>
  }
>
```

Each path in your schema can have:

- `params` - Schema for URL path parameters (e.g., `:id`)
- `query` - Schema for query string parameters
- `body` - Schema for request body (automatically sets method to POST)
- `response` - Schema for response validation

**Important:** If your path contains parameters (e.g., `/users/:id`), you **must** define a `params` schema. Attempting to use a parameterized path without a params schema will throw an error at runtime.

**Example:**

```typescript
import { z } from 'zod';

const api: ApiSchema = {
  '/users/:id': {
    params: z.object({ id: z.number() }),
    query: z.object({ fields: z.string().optional() }),
    response: z.object({ id: z.number(), name: z.string() })
  },
  '/users': {
    // GET /users - List users with pagination
    query: z.object({
      limit: z.number().optional(),
      offset: z.number().optional()
    }),
    response: z.array(z.object({ id: z.number(), name: z.string() }))
  }
};
```

### `ApiPath<T>`

Extract valid API paths from an API schema.

```typescript
type ApiPath<T extends ApiSchema> = keyof T & string
```

**Example:**

```typescript
const api = {
  '/users/:id': { /* ... */ },
  '/posts': { /* ... */ }
};

type Path = ApiPath<typeof api>;
// Path = '/users/:id' | '/posts'
```

### `FetchOptions<T, Path>`

Extract the required fetch options for a specific API path.

```typescript
type FetchOptions<T extends ApiSchema, Path extends ApiPath<T>> = 
  // Infers correct types for params, query, and body
```

Automatically infers the correct types for params, query, and body based on the schema definition for the given path.

**Example:**

```typescript
const api = {
  '/users/:id': {
    params: z.object({ id: z.number() }),
    query: z.object({ fields: z.string().optional() })
  }
};

type Options = FetchOptions<typeof api, '/users/:id'>;
// Options = { params: { id: number }, query?: { fields?: string } }
```

### `ApiData<Schemas, Path, Option>`

Extract the inferred data type for a specific schema option from an API path.

```typescript
type ApiData<
  Schemas extends ApiSchema,
  Path extends ApiPath<Schemas>,
  Option extends keyof Schemas[Path]
>
```

This is a flexible type that extracts the inferred TypeScript type for any schema option (`'params'`, `'query'`, `'body'`, or `'response'`) from a specific API path. This replaces the older `ApiResponse` type and provides more flexibility for working with different parts of your API schema.

**Parameters:**
- `Schemas` - The complete API schema type
- `Path` - The specific API path to extract from
- `Option` - The schema option to extract (`'params'`, `'query'`, `'body'`, or `'response'`)

**Examples:**

```typescript
const api = {
  '/users/:id': {
    params: z.object({ id: z.number() }),
    query: z.object({ fields: z.string().optional() }),
    response: z.object({ id: z.number(), name: z.string() })
  }
};

// Extract params type
type UserParams = ApiData<typeof api, '/users/:id', 'params'>;
// UserParams = { id: number }

// Extract query type
type UserQuery = ApiData<typeof api, '/users/:id', 'query'>;
// UserQuery = { fields?: string }

// Extract response type
type UserResponse = ApiData<typeof api, '/users/:id', 'response'>;
// UserResponse = { id: number; name: string }
```

**Note:** For backward compatibility, you can create a type alias:
```typescript
type ApiResponse<T extends ApiSchema, Path extends ApiPath<T>> = 
  ApiData<T, Path, 'response'>;
```

---

## Request Validation

what-the-fetch automatically validates all request data (params, query, and body) as well as response data using the schemas you provide. The validation happens concurrently for optimal performance.

### Validation Behavior

- **Params validation**: If your path contains parameters (e.g., `/users/:id`), you must provide a `params` schema. The library will throw an error if a parameterized path is missing a params schema.
- **Query validation**: Query parameters are validated if a `query` schema is provided.
- **Body validation**: Request body is validated if a `body` schema is provided.
- **Response validation**: Response data is validated if a `response` schema is provided.
- **Concurrent validation**: All request validations (params, query, body) run concurrently using `Promise.all()` for better performance.

### Validation Errors

When validation fails, the library throws an error with details about the validation issues:

```typescript
const api = {
  '/users/:id': {
    params: z.object({ id: z.number() }),
    response: z.object({ id: z.number(), name: z.string() })
  }
};

const apiFetch = createFetch(api, 'https://api.example.com');

// This will throw a validation error
try {
  await apiFetch('/users/:id', { params: { id: 'invalid' } });
} catch (error) {
  console.error(error.message); // "Validation failed: [...]"
}
```

### Required Params Schema

If your path contains URL parameters, you **must** define a params schema:

```typescript
// ❌ This will throw an error
const badApi = {
  '/users/:id': {
    response: z.object({ id: z.number(), name: z.string() })
    // Missing params schema!
  }
};

// ✅ This is correct
const goodApi = {
  '/users/:id': {
    params: z.object({ id: z.number() }),
    response: z.object({ id: z.number(), name: z.string() })
  }
};
```

The library detects parameterized paths using the pattern `/:paramName` and will throw this error if no params schema is found:
```
Path contains parameters but no "params" schema is defined.
```

---

## Request Examples

### GET Request with Path Parameters

```typescript
const api = {
  '/users/:id': {
    params: z.object({ id: z.number() }),
    response: z.object({
      id: z.number(),
      name: z.string(),
      email: z.string()
    })
  }
};

const apiFetch = createFetch(api, 'https://api.example.com');

const user = await apiFetch('/users/:id', {
  params: { id: 123 }
});
// GET https://api.example.com/users/123
```

### GET Request with Query Parameters

```typescript
const api = {
  '/users': {
    query: z.object({
      limit: z.number().optional(),
      offset: z.number().optional(),
      sort: z.string().optional()
    }),
    response: z.array(z.object({
      id: z.number(),
      name: z.string()
    }))
  }
};

const apiFetch = createFetch(api, 'https://api.example.com');

const users = await apiFetch('/users', {
  query: { limit: 10, offset: 0, sort: 'name' }
});
// GET https://api.example.com/users?limit=10&offset=0&sort=name
```

### GET Request with Both Path and Query Parameters

```typescript
const api = {
  '/users/:id/posts': {
    params: z.object({ id: z.number() }),
    query: z.object({
      limit: z.number().optional(),
      published: z.boolean().optional()
    }),
    response: z.array(z.object({
      id: z.number(),
      title: z.string()
    }))
  }
};

const apiFetch = createFetch(api, 'https://api.example.com');

const posts = await apiFetch('/users/:id/posts', {
  params: { id: 123 },
  query: { limit: 10, published: true }
});
// GET https://api.example.com/users/123/posts?limit=10&published=true
```

### POST Request with Body

```typescript
const api = {
  '/users': {
    body: z.object({
      name: z.string(),
      email: z.string().email()
    }),
    response: z.object({
      id: z.number(),
      name: z.string(),
      email: z.string()
    })
  }
};

const apiFetch = createFetch(api, 'https://api.example.com');

const newUser = await apiFetch('/users', {
  body: {
    name: 'John Doe',
    email: 'john@example.com'
  }
});
// POST https://api.example.com/users
// Content-Type: application/json
// Body: {"name":"John Doe","email":"john@example.com"}
```

### Custom Headers and Options

```typescript
const api = {
  '/users/:id': {
    params: z.object({ id: z.number() }),
    response: z.object({ id: z.number(), name: z.string() })
  }
};

const apiFetch = createFetch(api, 'https://api.example.com');

const user = await apiFetch(
  '/users/:id',
  { params: { id: 123 } },
  {
    headers: {
      'Authorization': 'Bearer token',
      'X-Custom-Header': 'value'
    },
    cache: 'no-cache',
    signal: abortController.signal
  }
);
```

---

## HTTP Methods

what-the-fetch automatically infers HTTP methods based on the presence of a request body. You can optionally use the `@method` prefix for explicit control or to specify methods other than GET and POST.

### Automatic Method Inference

By default, what-the-fetch infers the HTTP method:
- Requests **with a body** → `POST`
- Requests **without a body** → `GET`

```typescript
const api = {
  '/users/:id': { /* Uses GET (no body) */ },
  '/users': { body: z.object({...}), /* Uses POST (has body) */ }
};
```

### Method Prefix Syntax (Optional)

For explicit control or to use other HTTP methods, add the `@method` prefix:

```typescript
const api = {
  '@get/resource': { /* Explicit GET - same as /resource */ },
  '/resource': { /* Implicit GET */ },
  '@post/resource': { /* Explicit POST */ },
  '@put/resource/:id': { /* PUT - prefix required */ },
  '@patch/resource/:id': { /* PATCH - prefix required */ },
  '@delete/resource/:id': { /* DELETE - prefix required */ }
};
```

**Note:** `/users/:id` and `@get/users/:id` are completely equivalent and both result in a GET request.

### Supported HTTP Methods

All standard HTTP methods are supported:

- `@get` - GET requests (retrieve data)
- `@post` - POST requests (create new resources)
- `@put` - PUT requests (replace existing resources)
- `@patch` - PATCH requests (partially update resources)
- `@delete` - DELETE requests (remove resources)
- `@head` - HEAD requests (retrieve headers only)
- `@options` - OPTIONS requests (check available methods)

### Complete Example

```typescript
import { createFetch } from 'what-the-fetch';
import { z } from 'zod';

const api = {
  // GET - Retrieve a user
  '@get/users/:id': {
    params: z.object({ id: z.number() }),
    response: z.object({
      id: z.number(),
      name: z.string(),
      email: z.string()
    })
  },
  
  // POST - Create a new user
  '@post/users': {
    body: z.object({
      name: z.string(),
      email: z.string().email()
    }),
    response: z.object({
      id: z.number(),
      name: z.string(),
      email: z.string()
    })
  },
  
  // PUT - Replace entire user
  '@put/users/:id': {
    params: z.object({ id: z.number() }),
    body: z.object({
      name: z.string(),
      email: z.string().email()
    }),
    response: z.object({
      id: z.number(),
      name: z.string(),
      email: z.string()
    })
  },
  
  // PATCH - Partially update user
  '@patch/users/:id': {
    params: z.object({ id: z.number() }),
    body: z.object({
      name: z.string().optional(),
      email: z.string().email().optional()
    }),
    response: z.object({
      id: z.number(),
      name: z.string(),
      email: z.string()
    })
  },
  
  // DELETE - Remove a user
  '@delete/users/:id': {
    params: z.object({ id: z.number() }),
    response: z.object({ success: z.boolean() })
  }
} as const;

const apiFetch = createFetch(api, 'https://api.example.com');

// Make requests with explicit methods
const user = await apiFetch('@get/users/:id', { params: { id: 123 } });
// GET https://api.example.com/users/123

const newUser = await apiFetch('@post/users', {
  body: { name: 'John Doe', email: 'john@example.com' }
});
// POST https://api.example.com/users

await apiFetch('@put/users/:id', {
  params: { id: 123 },
  body: { name: 'Jane Doe', email: 'jane@example.com' }
});
// PUT https://api.example.com/users/123

await apiFetch('@patch/users/:id', {
  params: { id: 123 },
  body: { name: 'Jane Smith' }
});
// PATCH https://api.example.com/users/123

await apiFetch('@delete/users/:id', { params: { id: 123 } });
// DELETE https://api.example.com/users/123
```

### Equivalence Examples

The following path definitions are equivalent:

```typescript
const api = {
  // These two are identical - both use GET
  '/users/:id': {
    params: z.object({ id: z.number() }),
    response: z.object({ id: z.number(), name: z.string() })
  },
  '@get/users/:id': {  // Same as above - @get is optional
    params: z.object({ id: z.number() }),
    response: z.object({ id: z.number(), name: z.string() })
  },
  
  // POST is inferred from body
  '/users': {
    body: z.object({ name: z.string(), email: z.string() }),
    response: z.object({ id: z.number(), name: z.string() })
  }
};

const apiFetch = createFetch(api, 'https://api.example.com');

// Both calls are equivalent - both use GET
const user1 = await apiFetch('/users/:id', { params: { id: 123 } });
const user2 = await apiFetch('@get/users/:id', { params: { id: 123 } });

// POST (inferred from body)
const newUser = await apiFetch('/users', {
  body: { name: 'John', email: 'john@example.com' }
});
```

**Method inference rules:**
- Request has a `body` → `POST`
- Request has no `body` → `GET`
- Method prefix specified → Uses that method

### Method Prefix with Path Parameters

The method prefix works seamlessly with path parameters:

```typescript
const api = {
  '@put/users/:userId/posts/:postId': {
    params: z.object({
      userId: z.number(),
      postId: z.number()
    }),
    body: z.object({ title: z.string(), content: z.string() }),
    response: z.object({ success: z.boolean() })
  }
};

const apiFetch = createFetch(api, 'https://api.example.com');

await apiFetch('@put/users/:userId/posts/:postId', {
  params: { userId: 123, postId: 456 },
  body: { title: 'Updated', content: 'New content' }
});
// PUT https://api.example.com/users/123/posts/456
```

### Case Insensitive

Method prefixes are case-insensitive and converted to uppercase:

```typescript
const api = {
  '@GET/users': { /* ... */ },
  '@get/posts': { /* ... */ },
  '@GeT/comments': { /* ... */ }
};

// All are treated as GET
```

### Best Practices

1. **Be explicit with methods**: Use method prefixes for clarity, especially for PUT, PATCH, and DELETE operations
2. **RESTful design**: Follow REST conventions:
   - `@get` for retrieval
   - `@post` for creation
   - `@put` for full replacement
   - `@patch` for partial updates
   - `@delete` for removal
3. **Consistency**: Choose either to always use method prefixes or rely on automatic detection - be consistent across your API schema

---

## Advanced Usage

### Shared Request Configuration

Use the third parameter of `createFetch()` to set shared options for all requests:

```typescript
import { createFetch } from 'what-the-fetch';
import { z } from 'zod';

const api = {
  '/users/:id': {
    params: z.object({ id: z.number() }),
    response: z.object({ id: z.number(), name: z.string() })
  },
  '/posts': {
    response: z.array(z.object({ id: z.number(), title: z.string() }))
  }
};

// Create fetch with shared headers that apply to all requests
const apiFetch = createFetch(
  api,
  'https://api.example.com',
  {
    headers: {
      'Authorization': 'Bearer my-token',
      'X-API-Version': '2.0'
    },
    cache: 'no-cache'
  }
);

// All requests automatically include shared headers
const user = await apiFetch('/users/:id', { params: { id: 123 } });
const posts = await apiFetch('/posts');

// Override or add additional headers per request
const userWithCustomHeader = await apiFetch(
  '/users/:id',
  { params: { id: 456 } },
  {
    headers: {
      'X-Custom-Header': 'value' // Merged with shared headers
    }
  }
);
```

### Multiple Endpoints with Shared Schemas

```typescript
import { createFetch } from 'what-the-fetch';
import { z } from 'zod';

// Define reusable schemas
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string()
});

const PostSchema = z.object({
  id: z.number(),
  title: z.string(),
  content: z.string(),
  authorId: z.number()
});

// Use schemas in multiple endpoints
const api = {
  '/users/:id': {
    params: z.object({ id: z.number() }),
    response: UserSchema
  },
  '/users': {
    response: z.array(UserSchema)
  },
  '/posts/:id': {
    params: z.object({ id: z.number() }),
    response: PostSchema
  },
  '/users/:id/posts': {
    params: z.object({ id: z.number() }),
    response: z.array(PostSchema)
  }
};

const apiFetch = createFetch(api, 'https://api.example.com');
```

### Building a Complete API Client

```typescript
import { createFetch } from 'what-the-fetch';
import { z } from 'zod';

// Define comprehensive API schema
const api = {
  '/users': {
    // GET /users - List users with pagination
    query: z.object({
      page: z.number().optional(),
      limit: z.number().optional()
    }),
    response: z.object({
      users: z.array(z.object({
        id: z.number(),
        name: z.string()
      })),
      total: z.number()
    })
  },
  '/users/:id': {
    // GET /users/:id - Get user by ID
    params: z.object({ id: z.number() }),
    response: z.object({
      id: z.number(),
      name: z.string(),
      email: z.string(),
      createdAt: z.string()
    })
  },
  '/users/create': {
    // POST /users/create - Create new user
    body: z.object({
      name: z.string().min(3),
      email: z.string().email(),
      password: z.string().min(8)
    }),
    response: z.object({
      id: z.number(),
      name: z.string(),
      email: z.string()
    })
  },
  '/users/:id/update': {
    // PATCH /users/:id/update - Update user
    params: z.object({ id: z.number() }),
    body: z.object({
      name: z.string().min(3).optional(),
      email: z.string().email().optional()
    }),
    response: z.object({
      id: z.number(),
      name: z.string(),
      email: z.string()
    })
  }
} as const;

// Create client with shared configuration
class APIClient {
  private fetch;

  constructor(baseUrl: string, apiKey: string) {
    // Use shared headers for authentication
    this.fetch = createFetch(
      api,
      baseUrl,
      {
        headers: {
          'Authorization': `Bearer ${apiKey}`
        }
      }
    );
  }

  async getUsers(page = 1, limit = 10) {
    // No need to pass auth header - it's shared
    return this.fetch('/users', { query: { page, limit } });
  }

  async getUser(id: number) {
    return this.fetch('/users/:id', { params: { id } });
  }

  async createUser(data: { name: string; email: string; password: string }) {
    return this.fetch('/users/create', { body: data });
  }

  async updateUser(id: number, data: { name?: string; email?: string }) {
    return this.fetch('/users/:id/update', { params: { id }, body: data });
  }
}

// Usage
const client = new APIClient('https://api.example.com', 'your-api-key');
const users = await client.getUsers(1, 20);
const user = await client.getUser(123);
```

---

## Standard Schema Support

what-the-fetch works with any schema library that implements [Standard Schema](https://standardschema.dev/):

### Zod

```typescript
import { z } from 'zod';

const api = {
  '/users/:id': {
    params: z.object({ id: z.number() }),
    response: z.object({ id: z.number(), name: z.string() })
  }
};
```

### Valibot

```typescript
import * as v from 'valibot';

const api = {
  '/users/:id': {
    params: v.object({ id: v.number() }),
    response: v.object({ id: v.number(), name: v.string() })
  }
};
```

### ArkType

```typescript
import { type } from 'arktype';

const api = {
  '/users/:id': {
    params: type({ id: 'number' }),
    response: type({ id: 'number', name: 'string' })
  }
};
```

---

## Error Handling

### HTTP Errors

what-the-fetch automatically throws an error for non-2xx status codes:

```typescript
try {
  const user = await apiFetch('/users/:id', { params: { id: 999 } });
} catch (error) {
  console.error('HTTP error:', error.message);
  // → "HTTP error! status: 404"
}
```

### Validation Errors

Schema validation errors are thrown when response doesn't match the schema:

```typescript
const api = {
  '/users/:id': {
    params: z.object({ id: z.number() }),
    response: z.object({
      id: z.number(),
      name: z.string() // Required field
    })
  }
};

try {
  // If server returns { id: 123 } without name field
  const user = await apiFetch('/users/:id', { params: { id: 123 } });
} catch (error) {
  console.error('Validation error:', error);
  // Schema validation failed
}
```

---

## TypeScript Features

### Full Type Inference

```typescript
const api = {
  '/users/:id': {
    params: z.object({ id: z.number() }),
    response: z.object({ id: z.number(), name: z.string() })
  }
} as const;

const apiFetch = createFetch(api, 'https://api.example.com');

// TypeScript infers everything:
const user = await apiFetch('/users/:id', {
  params: { id: 123 } // TypeScript knows params is required and id must be number
});
// TypeScript knows user is { id: number; name: string }
```

### Autocomplete Support

When using what-the-fetch in TypeScript, you get full autocomplete for:

- Available API paths
- Required options (params, query, body)
- Field names and types
- Response properties

### Compile-Time Validation

```typescript
// ✅ This compiles
await apiFetch('/users/:id', { params: { id: 123 } });

// ❌ TypeScript error: Property 'params' is missing
await apiFetch('/users/:id', {});

// ❌ TypeScript error: Type 'string' is not assignable to type 'number'
await apiFetch('/users/:id', { params: { id: '123' } });

// ❌ TypeScript error: '/invalid' is not assignable to type '/users/:id' | ...
await apiFetch('/invalid', {});
```

---

## Performance Considerations

what-the-fetch is designed to be performant:

- **Small bundle size**: Minimal dependencies (only fast-url for URL building and [Standard Schema](https://standardschema.dev) spec)
- **Efficient URL building**: Leverages fast-url library's optimized implementation
- **Type-level computation**: Most type checking happens at compile time
- **Runtime validation**: Only validates responses, not TypeScript types

### Bundle Size

what-the-fetch adds minimal overhead to your bundle:

- Core library: ~2KB minified + gzipped
- Plus your choice of schema library (Zod, Valibot, etc.)

---

## Browser and Runtime Compatibility

what-the-fetch works in all modern JavaScript environments:

- ✅ Node.js 18+
- ✅ Bun
- ✅ Deno
- ✅ Modern browsers (Chrome, Firefox, Safari, Edge)
- ✅ React Native
- ✅ Electron

**Requirements:**

- `fetch` API support (built-in in modern environments)
- [Standard Schema](https://standardschema.dev)-compatible validation library
