Skip to content

loresclement/quick-fetch

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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-fetch

Runtime 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: "ada@example.com"
	} 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# quick-fetch

About

A modern, fetch-based HTTP client: zero-dependency, typed, minimal, and secure by default.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors