Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 65 additions & 4 deletions docs/v1/create-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type ClientConfig = {
hooks?: {
// see Hooks section
};
retry?: RetryConfig;
};
```

Expand Down Expand Up @@ -141,6 +142,7 @@ type RequestOptions = {
timeout?: number;
retry?: RetryConfig;
signal?: AbortSignal;
requestId?: string;
};
```

Expand Down Expand Up @@ -172,32 +174,91 @@ type RequestConfig = {
timeout?: number;
retry?: RetryConfig;
signal?: AbortSignal;
requestId?: string;
};
```

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

This allows tracing requests across services.

### Example

```ts
await client.get('/users', {
requestId: 'req_123',
});
```

You can also override the header directly:

```ts
await client.get('/users', {
headers: {
'x-request-id': 'custom-id',
},
});
```

## Request cancellation

Requests can be cancelled using `AbortSignal`:

```ts
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`

## Header behavior

Headers are merged in this order:
Headers are resolved as part of the request lifecycle in the following order:

1. default headers
2. client headers
3. request headers
4. auth modifications
5. beforeRequest hook modifications

That means request-level headers override client-level headers, and auth can still overwrite auth-related header values.
This means request-level headers override client-level headers, and auth can still overwrite auth-related header values.

## Timeout behavior

Timeout is resolved in this order:
Timeout is resolved as part of the request lifecycle:

1. request-level timeout
2. client-level timeout
3. default timeout: `5000`

## Response parsing

Responses are parsed automatically:
Responses are parsed automatically during the response phase:

- `application/json` → parsed JSON
- other content types → text
Expand Down
13 changes: 7 additions & 6 deletions docs/v1/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@

## Error classes

- `DfsyncError`
- `HttpError`
- `NetworkError`
- `TimeoutError`
- `DfsyncError` - basic Error class
- `HttpError` — non-2xx responses
- `NetworkError` — network failures
- `TimeoutError` — request timed out
- `RequestAbortedError` — request was cancelled

- This allows you to handle failures more precisely.

## Base error

Expand Down Expand Up @@ -167,9 +170,7 @@ if (error instanceof HttpError) {
Errors thrown inside:

- custom auth

- `beforeRequest`

- `afterResponse`

are rethrown as-is.
Expand Down
76 changes: 23 additions & 53 deletions docs/v1/getting-started.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Getting Started

`@dfsync/client` is a lightweight TypeScript HTTP client designed for reliable service-to-service communication.
`@dfsync/client` is a lightweight HTTP client built around a predictable request lifecycle for service-to-service communication in Node.js.

It provides sensible defaults for:

Expand All @@ -11,19 +11,20 @@ It provides sensible defaults for:

The client focuses on predictable behavior, extensibility, and a clean developer experience.

## What you get
## 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`, `onError`

- typed responses
- simple client creation
- request timeout support
- automatic JSON parsing
- text response support
- consistent error handling with structured error classes
- auth support: `bearer`, `API key` and custom
- lifecycle hooks: `beforeRequest`, `afterResponse`, `onError`
- consistent error handling

- auth support: bearer, API key, custom
- support for `GET`, `POST`, `PUT`, `PATCH`, and `DELETE`
- retry policies
- custom `fetch` support

## Quick example

Expand All @@ -41,53 +42,22 @@ const client = createClient({
});

const user = await client.get<User>('/users/1');

console.log(user.id);
console.log(user.name);
```

## How requests work

A request in `@dfsync/client` goes through the following lifecycle:

1. Build request URL

The final URL is constructed from `baseUrl`, `path`, and optional query parameters.

2. Merge headers

Default headers, client-level headers, and request-level headers are combined.

3. Apply authentication

The configured auth strategy (Bearer, API key, or custom) is applied to the request.

4. Run `beforeRequest` hooks

Hooks can modify the request before it is sent.

5. Execute the HTTP request

The request is sent using the Fetch API.

6. Retry if necessary

If the request fails with a retryable error, it may be retried according to the configured retry policy.

7. Parse the response

The response body is parsed automatically:
- JSON → parsed object
- text → string
- `204 No Content` → `undefined`

8. Handle errors

Non-success responses and network failures are converted into structured errors.

9. Run response hooks
- `afterResponse` runs for successful responses
- `onError` runs when an error occurs
A request in `@dfsync/client` follows a predictable lifecycle:

1. create request context
2. build final URL from `baseUrl`, `path`, and 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. retry on failure (if configured)
9. parse response (JSON, text, or `undefined`)
10. run `afterResponse` or `onError` hooks

## Runtime requirements

Expand Down
18 changes: 18 additions & 0 deletions docs/v1/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ Each hook can be:

Hooks run sequentially in the order you provide them.

## Request metadata

Hooks receive a rich lifecycle context, including request metadata and execution details.

```ts
const client = createClient({
baseUrl: 'https://api.example.com',
hooks: {
beforeRequest: (ctx) => {
console.log(ctx.requestId, ctx.attempt);
},
onError: (ctx) => {
console.error(ctx.requestId, ctx.error);
},
},
});
```

## beforeRequest

Use `beforeRequest` to mutate headers or the final request URL before `fetch` is called.
Expand Down
16 changes: 8 additions & 8 deletions src/components/Features/Features.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import HubIcon from '@mui/icons-material/Hub';
import SecurityIcon from '@mui/icons-material/Security';
import AutorenewIcon from '@mui/icons-material/Autorenew';
import LockIcon from '@mui/icons-material/Lock';
import ReplayIcon from '@mui/icons-material/Replay';
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
Expand Down Expand Up @@ -35,23 +35,23 @@ const items = [
description: 'Built-in request lifecycle hooks like beforeRequest, afterResponse, and onError.',
},
{
icon: <SecurityIcon fontSize="large" />,
title: 'Production-oriented',
description:
'Designed for reliability, clear request behavior, and maintainable service communication.',
icon: <AutorenewIcon fontSize="large" />,
title: 'Predictable lifecycle',
description: 'Every request follows a clear and controllable lifecycle.',
},
];

export const Features = () => {
return (
<Container maxWidth="lg" sx={{ pb: { xs: 8, md: 12 } }}>
<Container maxWidth="lg" sx={{ py: { xs: 2, md: 4 } }}>
<Stack spacing={2} sx={{ mb: 5 }}>
<Typography variant="h2" sx={{ fontSize: { xs: '2rem', md: '3rem' } }}>
Why @dfsync/client
</Typography>
<Typography color="text.secondary" sx={{ maxWidth: 720 }}>
A lightweight HTTP client for service-to-service communication, with sensible defaults,
authentication strategies, lifecycle hooks, and retry support.
A lightweight HTTP client with a predictable request lifecycle for service-to-service
communication with sensible defaults, authentication strategies, lifecycle hooks, and
retry support.
</Typography>
</Stack>

Expand Down
21 changes: 12 additions & 9 deletions src/components/Hero/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const Hero = () => {
<Box
component="section"
sx={{
py: { xs: 6, md: 12 },
py: { xs: 2, md: 4 },
background: 'radial-gradient(circle at top, rgba(56,189,248,0.15), transparent 40%)',
}}
>
Expand Down Expand Up @@ -55,9 +55,9 @@ export const Hero = () => {
lineHeight: 1.6,
}}
>
The first package, <strong>@dfsync/client</strong>, provides a lightweight and
reliable HTTP client for service-to-service communication in Node.js, with built-in
retry, authentication, and lifecycle hooks.
The first package, <strong>@dfsync/client</strong>, is a lightweight HTTP client built
around a predictable request lifecycle for service-to-service communication in
Node.js.
</Typography>
</Box>

Expand Down Expand Up @@ -97,7 +97,7 @@ export const Hero = () => {
width: '100%',
p: { xs: 3, md: 4 },
borderRadius: 1,
bgcolor: 'background.paper',
backgroundColor: 'background.paper',
border: '1px solid',
borderColor: 'divider',
overflowX: 'auto',
Expand All @@ -113,14 +113,17 @@ export const Hero = () => {
color: 'text.primary',
}}
>
{`import { createClient } from "@dfsync/client";
{`import { createClient } from '@dfsync/client';

const client = createClient({
baseURL: "https://api.example.com",
retry: { attempts: 3 }
baseURL: 'https://api.example.com',
retry: { attempts: 3 },
});

const users = await client.get("/users");`}
const users = await client.get('/users', {
requestId: 'req_123',
});
`}
</Typography>
</Box>
</Stack>
Expand Down
Loading
Loading