diff --git a/docs/v1/create-client.md b/docs/v1/create-client.md index 4f8f02b..973e17a 100644 --- a/docs/v1/create-client.md +++ b/docs/v1/create-client.md @@ -55,6 +55,7 @@ type ClientConfig = { hooks?: { // see Hooks section }; + retry?: RetryConfig; }; ``` @@ -141,6 +142,7 @@ type RequestOptions = { timeout?: number; retry?: RetryConfig; signal?: AbortSignal; + requestId?: string; }; ``` @@ -172,12 +174,71 @@ 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 @@ -185,11 +246,11 @@ Headers are merged in this order: 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 @@ -197,7 +258,7 @@ Timeout is resolved in this order: ## Response parsing -Responses are parsed automatically: +Responses are parsed automatically during the response phase: - `application/json` → parsed JSON - other content types → text diff --git a/docs/v1/errors.md b/docs/v1/errors.md index 555efc2..3577e8b 100644 --- a/docs/v1/errors.md +++ b/docs/v1/errors.md @@ -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 @@ -167,9 +170,7 @@ if (error instanceof HttpError) { Errors thrown inside: - custom auth - - `beforeRequest` - - `afterResponse` are rethrown as-is. diff --git a/docs/v1/getting-started.md b/docs/v1/getting-started.md index ec38c0f..6be5023 100644 --- a/docs/v1/getting-started.md +++ b/docs/v1/getting-started.md @@ -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: @@ -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 @@ -41,53 +42,22 @@ const client = createClient({ }); const user = await client.get('/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 diff --git a/docs/v1/hooks.md b/docs/v1/hooks.md index 27659e0..5cd8d7e 100644 --- a/docs/v1/hooks.md +++ b/docs/v1/hooks.md @@ -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. diff --git a/src/components/Features/Features.tsx b/src/components/Features/Features.tsx index e3ab126..6ffe418 100644 --- a/src/components/Features/Features.tsx +++ b/src/components/Features/Features.tsx @@ -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'; @@ -35,23 +35,23 @@ const items = [ description: 'Built-in request lifecycle hooks like beforeRequest, afterResponse, and onError.', }, { - icon: , - title: 'Production-oriented', - description: - 'Designed for reliability, clear request behavior, and maintainable service communication.', + icon: , + title: 'Predictable lifecycle', + description: 'Every request follows a clear and controllable lifecycle.', }, ]; export const Features = () => { return ( - + Why @dfsync/client - 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. diff --git a/src/components/Hero/Hero.tsx b/src/components/Hero/Hero.tsx index 103deb1..c250460 100644 --- a/src/components/Hero/Hero.tsx +++ b/src/components/Hero/Hero.tsx @@ -10,7 +10,7 @@ export const Hero = () => { @@ -55,9 +55,9 @@ export const Hero = () => { lineHeight: 1.6, }} > - The first package, @dfsync/client, 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, @dfsync/client, is a lightweight HTTP client built + around a predictable request lifecycle for service-to-service communication in + Node.js. @@ -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', @@ -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', +}); +`} diff --git a/src/components/Problem/Problem.tsx b/src/components/Problem/Problem.tsx new file mode 100644 index 0000000..71f65f6 --- /dev/null +++ b/src/components/Problem/Problem.tsx @@ -0,0 +1,56 @@ +import { Container, Grid, Typography, Paper } from '@mui/material'; +const items = [ + { + title: 'Repeated boilerplate', + description: 'Every project reimplements retry, auth, and error handling differently.', + }, + { + title: 'Unpredictable behavior', + description: 'Request flows vary across services, making debugging and maintenance harder.', + }, + { + title: 'Lack of control', + description: 'No unified way to control retries, cancellation, and request lifecycle.', + }, +]; + +export function Problem() { + return ( + + + What problem it solves + + + + In most projects, HTTP clients are rebuilt again and again — with slightly different retry + logic, error handling, and request flows. +
+
+ @dfsync/client provides a predictable and reusable request lifecycle out of + the box for service-to-service communication. +
+ + + {items.map(({ title, description }) => ( + + + + {title} + + + {description} + + + + ))} + +
+ ); +} diff --git a/src/components/index.ts b/src/components/index.ts index 7f27aa8..ca6173e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -3,3 +3,4 @@ export { Footer } from './Footer/Footer'; export { Header } from './Header/Header'; export { Hero } from './Hero/Hero'; export { ProjectBadges } from './ProjectBadges/ProjectBadges'; +export { Problem } from './Problem/Problem'; diff --git a/src/pages/Home/HomePage.tsx b/src/pages/Home/HomePage.tsx index 13d6e5f..3e060ba 100644 --- a/src/pages/Home/HomePage.tsx +++ b/src/pages/Home/HomePage.tsx @@ -1,6 +1,6 @@ import { useContext } from 'react'; import { Box } from '@mui/material'; -import { Header, Hero, Features, Footer } from '../../components'; +import { Header, Hero, Features, Footer, Problem } from '../../components'; import { ThemeModeContext } from '../../app/themeContext'; const HomePage = () => { @@ -19,6 +19,7 @@ const HomePage = () => {
+