Fetch Wrapper
A typed fetch wrapper that auto-serializes request bodies, checks response.ok, and parses the JSON response.
The snippet
type FetchOptions = Omit<RequestInit, 'body'> & {
body?: unknown
}
async function fetchJson<T>(url: string, options: FetchOptions = {}): Promise<T> {
const { body, headers, ...rest } = options
const res = await fetch(url, {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
...headers,
},
body: body !== undefined ? JSON.stringify(body) : undefined,
...rest,
})
if (!res.ok) {
throw new Error(`${res.status} ${res.statusText}`)
}
return res.json() as Promise<T>
}Types
FetchOptions extends RequestInit but swaps body from BodyInit to unknown so you can pass a plain object — the wrapper handles JSON.stringify for you. The generic T flows through to the return type so the caller gets typed data back.
type FetchOptions = Omit<RequestInit, 'body'> & {
body?: unknown // pass an object, not a string
}
async function fetchJson<T>(
url: string,
options?: FetchOptions,
): Promise<T> // caller gets T backUsage
interface User {
id: number
name: string
email: string
}
// GET
const users = await fetchJson<User[]>('/api/users')
// POST
const created = await fetchJson<User>('/api/users', {
method: 'POST',
body: { name: 'Ada', email: 'ada@example.com' },
})
// with error handling
try {
const user = await fetchJson<User>(`/api/users/${id}`)
console.log(user.name)
} catch (err) {
// "404 Not Found" or network error
console.error(err instanceof Error ? err.message : 'Unknown error')
}Notes
- Why throw on
!res.ok?fetchonly rejects on network failures — a 404 or 500 resolves normally. Checkingres.okcatches HTTP errors before you try to parse bad JSON. - Why
as Promise<T>instead ofawait res.json() as T? Both work. Returning the promise directly avoids an extra microtick and makes the cast more visible. - Adding auth? Slot an
Authorizationheader into the defaults or pass it per-call viaoptions.headers. - Non-JSON responses? This wrapper assumes JSON. For blobs, text, or streaming responses, use
fetchdirectly. - AbortController. Pass
{ signal: controller.signal }in options to make requests cancellable — useful inuseEffectcleanup.
Last updated on