Node.js
Complete integration guide for ImmutableLog in Node.js and TypeScript. Choose the approach that best fits your project: automatic middleware/plugin for web frameworks, direct integration with `fetch`, or wrappers to instrument specific functions.
Framework middlewares
The fastest way to integrate ImmutableLog is via the framework's native middleware or plugin. Every HTTP request is automatically captured — errors, successes, latency, and context — without changing any route. Click the framework to see the complete ready-to-use code.
Middleware function with `app.use()`. Captures via `res.on('finish')` and `res.on('error')`. Zero extra dependencies.
import { immutableLogMiddleware } from './middleware/immutablelog';
app.use(immutableLogMiddleware({
apiKey: process.env.IMTBL_API_KEY!,
apiUrl: 'https://api.immutablelog.com',
serviceName: 'express-service',
}));View full documentation →
Plugin with `fastify-plugin` and `onRequest`, `onResponse`, `onError` hooks. Supports TypeScript type augmentation.
import immutableLogPlugin from './plugins/immutablelog';
await fastify.register(immutableLogPlugin, {
apiKey: process.env.IMTBL_API_KEY!,
apiUrl: 'https://api.immutablelog.com',
serviceName: 'fastify-service',
});View full documentation →
Interceptor with RxJS `tap`/`catchError`. Supports global registration via `APP_INTERCEPTOR`, per module, or per controller.
// app.module.ts
ImmutableLogModule.forRoot({
apiKey: process.env.IMTBL_API_KEY!,
apiUrl: 'https://api.immutablelog.com',
serviceName: 'nestjs-service',
})View full documentation →
Integration with fetch
Use Node.js 18+ native `fetch` API to send events directly to ImmutableLog without depending on a web framework. Ideal for workers, scripts, jobs, and any Node.js code that needs to record events in the ledger.
Helper function sendEvent
Create a reusable utility function to encapsulate the sending logic. Fully typed in TypeScript — serializes the payload, generates required headers, and POSTs to the ingestion endpoint.
// src/lib/immutablelog.ts
import crypto from 'crypto';
const IMTBL_API_KEY = process.env.IMTBL_API_KEY ?? '';
const IMTBL_URL = process.env.IMTBL_URL ?? 'https://api.immutablelog.com';
export interface SendEventOptions {
eventName: string;
payload: Record<string, unknown>;
kind?: 'success' | 'error' | 'info' | 'warning';
service?: string;
env?: string;
}
export async function sendEvent(opts: SendEventOptions): Promise<{ tx_id: string; payload_hash: string }> {
const { eventName, payload, kind = 'info', service = 'node-service', env = 'production' } = opts;
const requestId = crypto.randomUUID();
const payloadStr = JSON.stringify({
...payload,
timestamp: new Date().toISOString(),
});
const event = {
payload: payloadStr,
meta: {
type: kind,
event_name: eventName,
service,
request_id: requestId,
env,
},
};
const res = await fetch(`${IMTBL_URL}/v1/events`, {
method: 'POST',
headers: {
Authorization: `Bearer ${IMTBL_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': `${eventName}-${requestId}`,
'Request-Id': requestId,
},
body: JSON.stringify(event),
signal: AbortSignal.timeout(5000),
});
if (!res.ok) throw new Error(`ImmutableLog error: ${res.status}`);
return res.json() as Promise<{ tx_id: string; payload_hash: string }>;
}
// Uso / Usage
await sendEvent({
eventName: 'payment.approved',
payload: { paymentId: 'pay_abc123', amount: 299.90, currency: 'BRL' },
kind: 'success',
service: 'payments-service',
});Retry with exponential backoff
For high-availability production, add automatic retry with exponential backoff. ImmutableLog returns `202` on success and `429` when the monthly limit is reached — never retry on 429.
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
async function sendEventWithRetry(
opts: SendEventOptions,
maxRetries = 3,
baseDelay = 500,
): Promise<{ tx_id: string } | null> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await sendEvent(opts);
} catch (err) {
const status = (err as { status?: number }).status;
// 429 = limite mensal — nao fazer retry / monthly limit — do not retry
if (status === 429) {
console.warn('[ImmutableLog] Monthly limit reached, skipping retry.');
return null;
}
if (attempt === maxRetries - 1) {
console.error('[ImmutableLog] Max retries reached:', err);
return null;
}
const delay = baseDelay * 2 ** attempt; // 500ms, 1s, 2s
console.warn(`[ImmutableLog] Retry ${attempt + 1}/${maxRetries} in ${delay}ms`);
await sleep(delay);
}
}
return null;
}Batch sending
In high-frequency workers, accumulate events and send in parallel using `Promise.allSettled`. Each event is still sent individually — use `Promise.allSettled` to parallelize without failing the batch.
async function sendEventsBatch(events: SendEventOptions[]): Promise<void> {
const results = await Promise.allSettled(events.map((ev) => sendEvent(ev)));
results.forEach((result, i) => {
if (result.status === 'rejected') {
console.warn(`[ImmutableLog] Batch item ${i} failed:`, result.reason);
}
});
}
// Uso / Usage
await sendEventsBatch([
{ eventName: 'order.created', payload: { orderId: 'ord_1' }, kind: 'success' },
{ eventName: 'order.created', payload: { orderId: 'ord_2' }, kind: 'success' },
{ eventName: 'payment.failed', payload: { orderId: 'ord_3' }, kind: 'error' },
]);Function wrappers
Wrappers allow you to instrument specific functions without modifying their internal logic. Ideal for marking critical business operations (e.g., process payment, create user) where you want to ensure traceability regardless of the framework used.
Sync / async wrapper
A single wrapper automatically detects if the function is async via `fn.constructor.name`. The event is sent in the `finally` block — ensuring recording even when the function throws an exception.
function withAudit<T extends (...args: unknown[]) => unknown>(
fn: T,
eventName?: string,
kind: 'success' | 'info' = 'success',
service = 'node-service',
): T {
const isAsync = fn.constructor.name === 'AsyncFunction';
const name = eventName ?? `fn.${fn.name}`;
const handle = (startedAt: number, err: Error | null) => {
const latencyMs = Date.now() - startedAt;
const actualKind = err ? 'error' : kind;
const payload: Record<string, unknown> = {
id: crypto.randomUUID(),
kind: actualKind,
message: err ? `${name} failed: ${err.constructor.name}` : `${name} executed`,
metrics: { latencyMs },
...(err && { error: { exception: err.constructor.name, exceptionMessage: err.message } }),
};
// fire-and-forget
sendEvent({ eventName: name, payload, kind: actualKind, service }).catch(() => {});
};
if (isAsync) {
return (async (...args: unknown[]) => {
const startedAt = Date.now();
try { return await (fn as (...a: unknown[]) => Promise<unknown>)(...args); }
catch (err) { handle(startedAt, err as Error); throw err; }
finally { handle(startedAt, null); }
}) as unknown as T;
}
return ((...args: unknown[]) => {
const startedAt = Date.now();
try { return fn(...args); }
catch (err) { handle(startedAt, err as Error); throw err; }
finally { handle(startedAt, null); }
}) as unknown as T;
}
// Uso / Usage
const processPayment = withAudit(
async (paymentId: string) => {
// ... logica de negocio / business logic ...
return { status: 'approved' };
},
'payment.process',
'success',
'payments-service',
);
await processPayment('pay_abc123');AuditClient class (configurable)
For larger projects, use a class to centralize configuration. Instantiate once at startup and use `audit.wrap()` on any sync or async function.
import crypto from 'crypto';
interface AuditClientOptions {
apiKey?: string;
apiUrl?: string;
service?: string;
env?: string;
}
class AuditClient {
private readonly apiKey: string;
private readonly apiUrl: string;
private readonly service: string;
private readonly env: string;
constructor(opts: AuditClientOptions = {}) {
this.apiKey = opts.apiKey ?? process.env.IMTBL_API_KEY ?? '';
this.apiUrl = opts.apiUrl ?? process.env.IMTBL_URL ?? 'https://api.immutablelog.com';
this.service = opts.service ?? 'node-service';
this.env = opts.env ?? 'production';
}
wrap<T extends (...args: unknown[]) => unknown>(
fn: T,
eventName?: string,
kind: 'success' | 'info' = 'success',
): T {
const name = eventName ?? `fn.${fn.name}`;
const isAsync = fn.constructor.name === 'AsyncFunction';
const emit = (startedAt: number, err: Error | null) => {
const payload = {
id: crypto.randomUUID(),
kind: err ? 'error' : kind,
message: err ? `${name} failed: ${err.constructor.name}` : `${name} executed`,
metrics: { latencyMs: Date.now() - startedAt },
...(err && { error: { exception: err.constructor.name, exceptionMessage: err.message } }),
};
this._send(name, payload, err ? 'error' : kind);
};
if (isAsync) {
return (async (...args: unknown[]) => {
const t = Date.now();
try { return await (fn as (...a: unknown[]) => Promise<unknown>)(...args); }
catch (e) { emit(t, e as Error); throw e; }
finally { emit(t, null); }
}) as unknown as T;
}
return ((...args: unknown[]) => {
const t = Date.now();
try { return fn(...args); }
catch (e) { emit(t, e as Error); throw e; }
finally { emit(t, null); }
}) as unknown as T;
}
private _send(eventName: string, payload: object, kind: string) {
const requestId = crypto.randomUUID();
fetch(`${this.apiUrl}/v1/events`, {
method: 'POST',
headers: {
Authorization: `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
'Idempotency-Key': `${eventName}-${requestId}`,
'Request-Id': requestId,
},
body: JSON.stringify({
payload: JSON.stringify(payload),
meta: { type: kind, event_name: eventName, service: this.service, env: this.env },
}),
signal: AbortSignal.timeout(5000),
}).catch(() => {});
}
}
// Inicializar uma vez / Initialize once
const audit = new AuditClient({
service: 'payments-service',
env: 'production',
});
// Usar em qualquer funcao / Use on any function
const processPayment = audit.wrap(
async (id: string) => ({ status: 'approved', id }),
'payment.process',
);
const deleteUser = audit.wrap(
async (userId: number) => { /* ... */ },
'user.delete',
'info',
);TypeScript Decorator (experimental)
If your project uses TypeScript with `experimentalDecorators: true`, you can use the `@AuditLog()` decorator directly on class methods. Works with sync and async methods.
// tsconfig.json: "experimentalDecorators": true
function AuditLog(eventName?: string, kind: 'success' | 'info' = 'success') {
return function (
_target: object,
propertyKey: string,
descriptor: PropertyDescriptor,
): PropertyDescriptor {
const original = descriptor.value as (...args: unknown[]) => unknown;
const name = eventName ?? `method.${propertyKey}`;
const isAsync = original.constructor.name === 'AsyncFunction';
descriptor.value = isAsync
? async function (this: unknown, ...args: unknown[]) {
const startedAt = Date.now();
let err: Error | null = null;
try {
return await original.apply(this, args);
} catch (e) {
err = e as Error;
throw e;
} finally {
emitAudit(name, kind, startedAt, err);
}
}
: function (this: unknown, ...args: unknown[]) {
const startedAt = Date.now();
let err: Error | null = null;
try {
return original.apply(this, args);
} catch (e) {
err = e as Error;
throw e;
} finally {
emitAudit(name, kind, startedAt, err);
}
};
return descriptor;
};
}
function emitAudit(name: string, kind: string, startedAt: number, err: Error | null) {
const actualKind = err ? 'error' : kind;
sendEvent({
eventName: name,
payload: {
message: err ? `${name} failed: ${err.constructor.name}` : `${name} executed`,
metrics: { latencyMs: Date.now() - startedAt },
...(err && { error: { exception: err.constructor.name, exceptionMessage: err.message } }),
},
kind: actualKind as 'success' | 'error' | 'info',
}).catch(() => {});
}
// Uso em classe / Usage in class
class PaymentService {
@AuditLog('payment.process', 'success')
async processPayment(paymentId: string): Promise<{ status: string }> {
// ... logica de negocio / business logic ...
return { status: 'approved' };
}
@AuditLog('invoice.generate')
async generateInvoice(orderId: string): Promise<void> {
// ...
}
}This documentation reflects the current API behavior. For questions or advanced integrations, contact the support team.
