@omnific/request
v0.2.2
Published
Web request utilities for fetch and XHR
Downloads
271
Readme
@omnific/request
Lightweight browser-side request utilities built on top of fetch, with an XMLHttpRequest upload helper when you need progress events.
Features
fetchas the default transport for regular HTTP requestsXMLHttpRequestupload helper withonUploadProgress- Callable request instance plus
get,post,put,patch,delete,head, andoptionshelpers createRequest({ baseURL })for building preconfigured clients- Automatic query string serialization for
params - Automatic request body serialization for plain objects and
URLSearchParams - Shared response shape across
fetchand XHR - Timeout and abort support, plus
isCancelfor cancellation detection withCredentialssupport for both fetch and XHR
Install
pnpm add @omnific/requestBreaking Changes in 0.1.0
FetchResponseTypeis no longer exported. Use the sharedResponseTypeexport instead.- Fetch
responseTypevalues now use the same lowercase names asResponseType:arraybufferandformdatareplace the previousarrayBufferandformDatanames.
Quick Start
import { createRequest } from '@omnific/request';
type User = {
id: string;
name: string;
};
const api = createRequest({
baseURL: 'https://api.example.com',
});
const response = await api.get<User>('/users/1');
console.log(response.data.name);Request Instances
The package exports:
request: a ready-to-use request instance created bycreateRequest()createRequest(config?): a factory for creating new request instances
Each instance is callable and uses fetch by default:
import { request } from '@omnific/request';
const response = await request<{ ok: boolean }>({
url: 'https://api.example.com/health',
});If you want a shared baseURL, create an instance once and reuse it:
import { createRequest } from '@omnific/request';
const api = createRequest({
baseURL: 'https://api.example.com/v1',
});
await api.get('/users');
await api.post('/users', {
data: {
name: 'Ada Lovelace',
},
});baseURL is an instance-level option. It is not part of the per-request RequestConfig.
Uploads
request.upload() uses XMLHttpRequest so it can expose upload progress events. It always sends a POST request.
import { createRequest } from '@omnific/request';
const api = createRequest({
baseURL: 'https://api.example.com',
});
const formData = new FormData();
formData.append('file', file);
const response = await api.upload<{ url: string }>('/upload', {
data: formData,
onUploadProgress(event) {
console.log(event.loaded, event.total, event.progress);
},
});
console.log(response.data.url);API
request(config)
request<T>(config: FetchRequestConfig): Promise<FetchResponse<T>>Verb helpers
These helpers all use the fetch transport:
request.get<T>(url: string, config?: FetchRequestConfig): Promise<FetchResponse<T>>
request.delete<T>(url: string, config?: FetchRequestConfig): Promise<FetchResponse<T>>
request.head<T>(url: string, config?: FetchRequestConfig): Promise<FetchResponse<T>>
request.options<T>(url: string, config?: FetchRequestConfig): Promise<FetchResponse<T>>
request.post<T>(url: string, config?: FetchRequestConfig): Promise<FetchResponse<T>>
request.put<T>(url: string, config?: FetchRequestConfig): Promise<FetchResponse<T>>
request.patch<T>(url: string, config?: FetchRequestConfig): Promise<FetchResponse<T>>The helper method sets method and url for you. If you pass method or url again in config, the helper value wins.
request.upload(url, config)
request.upload<T>(url: string, config?: XhrRequestConfig): Promise<XhrResponse<T>>This helper always uses XMLHttpRequest and always sends POST.
createRequest(config?)
createRequest(config?: {
baseURL?: string;
withCredentials?: boolean;
}): ApiRequestbaseURL is prepended to relative request URLs. Absolute request URLs are left untouched.
withCredentials sets the default credential behavior for created request helpers and can be overridden per request.
Request Config
Shared request options:
type RequestConfig<D = unknown> = {
url?: string;
method?: Method;
timeout?: number;
params?: Record<string, QueryValue | QueryValue[]> | URLSearchParams;
data?: D;
headers?: HeadersInit;
signal?: AbortSignal;
withCredentials?: boolean;
onUploadProgress?: (event: {
loaded: number;
total?: number;
progress?: number;
event?: ProgressEvent<XMLHttpRequestEventTarget>;
}) => void;
responseType?: ResponseType;
};url is required when making a request.
Fetch config
FetchRequestConfig currently uses the same option set as RequestConfig:
type FetchRequestConfig<D = unknown> = RequestConfig<D>;Notes:
responseTypedefaults to'json'- supported
responseTypevalues are'arraybuffer','blob','formdata','json', and'text' documentis not supported by fetch and rejects with an errorwithCredentials: truemaps tocredentials: 'include';withCredentials: falsemaps tocredentials: 'omit'onUploadProgressis not supported by fetch and is ignored by regular fetch requests
XHR config
type XhrRequestConfig<D = unknown> = RequestConfig<D>;Notes:
responseTypedefaults to'json'- supported
responseTypevalues are'arraybuffer','blob','document','json', and'text' formdatais not supported by XMLHttpRequest and rejects with an errorwithCredentialsmaps toXMLHttpRequest.withCredentialsonUploadProgressis only available through the XMLHttpRequest upload channel, exposed byrequest.upload()
URL Handling
Requests are resolved in this order:
createRequest({ baseURL })prependsbaseURLto relative URLsparamsare serialized onto the final URL
Serialization behavior:
nullandundefinedquery values are skipped- array values are expanded as repeated keys
- hash fragments are removed before query params are appended
- existing query strings are preserved
const api = createRequest({
baseURL: 'https://api.example.com',
});
await api.get('/users?page=1#team', {
params: {
role: ['admin', 'editor'],
keyword: undefined,
},
});This resolves to:
https://api.example.com/users?page=1&role=admin&role=editorRequest Body Serialization
The package transforms request bodies as follows:
FormData,Blob, andArrayBufferare sent as-isURLSearchParamsare serialized asapplication/x-www-form-urlencoded;charset=utf-8- plain objects are serialized as JSON
- any request with
content-type: application/jsonis serialized as JSON accept: application/json, text/plain, */*is applied by default unless already set
Response Shape
Both transports resolve to the same structure:
type Response<T, R> = {
data: T;
status: number;
statusText: string;
headers: Headers;
request: R;
};FetchResponse<T>usesRequestasrequestXhrResponse<T>usesXMLHttpRequestasrequest
Errors and Cancellation
Requests reject when:
- the response status is outside the
2xxrange - the network request fails
- the request times out
- the request is aborted
- an unsupported fetch
responseTypeis used urlis missing
isCancel detects aborted requests:
import { isCancel, request } from '@omnific/request';
const controller = new AbortController();
const promise = request({
url: 'https://api.example.com/users',
signal: controller.signal,
});
controller.abort();
try {
await promise;
} catch (error) {
if (isCancel(error)) {
console.log('request canceled');
}
}