Skip to Content
SnippetsFetch Wrapper

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 back

Usage

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? fetch only rejects on network failures — a 404 or 500 resolves normally. Checking res.ok catches HTTP errors before you try to parse bad JSON.
  • Why as Promise<T> instead of await res.json() as T? Both work. Returning the promise directly avoids an extra microtick and makes the cast more visible.
  • Adding auth? Slot an Authorization header into the defaults or pass it per-call via options.headers.
  • Non-JSON responses? This wrapper assumes JSON. For blobs, text, or streaming responses, use fetch directly.
  • AbortController. Pass { signal: controller.signal } in options to make requests cancellable — useful in useEffect cleanup.
Last updated on