@zap-studio/fetch
v2.1.1
Published
A type-safe, tree-shakeable fetch wrapper for HTTP requests with runtime schema validation.
Maintainers
Readme
@zap-studio/fetch
A small fetch wrapper with Standard Schema response validation.
Full documentation: zapstudio.dev/fetch
Motivation
Raw fetch does not throw on HTTP errors like 404 or 500 — you must check response.ok yourself. And it does not check that the JSON body matches what your code expects.
So most projects add the same pattern at every call site: check the status, parse the JSON, then validate it with schema.parse(await res.json()). It is easy to forget one of these steps somewhere, and that is how bad data or a network error goes unnoticed until it breaks something downstream.
@zap-studio/fetch makes this pattern the default, not something you write by hand. api.get(url, UserSchema) checks the response, validates the body, and throws a clear, typed error — FetchError for a bad HTTP status, ValidationError for a bad shape — so you always know what went wrong and where.
It uses Standard Schema, so you are not locked into one validation library: start with Zod, move to Valibot later, and the call sites do not change. And because it only wraps the global fetch function, it is not a Node-only HTTP client — it runs the same way on Bun, Deno, Cloudflare Workers, and in the browser.
Installation
npm install @zap-studio/fetchYou also need a schema library that implements Standard Schema, such as Zod, Valibot, or ArkType.
Features
- Raw fetch mode through
$fetch(input, options)— behaves like nativefetchand returns theResponse. - Validated fetch mode through
$fetch(input, schema, options)— parses and validates the JSON response. - HTTP method helpers through
api.get,api.post,api.put,api.patch, andapi.delete. - Configured clients through
createFetch(...)with sharedbaseURL, headers, query params, and error defaults. - JSON convenience through the
jsonoption, which serializes the request body and setsContent-Type. - Structured errors with
FetchErrorfor HTTP failures andValidationErrorfor schema failures. - Validator-agnostic — works with any library that implements Standard Schema.
- Optional logging through
createFetch({ logger })(@zap-studio/logger) — omit it and there's zero logging overhead. - Tree-shakeable — every export is a standalone function with no shared internal state; unused exports are dropped by any modern bundler.
Quick Start
import { ConsoleLogger } from "@zap-studio/logger";
import { createFetch } from "@zap-studio/fetch";
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.email(),
});
const logger = new ConsoleLogger({ minLevel: "debug" });
const { api } = createFetch({ logger });
const user = await api.get("https://api.example.com/users/1", UserSchema);
console.log(user.name); // typed as string, validated at runtimeRaw Fetch Mode
Behaves like native fetch and returns the Response.
import { $fetch } from "@zap-studio/fetch";
const response = await $fetch("/api/users/1");
const user = await response.json();Validated Fetch Mode
Parses and validates the JSON response.
import { $fetch } from "@zap-studio/fetch";
const user = await $fetch("/api/users/1", UserSchema);HTTP Method Helpers
api.get, api.post, api.put, api.patch, and api.delete.
import { api } from "@zap-studio/fetch";
const created = await api.post("/api/users", UserSchema, {
json: { name: "Ada", email: "[email protected]" },
});Configured Clients
Shared baseURL, headers, query params, and error defaults.
import { createFetch } from "@zap-studio/fetch";
const { api } = createFetch({
baseURL: "https://api.example.com",
headers: { Authorization: `Bearer ${token}` },
});
const user = await api.get("/users/1", UserSchema);JSON Convenience
Serializes the request body and sets Content-Type.
await api.post("/api/users", UserSchema, {
json: { name: "Ada" }, // mutually exclusive with `body`
});Structured Errors
FetchError for HTTP failures and ValidationError for schema failures.
import { FetchError } from "@zap-studio/fetch";
import { ValidationError } from "@zap-studio/validation";
try {
await api.get("/api/users/1", UserSchema);
} catch (error) {
if (error instanceof FetchError) console.error(error.status);
if (error instanceof ValidationError) console.error(error.issues);
}Validator-Agnostic
Works with any library that implements Standard Schema.
// UserSchema can come from Zod, Valibot, ArkType, or any Standard Schema-compatible library
import type { StandardSchemaV1 } from "@zap-studio/validation";Logging
Pass a logger?: Logger from @zap-studio/logger to createFetch(...) to observe requests, responses, and validation failures. Omit it and nothing is logged.
import { ConsoleLogger } from "@zap-studio/logger";
import { createFetch } from "@zap-studio/fetch";
const logger = new ConsoleLogger({ minLevel: "debug" });
const { api } = createFetch({ baseURL: "https://api.example.com", logger });Outgoing requests log at debug, response status logs at debug (2xx) or warn (non-2xx), and schema validation failures log at error.
OpenTelemetry
@opentelemetry/api is a required peer dependency. It's a tiny, side-effect-free package that's a no-op until an app registers a real SDK, so installing it costs nothing at runtime for consumers who never set one up.
Every request gets a CLIENT span (http.request.method, url.full, http.response.status_code), and the trace context is injected into the outgoing request's headers so the call continues the caller's distributed trace:
npm install @opentelemetry/apiimport { createFetch } from "@zap-studio/fetch";
const { api } = createFetch({ baseURL: "https://api.example.com" });
// If your app has registered an OpenTelemetry SDK, this call now produces a
// CLIENT span and injects `traceparent` into the outgoing request headers.
// If not, it's a no-op — no wiring required either way.
await api.get("/users/1", UserSchema);On failure — a non-2xx response or a thrown error — the span is marked ERROR; thrown errors are also recorded as span exceptions.
Using with @zap-studio/monads
This package has no dependency on @zap-studio/monads — nothing is added to your
bundle unless you install it yourself. If you want a Result instead of throw/catch,
wrap the call with @zap-studio/monads's fromPromise, mapping the rejection into
your error type:
import { fromPromise } from "@zap-studio/monads";
import { api } from "@zap-studio/fetch";
const result = fromPromise(
api.get("/users/1", UserSchema, { throwOnFetchError: true, throwOnValidationError: true }),
(error) => error,
);Runtime Support
| Runtime | Minimum version |
| ------------------ | ------------------------------------------------ |
| Node.js | 18.0.0 (ships native fetch) |
| Bun | 1.0.0 |
| Deno | 1.42 |
| Cloudflare Workers | Any current release |
| Browsers | Latest evergreen (Chrome, Edge, Firefox, Safari) |
The package relies on the global fetch API and ships standard ESM only. Deno 1.42 is the first release that can install packages from JSR (deno add jsr:@zap-studio/fetch).
License
MIT
