From acf4c6d91cfb2bf890e5bf45609b07afe870ccda Mon Sep 17 00:00:00 2001 From: Roman Onishchenko Date: Mon, 27 Apr 2026 17:52:18 +0200 Subject: [PATCH 1/3] update docs for release 0.8.x --- docs/v1/api-reference.md | 65 +++++++++++++-- docs/v1/create-client.md | 60 +++++++++++++- docs/v1/errors.md | 66 +++++++++++++++- docs/v1/examples.md | 53 ++++++++++++- docs/v1/getting-started.md | 4 +- docs/v1/hooks.md | 42 ++++++++-- docs/v1/observability.md | 2 + docs/v1/response-handling.md | 145 ++++++++++++++++++++++++++++++++++ docs/v1/retry.md | 35 ++++++-- src/content/docsContent.ts | 2 + src/content/docsNavigation.ts | 2 + 11 files changed, 448 insertions(+), 28 deletions(-) diff --git a/docs/v1/api-reference.md b/docs/v1/api-reference.md index cb56230..70b7986 100644 --- a/docs/v1/api-reference.md +++ b/docs/v1/api-reference.md @@ -4,21 +4,58 @@ Creates a configured HTTP client. +```ts +import { createClient } from '@dfsync/client'; +``` + ## Client methods -- `get` -- `post` -- `put` -- `patch` -- `delete` +```text +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) +``` ## Configuration - `baseUrl` - `timeout` +- `headers` +- `fetch` - `retry` - `auth` - `hooks` +- `validateResponse` + +## Request options + +- `query` +- `headers` +- `timeout` +- `retry` +- `signal` +- `requestId` +- `idempotencyKey` +- `validateResponse` + +## Retry + +- `attempts` +- `backoff` +- `baseDelayMs` +- `retryOn` +- `retryMethods` + +## Response validation + +```ts +type ResponseValidator = (data: TData) => boolean | void | Promise; +``` ## Hooks @@ -32,4 +69,22 @@ Creates a configured HTTP client. - `HttpError` - `NetworkError` - `TimeoutError` +- `ValidationError` - `RequestAbortedError` + +## Exported types + +- `AuthConfig` +- `Client` +- `ClientConfig` +- `RetryConfig` +- `RetryCondition` +- `RetryBackoff` +- `ResponseValidator` +- `BeforeRequestContext` +- `AfterResponseContext` +- `ErrorContext` +- `RetryContext` +- `HooksConfig` +- `RequestConfig` +- `RequestOptions` diff --git a/docs/v1/create-client.md b/docs/v1/create-client.md index 0516cd1..212a1a0 100644 --- a/docs/v1/create-client.md +++ b/docs/v1/create-client.md @@ -9,6 +9,7 @@ It provides a consistent way to configure: - auth - lifecycle hooks - request observability metadata +- response validation ## Basic client @@ -64,6 +65,7 @@ type ClientConfig = { // see Hooks section }; retry?: RetryConfig; + validateResponse?: ResponseValidator; }; ``` @@ -100,7 +102,7 @@ client.request(config) - `get` and `delete` do not accept body - `post`, `put`, and `patch` accept request body as the second argument -- `options` is used for headers, query, timeout, retry, and other settings +- `options` is used for headers, query, timeout, retry, validation, idempotency keys, and other settings ## GET request @@ -165,6 +167,8 @@ type RequestOptions = { retry?: RetryConfig; signal?: AbortSignal; requestId?: string; + idempotencyKey?: string; + validateResponse?: ResponseValidator; }; ``` @@ -172,6 +176,8 @@ type RequestOptions = { Request-level `retry` overrides client-level retry settings. +Request-level `validateResponse` overrides client-level response validation. + ## Low-level request ```ts @@ -201,6 +207,8 @@ type RequestConfig = { retry?: RetryConfig; signal?: AbortSignal; requestId?: string; + idempotencyKey?: string; + validateResponse?: ResponseValidator; }; ``` @@ -258,6 +266,55 @@ await client.get('/users', { }); ``` +## Idempotency key + +Use `idempotencyKey` for non-idempotent operations that may be retried safely. + +```ts +await client.post( + '/payments', + { amount: 100 }, + { + idempotencyKey: 'payment-123', + }, +); +``` + +This adds: + +```http +idempotency-key: payment-123 +``` + +If both `idempotencyKey` and an explicit `idempotency-key` header are provided, the explicit header takes precedence. + +`POST` and `PATCH` requests are retried only when the method is included in `retry.retryMethods` and the request provides `idempotencyKey`. + +## Response validation + +Use `validateResponse` to validate parsed response data before it is returned. + +```ts +const client = createClient({ + baseUrl: 'https://api.example.com', + validateResponse(data) { + return typeof data === 'object' && data !== null && 'id' in data; + }, +}); +``` + +You can override validation per request: + +```ts +await client.get('/users/1', { + validateResponse(data) { + return typeof data === 'object' && data !== null && 'email' in data; + }, +}); +``` + +Returning `false` throws `ValidationError`. Returning `true` or `undefined` passes validation. + ## Request cancellation Requests can be cancelled using `AbortSignal`: @@ -361,5 +418,6 @@ If the header value is invalid, `@dfsync/client` falls back to normal retry back ## Related guides - See **Hooks** for lifecycle hooks and observability metadata +- See **Response Handling** for parsing and response validation - See **Retry** for retry conditions, backoff, and `Retry-After` - See **Errors** for failure behavior and error types diff --git a/docs/v1/errors.md b/docs/v1/errors.md index 3577e8b..2d14ac0 100644 --- a/docs/v1/errors.md +++ b/docs/v1/errors.md @@ -8,9 +8,10 @@ - `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. +This allows you to handle failures more precisely. ## Base error @@ -114,14 +115,68 @@ try { Properties: -- `code` → `"NETWORK_ERROR"` +- `code` → `"TIMEOUT_ERROR"` - `timeout` - optional `cause` +## ValidationError + +Thrown when a successful response fails `validateResponse`. + +```ts +import { ValidationError } from '@dfsync/client'; + +try { + await client.get('/users/1'); +} catch (error) { + if (error instanceof ValidationError) { + console.error(error.data); + console.error(error.response.status); + } +} +``` + +Properties: + +- `code` → `"VALIDATION_ERROR"` +- `data` +- `response` + +Validation failures are not retried. + +## RequestAbortedError + +Thrown when the request is cancelled by an external `AbortSignal`. + +```ts +import { RequestAbortedError } from '@dfsync/client'; + +const controller = new AbortController(); + +const promise = client.get('/users', { + signal: controller.signal, +}); + +controller.abort(); + +try { + await promise; +} catch (error) { + if (error instanceof RequestAbortedError) { + console.error('Request was cancelled'); + } +} +``` + +Properties: + +- `code` → `"REQUEST_ABORTED"` +- optional `cause` + ## Error handling example ```ts -import { HttpError, NetworkError, TimeoutError } from '@dfsync/client'; +import { HttpError, NetworkError, TimeoutError, ValidationError } from '@dfsync/client'; try { const result = await client.get('/users/1'); @@ -143,6 +198,11 @@ try { throw error; } + if (error instanceof ValidationError) { + console.error('Unexpected response payload:', error.data); + throw error; + } + throw error; } ``` diff --git a/docs/v1/examples.md b/docs/v1/examples.md index cfa6867..4422778 100644 --- a/docs/v1/examples.md +++ b/docs/v1/examples.md @@ -3,8 +3,10 @@ ## Basic client ```ts +import { createClient } from '@dfsync/client'; + const client = createClient({ - baseURL: 'https://api.example.com', + baseUrl: 'https://api.example.com', }); ``` @@ -49,10 +51,53 @@ const singleUser = await client.request({ ```ts const client = createClient({ - baseURL: 'https://api.example.com', + baseUrl: 'https://api.example.com', + auth: { + type: 'bearer', + token: 'TOKEN', + }, +}); +``` + +## Response validation + +```ts +import { ValidationError } from '@dfsync/client'; + +const client = createClient({ + baseUrl: 'https://api.example.com', + validateResponse(data) { + return typeof data === 'object' && data !== null && 'id' in data; + }, +}); + +try { + const user = await client.get('/users/1'); + console.log(user); +} catch (error) { + if (error instanceof ValidationError) { + console.error(error.data); + } +} +``` + +## Safe POST retry - auth: async ({ request }) => { - request.headers.set('Authorization', 'Bearer TOKEN'); +```ts +const client = createClient({ + baseUrl: 'https://api.example.com', + retry: { + attempts: 2, + retryMethods: ['POST'], + retryOn: ['5xx'], }, }); + +const payment = await client.post( + '/payments', + { amount: 100 }, + { + idempotencyKey: 'payment-123', + }, +); ``` diff --git a/docs/v1/getting-started.md b/docs/v1/getting-started.md index 757ef4b..aa3f103 100644 --- a/docs/v1/getting-started.md +++ b/docs/v1/getting-started.md @@ -17,13 +17,15 @@ The client focuses on predictable behavior, extensibility, and a clean developer - request ID propagation (`x-request-id`) - request cancellation via `AbortSignal` - built-in retry with configurable policies -- lifecycle hooks: `beforeRequest`, `afterResponse`, `onError` +- lifecycle hooks: `beforeRequest`, `afterResponse`, `onError`, `onRetry` - typed responses - automatic JSON parsing +- response validation with `ValidationError` - consistent error handling - auth support: bearer, API key, custom +- idempotency key support for safer retries - support for `GET`, `POST`, `PUT`, `PATCH`, and `DELETE` ## Quick example diff --git a/docs/v1/hooks.md b/docs/v1/hooks.md index 5f7726b..0a32ede 100644 --- a/docs/v1/hooks.md +++ b/docs/v1/hooks.md @@ -35,6 +35,8 @@ Retry-specific hooks also expose: - `retryReason` - `retrySource` +`afterResponse` also exposes validation metadata when response validation is configured and passes. + ```ts const client = createClient({ baseUrl: 'https://api.example.com', @@ -90,7 +92,7 @@ const client = createClient({ }); ``` -### Multiple beforeRequest hooks +### Multiple afterResponse hooks ```ts const client = createClient({ @@ -152,10 +154,31 @@ const client = createClient({ If an `afterResponse` hook throws, that hook error is rethrown. +### Validation metadata + +When `validateResponse` is configured and passes, `afterResponse` receives `validation`. + +```ts +const client = createClient({ + baseUrl: 'https://api.example.com', + validateResponse(data) { + return typeof data === 'object' && data !== null && 'id' in data; + }, + hooks: { + afterResponse: ({ validation }) => { + console.log(validation); + // { enabled: true, passed: true } + }, + }, +}); +``` + +If validation is not configured, `validation` is not present. + ### afterResponse context ```text -request, url, headers, signal, attempt, maxAttempts, requestId, startedAt, endedAt, durationMs, response, data +request, url, headers, signal, attempt, maxAttempts, requestId, startedAt, endedAt, durationMs, response, data, validation? ``` ## onError @@ -178,6 +201,8 @@ onError runs for: - HttpError - NetworkError - TimeoutError +- ValidationError +- RequestAbortedError ### Important behavior @@ -234,7 +259,7 @@ request, url, headers, signal, attempt, maxAttempts, requestId, startedAt Additional fields: -- `afterResponse` → `endedAt`, `durationMs`, `response`,`data` +- `afterResponse` → `endedAt`, `durationMs`, `response`, `data`, optional `validation` - `onError` → `endedAt`, `durationMs`, `error` - `onRetry` → `endedAt`, `durationMs`, `error`, `retryDelayMs`, `retryReason`, `retrySource` @@ -246,16 +271,19 @@ Request lifecycle order is: 2. `beforeRequest` 3. fetch execution 4. response parsing -5. `afterResponse` on success +5. response validation, if configured +6. `afterResponse` on success Retry flow: 1. auth 2. `beforeRequest` 3. fetch execution -4. retry decision -5. `onRetry` before the next attempt -6. next retry attempt +4. response parsing +5. response validation, if configured +6. retry decision +7. `onRetry` before the next attempt +8. next retry attempt Failure flow: diff --git a/docs/v1/observability.md b/docs/v1/observability.md index cb78d63..057ced2 100644 --- a/docs/v1/observability.md +++ b/docs/v1/observability.md @@ -7,6 +7,7 @@ Each request exposes: - **requestId** — stable identifier across retries - **attempt / maxAttempts** — retry progress - **startedAt / endedAt / durationMs** — timing information +- **validation** — response validation metadata in `afterResponse`, when validation is configured - **retryReason** — why a retry happened (`network-error`, `5xx`, `429`) - **retryDelayMs** — delay before the next retry - **retrySource** — delay source (`backoff` or `retry-after`) @@ -38,5 +39,6 @@ const client = createClient({ This makes it easier to understand: - what happened during a request +- whether response validation ran and passed - how retries behaved - how long requests actually took diff --git a/docs/v1/response-handling.md b/docs/v1/response-handling.md index e69de29..6c74081 100644 --- a/docs/v1/response-handling.md +++ b/docs/v1/response-handling.md @@ -0,0 +1,145 @@ +# Response Handling + +`@dfsync/client` parses successful and failed HTTP responses before returning data or throwing an error. + +Response handling has three steps: + +1. parse the response body +2. throw `HttpError` for non-2xx responses +3. validate successful response data when `validateResponse` is configured + +## Response parsing + +Responses are parsed automatically: + +- `application/json` responses are parsed with `response.json()` +- other response types are returned as text +- `204 No Content` returns `undefined` + +```ts +const user = await client.get('/users/1'); +``` + +The generic type controls the TypeScript return type. Runtime validation is separate and only runs when you configure `validateResponse`. + +## Failed HTTP responses + +For non-2xx responses, the client parses the response body first and throws `HttpError`. + +```ts +import { HttpError } from '@dfsync/client'; + +try { + await client.get('/users/unknown'); +} catch (error) { + if (error instanceof HttpError) { + console.log(error.status); + console.log(error.data); + } +} +``` + +`validateResponse` does not run for non-2xx responses. + +## Response validation + +Use `validateResponse` when a successful HTTP response still needs a runtime shape check before your application uses it. + +```ts +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'); +``` + +The validator receives parsed response data. + +Validation passes when the validator: + +- returns `true` +- returns `undefined` + +Validation fails when the validator returns `false`. + +Return `false` for expected validation failures. If the validator itself throws, that error follows the normal request error path instead of becoming `ValidationError`. + +## Async validation + +Validators can be async. + +```ts +const client = createClient({ + baseUrl: 'https://api.example.com', + async validateResponse(data) { + return typeof data === 'object' && data !== null && 'id' in data; + }, +}); +``` + +## Request-level validation + +You can override client-level validation for one request. + +```ts +await client.get('/users/1', { + validateResponse(data) { + return typeof data === 'object' && data !== null && 'email' in data; + }, +}); +``` + +Request-level `validateResponse` takes precedence over client-level `validateResponse`. + +## ValidationError + +When validation returns `false`, the client throws `ValidationError`. + +```ts +import { ValidationError } from '@dfsync/client'; + +try { + await client.get('/users/1'); +} catch (error) { + if (error instanceof ValidationError) { + console.log(error.data); + console.log(error.response.status); + } +} +``` + +`ValidationError` includes: + +- `code` -> `"VALIDATION_ERROR"` +- `data` -> parsed response data +- `response` -> original `Response` + +Validation failures are not retried. + +## Hooks and validation + +When response validation is configured and passes, `afterResponse` receives validation metadata. + +```ts +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 } + }, + }, +}); +``` + +If validation is not configured, `ctx.validation` is not present. + +If validation returns `false`, `afterResponse` is not called. The request fails with `ValidationError`, and `onError` runs after the final failure. diff --git a/docs/v1/retry.md b/docs/v1/retry.md index 3273806..edcf8b7 100644 --- a/docs/v1/retry.md +++ b/docs/v1/retry.md @@ -121,9 +121,14 @@ By default retries apply to: - `PUT` - `DELETE` -POST requests are **not retried by default**. +`POST` and `PATCH` requests are **not retried by default**. -Example enabling POST retries: +To retry `POST` or `PATCH`, both conditions must be true: + +- the method is explicitly included in `retry.retryMethods` +- the request provides `idempotencyKey` + +Example enabling safe POST retries: ```ts const client = createClient({ @@ -131,19 +136,35 @@ const client = createClient({ retry: { attempts: 2, retryMethods: ['GET', 'POST'], + retryOn: ['5xx'], }, }); + +await client.post( + '/payments', + { amount: 100 }, + { + idempotencyKey: 'payment-123', + }, +); ``` +The idempotency key is propagated as the `idempotency-key` header. + +If `POST` or `PATCH` is included in `retryMethods` without `idempotencyKey`, the request is not retried. + +Validation failures are not retried. + ## Retry and hooks Hooks behave as follows when retries are enabled: -| Hook | Behavior | -| --------------- | ------------------------------------- | -| `beforeRequest` | executed on every retry attempt | -| `afterResponse` | executed only on successful response | -| `onError` | executed once after the final failure | +| Hook | Behavior | +| --------------- | -------------------------------------- | +| `beforeRequest` | executed on every retry attempt | +| `afterResponse` | executed only on successful response | +| `onRetry` | executed before the next retry attempt | +| `onError` | executed once after the final failure | Example: diff --git a/src/content/docsContent.ts b/src/content/docsContent.ts index df76949..7666b87 100644 --- a/src/content/docsContent.ts +++ b/src/content/docsContent.ts @@ -3,12 +3,14 @@ export const docsContent = { 'getting-started': () => import('../../docs/v1/getting-started.md?raw'), installation: () => import('../../docs/v1/installation.md?raw'), 'create-client': () => import('../../docs/v1/create-client.md?raw'), + 'response-handling': () => import('../../docs/v1/response-handling.md?raw'), auth: () => import('../../docs/v1/auth.md?raw'), hooks: () => import('../../docs/v1/hooks.md?raw'), retry: () => import('../../docs/v1/retry.md?raw'), errors: () => import('../../docs/v1/errors.md?raw'), examples: () => import('../../docs/v1/examples.md?raw'), observability: () => import('../../docs/v1/observability.md?raw'), + 'api-reference': () => import('../../docs/v1/api-reference.md?raw'), }, } as const; diff --git a/src/content/docsNavigation.ts b/src/content/docsNavigation.ts index 7d38132..cbe035c 100644 --- a/src/content/docsNavigation.ts +++ b/src/content/docsNavigation.ts @@ -2,12 +2,14 @@ export const docsNavigation = [ { label: 'Getting Started', slug: 'getting-started' }, { label: 'Installation', slug: 'installation' }, { label: 'Create Client', slug: 'create-client' }, + { label: 'Response Handling', slug: 'response-handling' }, { label: 'Auth', slug: 'auth' }, { label: 'Hooks', slug: 'hooks' }, { label: 'Observability', slug: 'observability' }, { label: 'Retry', slug: 'retry' }, { label: 'Errors', slug: 'errors' }, { label: 'Examples', slug: 'examples' }, + { label: 'API Reference', slug: 'api-reference' }, ] as const; export const defaultDocsVersion = 'v1'; From ce906d0ac717df0b6865f1f1c2ff45124d49e3a1 Mon Sep 17 00:00:00 2001 From: Roman Onishchenko Date: Mon, 27 Apr 2026 18:00:16 +0200 Subject: [PATCH 2/3] add claude files, update readme --- CLAUDE.md | 106 ++++++++++++++++++++++++++++++++++++ README.md | 157 +++++++++++++++++++++++++++++++++--------------------- 2 files changed, 203 insertions(+), 60 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2760e54 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,106 @@ +# CLAUDE.md + +## Project purpose + +This repository is the documentation website for `@dfsync/client`. + +`@dfsync/client` is a Node.js and TypeScript HTTP client for service-to-service communication. It focuses on reliable backend HTTP calls with retry, auth, lifecycle hooks, request metadata, response validation, and structured errors. + +Package source: + +- https://github.com/dfsyncjs/dfsync/tree/main/packages/client + +Documentation website: + +- https://github.com/dfsyncjs/dfsyncjs.github.io + +## Source of truth + +Documentation must match the current release branch or PR in the `dfsync` monorepo. + +Before updating docs for a release, inspect the package source and tests instead of guessing behavior: + +- `packages/client/src/index.ts` +- `packages/client/src/types/*` +- `packages/client/src/errors/*` +- `packages/client/src/core/request.ts` +- `packages/client/src/core/should-retry.ts` +- `packages/client/tests` + +If behavior is unclear, check the release branch or ask before documenting it. + +## Documentation structure + +Markdown docs live in: + +- `docs/v1/*.md` + +Docs are loaded by: + +- `src/content/docsContent.ts` + +Docs navigation is defined in: + +- `src/content/docsNavigation.ts` + +When adding a new markdown page, add it to both `docsContent.ts` and `docsNavigation.ts` unless the page is intentionally hidden. + +## Current docs pages + +- Getting Started +- Installation +- Create Client +- Response Handling +- Auth +- Hooks +- Observability +- Retry +- Errors +- Examples +- API Reference + +## Important API concepts + +Keep these names and behaviors consistent across the docs: + +- `baseUrl`, never `baseURL` +- `createClient` +- `get`, `delete`, `post`, `put`, `patch`, `request` +- auth strategies: `bearer`, `apiKey`, `custom` +- retry config: `attempts`, `retryOn`, `retryMethods`, `backoff`, `baseDelayMs` +- request metadata: `requestId`, `x-request-id` +- idempotency: `idempotencyKey`, `idempotency-key` +- response validation: `validateResponse`, `ResponseValidator`, `ValidationError` +- hooks: `beforeRequest`, `afterResponse`, `onError`, `onRetry` +- errors: `DfsyncError`, `HttpError`, `NetworkError`, `TimeoutError`, `ValidationError`, `RequestAbortedError` + +## Release docs checklist + +When preparing docs for a new `@dfsync/client` release: + +1. Compare the release branch against `main` in `packages/client`. +2. Check public exports from `packages/client/src/index.ts`. +3. Check public config, request, hook, and auth types. +4. Check new or changed error classes. +5. Check tests for behavioral rules. +6. Update relevant docs pages. +7. Update `docsContent.ts` and `docsNavigation.ts` if adding pages. +8. Run verification commands. + +## Local commands + +```bash +npm run dev +npm run build +npm run typecheck +npm run lint +npm run format:check +``` + +## Writing rules + +- Keep docs factual and implementation-aligned. +- Prefer examples that compile against the real public API. +- Do not document behavior unless it is confirmed by source or tests. +- Keep examples focused on backend and service-to-service use cases. +- Keep release notes and docs versioned around `@dfsync/client`, not the full monorepo. diff --git a/README.md b/README.md index c987b94..1a5927f 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,110 @@ -# React + TypeScript + Vite +# dfsyncjs.github.io -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +Documentation website for [`@dfsync/client`](https://github.com/dfsyncjs/dfsync/tree/main/packages/client). -Currently, two official plugins are available: +`@dfsync/client` is a Node.js and TypeScript HTTP client for service-to-service communication. It provides retries, auth, lifecycle hooks, request metadata, response validation, and structured errors. -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +Website repository: -## React Compiler +- https://github.com/dfsyncjs/dfsyncjs.github.io -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). +Package monorepo: -## Expanding the ESLint configuration +- https://github.com/dfsyncjs/dfsync -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: +## Tech stack -```js -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... +- React +- TypeScript +- Vite +- Material UI +- React Router +- `react-markdown` with `remark-gfm` - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, +## Project structure - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]); +```text +docs/v1/ Markdown documentation content +src/content/docsContent.ts Markdown import map +src/content/docsNavigation.ts Docs sidebar/navigation +src/pages/Docs/ Documentation page renderer +src/components/ Shared UI components +src/services/analytics/ Analytics helpers ``` -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x'; -import reactDom from 'eslint-plugin-react-dom'; - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]); +## Documentation pages + +The current `v1` docs include: + +- Getting Started +- Installation +- Create Client +- Response Handling +- Auth +- Hooks +- Observability +- Retry +- Errors +- Examples +- API Reference + +When adding a new markdown file under `docs/v1`, also add it to: + +- `src/content/docsContent.ts` +- `src/content/docsNavigation.ts` + +## Source of truth + +Docs should match the current `@dfsync/client` source in the monorepo: + +- `packages/client/src/index.ts` +- `packages/client/src/types/*` +- `packages/client/src/errors/*` +- `packages/client/src/core/*` +- `packages/client/tests` + +For release documentation, compare the release branch or PR against `main` before editing this site. + +## Development + +Install dependencies: + +```bash +npm install +``` + +Start the dev server: + +```bash +npm run dev +``` + +Build for production: + +```bash +npm run build +``` + +Preview the production build: + +```bash +npm run preview ``` + +## Verification + +Run these before opening or merging documentation changes: + +```bash +npm run format:check +npm run typecheck +npm run lint +npm run build +``` + +## Notes for docs updates + +- Use `baseUrl`, not `baseURL`. +- Keep examples aligned with the public `@dfsync/client` API. +- Do not document behavior unless it is confirmed by source or tests. +- If a behavior is release-specific, check the active release branch or PR. From 552d62b8ce001834fed79c38411738e00396a2d3 Mon Sep 17 00:00:00 2001 From: Roman Onishchenko Date: Mon, 27 Apr 2026 18:57:04 +0200 Subject: [PATCH 3/3] refactor docs structure --- CLAUDE.md | 34 +++++++- README.md | 33 ++++++-- docs/{ => client}/v1/api-reference.md | 0 docs/{ => client}/v1/auth.md | 0 docs/{ => client}/v1/create-client.md | 0 docs/{ => client}/v1/errors.md | 0 docs/{ => client}/v1/examples.md | 0 docs/{ => client}/v1/getting-started.md | 0 docs/{ => client}/v1/hooks.md | 0 docs/{ => client}/v1/installation.md | 0 docs/{ => client}/v1/observability.md | 0 docs/{ => client}/v1/response-handling.md | 0 docs/{ => client}/v1/retry.md | 0 src/app/layout.ts | 3 + src/app/router.tsx | 19 ++++- src/components/Features/Features.tsx | 24 +++++- src/components/Header/Header.tsx | 6 +- src/components/Hero/Hero.tsx | 11 ++- src/components/Problem/Problem.tsx | 11 ++- src/content/docsContent.ts | 51 ++++++++---- src/content/docsNavigation.ts | 22 ++---- src/pages/Docs/DocsPage.tsx | 94 +++++++++++++++++++---- 22 files changed, 240 insertions(+), 68 deletions(-) rename docs/{ => client}/v1/api-reference.md (100%) rename docs/{ => client}/v1/auth.md (100%) rename docs/{ => client}/v1/create-client.md (100%) rename docs/{ => client}/v1/errors.md (100%) rename docs/{ => client}/v1/examples.md (100%) rename docs/{ => client}/v1/getting-started.md (100%) rename docs/{ => client}/v1/hooks.md (100%) rename docs/{ => client}/v1/installation.md (100%) rename docs/{ => client}/v1/observability.md (100%) rename docs/{ => client}/v1/response-handling.md (100%) rename docs/{ => client}/v1/retry.md (100%) create mode 100644 src/app/layout.ts diff --git a/CLAUDE.md b/CLAUDE.md index 2760e54..c918d93 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,17 +33,33 @@ If behavior is unclear, check the release branch or ask before documenting it. Markdown docs live in: -- `docs/v1/*.md` +- `docs///*.md` + +Current `@dfsync/client` docs live in: + +- `docs/client/v1/*.md` Docs are loaded by: - `src/content/docsContent.ts` -Docs navigation is defined in: +Default docs constants are defined in: - `src/content/docsNavigation.ts` -When adding a new markdown page, add it to both `docsContent.ts` and `docsNavigation.ts` unless the page is intentionally hidden. +Package docs navigation and markdown imports are defined in `docsPackages` inside `src/content/docsContent.ts`. + +When adding a new markdown page, add it to the relevant package/version entry in `docsPackages` unless the page is intentionally hidden. + +The default docs package is `client`, so `#/docs` is treated as `@dfsync/client` documentation. + +Canonical docs URLs use: + +- `#/docs///` + +For example: + +- `#/docs/client/v1/getting-started` ## Current docs pages @@ -84,9 +100,19 @@ When preparing docs for a new `@dfsync/client` release: 4. Check new or changed error classes. 5. Check tests for behavioral rules. 6. Update relevant docs pages. -7. Update `docsContent.ts` and `docsNavigation.ts` if adding pages. +7. Update `docsPackages` in `docsContent.ts` if adding pages or packages. 8. Run verification commands. +## Adding another package + +When adding docs for another package: + +1. Create `docs//`. +2. Add the package to `docsPackages` in `src/content/docsContent.ts`. +3. Define package label, default version, default page slug, navigation, and markdown imports. +4. Keep `client` as the default docs package unless the product direction changes. +5. Verify package routes under `#/docs///`. + ## Local commands ```bash diff --git a/README.md b/README.md index 1a5927f..d8bd23d 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,10 @@ Package monorepo: ## Project structure ```text -docs/v1/ Markdown documentation content -src/content/docsContent.ts Markdown import map -src/content/docsNavigation.ts Docs sidebar/navigation +docs/// Markdown documentation content +docs/client/v1/ @dfsync/client v1 documentation +src/content/docsContent.ts Package docs registry and markdown import map +src/content/docsNavigation.ts Default docs package/version constants src/pages/Docs/ Documentation page renderer src/components/ Shared UI components src/services/analytics/ Analytics helpers @@ -48,10 +49,32 @@ The current `v1` docs include: - Examples - API Reference -When adding a new markdown file under `docs/v1`, also add it to: +`#/docs` is the default documentation entry point for `@dfsync/client`. + +Canonical documentation URLs use: + +```text +#/docs/// +``` + +For example: + +```text +#/docs/client/v1/getting-started +``` + +When adding a new markdown file under `docs//`, also add it to the relevant package/version entry in: - `src/content/docsContent.ts` -- `src/content/docsNavigation.ts` + +## Adding package docs + +To add documentation for another package: + +1. Create `docs//`. +2. Add the package to `docsPackages` in `src/content/docsContent.ts`. +3. Define the package label, default version, default page, navigation, and markdown imports. +4. Verify routes under `#/docs///`. ## Source of truth diff --git a/docs/v1/api-reference.md b/docs/client/v1/api-reference.md similarity index 100% rename from docs/v1/api-reference.md rename to docs/client/v1/api-reference.md diff --git a/docs/v1/auth.md b/docs/client/v1/auth.md similarity index 100% rename from docs/v1/auth.md rename to docs/client/v1/auth.md diff --git a/docs/v1/create-client.md b/docs/client/v1/create-client.md similarity index 100% rename from docs/v1/create-client.md rename to docs/client/v1/create-client.md diff --git a/docs/v1/errors.md b/docs/client/v1/errors.md similarity index 100% rename from docs/v1/errors.md rename to docs/client/v1/errors.md diff --git a/docs/v1/examples.md b/docs/client/v1/examples.md similarity index 100% rename from docs/v1/examples.md rename to docs/client/v1/examples.md diff --git a/docs/v1/getting-started.md b/docs/client/v1/getting-started.md similarity index 100% rename from docs/v1/getting-started.md rename to docs/client/v1/getting-started.md diff --git a/docs/v1/hooks.md b/docs/client/v1/hooks.md similarity index 100% rename from docs/v1/hooks.md rename to docs/client/v1/hooks.md diff --git a/docs/v1/installation.md b/docs/client/v1/installation.md similarity index 100% rename from docs/v1/installation.md rename to docs/client/v1/installation.md diff --git a/docs/v1/observability.md b/docs/client/v1/observability.md similarity index 100% rename from docs/v1/observability.md rename to docs/client/v1/observability.md diff --git a/docs/v1/response-handling.md b/docs/client/v1/response-handling.md similarity index 100% rename from docs/v1/response-handling.md rename to docs/client/v1/response-handling.md diff --git a/docs/v1/retry.md b/docs/client/v1/retry.md similarity index 100% rename from docs/v1/retry.md rename to docs/client/v1/retry.md diff --git a/src/app/layout.ts b/src/app/layout.ts new file mode 100644 index 0000000..7bb83f6 --- /dev/null +++ b/src/app/layout.ts @@ -0,0 +1,3 @@ +export const APP_HEADER_HEIGHT = 72; +export const DOCS_SIDEBAR_GAP = 24; +export const DOCS_SIDEBAR_OFFSET = APP_HEADER_HEIGHT + DOCS_SIDEBAR_GAP; diff --git a/src/app/router.tsx b/src/app/router.tsx index a6e2fd0..f269625 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -1,7 +1,11 @@ import { lazy } from 'react'; import { Navigate } from 'react-router-dom'; import type { RouteObject } from 'react-router-dom'; -import { defaultDocsSlug, defaultDocsVersion } from '../content/docsNavigation.ts'; +import { + defaultDocsPackage, + defaultDocsSlug, + defaultDocsVersion, +} from '../content/docsNavigation.ts'; const HomePage = lazy(() => import('../pages/Home/HomePage')); const DocsPage = lazy(() => @@ -18,10 +22,19 @@ export const routes: RouteObject[] = [ }, { path: '/docs', - element: , + element: ( + + ), }, { - path: '/docs/:version/:slug', + path: '/docs/:packageSlug', + element: , + }, + { + path: '/docs/:packageSlug/:version/:slug', element: , }, { diff --git a/src/components/Features/Features.tsx b/src/components/Features/Features.tsx index 7e380b7..28f0819 100644 --- a/src/components/Features/Features.tsx +++ b/src/components/Features/Features.tsx @@ -5,6 +5,8 @@ import ReplayIcon from '@mui/icons-material/Replay'; import DeviceHubIcon from '@mui/icons-material/DeviceHub'; import InsightsIcon from '@mui/icons-material/Insights'; import BoltIcon from '@mui/icons-material/Bolt'; +import FactCheckIcon from '@mui/icons-material/FactCheck'; +import KeyIcon from '@mui/icons-material/Key'; import { Card, CardContent, Container, Grid, Stack, Typography } from '@mui/material'; const items = [ @@ -28,7 +30,19 @@ const items = [ { icon: , title: 'Retry support', - description: 'Built-in retry policies with Retry-After support and full retry visibility..', + description: 'Built-in retry policies with Retry-After support and full retry visibility.', + }, + { + icon: , + title: 'Response validation', + description: + 'Validate successful responses and fail fast with ValidationError when payloads drift.', + }, + { + icon: , + title: 'Idempotency keys', + description: + 'Attach idempotency keys for safer retries of non-idempotent POST and PATCH requests.', }, { icon: , @@ -56,8 +70,9 @@ export const Features = () => { Why @dfsync/client - A lightweight HTTP client with a predictable request lifecycle, built-in retries, and - request observability for service-to-service communication. + A lightweight HTTP client with a predictable request lifecycle, built-in retries, response + validation, idempotency keys, and request observability for service-to-service + communication. @@ -77,7 +92,8 @@ export const Features = () => { ))} - Includes request timing, retry reasons, and stable request IDs across retries. + Includes request timing, retry reasons, validation metadata, and stable request IDs across + retries. ); diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 7c89558..290c997 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -4,6 +4,7 @@ import { Link as RouterLink } from 'react-router-dom'; import { Brand } from '../Brand/Brand'; import { ThemeToggle } from '../ThemeToggle/ThemeToggle'; import { createTrackedLinkHandler } from '../../services/analytics'; +import { APP_HEADER_HEIGHT } from '../../app/layout'; export const Header = () => { return ( @@ -18,7 +19,10 @@ export const Header = () => { }} > - +