@clement_lores/quick-fetch
v1.0.0
Published
A modern, fetch-based HTTP client: zero-dependency, typed, minimal, and secure by default.
Downloads
91
Maintainers
Readme
quick-fetch
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.
Why quick-fetch
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.
Highlights
- 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
Install
npm:
npm install @clement_lores/quick-fetchRuntime Support
quick-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.
Quick Start
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"
}
});API Overview
createClient(options?)
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"
});ClientOptions
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.
Client Methods
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?)RequestOptions
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.
REST Examples (TypeScript)
GET with Query Parameters
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.
POST JSON Body
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: "[email protected]"
} satisfies CreateUserPayload
});When json is provided:
- Request body is JSON.stringify(json).
- content-type: application/json is set automatically.
POST FormData (File Upload)
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.
Per-request Timeout Override
const slowReport = await api.get<{ status: string }>("/reports/daily", {
timeout: 15000
});Manual Cancellation
const controller = new AbortController();
const pending = api.get<{ ok: true }>("/long-task", {
signal: controller.signal
});
controller.abort();
await pending;Error Handling
quick-fetch throws explicit, typed errors for common API failure modes.
Error Types
HttpError
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)
TimeoutError
Thrown when request timeout is reached.
MalformedParamsError
Thrown when both json and formdata are provided in the same request.
Handling Errors Safely
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.
Response Parsing
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).
Logging Modes
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.
Header Strategy
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"
}
});Best Practices
- 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.
Comparison
quick-fetch vs native fetch
- 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 vs axios
- 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.).
Current Scope and Limitations
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
Stability and Versioning
- API stability target: stable
- Versioning strategy: semantic versioning (SemVer)
FAQ
Does quick-fetch replace fetch?
No. It builds on top of native fetch and keeps fetch semantics.
Can I use absolute URLs without baseUrl?
Yes. baseUrl is optional. However, using baseUrl is recommended for consistency and maintainability.
Does quick-fetch parse error responses?
Yes. Non-2xx responses throw HttpError with a parsed body field when possible.
What happens with HEAD requests?
A 204 response returns undefined. For non-JSON textual responses, text is returned.
Is this package browser-only?
No. It is runtime-agnostic as long as native fetch-compatible APIs are available.
License
MIT
Copyright (c) 2026 Clément LORES
Author
GitHub: https://github.com/loresclement
Repository
https://github.com/loresclement/quick-fetch
