Skip to content

Making Requests

FetchEngine provides type-safe HTTP methods that return rich response objects with full request context.

HTTP Methods

All request methods return a FetchPromise<T> that resolves to FetchResponse<T>.

GET

typescript
api.get<Res, ResHdr>(path: string, options?: CallConfig): FetchPromise<Res>

Retrieve data from the server.

typescript
const users = await api.get<User[]>('/users');
if (users.ok) console.log(users.data); // User[]

const user = await api.get<User>('/users/123', {
    params: { include: 'profile' }
});
if (user.ok) console.log(user.data); // User

POST

typescript
api.post<Res, Data, ResHdr>(path: string, payload?: Data, options?: CallConfig): FetchPromise<Res>

Create a new resource.

typescript
const created = await api.post<User, CreateUserData>('/users', {
    name: 'John Doe',
    email: '[email protected]'
});
if (created.ok) console.log(created.data); // User

PUT

typescript
api.put<Res, Data, ResHdr>(path: string, payload?: Data, options?: CallConfig): FetchPromise<Res>

Replace a resource.

typescript
const replaced = await api.put<User, UpdateUserData>('/users/123', {
    name: 'Jane Doe',
    email: '[email protected]'
});
if (replaced.ok) console.log(replaced.data); // User

PATCH

typescript
api.patch<Res, Data, ResHdr>(path: string, payload?: Data, options?: CallConfig): FetchPromise<Res>

Partially update a resource.

typescript
const updated = await api.patch<User, Partial<User>>('/users/123', {
    email: '[email protected]'
});
if (updated.ok) console.log(updated.data); // User

DELETE

typescript
api.delete<Res, Data, ResHdr>(path: string, payload?: Data, options?: CallConfig): FetchPromise<Res>

Remove a resource.

typescript
await api.delete('/users/123');

// With request body
await api.delete('/users/batch', { ids: ['1', '2', '3'] });

OPTIONS

typescript
api.options<Res, ResHdr>(path: string, options?: CallConfig): FetchPromise<Res>

Check server capabilities.

typescript
const { headers } = await api.options('/users');
typescript
api.head<ResHdr>(path: string, options?: CallConfig): FetchPromise<null>

Retrieve headers only (no body).

typescript
const { headers } = await api.head('/users/123');

Generic Request

typescript
api.request<Res, Data, ResHdr>(
    method: HttpMethods,
    path: string,
    options?: CallConfig & { payload?: Data }
): FetchPromise<Res>

Make a request with any HTTP method.

typescript
const res = await api.request<User>('PATCH', '/users/123', {
    payload: { name: 'Updated' },
    headers: { 'X-Custom': 'value' }
});
if (res.ok) console.log(res.data); // User

FetchResponse

Every HTTP method returns a FetchResponse — a discriminated union on ok. Every completed exchange resolves this way, including non-2xx status; only a transport failure (abort, timeout, connection lost, parse failure on an ok: true body) rejects instead as a FetchError.

typescript
type FetchResponse<T, H, P, RH> =
    | { ok: true; data: T; headers: Partial<RH>; status: number; request: Request; config: FetchConfig<H, P> }
    | { ok: false; data: unknown; headers: Partial<RH>; status: number; request: Request; config: FetchConfig<H, P> };

Example:

typescript
const response = await api.get<User[]>('/users');

if (!response.ok) {
    console.error('Request failed:', response.status, response.data);
}
else {
    console.log(response.data);      // User[] — narrowed by the ok check
    console.log(response.status);    // 200
    console.log(response.headers);   // { 'content-type': 'application/json', ... }
    console.log(response.config);    // { baseUrl: '...', headers: {...}, ... }
}

Per-Request Options

Override instance configuration for individual requests using CallConfig:

OptionTypeDescription
headersDictAndT<H>Request-specific headers
paramsDictAndT<P>Request-specific URL parameters
totalTimeoutnumberTotal timeout including retries (ms)
attemptTimeoutnumberPer-attempt timeout (ms)
retryRetryConfig | booleanOverride retry configuration
abortControllerAbortControllerCustom abort controller
determineTypeDetermineTypeFnCustom response type detection
onBeforeReq(opts) => voidCalled before request
onAfterReq(response, opts) => voidCalled after response
onError(err) => voidCalled on error

Example:

typescript
const res = await api.get<User>('/users/123', {
    headers: { 'X-Include': 'profile' },
    params: { version: 'v2' },
    totalTimeout: 60000,
    retry: { maxAttempts: 5 }
});
if (res.ok) console.log(res.data); // User

Response Chaining

Declare how the response body should be parsed by chaining a directive method before awaiting. Without a directive, the response is auto-parsed based on content-type (backwards compatible).

typescript
// Explicit response type via chaining
const user = await api.get<User>('/users/123').json();   // FetchResponse<User>
const html = await api.get('/page').text();              // FetchResponse<string>
const file = await api.get('/file').blob();              // FetchResponse<Blob>
const buf  = await api.get('/binary').arrayBuffer();     // FetchResponse<ArrayBuffer>
const form = await api.get('/form').formData();          // FetchResponse<FormData>
const raw  = await api.get('/endpoint').raw();           // FetchResponse<Response>

// The directive declares the parse type; `data` still narrows on `ok`
if (user.ok) render(user.data); // User

// No directive — auto-parse based on content-type
const auto = await api.get<User>('/users/123');

Available directives:

MethodReturnsDescription
.json()FetchPromise<T>Parse as JSON (preserves generic type)
.text()FetchPromise<string>Parse as plain text
.blob()FetchPromise<Blob>Parse as Blob
.arrayBuffer()FetchPromise<ArrayBuffer>Parse as ArrayBuffer
.formData()FetchPromise<FormData>Parse as FormData
.raw()FetchPromise<Response>Return raw Response without parsing
.stream()FetchStreamPromiseStream mode with async iteration

Override Guard

Setting a directive more than once throws an error. This prevents accidental double-calls that would silently discard the first directive:

typescript
// Throws: 'Response type already set'
api.get('/users').json().text();

Stream Mode

Use .stream() to get raw Response objects with unconsumed body streams. Cache and deduplication are skipped (each caller needs its own readable stream). Rate limiting and lifecycle events still fire normally.

.stream() returns a FetchStreamPromise which supports for await iteration over Uint8Array chunks:

typescript
// Async iteration over response body chunks
for await (const chunk of api.get('/events').stream()) {
    console.log(new TextDecoder().decode(chunk));
}

// With error handling
const stream = api.get('/events').stream();
const [, err] = await attempt(async () => {
    for await (const chunk of stream) {
        console.log(new TextDecoder().decode(chunk));
    }
});
if (err) console.error('Stream failed:', err.message);

// Works with all HTTP methods
for await (const chunk of api.post('/upload-stream', largePayload).stream()) {
    process(chunk);
}

FetchPromise

All HTTP methods return a FetchPromise — an extended Promise that supports abort, response chaining, and streaming:

typescript
interface FetchPromise<T, H, P, RH> extends Promise<FetchResponse<T, H, P, RH>> {

    isFinished: boolean;  // Whether request completed
    isAborted: boolean;   // Whether request was aborted
    abort(reason?: string): void;  // Cancel the request

    // Response chaining directives
    json(): FetchPromise<T, H, P, RH>;
    text(): FetchPromise<string, H, P, RH>;
    blob(): FetchPromise<Blob, H, P, RH>;
    arrayBuffer(): FetchPromise<ArrayBuffer, H, P, RH>;
    formData(): FetchPromise<FormData, H, P, RH>;
    raw(): FetchPromise<Response, H, P, RH>;
    stream(): FetchStreamPromise<H, P, RH>;
}

Example:

typescript
const request = api.get('/slow-endpoint');

// Abort after 5 seconds
setTimeout(() => {
    if (!request.isFinished) {
        request.abort('Timeout');
    }
}, 5000);

const [data, err] = await attempt(() => request);

if (err && request.isAborted) {
    console.log('Request was cancelled');
}

URL Handling

Relative Paths

Relative paths are joined with the base URL:

typescript
const api = new FetchEngine({ baseUrl: 'https://api.example.com' });

await api.get('/users');  // → https://api.example.com/users
await api.get('/users/123');  // → https://api.example.com/users/123

Absolute URLs

Absolute URLs bypass the base URL:

typescript
// Uses external URL directly
const res = await api.get('https://other-api.com/data');

URL Parameters

Parameters are appended to the query string:

typescript
await api.get('/users', {
    params: { page: '1', limit: '10' }
});
// → https://api.example.com/users?page=1&limit=10

// Combined with existing query string
await api.get('/users?active=true', {
    params: { page: '1' }
});
// → https://api.example.com/users?active=true&page=1

Request Lifecycle Hooks

Add per-request callbacks:

typescript
const res = await api.post('/users', userData, {
    onBeforeReq: (opts) => {
        console.log('Starting request:', opts.method, opts.url);
    },

    onAfterReq: (response, opts) => {
        console.log('Completed:', response.status);
    },

    onError: (err) => {
        console.error('Failed:', err.message);
    }
});

Type Safety

FetchEngine supports full TypeScript generics:

typescript
interface User {
    id: string;
    name: string;
    email: string;
}

interface CreateUserData {
    name: string;
    email: string;
}

// Response type flows through the union — narrow on `ok` to read it
const res = await api.get<User>('/users/123');
if (res.ok) {
    res.data; // User
}

// Payload type is validated
const newUser = await api.post<User, CreateUserData>('/users', {
    name: 'John',
    email: '[email protected]'
    // TypeScript error if payload doesn't match CreateUserData
});
if (newUser.ok) {
    newUser.data; // User
}