npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dfsync/client

v0.8.0

Published

Reliable service-to-service HTTP communication toolkit for Node.js and TypeScript

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.

npm version npm downloads License: MIT

Home page: https://dfsyncjs.github.io

Full documentation: https://dfsyncjs.github.io/#/docs/client

Install

npm install @dfsync/client

Quick 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, and DELETE
  • 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:

  1. create request context
  2. build final URL from baseUrl, path, and optional query params
  3. merge client and request headers
  4. apply authentication
  5. attach request metadata (e.g. x-request-id)
  6. run beforeRequest hooks
  7. send request with fetch
  8. run onRetry before a retry attempt
  9. retry on failure (if configured)
  10. parse response (JSON, text, or undefined for 204)
  11. validate response data (if configured)
  12. run afterResponse or onError hooks

Request context

Each request is executed within a request context that contains:

  • requestId — unique identifier for the request
  • attempt — current retry attempt
  • signal — AbortSignal for cancellation
  • startedAt — 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-id header

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 responses
  • NetworkError — network failures
  • TimeoutError — request timed out
  • ValidationError — response validation failed
  • RequestAbortedError — 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-123

POST 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 (backoff or retry-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