Skip to content

Commit c8a5f97

Browse files
Merge pull request #17 from developerEhsan/feat/api-client-overhaul
feat: major overhaul — DX, type-safety, modules-beyond-HTTP, roadmap features
2 parents cda09d8 + 5aeb8b0 commit c8a5f97

106 files changed

Lines changed: 7816 additions & 2683 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/api-client-overhaul.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
"@developerehsan/api-client": major
3+
"@developerehsan/api-client-cli": major
4+
"@developerehsan/api-client-vite": major
5+
---
6+
7+
Major overhaul: developer experience, type-safety, modular logic, and roadmap features.
8+
9+
**Breaking / behavioral**
10+
11+
- `ctx.request` is now generic over the literal `path` (`request<T, const P>`): a path
12+
with `{placeholders}` requires exactly those `pathParams` at compile time (missing/typo
13+
is now a type error). Explicit `request<T>(...)` still works.
14+
- Lifecycle hooks now COMPOSE across global → module → per-call (all fire; transforming
15+
hooks chain). Transforming-hook return types widened to allow a `void` pass-through.
16+
- `createClient` fail-fasts on nonsensical config (negative ttl/attempts, bad queue
17+
concurrency, SWR with cache disabled, oauth2 without a refresh endpoint).
18+
- The dead parallel `http/pipeline.ts` implementation was removed.
19+
20+
**New capabilities**
21+
22+
- Config & hooks parity: `hooks` on module + per-call config; new `onSuccess`/`onSettled`;
23+
`onRetry` is now actually fired; per-call `queue` opt-out; `client.config.resolve()`
24+
returns a redacted resolved-config snapshot.
25+
- Type-safety: template-literal path-param inference, a typed `ClientEventMap` for
26+
`on`/`off`, and a `*.test-d.ts` regression suite.
27+
- Config-driven codegen: `api-client.config.*` files, real per-operation `diff`,
28+
`watchAndGenerate` (local + remote-URL polling with ETag/hash), `generate --check`
29+
for CI, and secret-safe remote spec fetching.
30+
- Modules beyond HTTP: `ctx.run` (opt-in queue/dedup/retry/timeout for any async work),
31+
`ctx.stream` (NDJSON/SSE/raw async iterables), `ctx.emit`/`ctx.logger`/`ctx.config`,
32+
and `extends: 'auto'` from the runtime schema. New `OperationError`.
33+
- RPC bridge: request batching (`{ __rpcBatch }`, per-sub-call validation, `maxBatchSize`),
34+
a built-in rate limiter (`createRateLimiter`), and TanStack Start / Remix route adapters.
35+
- Pluggable persistent cache stores (`@developerehsan/api-client/cache-stores`:
36+
memory / IndexedDB / Redis) layered behind the sync in-memory LRU.
37+
- React Query hooks emission (`emitReactQueryHooks`).
38+
- Framework glue: `withApiClientCodegen` for Next.js and the new
39+
`@developerehsan/api-client-vite` plugin (also covers TanStack Start).
40+
41+
Security-reviewed: the RPC trust boundary, batching, prototype-pollution guards,
42+
`sanitizePerCall` closed-set, rate-limiter key handling, and codegen fetch hygiene were
43+
adversarially reviewed with numbered threat-case tests.
44+
45+
> Note: loading a **TypeScript** `api-client.config.ts` requires the optional `jiti`
46+
> dependency (`pnpm add -D jiti`); `.mjs`/`.js`/`.json` config files work with no extra deps.

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"cSpell.words": ["Codegen"]
3+
}

README.md

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ code stays clean.
8585
| **Validation** | Runtime response validation + schema drift detection |
8686
| **TanStack Query** | Typed `queryOptions` / `mutationOptions` / `infiniteQueryOptions` for React, Vue, Solid |
8787
| **SSR RPC bridge** | Call `api.module.method()` from client components in Next.js / TanStack Start **without** exposing the backend URL, paths, or OpenAPI to the browser |
88+
| **RPC batching** | Coalesce same-tick bridge calls into one round-trip; each sub-call validated + authorized individually |
89+
| **RPC rate limiter** | Built-in `createRateLimiter` for the handler `onRequest` (per-IP / per-session, pluggable store) |
90+
| **Streaming** | `ctx.stream()``AsyncIterable` for NDJSON / SSE / raw byte streams (client-side) |
91+
| **Modules beyond HTTP** | `ctx.run()` runs any async logic with opt-in queue/dedup/retry/timeout; `ctx.emit` / `ctx.logger` / `ctx.config` |
92+
| **Cache persistence** | Pluggable L2 stores (IndexedDB / Redis) behind the in-memory LRU |
93+
| **Auto codegen** | Config-file-driven `generate` / watch / `--check`; Vite plugin + Next.js integration |
94+
| **Hooks** | `onRequest`/`onResponse`/`onError`/`onRetry`/`onSuccess`/`onSettled` composed across global → module → per-call |
8895
| **Testing** | `createMockClient` + `MockAdapter` |
8996

9097
### Packages
@@ -1058,7 +1065,7 @@ ergonomics.
10581065
```text
10591066
Browser (client component) Server (Node / edge)
10601067
─────────────────────────── ─────────────────────────────
1061-
api.pet.getPetById({ petId }) createRpcHandler(realApi, { expose })
1068+
api.products.getProductById({ id }) createRpcHandler(realApi, { expose })
10621069
│ proxy, typed via `typeof serverApi` │ allowlist → authorize → dispatch
10631070
│ (type-only, erased at build) ▼ runs the REAL client (holds secrets)
10641071
▼ POST same-origin { module,method,args }
@@ -1150,7 +1157,7 @@ import { ApiError } from '@developerehsan/api-client/browser'
11501157

11511158
async function load() {
11521159
try {
1153-
const pet = await api.pet.getPetById({ petId: 1 }) //Pet, fully typed
1160+
const product = await api.products.getProductById({ id: 1 }) //Product, typed
11541161
} catch (e) {
11551162
if (e instanceof ApiError) console.log(e.status, e.message) // rehydrated!
11561163
}
@@ -1170,7 +1177,7 @@ export const q = createQueryIntegration(api, { modules: rpcModules })
11701177

11711178
### Cancellation
11721179

1173-
Pass an `AbortSignal` as usual — `api.pet.getPetById({ petId }, { signal })`. The
1180+
Pass an `AbortSignal` as usual — `api.products.getProductById({ id }, { signal })`. The
11741181
signal is **not** sent over the wire (it isn't serializable); it's honored
11751182
locally and rejects the promise with an `AbortError` on abort.
11761183

@@ -1189,7 +1196,7 @@ The handler enforces all of the following before dispatch:
11891196
| Error leakage | Only `{ name, status, code, message }` cross the wire; stacks, request URLs, and headers never do (`details` only when `dev: true`) |
11901197

11911198
> **Note:** the bridge client *type* mirrors your whole API surface, so
1192-
> `api.pet.deletePet(...)` still type-checks even if it isn't exposed — the
1199+
> `api.products.deleteProduct(...)` still type-checks even if it isn't exposed — the
11931200
> `expose` allowlist is the runtime gate, and an un-exposed call is denied.
11941201
11951202
A complete, runnable example lives in [`examples/nextjs`](./examples/nextjs).
@@ -1423,28 +1430,41 @@ Monorepo layout: `packages/core` (runtime), `packages/cli` (codegen),
14231430

14241431
---
14251432

1426-
## 28. Roadmap (planned features)
1427-
1428-
These are planned or under consideration — not yet shipped. Contributions and
1429-
feedback welcome.
1430-
1431-
- **`extends: 'auto'` auto-modules from the runtime schema** — build module
1432-
methods directly from a runtime-fetched OpenAPI document (today auto-modules
1433-
come from the build-time codegen descriptor; the runtime path is stubbed).
1434-
- **Batching over the RPC bridge** — coalesce multiple `api.*.*()` calls made in
1435-
the same tick into a single round-trip, with per-sub-call allowlist/authorize.
1436-
- **Streaming / async-iterable responses** — first-class support for
1437-
`text/event-stream` and chunked responses through the pipeline and the bridge.
1438-
- **Built-in rate limiter** — a ready-to-use limiter for `onRequest` (per-IP /
1439-
per-session), instead of only the hook point.
1440-
- **Richer per-module method-name autocomplete in `config.modules`** — close the
1441-
one remaining inference gap so the plain-object form matches the definer form.
1442-
- **First-party TanStack Start & Remix adapters** — thin glue on top of the
1443-
generic HTTP transport, mirroring the Next.js Server Action helper.
1433+
## 28. Roadmap
1434+
1435+
### Shipped (major overhaul)
1436+
1437+
- **`extends: 'auto'` auto-modules from the runtime schema** — module methods
1438+
are now derived at runtime from a fetched OpenAPI document (resolved lazily as
1439+
the schema loads).
1440+
- **Batching over the RPC bridge**`createRpcClient(transport, { batch: true })`
1441+
coalesces same-tick calls into one round-trip; each sub-call is validated and
1442+
authorized individually server-side (`maxBatchSize`, nested-batch rejection).
1443+
- **Client-side streaming / async-iterable responses** — `ctx.stream(spec, { mode:
1444+
'ndjson' | 'sse' | 'raw' })` returns an `AsyncIterable`, plus `parseSse` /
1445+
`parseNdjson` helpers. (Streaming *through the RPC bridge* remains a follow-up.)
1446+
- **Built-in rate limiter**`createRateLimiter({ windowMs, max })` wires into the
1447+
handler `onRequest` (per-IP with `trustProxy`, or a custom `keyFor`; pluggable store).
1448+
- **Pluggable cache persistence**`@developerehsan/api-client/cache-stores`
1449+
(memory / IndexedDB / Redis) layered behind the in-memory LRU via
1450+
`cache.persistentStore`.
1451+
- **First-party TanStack Start & Remix adapters**`createStartRpcRoute` /
1452+
`createRemixRpcAction` over the framework-agnostic route handler.
1453+
- **Config-driven codegen + framework glue**`api-client.config.*`, watch with
1454+
remote-URL polling, `generate --check` for CI, plus `withApiClientCodegen`
1455+
(Next.js) and `@developerehsan/api-client-vite` (Vite / TanStack Start).
1456+
- **Codegen: React Query hooks emission**`emitReactQueryHooks` emits typed
1457+
`useXxx` hooks alongside the option factories.
1458+
- **Modules beyond HTTP**`ctx.run` gives any async module logic opt-in
1459+
queue / dedup / retry / timeout; `ctx.emit` / `ctx.logger` / `ctx.config` round
1460+
out the module context.
1461+
1462+
### Still planned
1463+
1464+
- **Richer per-module method-name autocomplete in the plain-object `config.modules`**
1465+
form — use `createModuleDefiner` today for full operation-aware autocomplete.
1466+
- **Streaming through the RPC bridge** — route-only, with duration/idle/backpressure
1467+
hardening (Server Actions can't stream).
14441468
- **OpenAPI 3.1 & webhooks** — expand the parser beyond 3.0.x.
1445-
- **GraphQL transport** — an alternative adapter for GraphQL backends behind the
1446-
same module/method surface.
1447-
- **Response caching persistence** — pluggable stores (IndexedDB, Redis) beyond
1448-
the in-memory LRU.
1449-
- **Codegen: React Query hooks emission** — optionally emit `useXxx` hooks in
1450-
addition to the option factories.
1469+
- **GraphQL transport** — an alternative adapter behind the same module/method
1470+
surface (largely a `ctx.run`-based module on top of the new runner).

examples/nextjs/app/PetDemo.tsx

Lines changed: 0 additions & 215 deletions
This file was deleted.

0 commit comments

Comments
 (0)