jervis-connect
v1.0.0-alpha.2
Published
A lightweight, zero-dependency, plugin-based HTTP client for all JavaScript runtimes. Axios-like API with modern fetch.
Maintainers
Readme
JervisConnect
A lightweight, zero-dependency, plugin-based HTTP client for all JavaScript runtimes. Axios-like API with modern
fetchunder the hood.
Works everywhere: Browser, Node.js 18+, Deno, Bun, Cloudflare Workers, Edge, React Native, Electron, Service Workers.
Frameworks: Vue / Nuxt, React / Next.js, Angular, Svelte / SvelteKit, Remix, Gatsby, Expo.
Why JervisConnect?
| | jervis-connect | axios | ky | got | | --------------------------- | :------------: | :---: | :-: | :-: | | Zero dependencies | ✅ | ❌ | ✅ | ❌ | | Fetch-based | ✅ | ❌ | ✅ | ❌ | | Plugin system | ✅ | ❌ | ❌ | ❌ | | Auth (JWT/OAuth2/API key) | ✅ | ❌ | ❌ | ❌ | | Circuit breaker | ✅ | ❌ | ❌ | ❌ | | OWASP security built-in | ✅ | ❌ | ❌ | ❌ | | SSE / streaming | ✅ | ❌ | ❌ | ✅ | | HMAC signing | ✅ | ❌ | ❌ | ❌ | | GraphQL helpers | ✅ | ❌ | ❌ | ❌ | | Response caching (RFC 7234) | ✅ | ❌ | ❌ | ❌ | | Cookie jar | ✅ | ❌ | ❌ | ✅ | | Request inspector | ✅ | ❌ | ❌ | ❌ | | Tree-shakeable | ✅ | ❌ | ✅ | ❌ |
Features
Core
- Universal — built on
fetch, works in every modern runtime - Plugin system —
api.use(plugin)for modular features - Request & response interceptors (Axios-compatible)
- Multiple configured instances
- Timeout with
AbortController - Auto-retry with exponential backoff + jitter
- Cancel tokens +
AbortSignal - File upload with progress tracking
- Upload & download progress callbacks
- Request/response data transformers
Plugin Ecosystem (v2)
- Auth — JWT auto-attach/refresh, API Key, OAuth2 client credentials
- Circuit breaker — auto-stop after repeated failures, auto-recover
- Concurrency control — limit parallel in-flight requests
- Priority queue — high/normal/low request prioritization
- Request batching — combine requests within a time window
- Offline mode — queue mutations, auto-replay on reconnect
- Runtime validation — validate responses against schemas
- DevTools — real-time request monitoring panel
Security (OWASP)
- SSRF protection (IPv4 + IPv6)
- Header injection prevention
- Prototype pollution defense
- HMAC request signing (SHA-256/384/512)
- Nonce / replay attack prevention
- Webhook signature verification
- Security header validation & grading
- Request tamper detection
Advanced
- Response caching (RFC 7234, LRU, ETag/conditional)
- Response streaming + SSE parser
- In-memory cookie jar (domain/path/secure matching)
- GraphQL error detection & categorization
- Lifecycle hooks (
beforeRequest,afterResponse,onError) - Fluent request builder pattern
- FormData builder (chainable)
- Request inspector / debugger (DevTools-like)
- HTTP compression helpers
- TTFB tracking & rate limit detection
- Mock adapter for testing
- Structured logger middleware
Installation
npm install jervis-connectyarn add jervis-connectpnpm add jervis-connectQuick Start
import { createInstance } from "jervis-connect";
const api = createInstance({
baseURL: "https://api.example.com",
timeout: 10000,
});
// GET
const { data } = await api.get("/users");
// POST
await api.post("/users", { name: "Ahmed", email: "[email protected]" });Query Parameters
Pass params as an object — scalars and arrays are supported out of the box:
// Scalar params → ?page=1&limit=10
await api.get("/users", { params: { page: 1, limit: 10 } });
// Array params (default: indices format)
// → ?scopes[0]=withChildren&scopes[1]=isParent
await api.get("/users", {
params: { scopes: ["withChildren", "isParent"] },
});Array Serialization Formats
Control how arrays are serialized via paramsSerializer:
const params = { scopes: ["withChildren", "isParent"] };
// indices (default) → scopes[0]=withChildren&scopes[1]=isParent
await api.get("/users", { params, paramsSerializer: "indices" });
// brackets → scopes[]=withChildren&scopes[]=isParent
await api.get("/users", { params, paramsSerializer: "brackets" });
// repeat → scopes=withChildren&scopes=isParent
await api.get("/users", { params, paramsSerializer: "repeat" });
// comma → scopes=withChildren,isParent
await api.get("/users", { params, paramsSerializer: "comma" });Set a Default Format
const api = createInstance({
baseURL: "https://api.example.com",
paramsSerializer: "brackets", // All requests use brackets
});Custom Serializer Function
For full control, pass a function:
await api.get("/search", {
params: { tags: ["vue", "react"], page: 1 },
paramsSerializer: (params) =>
Object.entries(params)
.map(([k, v]) => `${k}=${Array.isArray(v) ? v.join("|") : v}`)
.join("&"),
});
// → /search?tags=vue|react&page=1Plugin System (v2)
Install features as tree-shakeable plugins:
import { createInstance } from "jervis-connect";
import { authPlugin } from "jervis-connect/plugins/auth";
import { cachePlugin } from "jervis-connect/plugins/cache";
import { circuitBreakerPlugin } from "jervis-connect/plugins/circuit-breaker";
const api = createInstance({ baseURL: "https://api.example.com" });
api
.use(
authPlugin({
type: "jwt",
getToken: () => localStorage.getItem("token"),
refreshToken: async () => {
const res = await fetch("/auth/refresh", { method: "POST" });
const { token } = await res.json();
localStorage.setItem("token", token);
return token;
},
}),
)
.use(cachePlugin({ maxEntries: 200, defaultTTL: 600 }))
.use(circuitBreakerPlugin({ failureThreshold: 5, cooldownMs: 30000 }));Available Plugins
| Plugin | Import Path | Description |
| ---------------- | -------------------------- | ------------------------------------ |
| Auth | plugins/auth | JWT, API Key, OAuth2, custom auth |
| Cache | plugins/cache | RFC 7234 response caching |
| Circuit Breaker | plugins/circuit-breaker | Stop requests after failures |
| Concurrency | plugins/concurrency | Limit parallel requests |
| Priority | plugins/priority | Request priority queue |
| Batch | plugins/batch | Combine requests in time window |
| Offline | plugins/offline | Queue mutations, replay on reconnect |
| Validate | plugins/validate | Runtime response validation |
| Logger | plugins/logger | Structured request logging |
| Cookies | plugins/cookies | In-memory cookie jar |
| GraphQL | plugins/graphql | GraphQL helper methods |
| HMAC | plugins/hmac | HMAC request signing |
| Compression | plugins/compression | Accept-Encoding negotiation |
| Security Headers | plugins/security-headers | Security header validation |
| Mock | plugins/mock | Mock adapter for testing |
| Dedup | plugins/dedup | Request deduplication |
| Retry | plugins/retry | Retry as plugin (vs built-in) |
| DevTools | devtools | Real-time monitoring panel |
Custom Plugins
import type { JervisPlugin, PluginHooks } from "jervis-connect";
function myPlugin(): JervisPlugin {
return {
name: "my-plugin",
install(instance): PluginHooks {
return {
beforeRequest(config) {
config.headers = { ...config.headers, "X-Custom": "value" };
return config;
},
};
},
};
}
api.use(myPlugin());Modular Imports
// Full library (backward compat)
import { createInstance } from "jervis-connect";
// Minimal core — only client + interceptors + cancel + errors
import { createInstance } from "jervis-connect/core";
// Individual plugins
import { authPlugin } from "jervis-connect/plugins/auth";
import { cachePlugin } from "jervis-connect/plugins/cache";Interceptors
// Request — add auth token
api.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) config.headers!["Authorization"] = `Bearer ${token}`;
return config;
});
// Response — handle errors globally
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Redirect to login
}
return Promise.reject(error);
},
);File Upload
const formData = new FormData();
formData.append("avatar", file);
await api.post("/upload", formData, {
isFile: true,
onUploadProgress: (p) => console.log(`${Math.round(p.progress * 100)}%`),
});Or use the FormData builder:
import { formData } from "jervis-connect";
const config = formData()
.field("name", "Ahmed")
.file("avatar", blob, "avatar.jpg")
.json("metadata", { role: "admin" })
.toConfig("/upload");
await api.request(config);Auto Retry
const api = createInstance({
baseURL: "https://api.example.com",
retry: 3, // Retry up to 3 times
retryDelay: 1000, // Exponential backoff with jitter
retryOn: [408, 429, 500, 502, 503, 504],
});Cancellation
const controller = new AbortController();
api.get("/users", { signal: controller.signal });
controller.abort();HMAC Request Signing
import { createHmacSigningInterceptor } from "jervis-connect";
api.interceptors.request.use(
createHmacSigningInterceptor({
secret: "my-secret-key",
algorithm: "SHA-256",
}),
);Response Caching
import { createCacheInterceptors } from "jervis-connect";
const cache = createCacheInterceptors({ maxEntries: 200, defaultTTL: 600 });
api.interceptors.request.use(cache.requestInterceptor);
api.interceptors.response.use(cache.responseInterceptor);GraphQL
import {
buildGraphQLRequest,
createGraphQLErrorInterceptor,
} from "jervis-connect";
api.interceptors.response.use(createGraphQLErrorInterceptor());
const config = buildGraphQLRequest("/graphql", {
query: `query { users { name email } }`,
});
const { data } = await api.request(config);SSE / Streaming
import { parseSSEStream } from "jervis-connect";
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
body: JSON.stringify({ model: "gpt-4", messages: [...], stream: true }),
});
for await (const event of parseSSEStream(response.body!)) {
if (event.data === "[DONE]") break;
const parsed = JSON.parse(event.data);
process.stdout.write(parsed.choices[0]?.delta?.content || "");
}Cookie Jar
import { createCookieJar } from "jervis-connect";
const jar = createCookieJar();
api.interceptors.request.use(jar.requestInterceptor);
api.interceptors.response.use(jar.responseInterceptor);Request Inspector
import { createInspector } from "jervis-connect";
const inspector = createInspector();
api.interceptors.request.use(inspector.requestInterceptor);
api.interceptors.response.use(
inspector.responseInterceptor,
inspector.errorInterceptor,
);
// Later
console.log(inspector.getSummary());
// { total: 42, successful: 40, failed: 2, averageResponseTime: 156, successRate: 0.952 }Lifecycle Hooks
import { createHooksInterceptors } from "jervis-connect";
const hooks = createHooksInterceptors({
beforeRequest: [
(config) => {
console.log(`→ ${config.method} ${config.url}`);
return config;
},
],
afterResponse: [
(res) => {
console.log(`← ${res.status}`);
return res;
},
],
onError: [
(err) => {
console.error(err);
throw err;
},
],
});
api.interceptors.request.use(hooks.requestInterceptor);
api.interceptors.response.use(
hooks.responseInterceptor,
hooks.errorInterceptor,
);Webhook Verification
import { verifyWebhookSignature } from "jervis-connect";
const isValid = await verifyWebhookSignature({
payload: req.body,
signature: req.headers["x-hub-signature-256"],
secret: process.env.WEBHOOK_SECRET!,
});Security Header Validation
import { validateSecurityHeaders } from "jervis-connect";
const result = validateSecurityHeaders(responseHeaders);
console.log(result.grade); // "A+", "A", "B", "C", "D", "F"
console.log(result.score); // 0–100Mock Adapter (Testing)
import { createInstance, createMockAdapter } from "jervis-connect";
const client = createInstance({ baseURL: "https://api.example.com" });
const mock = createMockAdapter(client);
mock.onGet("/users").replyWith(200, [{ id: 1, name: "Ahmed" }]);
const { data } = await client.get("/users");
// data === [{ id: 1, name: "Ahmed" }]Fluent Builder
const { data } = await api
.builder()
.post("/users", { name: "Ahmed" })
.header("X-Custom", "value")
.timeout(5000)
.retry(3, 1000)
.execute();Framework Examples
Vue 3 — useJervisFetch Composable
JervisConnect ships a first-class Vue 3 adapter via jervis-connect/vue:
import { createInstance } from "jervis-connect";
import { createVueAdapter } from "jervis-connect/vue";
const api = createInstance({ baseURL: "/api" });
const { useJervisFetch } = createVueAdapter(api);
export { useJervisFetch };Use the composable in any <script setup> component:
<script setup lang="ts">
import { useJervisFetch } from "@/lib/api";
const { data, loading, error, refresh } = useJervisFetch<User[]>("/users");
</script>
<template>
<p v-if="loading">Loading…</p>
<p v-else-if="error">{{ error.message }}</p>
<ul v-else>
<li v-for="u in data" :key="u.id">{{ u.name }}</li>
</ul>
<button @click="refresh">Reload</button>
</template>Key features:
- Reactive —
data,error,loading,statusare Vueref()s - Auto-execute on mount (or
lazy: truefor manual) - Watch reactive sources for auto re-fetch
- SSR —
onServerPrefetchsupport - Suspense —
await useJervisFetch(…)for<Suspense>boundaries - SWR —
swr: truekeeps stale data on revalidation error - Transform —
transform: (data) => …before settingdata.value - Abort — auto-aborts on scope dispose, manual
abort()method
Vue / Nuxt (manual)
import { createInstance } from "jervis-connect";
const api = createInstance({
baseURL: import.meta.env.VITE_API_URL,
timeout: 15000,
});
api.interceptors.request.use((config) => {
const token = useCookie("token").value;
if (token) config.headers!["Authorization"] = `Bearer ${token}`;
return config;
});
export default api;React / Next.js
import { createInstance } from "jervis-connect";
const api = createInstance({
baseURL: process.env.NEXT_PUBLIC_API_URL,
timeout: 10000,
});
api.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) config.headers!["Authorization"] = `Bearer ${token}`;
return config;
});
export default api;Node.js / Express
import { createInstance } from "jervis-connect";
const api = createInstance({
baseURL: "https://external-api.com",
timeout: 5000,
retry: 3,
});
app.get("/proxy/users", async (req, res) => {
const response = await api.get("/users");
res.json(response.data);
});UMD / Script Tag
<script src="https://unpkg.com/jervis-connect/dist/umd/index.min.js"></script>
<script>
const client = JervisConnect.createInstance({
baseURL: "https://api.example.com",
});
client.get("/users").then((res) => console.log(res.data));
</script>Build Outputs
| Output | Path | Description |
| -------------- | ------------------------- | ------------------------------------------------------------- |
| ESM (minified) | dist/index.mjs | ES Modules — tree-shakeable |
| CJS (minified) | dist/index.js | CommonJS — Node.js require() |
| DTS | dist/index.d.ts | TypeScript declarations |
| Core | dist/core.mjs | Minimal core entry point |
| Plugins | dist/plugins/*.mjs | Individual plugin modules |
| DevTools | dist/devtools/index.mjs | DevTools plugin |
| Vue Adapter | dist/vue/index.mjs | Vue 3 useJervisFetch composable |
| UMD | dist/umd/index.min.js | Browser <script> tag |
| Secure | dist/secure/ | Extra terser pass |
| Obfuscated | dist/obfuscated/ | Control-flow flattening, string encoding, dead code injection |
Security
JervisConnect ships with OWASP Top 10 security protections enabled by default:
- SSRF Prevention — blocks requests to private networks (IPv4 + IPv6, loopback, link-local)
- Header Injection — validates header names/values against injection patterns
- Prototype Pollution — deep sanitization of JSON responses
- Response Size Limits — configurable max response size
- Sensitive Header Redaction — auto-redacts
Authorization,Cookie, API keys in error objects
const api = createInstance({
baseURL: "https://api.example.com",
security: {
validateURLs: true,
ssrfProtection: true,
sanitizeHeaders: true,
prototypePollutionProtection: true,
maxResponseSize: 10 * 1024 * 1024, // 10MB
redactSensitiveHeaders: true,
},
});Documentation
Full documentation available at the JervisConnect Docs:
- Getting Started
- Plugin System
- Auth Plugin
- Circuit Breaker
- Making Requests
- Interceptors
- Caching
- GraphQL
- Streaming & SSE
- Security Overview
- Vue Adapter
- Migration v1 → v2
- API Reference
License
MIT © Kerolos Zakaria
