quick-fetch is a zero-dependency, TypeScript-first HTTP client built on native fetch.
It keeps everything you like about fetch, while removing repetitive boilerplate for day-to-day REST API calls.
Native fetch is powerful, but most API layers still repeat the same plumbing:
- Building URLs and query strings
- Handling JSON request/response bodies
- Managing per-request timeouts
- Throwing structured errors for non-2xx responses
- Repeating headers and base URL setup
quick-fetch gives you a clean, minimal abstraction without introducing heavy dependencies or a custom transport layer.
- ZERO dependencies
- TypeScript-first API
- ESM + CommonJS support
- Built on native fetch, AbortController, URL, and FormData
- Automatic response parsing (JSON, text, and 204 handling)
- Structured HttpError for non-2xx responses
- Built-in timeout support with TimeoutError
- Safe guardrail for invalid body options (json + formdata)
- Base URL, global headers, and per-request override support
- Logging modes: basic, detailed, full
npm:
npm install @clement_lores/quick-fetchquick-fetch works in modern runtimes that provide native web APIs used by fetch-based clients:
- Browsers (modern)
- Node.js 18+ (recommended)
- Deno
- Bun
- Edge runtimes with fetch-compatible APIs
No additional dependency is required by quick-fetch itself.
import { createClient } from "quick-fetch";
type User = {
id: string;
name: string;
email: string;
};
const api = createClient({
baseUrl: "https://api.example.com",
headers: {
Authorization: "Bearer <token>"
},
timeout: 8000,
logging: "basic"
});
const users = await api.get<User[]>("/users", {
query: {
page: 1,
search: "john"
}
});Creates a reusable HTTP client instance.
import { createClient } from "quick-fetch";
const api = createClient({
baseUrl: "https://api.example.com",
headers: { "x-app": "my-service" },
timeout: 5000,
logging: "detailed"
});type ClientOptions = {
baseUrl?: string;
headers?: HeadersInit;
timeout?: number;
logging?: "basic" | "detailed" | "full";
};Option details:
- baseUrl: Base URL used with relative request paths. Strongly recommended in production.
- headers: Default headers applied to every request.
- timeout: Default timeout in milliseconds for all requests.
- logging:
- basic: Logs request method/path and body type.
- detailed: Logs request details and body payload overview.
- full: Adds response status and duration.
All methods are generic and return Promise.
api.get<TResponse>(path, options?)
api.post<TResponse>(path, options?)
api.put<TResponse>(path, options?)
api.delete<TResponse>(path, options?)
api.patch<TResponse>(path, options?)
api.head<TResponse>(path, options?)
api.options<TResponse>(path, options?)type RequestOptions = {
headers?: HeadersInit;
query?: Record<string, QueryParamValue | QueryParamValue[]>;
json?: unknown;
formdata?: FormData;
timeout?: number;
signal?: AbortSignal;
};
type QueryParamValue = string | number | boolean | null | undefined;Rule:
- Use either json or formdata, never both in one request.
If both are provided, quick-fetch throws MalformedParamsError.
type ProductsResponse = {
items: Array<{ id: string; name: string }>;
total: number;
};
const products = await api.get<ProductsResponse>("/products", {
query: {
page: 1,
search: "helmet",
available: true,
brand: ["Honda", "Yamaha"],
unused: undefined,
nullable: null
}
});Behavior:
- Arrays are expanded as repeated query keys.
- null and undefined values are skipped.
type CreateUserPayload = {
name: string;
email: string;
};
type CreateUserResponse = {
id: string;
name: string;
email: string;
};
const created = await api.post<CreateUserResponse>("/users", {
json: {
name: "Ada Lovelace",
email: "ada@example.com"
} satisfies CreateUserPayload
});When json is provided:
- Request body is JSON.stringify(json).
- content-type: application/json is set automatically.
const form = new FormData();
form.append("avatar", fileInput.files?.[0] as File);
form.append("displayName", "Ada");
const result = await api.post<{ ok: boolean }>("/profile/avatar", {
formdata: form
});When formdata is provided:
- Body is sent as multipart/form-data.
- content-type boundary is handled by runtime automatically.
const slowReport = await api.get<{ status: string }>("/reports/daily", {
timeout: 15000
});const controller = new AbortController();
const pending = api.get<{ ok: true }>("/long-task", {
signal: controller.signal
});
controller.abort();
await pending;quick-fetch throws explicit, typed errors for common API failure modes.
Thrown for all non-2xx responses.
Properties:
- name: HttpError
- status: number
- statusText: string
- url: string
- method: HTTP method
- body: Parsed response body (JSON or text)
Thrown when request timeout is reached.
Thrown when both json and formdata are provided in the same request.
import { HttpError, TimeoutError, MalformedParamsError } from "quick-fetch";
try {
const data = await api.get<{ id: string }>("/users/unknown");
console.log(data);
} catch (error) {
if (error instanceof HttpError) {
console.error("HTTP failure", {
status: error.status,
statusText: error.statusText,
method: error.method,
url: error.url,
body: error.body
});
} else if (error instanceof TimeoutError) {
console.error("Request timeout", error.message);
} else if (error instanceof MalformedParamsError) {
console.error("Request options are invalid", error.message);
} else {
console.error("Unexpected error", error);
}
}Important behavior note:
- Timeout-triggered aborts become TimeoutError.
- Manual AbortSignal cancellation uses the native AbortError from fetch/runtime.
quick-fetch parses response bodies automatically:
- Status 204: returns undefined
- content-type includes application/json: returns parsed JSON
- Otherwise: returns text
This applies to both successful and error responses (HttpError.body is parsed too).
Set logging in createClient:
const api = createClient({
baseUrl: "https://api.example.com",
logging: "full"
});Modes:
- basic: Logs request method/path and body type indicator.
- detailed: Adds request payload details for easier debugging.
- full: Includes detailed request data plus colored response status and duration.
quick-fetch merges headers like this:
- Start from client-level headers
- Apply request-level headers on top
- Request-level values override duplicates
Example:
const api = createClient({
baseUrl: "https://api.example.com",
headers: {
Authorization: "Bearer token-1",
"x-app": "dashboard"
}
});
await api.get("/users", {
headers: {
Authorization: "Bearer token-2"
}
});- Always provide baseUrl for production clients.
- Type every response with method generics.
- Centralize HTTP error handling around HttpError.
- Use client-level headers for stable defaults.
- Use request-level timeout for known slow endpoints.
- Keep logging to basic or detailed in production unless actively debugging.
- quick-fetch adds typed method wrappers for REST calls.
- quick-fetch gives structured HttpError for non-2xx responses.
- quick-fetch auto-parses JSON/text and handles 204 gracefully.
- quick-fetch supports built-in timeout handling and query object serialization.
- native fetch remains the underlying transport layer.
- quick-fetch has zero dependencies.
- quick-fetch stays close to native fetch standards.
- quick-fetch is intentionally minimal and focused on REST request ergonomics.
- axios provides a broader feature set (interceptors, richer transforms, etc.).
quick-fetch intentionally stays small and focused.
Not included by design in current stable scope:
- Automatic retries
- Interceptors/middleware pipeline
- Built-in request/response schema validation
- Built-in caching strategy
- GraphQL-specific helpers
- API stability target: stable
- Versioning strategy: semantic versioning (SemVer)
No. It builds on top of native fetch and keeps fetch semantics.
Yes. baseUrl is optional. However, using baseUrl is recommended for consistency and maintainability.
Yes. Non-2xx responses throw HttpError with a parsed body field when possible.
A 204 response returns undefined. For non-JSON textual responses, text is returned.
No. It is runtime-agnostic as long as native fetch-compatible APIs are available.
MIT
Copyright (c) 2026 Clément LORES
GitHub: https://github.com/loresclement
https://github.com/loresclement/quick-fetch# quick-fetch