@dfsync/client
v0.8.0
Published
Reliable service-to-service HTTP communication toolkit for Node.js and TypeScript
Maintainers
Readme
@dfsync/client
A lightweight and reliable HTTP client for service-to-service communication in Node.js, with built-in retry, authentication, and lifecycle hooks.
Designed for backend services, microservices and internal APIs where consistent and reliable HTTP communication between services is required.
Home page: https://dfsyncjs.github.io
Full documentation: https://dfsyncjs.github.io/#/docs/client
Install
npm install @dfsync/clientQuick Start
import { createClient } from '@dfsync/client';
const client = createClient({
baseUrl: 'https://api.example.com',
retry: { attempts: 3 },
});
const users = await client.get('/users');
const createdUser = await client.post('/users', {
name: 'John',
});
const updatedUser = await client.patch('/users/1', {
name: 'Jane',
});HTTP methods
@dfsync/client provides a small and predictable method surface:
client.get(path, options?)
client.delete(path, options?)
client.post(path, body?, options?)
client.put(path, body?, options?)
client.patch(path, body?, options?)
client.request(config)get and delete do not accept body in options.
post, put, and patch accept request body as a separate second argument.
Main features
- predictable request lifecycle
- request ID propagation (
x-request-id) - request cancellation via
AbortSignal - built-in retry with configurable policies
- lifecycle hooks:
beforeRequest,afterResponse,onRetry,onError - request timeout support
- typed responses
- automatic JSON parsing
- consistent error handling
- auth support: bearer, API key, custom
- support for
GET,POST,PUT,PATCH, andDELETE - response validation with
ValidationError - idempotency key support for safer retries
It provides a predictable and controllable HTTP request lifecycle for service-to-service communication.
How requests work
A request in @dfsync/client follows a predictable lifecycle:
- create request context
- build final URL from
baseUrl,path, and optional query params - merge client and request headers
- apply authentication
- attach request metadata (e.g.
x-request-id) - run
beforeRequesthooks - send request with
fetch - run
onRetrybefore a retry attempt - retry on failure (if configured)
- parse response (JSON, text, or
undefinedfor204) - validate response data (if configured)
- run
afterResponseoronErrorhooks
Request context
Each request is executed within a request context that contains:
requestId— unique identifier for the requestattempt— current retry attemptsignal— AbortSignal for cancellationstartedAt— request start timestamp
This context is available in all lifecycle hooks.
Request ID
Each request has a requestId that is:
- automatically generated by default
- can be overridden per request
- propagated via the
x-request-idheader
Example
await client.get('/users', {
requestId: 'req_123',
});You can also override the header directly:
await client.get('/users', {
headers: {
'x-request-id': 'custom-id',
},
});Request cancellation
Requests can be cancelled using AbortSignal:
const controller = new AbortController();
const promise = client.get('/users', {
signal: controller.signal,
});
controller.abort();Cancellation is treated differently from timeouts:
- timeout →
TimeoutError - manual cancellation →
RequestAbortedError
Errors
@dfsync/client provides structured error types:
HttpError— non-2xx responsesNetworkError— network failuresTimeoutError— request timed outValidationError— response validation failedRequestAbortedError— request was cancelled
This allows you to handle failures more precisely.
Response validation
You can validate successful responses before they are returned to the caller.
This is useful when your service depends on another API and needs to fail fast when the response shape changes unexpectedly.
Instead of passing malformed data deeper into your application, validation turns the mismatch into a structured ValidationError.
Validation runs only after a successful HTTP response. Non-2xx responses still throw HttpError.
import { createClient } from '@dfsync/client';
const client = createClient({
baseUrl: 'https://api.example.com',
validateResponse(data) {
return typeof data === 'object' && data !== null && 'id' in data;
},
});
const user = await client.get('/users/1');Return false to fail validation. Returning true or nothing means validation passed.
You can also override validation per request:
await client.get('/users/1', {
validateResponse(data) {
return typeof data === 'object' && data !== null && 'email' in data;
},
});When validation fails, @dfsync/client throws ValidationError:
import { ValidationError } from '@dfsync/client';
try {
await client.get('/users/1');
} catch (error) {
if (error instanceof ValidationError) {
console.log(error.data);
}
}Validation failures are not retried by default.
Idempotency keys
For operations that may be retried safely, you can attach an idempotency key per request.
This helps protect non-idempotent operations, such as payments or job creation, from being applied more than once when a request is retried after a transient failure. The receiving service should treat repeated requests with the same idempotency key as the same logical operation.
await client.post(
'/payments',
{ amount: 100 },
{
idempotencyKey: 'payment-123',
},
);This adds the following header:
idempotency-key: payment-123POST and PATCH requests are not retried unless both conditions are true:
- the method is explicitly included in
retry.retryMethods - the request provides
idempotencyKey
By default, POST and PATCH are not retried. This keeps unsafe retries opt-in and makes the retry behavior explicit at the call site.
const client = createClient({
baseUrl: 'https://api.example.com',
retry: {
attempts: 3,
retryMethods: ['POST'],
retryOn: ['5xx'],
},
});
await client.post(
'/payments',
{ amount: 100 },
{
idempotencyKey: 'payment-123',
},
);Observability
@dfsync/client provides built-in request lifecycle metadata for better visibility and debugging.
Each request exposes:
- requestId — stable identifier across retries
- attempt / maxAttempts — retry progress
- startedAt / endedAt / durationMs — timing information
- retryReason — why a retry happened (
network-error,5xx,429) - retryDelayMs — delay before the next retry
- retrySource — delay source (
backofforretry-after)
Example
const client = createClient({
baseUrl: 'https://api.example.com',
retry: {
attempts: 2,
retryOn: ['5xx'],
},
hooks: {
onRetry(ctx) {
console.log({
requestId: ctx.requestId,
attempt: ctx.attempt,
maxAttempts: ctx.maxAttempts,
delay: ctx.retryDelayMs,
reason: ctx.retryReason,
source: ctx.retrySource,
});
},
},
});When response validation is configured and passes, afterResponse also receives validation metadata.
const client = createClient({
baseUrl: 'https://api.example.com',
validateResponse(data) {
return typeof data === 'object' && data !== null && 'id' in data;
},
hooks: {
afterResponse(ctx) {
console.log(ctx.validation);
// { enabled: true, passed: true }
},
},
});This makes it easier to understand:
- what happened during a request
- how retries behaved
- how long requests actually took
Roadmap
See the project roadmap
