Skip to content

Commit 5fb4736

Browse files
authored
Document HTTP methods feature with emphasis on automatic inference (#46)
1 parent 4793cff commit 5fb4736

File tree

4 files changed

+352
-3
lines changed

4 files changed

+352
-3
lines changed

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,58 @@ const newUser = await apiFetch('/users', {
105105
});
106106
```
107107

108+
### With HTTP Methods
109+
110+
what-the-fetch automatically infers HTTP methods: requests with a `body` use `POST`, and requests without a `body` use `GET`. You can also explicitly specify methods using the `@method` prefix for clarity or when you need other HTTP methods:
111+
112+
```typescript
113+
const api = {
114+
// Automatic method inference (these are equivalent)
115+
'/users/:id': { // Uses GET (no body)
116+
params: z.object({ id: z.number() }),
117+
response: z.object({ id: z.number(), name: z.string() }),
118+
},
119+
'@get/users/:id': { // Explicitly GET - same as above
120+
params: z.object({ id: z.number() }),
121+
response: z.object({ id: z.number(), name: z.string() }),
122+
},
123+
124+
// POST is inferred when body is present
125+
'/users': { // Uses POST (has body)
126+
body: z.object({ name: z.string(), email: z.string().email() }),
127+
response: z.object({ id: z.number(), name: z.string() }),
128+
},
129+
130+
// Explicit methods for PUT, PATCH, DELETE
131+
'@put/users/:id': {
132+
params: z.object({ id: z.number() }),
133+
body: z.object({ name: z.string(), email: z.string().email() }),
134+
response: z.object({ id: z.number(), name: z.string() }),
135+
},
136+
'@delete/users/:id': {
137+
params: z.object({ id: z.number() }),
138+
response: z.object({ success: z.boolean() }),
139+
},
140+
} as const;
141+
142+
const apiFetch = createFetch(api, 'https://api.example.com');
143+
144+
// These are equivalent - both use GET
145+
const user1 = await apiFetch('/users/:id', { params: { id: 123 } });
146+
const user2 = await apiFetch('@get/users/:id', { params: { id: 123 } });
147+
148+
// POST (inferred from body)
149+
const newUser = await apiFetch('/users', {
150+
body: { name: 'John Doe', email: '[email protected]' },
151+
});
152+
153+
// Explicit methods for clarity
154+
await apiFetch('@put/users/:id', {
155+
params: { id: 123 },
156+
body: { name: 'Jane Doe', email: '[email protected]' },
157+
});
158+
await apiFetch('@delete/users/:id', { params: { id: 123 } });
159+
108160
### With Shared Headers
109161

110162
You can provide shared headers when creating the fetch function:

docs/content/docs/api-reference.mdx

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,236 @@ const user = await apiFetch(
402402

403403
---
404404

405+
## HTTP Methods
406+
407+
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.
408+
409+
### Automatic Method Inference
410+
411+
By default, what-the-fetch infers the HTTP method:
412+
- Requests **with a body** → `POST`
413+
- Requests **without a body** → `GET`
414+
415+
```typescript
416+
const api = {
417+
'/users/:id': { /* Uses GET (no body) */ },
418+
'/users': { body: z.object({...}), /* Uses POST (has body) */ }
419+
};
420+
```
421+
422+
### Method Prefix Syntax (Optional)
423+
424+
For explicit control or to use other HTTP methods, add the `@method` prefix:
425+
426+
```typescript
427+
const api = {
428+
'@get/resource': { /* Explicit GET - same as /resource */ },
429+
'/resource': { /* Implicit GET */ },
430+
'@post/resource': { /* Explicit POST */ },
431+
'@put/resource/:id': { /* PUT - prefix required */ },
432+
'@patch/resource/:id': { /* PATCH - prefix required */ },
433+
'@delete/resource/:id': { /* DELETE - prefix required */ }
434+
};
435+
```
436+
437+
**Note:** `/users/:id` and `@get/users/:id` are completely equivalent and both result in a GET request.
438+
439+
### Supported HTTP Methods
440+
441+
All standard HTTP methods are supported:
442+
443+
- `@get` - GET requests (retrieve data)
444+
- `@post` - POST requests (create new resources)
445+
- `@put` - PUT requests (replace existing resources)
446+
- `@patch` - PATCH requests (partially update resources)
447+
- `@delete` - DELETE requests (remove resources)
448+
- `@head` - HEAD requests (retrieve headers only)
449+
- `@options` - OPTIONS requests (check available methods)
450+
451+
### Complete Example
452+
453+
```typescript
454+
import { createFetch } from 'what-the-fetch';
455+
import { z } from 'zod';
456+
457+
const api = {
458+
// GET - Retrieve a user
459+
'@get/users/:id': {
460+
params: z.object({ id: z.number() }),
461+
response: z.object({
462+
id: z.number(),
463+
name: z.string(),
464+
email: z.string()
465+
})
466+
},
467+
468+
// POST - Create a new user
469+
'@post/users': {
470+
body: z.object({
471+
name: z.string(),
472+
email: z.string().email()
473+
}),
474+
response: z.object({
475+
id: z.number(),
476+
name: z.string(),
477+
email: z.string()
478+
})
479+
},
480+
481+
// PUT - Replace entire user
482+
'@put/users/:id': {
483+
params: z.object({ id: z.number() }),
484+
body: z.object({
485+
name: z.string(),
486+
email: z.string().email()
487+
}),
488+
response: z.object({
489+
id: z.number(),
490+
name: z.string(),
491+
email: z.string()
492+
})
493+
},
494+
495+
// PATCH - Partially update user
496+
'@patch/users/:id': {
497+
params: z.object({ id: z.number() }),
498+
body: z.object({
499+
name: z.string().optional(),
500+
email: z.string().email().optional()
501+
}),
502+
response: z.object({
503+
id: z.number(),
504+
name: z.string(),
505+
email: z.string()
506+
})
507+
},
508+
509+
// DELETE - Remove a user
510+
'@delete/users/:id': {
511+
params: z.object({ id: z.number() }),
512+
response: z.object({ success: z.boolean() })
513+
}
514+
} as const;
515+
516+
const apiFetch = createFetch(api, 'https://api.example.com');
517+
518+
// Make requests with explicit methods
519+
const user = await apiFetch('@get/users/:id', { params: { id: 123 } });
520+
// GET https://api.example.com/users/123
521+
522+
const newUser = await apiFetch('@post/users', {
523+
body: { name: 'John Doe', email: '[email protected]' }
524+
});
525+
// POST https://api.example.com/users
526+
527+
await apiFetch('@put/users/:id', {
528+
params: { id: 123 },
529+
body: { name: 'Jane Doe', email: '[email protected]' }
530+
});
531+
// PUT https://api.example.com/users/123
532+
533+
await apiFetch('@patch/users/:id', {
534+
params: { id: 123 },
535+
body: { name: 'Jane Smith' }
536+
});
537+
// PATCH https://api.example.com/users/123
538+
539+
await apiFetch('@delete/users/:id', { params: { id: 123 } });
540+
// DELETE https://api.example.com/users/123
541+
```
542+
543+
### Equivalence Examples
544+
545+
The following path definitions are equivalent:
546+
547+
```typescript
548+
const api = {
549+
// These two are identical - both use GET
550+
'/users/:id': {
551+
params: z.object({ id: z.number() }),
552+
response: z.object({ id: z.number(), name: z.string() })
553+
},
554+
'@get/users/:id': { // Same as above - @get is optional
555+
params: z.object({ id: z.number() }),
556+
response: z.object({ id: z.number(), name: z.string() })
557+
},
558+
559+
// POST is inferred from body
560+
'/users': {
561+
body: z.object({ name: z.string(), email: z.string() }),
562+
response: z.object({ id: z.number(), name: z.string() })
563+
}
564+
};
565+
566+
const apiFetch = createFetch(api, 'https://api.example.com');
567+
568+
// Both calls are equivalent - both use GET
569+
const user1 = await apiFetch('/users/:id', { params: { id: 123 } });
570+
const user2 = await apiFetch('@get/users/:id', { params: { id: 123 } });
571+
572+
// POST (inferred from body)
573+
const newUser = await apiFetch('/users', {
574+
body: { name: 'John', email: '[email protected]' }
575+
});
576+
```
577+
578+
**Method inference rules:**
579+
- Request has a `body``POST`
580+
- Request has no `body``GET`
581+
- Method prefix specifiedUses that method
582+
583+
### Method Prefix with Path Parameters
584+
585+
The method prefix works seamlessly with path parameters:
586+
587+
```typescript
588+
const api = {
589+
'@put/users/:userId/posts/:postId': {
590+
params: z.object({
591+
userId: z.number(),
592+
postId: z.number()
593+
}),
594+
body: z.object({ title: z.string(), content: z.string() }),
595+
response: z.object({ success: z.boolean() })
596+
}
597+
};
598+
599+
const apiFetch = createFetch(api, 'https://api.example.com');
600+
601+
await apiFetch('@put/users/:userId/posts/:postId', {
602+
params: { userId: 123, postId: 456 },
603+
body: { title: 'Updated', content: 'New content' }
604+
});
605+
// PUT https://api.example.com/users/123/posts/456
606+
```
607+
608+
### Case Insensitive
609+
610+
Method prefixes are case-insensitive and converted to uppercase:
611+
612+
```typescript
613+
const api = {
614+
'@GET/users': { /* ... */ },
615+
'@get/posts': { /* ... */ },
616+
'@GeT/comments': { /* ... */ }
617+
};
618+
619+
// All are treated as GET
620+
```
621+
622+
### Best Practices
623+
624+
1. **Be explicit with methods**: Use method prefixes for clarity, especially for PUT, PATCH, and DELETE operations
625+
2. **RESTful design**: Follow REST conventions:
626+
- `@get` for retrieval
627+
- `@post` for creation
628+
- `@put` for full replacement
629+
- `@patch` for partial updates
630+
- `@delete` for removal
631+
3. **Consistency**: Choose either to always use method prefixes or rely on automatic detection - be consistent across your API schema
632+
633+
---
634+
405635
## Advanced Usage
406636

407637
### Shared Request Configuration

docs/content/docs/getting-started.mdx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,64 @@ const newUser = await apiFetch('/users', {
162162
// newUser is typed as { id: number; name: string; email: string }
163163
```
164164

165+
#### Using HTTP Methods
166+
167+
what-the-fetch automatically infers HTTP methods, but you can also specify them explicitly using the `@method` prefix:
168+
169+
```typescript
170+
const api = {
171+
// Automatic inference - these are equivalent
172+
'/users/:id': { // Uses GET (no body)
173+
params: z.object({ id: z.number() }),
174+
response: z.object({ id: z.number(), name: z.string() })
175+
},
176+
'@get/users/:id': { // Explicit GET - same as above
177+
params: z.object({ id: z.number() }),
178+
response: z.object({ id: z.number(), name: z.string() })
179+
},
180+
181+
// POST is inferred when body is present
182+
'/users': { // Uses POST (has body)
183+
body: z.object({ name: z.string(), email: z.string().email() }),
184+
response: z.object({ id: z.number(), name: z.string() })
185+
},
186+
187+
// Explicit methods for other HTTP verbs
188+
'@put/users/:id': {
189+
params: z.object({ id: z.number() }),
190+
body: z.object({ name: z.string() }),
191+
response: z.object({ id: z.number(), name: z.string() })
192+
},
193+
'@patch/users/:id': {
194+
params: z.object({ id: z.number() }),
195+
body: z.object({ name: z.string().optional() }),
196+
response: z.object({ id: z.number(), name: z.string() })
197+
},
198+
'@delete/users/:id': {
199+
params: z.object({ id: z.number() }),
200+
response: z.object({ success: z.boolean() })
201+
}
202+
};
203+
204+
const apiFetch = createFetch(api, 'https://api.example.com');
205+
206+
// These two are equivalent - both use GET
207+
const user1 = await apiFetch('/users/:id', { params: { id: 123 } });
208+
const user2 = await apiFetch('@get/users/:id', { params: { id: 123 } });
209+
210+
// POST inferred from body
211+
const created = await apiFetch('/users', { body: { name: 'John', email: '[email protected]' } });
212+
213+
// Explicit methods
214+
await apiFetch('@put/users/:id', { params: { id: 123 }, body: { name: 'Jane' } });
215+
await apiFetch('@patch/users/:id', { params: { id: 123 }, body: { name: 'Jane Doe' } });
216+
await apiFetch('@delete/users/:id', { params: { id: 123 } });
217+
```
218+
219+
<Callout title="Method Inference" type="info">
220+
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.
221+
</Callout>
222+
165223
## Step-by-Step Tutorial
166224

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

0 commit comments

Comments
 (0)