A small, framework-agnostic TypeScript HTTP client built on the native fetch
API. It is designed as an HTTP port and adapter for applications that use ports
and adapters / hexagonal architecture.
Application services depend on the HttpClient contract. FetchHttpClient is
the official native-fetch adapter. Each client instance owns its backend URL,
headers, timeout, and injected fetch implementation, so one application can use
multiple independently configured backends without global mutable state.
The package is prepared for public npm publication but has not been published yet.
Application or domain adapter
|
v
HttpClient port
|
v
FetchHttpClient adapter
|
v
Native fetch
Domain services receive an HttpClient and make relative requests. They do not
construct backend origins or depend directly on native fetch.
HttpClientport withrequest,get,post,put,patch, anddelete.- Instance-based
FetchHttpClientconfiguration. - Relative URL resolution against a configured
baseUrl. - Absolute URL support when passed deliberately.
- Default and per-request headers.
- Primitive and repeated-key query serialization.
- JSON serialization for object request bodies.
- Preservation of native bodies such as
FormData,Blob,ArrayBuffer,URLSearchParams, strings, and streams. - Automatic JSON, text, blob, empty-body,
204, and205response parsing. - Parsed response data plus the original
Response. - Distinct
HttpError,NetworkError,TimeoutError, andParseErrortypes. - Default and per-request timeouts.
- Caller-provided cancellation signals.
- Cross-origin credential protection for absolute URLs.
- Ordered, application-defined interceptors with immutable request transforms.
- Request metadata for transport-adjacent interceptor state.
- Injected
fetchsupport for tests and alternative runtimes. - Browser and modern Node.js compatibility through platform APIs.
Authentication, token storage, refresh behavior, retry policy, caching, and framework integrations remain outside this package.
Install dependencies, build, and run the test suite:
npm install
npm run build
npm testAfter publication, consumers will install the package with:
npm install @mikode/fetch-http-clientAvailable package commands:
| Command | Purpose |
|---|---|
npm run build |
Compile TypeScript into dist/. |
npm run check |
Run every local publication quality gate. |
npm run format |
Format supported project files with Prettier. |
npm run lint |
Check source and configuration files with ESLint. |
npm run package:check |
Build and validate the published package structure. |
npm test |
Run the unit test suite once. |
npm run test:watch |
Run tests in watch mode. |
npm run typecheck |
Type-check without emitting build output. |
npm run agents:setup |
Install/validate the project agent harness. |
npm run agents:doctor |
Diagnose the project agent harness without installing. |
The npm manifest uses a files allowlist. Publishing runs the TypeScript build
through prepack, and consumers receive only the compiled runtime package:
dist/ # JavaScript and TypeScript declarations
LICENSE # MIT license
package.json # npm metadata and public entry point
README.md # Package usage documentation
Development-only material is excluded from the npm tarball, including src/,
tests/, .agents/, .codex, .claude, scripts/, AGENTS.md, TypeScript
configuration, Vitest configuration, and development dependencies. There is no
consumer-facing postinstall hook.
import { FetchHttpClient } from '@mikode/fetch-http-client';
interface UserDto {
id: string;
name: string;
}
const client = new FetchHttpClient({
baseUrl: 'https://api.example.com/v1/',
headers: {
Accept: 'application/json',
},
timeout: 10_000,
});
const response = await client.get<UserDto>('users/1');
console.log(response.data.name);
console.log(response.status);
console.log(response.headers);
console.log(response.raw);import type { HttpClient } from '@mikode/fetch-http-client';
interface UserDto {
id: string;
name: string;
}
class UsersApi {
public constructor(private readonly httpClient: HttpClient) {}
public async getUser(userId: string): Promise<UserDto> {
const response = await this.httpClient.get<UserDto>(`users/${userId}`);
return response.data;
}
}const usersClient = new FetchHttpClient({
baseUrl: 'https://users.example.com/api/',
});
const billingClient = new FetchHttpClient({
baseUrl: 'https://billing.example.com/api/',
});
const usersApi = new UsersApi(usersClient);
const billingApi = new BillingApi(billingClient);Each API service continues to use relative paths and does not know the backend origin.
await client.get<UserDto[]>('users', {
query: {
page: 2,
active: true,
roles: ['admin', 'editor'],
optional: undefined,
empty: null,
},
});This produces:
?page=2&active=true&roles=admin&roles=editor&empty=
undefined values are omitted, null becomes an empty value, booleans become
true or false, and arrays become repeated keys. Nested query objects are not
supported.
interface CreateUserInput {
name: string;
}
const response = await client.post<UserDto, CreateUserInput>('users', {
name: 'Ada',
});Plain objects are serialized as JSON. Content-Type: application/json is added
unless the request already supplies a content type.
const controller = new AbortController();
const request = client.get<UserDto>('users/1', {
headers: { 'X-Trace-Id': 'trace-123' },
credentials: 'include',
timeout: 5_000,
signal: controller.signal,
});
controller.abort();
await request;Native AbortError values from caller cancellation are rethrown. A timeout
raised by the client becomes TimeoutError. The timeout remains active until
the response body has been consumed and parsed.
const text = await client.get<string>('health', {
responseType: 'text',
});
const file = await client.get<Blob>('reports/latest', {
responseType: 'blob',
});Without an explicit type, JSON content types are parsed as JSON, text/* as
text, and other content as a blob. Empty bodies and 204 or 205 responses
return undefined data.
Interceptors are generic middleware supplied when a client is created. They can
transform an immutable HttpRequest, inspect or replace responses, handle
errors, return synthetic responses, or implement application-defined retries.
import type { HttpInterceptor } from '@mikode/fetch-http-client';
const traceInterceptor: HttpInterceptor = {
async intercept(request, next) {
return next(request.withHeader('X-Trace-Id', 'trace-123'));
},
};
const client = new FetchHttpClient({
baseUrl: 'https://api.example.com/',
interceptors: [traceInterceptor],
});For [first, second], execution is nested and deterministic: first runs
before second, then transport runs, followed by second and first after
transport resolves. Calling next(request) runs only downstream interceptors
and transport. This permits a bounded retry interceptor to call next again
without re-entering itself or upstream middleware. The package does not add an
automatic retry policy.
request.withHeader(name, value), request.clone(changes), and
request.withMetadata(entries) return new requests. Metadata is copied through
these transforms and is never serialized into a URL, headers, or body. It is
intended for local middleware state such as a retry marker:
const retryMarker = request.withMetadata({ retried: true });Native Headers access is defensive: mutating request.headers does not
mutate that request. Use the immutable helper methods to pass a changed request
downstream.
URL composition is centralized and intentionally distinguishes paths with and without a leading slash:
baseUrl: https://api.example.com/v1/
path: users
result: https://api.example.com/v1/users
baseUrl: https://api.example.com/v1/
path: /users
result: https://api.example.com/users
A leading slash replaces the base URL path. A path without a leading slash is appended to it. Existing base URL query parameters are preserved and request query parameters are appended.
Absolute URLs are accepted. When an absolute URL has a different origin from
the configured baseUrl, or when no trusted baseUrl is configured, the
client removes Authorization, Proxy-Authorization, and Cookie headers and
resets the request credentials mode. Other headers are preserved.
If a dedicated client intentionally sends credentials to multiple origins, it must opt in explicitly:
const client = new FetchHttpClient({
baseUrl: 'https://api.example.com/',
allowCrossOriginCredentials: true,
});This opt-in applies to the whole client, so use it only with trusted absolute
URLs. Prefer a separate FetchHttpClient instance for each backend.
import { HttpError, NetworkError, ParseError, TimeoutError } from '@mikode/fetch-http-client';
try {
await client.get<UserDto>('users/1');
} catch (error) {
if (error instanceof HttpError) {
console.error(error.status, error.data);
} else if (error instanceof TimeoutError) {
console.error('The request exceeded its timeout.');
} else if (error instanceof NetworkError) {
console.error('No response was obtained.', error.cause);
} else if (error instanceof ParseError) {
console.error('The response could not be parsed.', error.cause);
} else {
// Includes caller-initiated AbortError values.
throw error;
}
}Non-success responses are parsed before HttpError is created, allowing a
consumer to inspect backend error data through error.data. Response parsing
consumes the native body stream, including the raw response body; use the
parsed data value for body access.
npm run check is the local and CI quality gate. It verifies formatting,
linting, TypeScript, tests, and the npm package structure. prepublishOnly runs
the same gate, while every build removes stale dist/ output before compiling.
GitHub Actions runs the gate on supported Node.js release lines. Publishing is
triggered by a published GitHub release and uses npm trusted publishing, which
adds provenance automatically without a long-lived npm token. Create the npm
package with an initial manual publish, then configure mikode13/fetch,
publish.yml, the npm environment, and the npm publish action as the
package's trusted publisher. Protect the GitHub npm environment if release
approval is desired. See npm's
trusted publishing guide for the
registry-side setup.
fetch can be injected for unit tests or compatible runtimes:
const fetchMock: typeof globalThis.fetch = async () =>
new Response(JSON.stringify({ id: '1', name: 'Ada' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
const client = new FetchHttpClient({
baseUrl: 'https://api.example.com/',
fetch: fetchMock,
});The implementation uses globalThis.fetch only when no implementation is
provided.
The repository includes a project-scoped multi-agent development harness. Its
canonical definitions live under .agents/:
.agents/
├── codex/
│ ├── config.toml
│ └── agents/
│ ├── explorer.toml
│ └── implementer.toml
└── claude/
└── agents/
└── verifier.md
.codex -> .agents/codex
.claude/agents -> ../.agents/claude/agents
The tool-specific discovery paths are symbolic links on POSIX systems and may
be junctions on Windows. .agents/ remains the single canonical location.
| Agent | Tool | Responsibility | Access |
|---|---|---|---|
| Root orchestrator | Codex | Requirements, architecture, plan, coordination, and acceptance | Workspace |
explorer |
Codex | Bounded codebase investigation and evidence gathering | Read-only |
implementer |
Codex | Bounded implementation of an approved plan | Workspace write |
verifier |
Claude Code | Independent review of the diff against the plan and AGENTS.md |
Read-only/plan |
Only independent read-heavy work should run in parallel. Agents that edit the shared working tree are serialized.
Current official installers for macOS and Linux:
curl -fsSL https://chatgpt.com/codex/install.sh | sh
curl -fsSL https://claude.ai/install.sh | bashWindows PowerShell:
powershell -ExecutionPolicy ByPass -c "irm https://chatgpt.com/codex/install.ps1 | iex"
irm https://claude.ai/install.ps1 | iexHomebrew on macOS:
brew install --cask codex
brew install --cask claude-codeInstallation references:
Each machine must install and authenticate both tools. Credentials are stored locally and must never be committed. Launch each CLI once to complete sign-in:
codex
claudeThe repository can install missing CLIs, create missing discovery paths, and run the doctor automatically:
npm run agents:setupThe setup process never overwrites an existing discovery path. Conflicts are left untouched and reported with guidance. It finishes by running the doctor as a post-setup hook.
The project deliberately does not use the package postinstall lifecycle. A
normal dependency installation must not install unrelated developer tools or
affect downstream consumers.
Run diagnostics without installing anything:
npm run agents:doctorThe doctor checks:
- Whether
codexandclaudeare available throughPATH. - CLI versions when available.
AGENTS.mdand all canonical agent definitions.- Codex and Claude project discovery paths.
- Whether discovery paths can drift from the canonical
.agents/directory. - The next authentication step for each installed CLI.
It exits non-zero when a required item is missing, so it can also validate a prepared development machine or CI image.
Platform scripts can be called directly:
./scripts/setup-agents.sh
./scripts/doctor-agents.sh.\scripts\setup-agents.ps1
.\scripts\doctor-agents.ps1See scripts/README.md for the focused setup reference.
No slash command is required. Start Codex in this repository and explicitly ask it to use the project harness:
Use the project-scoped multi-agent harness for this non-trivial task.
Task:
<describe the requested change>
Acceptance criteria:
- <observable requirement>
- <tests or edge cases>
Workflow:
1. Delegate bounded read-only investigation to the explorer.
2. Establish a concrete implementation plan.
3. Delegate the approved plan to one implementer.
4. Review the diff and run the relevant checks.
5. Prepare the diff and approved plan for the independent verifier.
To let the root orchestrator continue without pausing for plan approval, add:
You may approve the implementation plan and continue without waiting for me.
For small, low-risk changes, the root agent may work directly when delegation would add more coordination than value.
The verifier runs separately in Claude Code after implementation. Start Claude Code in the repository and send:
Use the verifier agent to review the current diff against AGENTS.md and this
approved implementation plan:
<paste the approved plan>
The verifier receives only the resulting diff, AGENTS.md, and the approved
plan. It does not receive or reconstruct the implementer's private reasoning.
The harness works when the repository is cloned or opened as the active project
on a supported machine. Installing a CLI once makes its command available from
any terminal where its installation directory is in PATH, but project agents
are discovered only when the CLI runs in this repository.
On Windows, Git may require Developer Mode or appropriate symlink support to preserve symbolic links. The PowerShell setup uses directory junctions when a discovery path is absent. It does not remove or replace an existing file, directory, link, or junction.
src/
├── client/ # Native fetch adapter
├── contracts/ # Public HTTP port and request/response contracts
├── errors/ # Public error types
└── utils/ # Internal URL, query, header, and parsing helpers
tests/ # Unit tests using injected fetch implementations
.agents/ # Canonical project agent definitions
scripts/ # Cross-platform harness setup and doctor tools
Internal helpers are intentionally not exported from the package root.
This package is generic HTTP transport infrastructure. It must not contain:
- Domain DTOs or business models.
- Authentication or token refresh policies.
- Token storage or login/logout behavior.
- React, Vue, Angular, Next.js, or NestJS integration.
- A dependency injection container or service locator.
- Server-state caching, deduplication, or background refetching.
- Automatic retries of unsafe requests.
- Global backend configuration.
Application-specific policies can be composed outside the package and injected through public contracts as the package evolves.
Read AGENTS.md before changing the implementation. A completed
change must preserve strict TypeScript, the HttpClient/FetchHttpClient
boundary, instance-based configuration, injected-fetch testability, explicit
error behavior, documentation, and passing build/tests.