@mounaji_npm/api-client
v0.4.0
Published
HTTP client, service base, and WebSocket connector for Mounaji-powered applications
Downloads
359
Maintainers
Readme
@mounaji_npm/api-client
HTTP client, service base class, and WebSocket connector for Mounaji-powered applications. Designed to pair with @mounaji_npm/auth for automatic token injection and 401 retry.
Install
npm install @mounaji_npm/api-clientNo peer dependencies. Works in any browser or Node.js environment with fetch and WebSocket.
Quick Start
// src/api/client.js
import { ApiClient } from '@mounaji_npm/api-client';
export const apiClient = new ApiClient({
baseUrl: import.meta.env.VITE_API_URL || process.env.NEXT_PUBLIC_API_URL,
});// src/api/services/AssistantService.js
import { ServiceBase } from '@mounaji_npm/api-client';
import { apiClient } from '../client.js';
class AssistantService extends ServiceBase {
constructor() { super(apiClient, '/assistants'); }
list() { return this.get('/'); }
getById(id) { return this.get(`/${id}`); }
create(data) { return this.post('/', data); }
update(id, d) { return this.put(`/${id}`, d); }
remove(id) { return this.delete(`/${id}`); }
}
export const assistantService = new AssistantService();// In a component
import { assistantService } from './api/services/AssistantService.js';
useEffect(() => {
assistantService.list().then(setAssistants);
}, []);ApiClient
Core HTTP client with token injection, 401 retry, and interceptors.
Setup
import { ApiClient } from '@mounaji_npm/api-client';
const apiClient = new ApiClient({
baseUrl: 'https://api.example.com',
timeout: 30_000, // ms (default: 30s)
defaultHeaders: {}, // added to every request
});Auth wiring
// Call after login / on token change
apiClient.setAuthToken(token);
// Provide a refresh function — called automatically on 401 TOKEN_EXPIRED
apiClient.setTokenRefresher(() => authAdapter.getToken(true));
// Listen for session expiry (when refresh fails)
window.addEventListener('auth:session-expired', () => router.push('/login'));HTTP methods
// GET
const list = await apiClient.get('/assistants');
const one = await apiClient.get('/assistants/123');
const page = await apiClient.get('/assistants', { params: { page: 1, limit: 20 } });
// POST / PUT / PATCH
const created = await apiClient.post('/assistants', { name: 'My Bot' });
await apiClient.put('/assistants/123', { name: 'Updated' });
await apiClient.patch('/assistants/123', { active: false });
// DELETE
await apiClient.delete('/assistants/123');
// Raw Response (bypass JSON parsing)
const res = await apiClient.get('/export', { raw: true });
const blob = await res.blob();Error handling
On non-2xx responses, ApiClient throws an enriched Error:
try {
await apiClient.get('/protected');
} catch (err) {
err.message // human-readable message
err.status // HTTP status code (e.g. 403)
err.code // server error code string (e.g. 'FORBIDDEN')
err.details // full parsed error body
}Interceptors
// Log every request
apiClient.addRequestInterceptor((url, config) => {
console.log('→', config.method, url);
return config;
});
// Log every response
apiClient.addResponseInterceptor(async (res, url) => {
console.log('←', res.status, url);
return res;
});ServiceBase
Base class for typed domain service objects.
import { ServiceBase } from '@mounaji_npm/api-client';
class DocumentService extends ServiceBase {
constructor() { super(apiClient, '/documents'); }
listByKb(kbId) { return this.get('/', { params: { knowledgeBaseId: kbId } }); }
upload(formData) { return this.post('/', formData); }
getById(id) { return this.get(`/${id}`); }
delete(id) { return this.delete(`/${id}`); }
getStatus(id) { return this.get(`/${id}/status`); }
}
export const documentService = new DocumentService();Available methods: this.get this.post this.put this.patch this.delete — all prefixed with the basePath passed to super().
WsConnector
WebSocket client with typed message routing, exponential backoff reconnect, and ping/pong keepalive.
import { WsConnector } from '@mounaji_npm/api-client';
const ws = new WsConnector({
url: 'wss://api.example.com/ws',
getToken: () => apiClient._token,
tokenMode: 'query', // 'query' (URL param) | 'message' (first message)
maxReconnects: 10,
reconnectBaseDelay:1000, // ms — doubles each attempt, caps at 30s
pingInterval: 30_000, // ms — 0 = disable keepalive
});
// Subscribe to typed messages
ws.on('chat:message', (data) => addMessage(data));
ws.on('assistant:done', (data) => setLoading(false));
ws.on('error', (err) => showToast(err.message));
ws.on('open', () => setConnected(true));
ws.on('close', ({ code, reason }) => setConnected(false));
ws.on('*', (data) => console.log('raw', data)); // wildcard
// Send
ws.connect();
ws.send('chat:message', { content: 'Hello', sessionId: '...' });
// Clean shutdown
ws.disconnect();
// Unsubscribe
ws.off('chat:message');Expected incoming message shape:
{ "type": "chat:message", "content": "Hello", "timestamp": 1234567890 }Reconnect delays (baseDelay=1000ms): 1s → 2s → 4s → 8s → 16s → 30s (capped)
Wiring with @mounaji_npm/auth
import { useEffect } from 'react';
import { AuthProvider } from '@mounaji_npm/auth';
import { createFirebaseAdapter } from '@mounaji_npm/auth/firebase';
import { apiClient } from './api/client.js';
const adapter = createFirebaseAdapter({ auth, googleProvider });
function ApiAuthBridge() {
useEffect(() => {
const unsub = adapter.onAuthChange((user, token) => {
apiClient.setAuthToken(token);
});
apiClient.setTokenRefresher(() => adapter.getToken(true));
return () => unsub?.();
}, []);
return null;
}
export default function Root() {
return (
<AuthProvider adapter={adapter}>
<ApiAuthBridge />
<App />
</AuthProvider>
);
}Recommended Project Layout
src/
└── api/
├── client.js ← shared ApiClient instance
├── services/
│ ├── AssistantService.js
│ ├── DocumentService.js
│ └── WorkflowService.js
└── connectors/
└── chatSocket.js ← WsConnector instanceFull API reference with all options: DOCS.md
