From ee588a52d1b7dbf0a9b07f4d4874c633cf6b46fd Mon Sep 17 00:00:00 2001 From: Roman Onishchenko Date: Fri, 13 Mar 2026 18:51:18 +0100 Subject: [PATCH 1/3] docs: update docs --- docs/v1/api-reference.md | 0 docs/v1/auth.md | 214 +++++++++++++++++- docs/v1/create-client.md | 181 ++++++++++++++- docs/v1/errors.md | 174 +++++++++++++- docs/v1/getting-started.md | 49 +++- docs/v1/hooks.md | 197 ++++++++++++++-- docs/v1/installation.md | 27 ++- docs/v1/response-handling.md | 0 .../ProjectBadges/ProjectBadges.tsx | 19 +- 9 files changed, 801 insertions(+), 60 deletions(-) create mode 100644 docs/v1/api-reference.md create mode 100644 docs/v1/response-handling.md diff --git a/docs/v1/api-reference.md b/docs/v1/api-reference.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/v1/auth.md b/docs/v1/auth.md index 003931b..c7df9f0 100644 --- a/docs/v1/auth.md +++ b/docs/v1/auth.md @@ -1,24 +1,222 @@ # Auth -`@dfsync/client` supports auth configuration so you can attach tokens or other credentials to requests. +`@dfsync/client` supports three auth strategies: -This allows you to centralize auth logic instead of repeating it in every request. +- bearer token +- API key +- custom auth -## Example +Auth is configured once at client creation time. + +## Bearer token ```ts -import { createClient } from '@dfsync/client'; +const client = createClient({ + baseUrl: 'https://api.example.com', + auth: { + type: 'bearer', + token: 'secret-token', + }, +}); +``` + +This adds: +```http +authorization: Bearer secret-token +``` + +## Async bearer token + +You can resolve the token lazily: + +```ts const client = createClient({ - baseURL: 'https://api.example.com', + baseUrl: 'https://api.example.com', + auth: { + type: 'bearer', + token: async () => { + return process.env.API_TOKEN!; + }, + }, +}); +``` + +## Custom bearer header name - auth: async ({ request }) => { - request.headers.set('Authorization', 'Bearer TOKEN'); +```ts +const client = createClient({ + baseUrl: 'https://api.example.com', + auth: { + type: 'bearer', + token: 'secret-token', + headerName: 'x-authorization', }, }); ``` -Every request sent by the client will include the Authorization header. +## API key in header + +By default, API key auth uses header mode and the header name `x-api-key`. + +```ts +const client = createClient({ + baseUrl: 'https://api.example.com', + auth: { + type: 'apiKey', + value: 'api-key-123', + }, +}); +``` + +This adds: + +```http +x-api-key: api-key-123 +``` + +## Async API key resolver + +```ts +const client = createClient({ + baseUrl: 'https://api.example.com', + auth: { + type: 'apiKey', + value: async () => { + return process.env.API_KEY!; + }, + }, +}); +``` + +## Custom API key header name + +```ts +const client = createClient({ + baseUrl: 'https://api.example.com', + auth: { + type: 'apiKey', + value: 'api-key-123', + name: 'x-service-key', + }, +}); +``` + +## API key in query string + +```ts +const client = createClient({ + baseUrl: 'https://api.example.com', + auth: { + type: 'apiKey', + value: 'query-key', + in: 'query', + name: 'api_key', + }, +}); +``` + +A request like: + +```ts +await client.get('/users', { + query: { page: 1 }, +}); +``` + +becomes: + +```text +https://api.example.com/users?page=1&api_key=query-key +``` + +## Custom auth + +Use custom auth when you need full control over headers and URL mutation. + +```ts +const client = createClient({ + baseUrl: 'https://api.example.com', + auth: { + type: 'custom', + apply: ({ headers, url, request }) => { + headers['x-service-name'] = 'billing-worker'; + url.searchParams.set('tenant', 'acme'); + }, + }, +}); +``` + +## Auth context + +Custom auth receives: + +```ts +{ + (request, url, headers); +} +``` + +This lets you inspect the request and modify the final URL or headers before the request is sent. + +## Auth execution order + +Auth runs before `beforeRequest` hooks. + +That means `beforeRequest` hooks can see and further modify headers or query params already produced by auth. + +## Auth precedence + +If auth writes to a header that already exists, auth wins. + +Example: + +```ts +const client = createClient({ + baseUrl: 'https://api.example.com', + headers: { + authorization: 'Bearer old-token', + }, + auth: { + type: 'bearer', + token: 'new-token', + }, +}); +``` + +Final header: + +```http +authorization: Bearer new-token +``` + +## Auth config reference + +```ts +type AuthValueResolver = string | (() => string | Promise); + +type BearerAuthConfig = { + type: 'bearer'; + token: AuthValueResolver; + headerName?: string; +}; + +type ApiKeyAuthConfig = { + type: 'apiKey'; + value: AuthValueResolver; + in?: 'header' | 'query'; + name?: string; +}; + +type CustomAuthConfig = { + type: 'custom'; + apply: (ctx: { + request: RequestConfig; + url: URL; + headers: Record; + }) => void | Promise; +}; +``` ## Common use cases diff --git a/docs/v1/create-client.md b/docs/v1/create-client.md index c71ee07..ab79470 100644 --- a/docs/v1/create-client.md +++ b/docs/v1/create-client.md @@ -1,8 +1,8 @@ # Create Client -Use `createClient` to configure a reusable HTTP client. +Use `createClient` to create a reusable HTTP client instance. -## Basic example +## Basic client ```ts import { createClient } from '@dfsync/client'; @@ -12,26 +12,187 @@ const client = createClient({ }); ``` -## Configuration +## Client with timeout ```ts const client = createClient({ - baseURL: 'https://api.example.com', + baseUrl: 'https://api.example.com', timeout: 5000, +}); +``` + +## Client with default headers + +```ts +const client = createClient({ + baseUrl: 'https://api.example.com', headers: { - Accept: 'application/json', + 'x-service-name': 'billing-worker', + }, +}); +``` + +## Client with custom fetch + +```ts +const client = createClient({ + baseUrl: 'https://api.example.com', + fetch: globalThis.fetch, +}); +``` + +## Client configuration + +```ts +type ClientConfig = { + baseUrl: string; + timeout?: number; + headers?: Record; + fetch?: typeof globalThis.fetch; + auth?: { + // see Auth section + }; + hooks?: { + // see Hooks section + }; +}; +``` + +## Supported request methods + +The client provides these convenience methods: + +- client.get(path, options?) +- client.post(path, body?, options?) +- client.put(path, body?, options?) +- client.delete(path, options?) + +It also provides: + +- client.request(config) + +## GET request + +```ts +type User = { + id: string; + name: string; +}; + +const user = await client.get('/users/1'); +``` + +## GET with query params + +```ts +const users = await client.get('/users', { + query: { + page: 1, + active: true, }, }); ``` -## Making requests +## POST request + +```ts +const created = await client.post('/users', { + name: 'Tom', + email: 'tom@example.com', +}); +``` + +## PUT request ```ts -const users = await client.get('/users'); +const updated = await client.put('/users/1', { + name: 'Tom Updated', +}); +``` + +## DELETE request + +```ts +const result = await client.delete('/users/1'); +``` + +## Low-level request API -const createdUser = await client.post('/users', { - name: 'Roman', +```ts +const result = await client.request({ + method: 'POST', + path: '/events', + body: { + type: 'user.created', + }, + headers: { + 'x-request-id': 'req-123', + }, + timeout: 3000, }); ``` -Using a shared client keeps configuration centralized and consistent across requests. +## Request options + +For `get()` and `delete()`: + +```ts +type RequestOptions = { + query?: Record; + headers?: Record; + timeout?: number; +}; +``` + +For `request()`: + +```ts +type RequestConfig = { + method: 'GET' | 'POST' | 'PUT' | 'DELETE'; + path: string; + query?: Record; + body?: unknown; + headers?: Record; + timeout?: number; +}; +``` + +## Header behavior + +Headers are merged in this 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. + +## Timeout behavior + +Timeout is resolved in this order: + +1. request-level timeout +2. client-level timeout +3. default timeout: `5000` + +## Response parsing + +Responses are parsed automatically: + +- `application/json` → parsed JSON +- other content types → text +- `204 No Content` → `undefined` + +## Body behavior + +If request body is an object, the client: + +- serializes it with `JSON.stringify(...)` +- sets `content-type: application/json` only if you did not set it yourself + +If request body is a string, the client: + +- sends it as-is +- does not force a `content-type` diff --git a/docs/v1/errors.md b/docs/v1/errors.md index 3f4356d..555efc2 100644 --- a/docs/v1/errors.md +++ b/docs/v1/errors.md @@ -1,24 +1,180 @@ # Errors -Requests may fail due to different reasons such as HTTP errors, network issues, or timeouts. +`@dfsync/client` throws structured error classes for different failure types. -Use standard `try/catch` handling to handle failures. +## Error classes -## Example +- `DfsyncError` +- `HttpError` +- `NetworkError` +- `TimeoutError` + +## Base error + +All library-specific errors extend `DfsyncError`. + +```ts +import { DfsyncError } from '@dfsync/client'; +``` + +It includes: + +- `message` +- `code` +- optional `cause` + +## HttpError + +Thrown when the server returns a non-2xx response. + +```ts +import { HttpError } from '@dfsync/client'; + +try { + await client.get('/users/999'); +} catch (error) { + if (error instanceof HttpError) { + console.log(error.status); + console.log(error.statusText); + console.log(error.data); + } +} +``` + +Properties: + +- `code` → `"HTTP_ERROR"` +- `status` +- `statusText` +- `data` +- `response` + +Example use: + +```ts +try { + await client.get('/users/999'); +} catch (error) { + if (error instanceof HttpError) { + if (error.status === 404) { + return null; + } + + if (error.status === 401) { + throw new Error('Unauthorized'); + } + + throw error; + } + + throw error; +} +``` + +## NetworkError + +Thrown when `fetch` fails before a valid HTTP response is received. + +```ts +import { NetworkError } from '@dfsync/client'; + +try { + await client.get('/users'); +} catch (error) { + if (error instanceof NetworkError) { + console.error(error.message); + console.error(error.cause); + } +} +``` + +Properties: + +- `code` → `"NETWORK_ERROR"` +- optional `cause` + +## TimeoutError + +Thrown when the request is aborted because it exceeded the configured timeout. ```ts +import { TimeoutError } from '@dfsync/client'; + try { - const users = await client.get('/users'); + await client.get('/slow-endpoint'); } catch (error) { - console.error(error); + if (error instanceof TimeoutError) { + console.error(error.timeout); + } } ``` -## Typical error types +Properties: + +- `code` → `"NETWORK_ERROR"` +- `timeout` +- optional `cause` + +## Error handling example + +```ts +import { HttpError, NetworkError, TimeoutError } from '@dfsync/client'; + +try { + const result = await client.get('/users/1'); + return result; +} catch (error) { + if (error instanceof TimeoutError) { + console.error('Request timed out'); + throw error; + } + + if (error instanceof NetworkError) { + console.error('Network failure'); + throw error; + } + + if (error instanceof HttpError) { + console.error('HTTP status:', error.status); + console.error('Response payload:', error.data); + throw error; + } + + throw error; +} +``` + +## How response bodies are exposed in errors + +For failed HTTP responses, the client parses the body first and stores it on `HttpError.data`. + +That means if the server responds with JSON: + +```json +{ "message": "Bad Request" } +``` + +you can access it as: + +```ts +if (error instanceof HttpError) { + console.log(error.data); +} +``` + +## What is not wrapped + +Errors thrown inside: + +- custom auth + +- `beforeRequest` + +- `afterResponse` + +are rethrown as-is. -- HTTP errors (non-2xx responses) -- network errors -- timeout errors +They are not converted into `DfsyncError` subclasses. ## Note diff --git a/docs/v1/getting-started.md b/docs/v1/getting-started.md index 589bd66..6a8d3e6 100644 --- a/docs/v1/getting-started.md +++ b/docs/v1/getting-started.md @@ -11,22 +11,55 @@ It provides sensible defaults for: The client focuses on predictable behavior, extensibility, and a clean developer experience. +## What you get + +- typed responses +- simple client creation +- request timeout support +- automatic JSON parsing +- text response support +- structured error classes +- auth support +- lifecycle hooks +- custom `fetch` support + ## Quick example ```ts import { createClient } from '@dfsync/client'; +type User = { + id: string; + name: string; +}; + const client = createClient({ - baseURL: 'https://api.example.com', + baseUrl: 'https://api.example.com', + timeout: 5000, }); -const users = await client.get('/users'); +const user = await client.get('/users/1'); + +console.log(user.id); +console.log(user.name); ``` -## Features +## How requests work -- simple HTTP client API -- built-in auth support -- lifecycle hooks -- consistent error handling -- good defaults for service communication +A request in `@dfsync/client` follows this flow: + +1. build final URL from `baseUrl`, `path`, and optional query params +2. merge default, client-level, and request-level headers +3. apply auth configuration +4. run `beforeRequest` hooks +5. send request with `fetch` +6. parse response as JSON, text, or `undefined` for `204` +7. throw structured errors for failed requests +8. run `afterResponse` or `onError` hooks + +## Runtime requirements + +- Node.js >= 20 +- a working fetch implementation + +If you do not pass a custom `fetch`, the client uses `globalThis.fetch`. diff --git a/docs/v1/hooks.md b/docs/v1/hooks.md index 31f5770..567eb8a 100644 --- a/docs/v1/hooks.md +++ b/docs/v1/hooks.md @@ -2,39 +2,206 @@ Hooks allow you to extend the behavior of the HTTP client during the request lifecycle. -They can be used to implement cross-cutting concerns such as logging, tracing, or instrumentation. +Supported hooks: -## Example +- `beforeRequest` +- `afterResponse` +- `onError` + +Each hook can be: + +- a single function +- an array of functions + +Hooks run sequentially in the order you provide them. + +## beforeRequest + +Use `beforeRequest` to mutate headers or the final request URL before `fetch` is called. + +### Add headers ```ts -import { createClient } from '@dfsync/client'; +const client = createClient({ + baseUrl: 'https://api.example.com', + hooks: { + beforeRequest: ({ headers }) => { + headers['x-request-id'] = crypto.randomUUID(); + }, + }, +}); +``` + +### Modify query params +```ts const client = createClient({ - baseURL: 'https://api.example.com', + baseUrl: 'https://api.example.com', + hooks: { + beforeRequest: ({ url }) => { + url.searchParams.set('trace', '1'); + }, + }, +}); +``` + +### Async beforeRequest + +```ts +const client = createClient({ + baseUrl: 'https://api.example.com', + hooks: { + beforeRequest: async ({ headers }) => { + const token = await Promise.resolve('abc'); + headers['x-async-token'] = token; + }, + }, +}); +``` + +### Multiple beforeRequest hooks + +```ts +const client = createClient({ + baseUrl: 'https://api.example.com', hooks: { beforeRequest: [ - async (ctx) => { - console.log('Outgoing request:', ctx.request.url); + ({ headers }) => { + headers['x-step-1'] = 'done'; + }, + ({ headers }) => { + headers['x-step-2'] = `${headers['x-step-1']}-next`; }, ], + }, +}); +``` + +If one `beforeRequest` hook throws, the request is not sent and the original error is rethrown. + +## afterResponse + +Use `afterResponse` to inspect successful responses after parsing. + +```ts +const client = createClient({ + baseUrl: 'https://api.example.com', + hooks: { + afterResponse: ({ response, data }) => { + console.log(response.status); + console.log(data); + }, + }, +}); +``` + +### Multiple beforeRequest hooks + +```ts +const client = createClient({ + baseUrl: 'https://api.example.com', + hooks: { afterResponse: [ - async (ctx) => { - console.log('Response status:', ctx.response.status); + ({ data }) => { + console.log('first hook', data); + }, + () => { + console.log('second hook'); }, ], }, }); ``` -## Common use cases +If an `afterResponse` hook throws, that hook error is rethrown. + +## onError + +Use `onError` to observe failed requests. + +```ts +const client = createClient({ + baseUrl: 'https://api.example.com', + hooks: { + onError: ({ error }) => { + console.error('request failed', error); + }, + }, +}); +``` + +onError runs for: + +- HttpError +- NetworkError +- TimeoutError + +### Important behavior + +If an `onError` hook itself throws, the original request error is preserved. + +This is intentional, so hook failures never hide the real request failure. + +## Hook context + +### beforeRequest context + +```ts +{ + (request, url, headers); +} +``` + +### afterResponse context + +```ts +{ + (request, url, headers, response, data); +} +``` + +### onError context + +```ts +{ + (request, url, headers, error); +} +``` + +## Hook order + +Request lifecycle order is: -Hooks can be used for: +1. auth +2. `beforeRequest` +3. fetch +4. response parsing +5. `afterResponse` on success +6. `onError` on failure -- request logging -- tracing and correlation IDs -- metrics and observability -- debugging outgoing requests -- modifying requests before they are sent +## Hook config example + +```ts +const client = createClient({ + baseUrl: 'https://api.example.com', + hooks: { + beforeRequest: [ + ({ headers }) => { + headers['x-service'] = 'gateway'; + }, + ({ headers }) => { + headers['x-request-id'] = crypto.randomUUID(); + }, + ], + afterResponse: ({ response }) => { + console.log('status:', response.status); + }, + onError: ({ error }) => { + console.error(error); + }, + }, +}); +``` ## Note diff --git a/docs/v1/installation.md b/docs/v1/installation.md index 008cba4..f76790c 100644 --- a/docs/v1/installation.md +++ b/docs/v1/installation.md @@ -2,25 +2,34 @@ Install the package with your preferred package manager. -## npm +Install the package from npm: ```bash npm install @dfsync/client ``` +or with yarn: + +```bash +yarn add @dfsync/client +``` + ## Requirements -- Node.js 18+ -- TypeScript (recommended) +- Node.js `>= 20` -## After installation +## ESM / CJS -Create your first client instance: +The package ships with both import and require entry points, plus TypeScript types. -```typescript +## ESM + +```ts import { createClient } from '@dfsync/client'; +``` + +## CommonJS -const client = createClient({ - baseURL: 'https://api.example.com', -}); +```javascript +const { createClient } = require('@dfsync/client'); ``` diff --git a/docs/v1/response-handling.md b/docs/v1/response-handling.md new file mode 100644 index 0000000..e69de29 diff --git a/src/components/ProjectBadges/ProjectBadges.tsx b/src/components/ProjectBadges/ProjectBadges.tsx index 61a217b..09dc19d 100644 --- a/src/components/ProjectBadges/ProjectBadges.tsx +++ b/src/components/ProjectBadges/ProjectBadges.tsx @@ -38,7 +38,23 @@ export const ProjectBadges = () => { /> - {/* LICENSE */} + {/* github stars */} + + + + + {/* LICENSE { sx={{ height: 20 }} /> + */} ); }; From 55849b5b39631d9a21079dda64781f3b308fcd9b Mon Sep 17 00:00:00 2001 From: Roman Onishchenko Date: Fri, 13 Mar 2026 18:56:18 +0100 Subject: [PATCH 2/3] update docs --- docs/v1/auth.md | 4 +++- docs/v1/hooks.md | 15 ++++++++++++--- package.json | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/v1/auth.md b/docs/v1/auth.md index c7df9f0..c0f5ceb 100644 --- a/docs/v1/auth.md +++ b/docs/v1/auth.md @@ -153,7 +153,9 @@ Custom auth receives: ```ts { - (request, url, headers); + request, + url, + headers } ``` diff --git a/docs/v1/hooks.md b/docs/v1/hooks.md index 567eb8a..a8ab5c4 100644 --- a/docs/v1/hooks.md +++ b/docs/v1/hooks.md @@ -148,7 +148,9 @@ This is intentional, so hook failures never hide the real request failure. ```ts { - (request, url, headers); + request, + url, + headers } ``` @@ -156,7 +158,11 @@ This is intentional, so hook failures never hide the real request failure. ```ts { - (request, url, headers, response, data); + request, + url, + headers, + response, + data } ``` @@ -164,7 +170,10 @@ This is intentional, so hook failures never hide the real request failure. ```ts { - (request, url, headers, error); + request, + url, + headers, + error; } ``` diff --git a/package.json b/package.json index 881fef7..858c313 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "eslint --fix", "prettier --write" ], - "*.{json,css,md,yml,yaml}": [ + "*.{json,css,yml,yaml}": [ "prettier --write" ] } From 24dce1fb56db5034ff2b8ad418f91555bd3bdef2 Mon Sep 17 00:00:00 2001 From: Roman Onishchenko Date: Fri, 13 Mar 2026 19:02:46 +0100 Subject: [PATCH 3/3] prettier fixes --- docs/v1/auth.md | 8 ++------ docs/v1/hooks.md | 27 ++++++--------------------- package.json | 2 +- 3 files changed, 9 insertions(+), 28 deletions(-) diff --git a/docs/v1/auth.md b/docs/v1/auth.md index c0f5ceb..ff75606 100644 --- a/docs/v1/auth.md +++ b/docs/v1/auth.md @@ -151,12 +151,8 @@ const client = createClient({ Custom auth receives: -```ts -{ - request, - url, - headers -} +```text +request, url, headers ``` This lets you inspect the request and modify the final URL or headers before the request is sent. diff --git a/docs/v1/hooks.md b/docs/v1/hooks.md index a8ab5c4..27659e0 100644 --- a/docs/v1/hooks.md +++ b/docs/v1/hooks.md @@ -146,35 +146,20 @@ This is intentional, so hook failures never hide the real request failure. ### beforeRequest context -```ts -{ - request, - url, - headers -} +```text +request, url, headers ``` ### afterResponse context -```ts -{ - request, - url, - headers, - response, - data -} +```text +request, url, headers, response, data ``` ### onError context -```ts -{ - request, - url, - headers, - error; -} +```text +request, url, headers, error ``` ## Hook order diff --git a/package.json b/package.json index 858c313..881fef7 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "eslint --fix", "prettier --write" ], - "*.{json,css,yml,yaml}": [ + "*.{json,css,md,yml,yaml}": [ "prettier --write" ] }