# Getting Started

> Quick start guide to install and use what-the-fetch in your JavaScript or TypeScript project

## Installation

Install what-the-fetch using your preferred package manager:

<Tabs>
  <Tab title="bun">
    ```bash
    bun add what-the-fetch
    ```
  </Tab>
  <Tab title="pnpm">
    ```bash
    pnpm add what-the-fetch
    ```
  </Tab>
  <Tab title="yarn">
    ```bash
    yarn add what-the-fetch
    ```
  </Tab>
  <Tab title="npm">
    ```bash
    npm install what-the-fetch
    ```
  </Tab>
  <Tab title="jsr">
    ```bash
    deno add jsr:@hckhanh/what-the-fetch
    ```
  </Tab>
</Tabs>

You'll also need a schema validation library that implements [Standard Schema](https://standardschema.dev):

<Tabs>
  <Tab title="Zod">
    ```bash
    npm install zod
    ```
  </Tab>
  <Tab title="Valibot">
    ```bash
    npm install valibot
    ```
  </Tab>
  <Tab title="ArkType">
    ```bash
    npm install arktype
    ```
  </Tab>
</Tabs>

<Note>
**TypeScript Support**

what-the-fetch is written in TypeScript and includes type definitions out of the
  box. No need to install separate `@types` packages!
</Note>

## Basic Usage

### Importing

```typescript
import { createFetch } from 'what-the-fetch';
import { z } from 'zod'; // or your preferred schema library
```

### Quick Examples

Here are some common use cases to get you started:

#### Simple GET Request

Define a schema and make a type-safe request:

```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(),
      email: z.string()
    })
  }
};

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

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

#### With Query Parameters

Add query parameters to your request:

```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: 20, sort: 'name' }
});
// users is typed as Array<{ id: number; name: string }>
```

#### POST Request with Body

Send data in the request 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'
  }
});
// newUser is typed as { id: number; name: string; email: string }
```

#### Using HTTP Methods

what-the-fetch automatically infers HTTP methods, but you can also specify them explicitly using the `@method` prefix:

```typescript
const api = {
  // Automatic inference - these are equivalent
  '/users/:id': {  // Uses GET (no body)
    params: z.object({ id: z.number() }),
    response: z.object({ id: z.number(), name: z.string() })
  },
  '@get/users/:id': {  // Explicit GET - same as above
    params: z.object({ id: z.number() }),
    response: z.object({ id: z.number(), name: z.string() })
  },
  
  // POST is inferred when body is present
  '/users': {  // Uses POST (has body)
    body: z.object({ name: z.string(), email: z.string().email() }),
    response: z.object({ id: z.number(), name: z.string() })
  },
  
  // Explicit methods for other HTTP verbs
  '@put/users/:id': {
    params: z.object({ id: z.number() }),
    body: z.object({ name: z.string() }),
    response: z.object({ id: z.number(), name: z.string() })
  },
  '@patch/users/:id': {
    params: z.object({ id: z.number() }),
    body: z.object({ name: z.string().optional() }),
    response: z.object({ id: z.number(), name: z.string() })
  },
  '@delete/users/:id': {
    params: z.object({ id: z.number() }),
    response: z.object({ success: z.boolean() })
  }
};

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

// These two 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 created = await apiFetch('/users', { body: { name: 'John', email: 'john@example.com' } });

// Explicit methods
await apiFetch('@put/users/:id', { params: { id: 123 }, body: { name: 'Jane' } });
await apiFetch('@patch/users/:id', { params: { id: 123 }, body: { name: 'Jane Doe' } });
await apiFetch('@delete/users/:id', { params: { id: 123 } });
```

<Note>
**Method Inference**

The `@method` prefix is **optional**. Without it, what-the-fetch automatically uses `POST` for requests with a body and `GET` for requests without a body. Both `/users/:id` and `@get/users/:id` are equivalent and result in the same GET request.
</Note>

## Step-by-Step Tutorial

Let's build a complete type-safe API client using what-the-fetch:

### Define Your API Schema
    
    Start by defining schemas for all your API endpoints:
    
    ```typescript
    import { z } from 'zod';
    
    const api = {
      '/posts': {
        // GET /posts - List posts with filters
        query: z.object({
          page: z.number().optional(),
          limit: z.number().optional(),
          tag: z.string().optional()
        }),
        response: z.array(z.object({
          id: z.number(),
          title: z.string(),
          excerpt: z.string()
        }))
      },
      '/posts/:id': {
        // GET /posts/:id - Get post by ID
        params: z.object({ id: z.number() }),
        response: z.object({
          id: z.number(),
          title: z.string(),
          content: z.string(),
          author: z.string()
        })
      },
      '/posts/create': {
        // POST /posts/create - Create new post
        body: z.object({
          title: z.string(),
          content: z.string()
        }),
        response: z.object({
          id: z.number(),
          title: z.string(),
          content: z.string()
        })
      }
    } as const;
    ```
  
### Create the Fetch Function
    
    Use createFetch to build your typed API client:
    
    ```typescript
    import { createFetch } from 'what-the-fetch';
    
    const apiFetch = createFetch(api, 'https://api.blog.com');
    ```
    
    You can also provide shared options that apply to all requests:
    
    ```typescript
    const apiFetch = createFetch(
      api,
      'https://api.blog.com',
      {
        headers: {
          'Authorization': 'Bearer your-api-key'
        }
      }
    );
    ```
  
### Make Type-Safe Requests
    
    Now you can make fully typed requests with autocomplete:
    
    ```typescript
    // Fetch posts with query parameters
    const posts = await apiFetch('/posts', {
      query: { page: 2, limit: 20, tag: 'javascript' }
    });
    // posts is typed as Array<{ id: number; title: string; excerpt: string }>
    
    // Fetch a single post
    const post = await apiFetch('/posts/:id', {
      params: { id: 42 }
    });
    // post is typed as { id: number; title: string; content: string; author: string }
    ```
  
### Add Custom Headers (Optional)
    
    You can pass custom headers using the third parameter:
    
    ```typescript
    const post = await apiFetch(
      '/posts/:id',
      { params: { id: 42 } },
      {
        headers: {
          'Authorization': 'Bearer your-api-key',
          'X-Custom-Header': 'value'
        }
      }
    );
    ```
  
## Common Patterns

### Using Different Schema Libraries

what-the-fetch works with any [Standard Schema](https://standardschema.dev) implementation:

<Tabs>
  <Tab title="Zod">
    ```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() })
      }
    };
    
    const apiFetch = createFetch(api, 'https://api.example.com');
    ```
  </Tab>
  <Tab title="Valibot">
    ```typescript
    import { createFetch } from 'what-the-fetch';
    import * as v from 'valibot';
    
    const api = {
      '/users/:id': {
        params: v.object({ id: v.number() }),
        response: v.object({ id: v.number(), name: v.string() })
      }
    };
    
    const apiFetch = createFetch(api, 'https://api.example.com');
    ```
  </Tab>
  <Tab title="ArkType">
    ```typescript
    import { createFetch } from 'what-the-fetch';
    import { type } from 'arktype';
    
    const api = {
      '/users/:id': {
        params: type({ id: 'number' }),
        response: type({ id: 'number', name: 'string' })
      }
    };
    
    const apiFetch = createFetch(api, 'https://api.example.com');
    ```
  </Tab>
</Tabs>

### Combining Path and Query Parameters

Mix path parameters with 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 }
});
// URL: https://api.example.com/users/123/posts?limit=10&published=true
```

### Custom Request Configuration

Pass additional fetch options as the third parameter:

```typescript
const user = await apiFetch(
  '/users/:id',
  { params: { id: 123 } },
  {
    headers: {
      'Authorization': 'Bearer token',
      'X-API-Key': 'your-key'
    },
    cache: 'no-cache',
    signal: abortController.signal
  }
);
```

### Reusing the Same Fetch Function

Create one fetch function and reuse it throughout your application:

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

// users.ts
import { apiFetch } from './api';

const user = await apiFetch('/users/:id', { params: { id: 123 } });

// posts.ts
import { apiFetch } from './api';

const posts = await apiFetch('/posts', { query: { limit: 10 } });
```

### Shared Configuration

Use shared configuration to set common options for all requests:

```typescript
// Create fetch with shared headers and options
const apiFetch = createFetch(
  api,
  'https://api.example.com',
  {
    headers: {
      'Authorization': 'Bearer my-token',
      'X-API-Version': '2.0'
    },
    cache: 'no-cache'
  }
);

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

// You can override or add headers per request
const post = await apiFetch(
  '/posts/:id',
  { params: { id: 42 } },
  {
    headers: {
      'X-Custom-Header': 'value' // Merged with shared headers
    }
  }
);
```

## Schema Validation Benefits

what-the-fetch validates both requests and responses automatically:

<Warning>
**Parameterized Paths Require Schema**

If your path contains parameters (e.g., `/users/:id`), you **must** define a `params` schema. The library will throw an error at runtime if you attempt to use a parameterized path without a params schema.
</Warning>

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

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

// ✅ Valid request
const user = await apiFetch('/users', {
  body: {
    name: 'John Doe',
    email: 'john@example.com'
  }
});

// ❌ This would fail at runtime with validation error
try {
  await apiFetch('/users', {
    body: {
      name: 'Jo', // Too short
      email: 'invalid-email' // Invalid format
    }
  });
} catch (error) {
  console.error('Validation failed:', error);
}
```

## Error Handling

what-the-fetch provides clear error messages for both validation and HTTP errors:

```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');

try {
  const user = await apiFetch('/users/:id', { params: { id: 123 } });
} catch (error) {
  // Could be:
  // 1. HTTP error (status !== 2xx)
  // 2. Schema validation error
  // 3. Network error
  console.error('Request failed:', error);
}
```

### Handling HTTP Errors

what-the-fetch automatically throws on non-2xx status codes:

```typescript
try {
  const user = await apiFetch('/users/:id', { params: { id: 999 } });
} catch (error) {
  if (error.message.includes('HTTP error')) {
    console.error('Request failed with status:', error);
    // Handle 404, 500, etc.
  }
}
```

### Handling Validation Errors

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

```typescript
try {
  const user = await apiFetch('/users/:id', { params: { id: 123 } });
} catch (error) {
  // Validation error if response doesn't match schema
  console.error('Invalid response:', error);
}
```

<Check>
**Type Safety**

With TypeScript, most parameter errors are caught at compile time, preventing
  runtime errors before they happen!
</Check>
