npm package: @paxe/hxp
Description: Tiny, typed HTTP client built on native fetch
Latest version: 0.1.0

# hxp

Tiny, fast HTTP client for JavaScript & TypeScript. Built on native `fetch`.

```
npm install hxp
```

**2.7 KB** gzipped core. Zero dependencies. Faster than axios and ky.

```ts
import { createClient } from 'hxp'

const api = createClient({
  baseURL: 'https://api.example.com',
  headers: { Authorization: 'Bearer token' },
})

const users = await api.get('/users')
const user = await api.post('/users', { body: { name: 'Alice' } })
```

## Features

- **TypeScript-first** with full type inference
- **Auto JSON** — serialize request bodies, parse responses
- **Typed errors** — `HttpError`, `TimeoutError`, `NetworkError`, `ParseError`
- **Retries** with linear/exponential backoff
- **Timeout** per-request and global, via `AbortController`
- **Middleware** pipeline — request, response, error, and fetch-level hooks
- **Deduplication** — identical in-flight GETs share one response
- **Schema validation** — plug in Zod, Valibot, or any `.parse()` compatible validator
- **Debug mode** — built-in request logging and cURL output
- **Response metadata** — timing, status, headers via `meta: true`
- **Better stack traces** — errors point to your code, not library internals
- **SSE & streaming** — Server-Sent Events parser and async text streams
- **Tree-shakeable** — ESM + CJS, split into `hxp`, `hxp/plugins`, `hxp/streaming`

**Runs everywhere:** Node 18+, Bun, Deno, Cloudflare Workers, browsers.

## API

### `createClient(config?)`

```ts
const api = createClient({
  baseURL: 'https://api.example.com',
  timeout: 5000,                          // ms, default 30000
  retry: { count: 3, backoff: 'exponential' },
  headers: { Authorization: 'Bearer ...' },
  debug: true,                            // or 'curl'
})
```

### Methods

```ts
api.get<T>(path, config?)      // GET
api.post<T>(path, config?)     // POST
api.put<T>(path, config?)      // PUT
api.patch<T>(path, config?)    // PATCH
api.delete<T>(path, config?)   // DELETE
api.head(path, config?)        // HEAD
```

### Request config

```ts
await api.get('/users', {
  params: { page: 1, active: true },     // → ?page=1&active=true
  headers: { 'X-Custom': 'value' },
  timeout: 3000,
  retry: { count: 2, delay: 500, backoff: 'exponential' },
  signal: controller.signal,              // AbortController
  schema: UserSchema,                     // Zod / Valibot
  meta: true,                            // return { data, duration, status, headers }
  debug: 'curl',                          // log equivalent curl command
})
```

### Response metadata

```ts
const { data, duration, status, headers } = await api.get('/users', { meta: true })
// duration: 42.17 (ms)
```

### Error handling

```ts
import { HttpError, TimeoutError, NetworkError, ParseError } from 'hxp'

try {
  await api.get('/users')
} catch (e) {
  if (e instanceof HttpError) {
    console.log(e.status)    // 404
    console.log(e.data)      // parsed response body
  }
  if (e instanceof TimeoutError) {
    console.log(e.timeout)   // 5000
  }
}
```

All errors include `requestStack` pointing to your call site, not library internals.

### cURL output

```ts
await api.post('/users', {
  body: { name: 'Alice' },
  debug: 'curl',
})
// → curl -X POST -H 'content-type: application/json' -d '{"name":"Alice"}' 'https://api.example.com/users'
```

Or generate it programmatically:

```ts
import { toCurl } from 'hxp'
```

## Plugins

```ts
import { retry, dedupe, logger, validate } from 'hxp/plugins'
```

### Retry

```ts
api.use(retry({ count: 3, backoff: 'exponential', delay: 500 }))
```

Retries on 5xx, 429, timeout, and network errors. Skips 4xx.

### Deduplication

```ts
api.use(dedupe())
```

Concurrent identical GET/HEAD requests share a single fetch. Each caller gets an independent clone.

### Logger

```ts
api.use(logger())
// → GET https://api.example.com/users
// ← 200 GET https://api.example.com/users

api.use(logger({ log: myLogger }))
```

### Schema validation

```ts
import { z } from 'zod'

const User = z.object({ id: z.number(), name: z.string() })

api.use(validate(User))  // validate every response

// or per-request:
const user = await api.get('/users/1', { schema: User })
```

Works with any library that exposes `.parse(data)` — Zod, Valibot, ArkType, custom validators.

## Middleware

```ts
api.use({
  name: 'auth',
  onRequest(config) {
    config.headers.set('Authorization', `Bearer ${getToken()}`)
    return config
  },
  onResponse(response, request) {
    // transform or inspect
    return response
  },
  onError(error, request) {
    // log, transform, or re-throw
    return error
  },
  onFetch(request, next) {
    // wrap the actual fetch call (used by dedupe plugin)
    return next(request)
  },
})
```

Middleware runs in registration order. All hooks are optional and support `async`.

## Streaming

```ts
import { parseSSE, decodeStream } from 'hxp/streaming'
```

### Server-Sent Events (SSE)

```ts
const response = await fetch('https://api.openai.com/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: 'Bearer sk-...',
  },
  body: JSON.stringify({
    model: 'gpt-4',
    messages: [{ role: 'user', content: 'Hello' }],
    stream: true,
  }),
})

for await (const event of parseSSE(response.body)) {
  if (event.data === '[DONE]') break
  const chunk = JSON.parse(event.data)
  process.stdout.write(chunk.choices[0].delta.content ?? '')
}
```

Spec-compliant SSE parser. Handles `event:`, `data:` (multi-line), `id:`, `retry:`, comments, and chunked delivery split across boundaries.

### Raw text streaming

```ts
const response = await fetch('https://example.com/large-file')

for await (const chunk of decodeStream(response.body)) {
  process.stdout.write(chunk)
}
```

## Benchmarks

5000 iterations, 500 warmup, local HTTP server (Node 22):

| Test | hxp | ky | axios | native fetch |
|---|---|---|---|---|
| **GET small JSON** | **0.059ms** | 0.061ms | 0.057ms | 0.051ms |
| **GET large JSON** | **0.084ms** | 0.089ms | 0.089ms | 0.082ms |
| **POST JSON** | **0.071ms** | 0.088ms | 0.063ms | 0.067ms |
| **Error (500)** | **0.067ms** | 0.066ms | 0.075ms | 0.048ms |

hxp beats ky on every test and axios on 3 of 4, while providing retry, middleware, typed errors, SSE, schema validation, and debug tools.

Run it yourself: `node bench/run.mjs`

## Bundle size

| Entry | Raw | Gzipped |
|---|---|---|
| `hxp` (core) | 7.0 KB | **2.7 KB** |
| `hxp/plugins` | 1.2 KB | 0.6 KB |
| `hxp/streaming` | 1.1 KB | 0.6 KB |
| **All combined** | 9.3 KB | **3.5 KB** |

Zero dependencies. Tree-shakeable. ESM + CJS dual output.

## TypeScript

Full type inference out of the box:

```ts
interface User {
  id: number
  name: string
}

// Typed response
const user = await api.get<User>('/users/1')
//    ^? User

// With Zod schema — infers from schema
const user = await api.get('/users/1', { schema: UserSchema })
//    ^? z.infer<typeof UserSchema>

// With meta — returns HxpResponse<T>
const { data, duration } = await api.get<User>('/users/1', { meta: true })
//      ^? User
```

## License

MIT