Production-ready JSON-RPC 2.0 library for Node.js and Bun with TCP, WebSocket, and HTTP transports.
npm install flex-rpc
# or
bun add flex-rpcimport { createServer, TcpServerTransport } from "flex-rpc";
const server = createServer();
server.addTransport(new TcpServerTransport(3000));
// Expose methods
server.expose("add", (params) => params[0] + params[1]);
server.expose("greet", (params) => `Hello, ${params.name}!`);
await server.listen();
console.log("Server listening on port 3000");import { createClient, TcpClientTransport } from "flex-rpc";
const client = createClient(new TcpClientTransport("localhost", 3000));
// Make calls
const sum = await client.call("add", [1, 2]);
console.log(sum); // 3
const greeting = await client.call("greet", { name: "World" });
console.log(greeting); // "Hello, World!"
await client.close();Best for: High-performance internal services, persistent connections
import { TcpClientTransport, TcpServerTransport } from "flex-rpc";
// Server
const serverTransport = new TcpServerTransport(3000, {
host: "0.0.0.0",
keepAlive: true,
});
// Client
const clientTransport = new TcpClientTransport("localhost", 3000, {
autoReconnect: true,
connectionTimeout: 5000,
});Best for: Browser clients, bidirectional communication, real-time apps
import { WsClientTransport, WsServerTransport } from "flex-rpc";
// Server
const serverTransport = new WsServerTransport(3001, {
path: "/rpc",
pingInterval: 30000,
});
// Client
const clientTransport = new WsClientTransport("ws://localhost:3001/rpc", {
autoReconnect: true,
protocols: ["json-rpc"],
});Best for: REST-like APIs, stateless services, serverless
import { HttpClientTransport, HttpServerTransport } from "flex-rpc";
// Server
const serverTransport = new HttpServerTransport(3002, {
cors: true,
path: "/api/rpc",
});
// Client
const clientTransport = new HttpClientTransport("http://localhost:3002/api/rpc", {
retry: true,
maxRetries: 3,
});Get full TypeScript support with the typed proxy:
// Define your API interface
interface MyApi {
math: {
add(a: number, b: number): number;
multiply(a: number, b: number): number;
};
users: {
create(user: { name: string }): { id: string };
get(id: string): { id: string; name: string };
};
}
// Server: expose namespaced methods
server.expose("math.add", ([a, b]) => a + b);
server.expose("math.multiply", ([a, b]) => a * b);
server.expose("users.create", (params) => ({ id: "123", ...params }));
server.expose("users.get", ([id]) => ({ id, name: "Alice" }));
// Client: use typed proxy
const api = client.proxy<MyApi>();
const sum = await api.math.add(1, 2); // TypeScript knows this returns number
const user = await api.users.get("123"); // TypeScript knows the return typeSend notifications from server to clients (TCP & WebSocket only):
// Server
server.expose("subscribe", (params, ctx) => {
// ctx.client contains the client connection
console.log(`Client ${ctx.client.id} subscribed`);
return { subscribed: true };
});
// Notify specific client
await server.notify(clientId, "update", { data: "new value" });
// Broadcast to all clients
await server.broadcast("announcement", { message: "Server restarting" });
// Client: handle notifications
client.onNotification("update", (params) => {
console.log("Received update:", params);
});Add cross-cutting concerns:
// Logging middleware
server.use(async (ctx, next) => {
const start = Date.now();
console.log(`→ ${ctx.request.method}`);
try {
const result = await next();
console.log(`← ${ctx.request.method} (${Date.now() - start}ms)`);
return result;
} catch (error) {
console.log(`✗ ${ctx.request.method} (${Date.now() - start}ms)`);
throw error;
}
});
// Auth middleware
server.use(async (ctx, next) => {
const token = ctx.meta.get("authToken");
if (!token && ctx.request.method !== "auth.login") {
throw new Error("Unauthorized");
}
return next();
});Typed errors with JSON-RPC error codes:
import {
RpcError,
MethodNotFoundError,
InvalidParamsError,
TimeoutError,
} from "flex-rpc";
// Server-side
server.expose("validate", (params) => {
if (!params.name) {
throw new InvalidParamsError("name is required");
}
return { valid: true };
});
// Client-side
try {
await client.call("unknown-method");
} catch (error) {
if (error instanceof MethodNotFoundError) {
console.log("Method not found:", error.method);
} else if (error instanceof TimeoutError) {
console.log("Request timed out after", error.timeoutMs, "ms");
}
}const client = createClient(transport, {
requestTimeout: 30000, // Default timeout for requests
autoConnect: true, // Connect on first request
strictMode: true, // Throw on error responses
});// TCP
new TcpClientTransport(host, port, {
connectionTimeout: 10000,
requestTimeout: 30000,
keepAlive: true,
autoReconnect: true,
maxReconnectAttempts: 5,
reconnectDelay: 1000,
});
// WebSocket
new WsClientTransport(url, {
protocols: ["json-rpc"],
pingInterval: 30000,
pongTimeout: 5000,
});
// HTTP
new HttpClientTransport(url, {
headers: { Authorization: "Bearer token" },
retry: true,
maxRetries: 3,
});const results = await client.batch([
{ method: "add", params: [1, 2] },
{ method: "multiply", params: [3, 4] },
{ method: "greet", params: { name: "World" } },
]);
results.forEach((result, i) => {
if (result.success) {
console.log(`Call ${i}: ${result.result}`);
} else {
console.log(`Call ${i} failed: ${result.error.message}`);
}
});const controller = new AbortController();
// Cancel after 1 second
setTimeout(() => controller.abort(), 1000);
try {
await client.call("slow-operation", params, { signal: controller.signal });
} catch (error) {
if (error instanceof AbortError) {
console.log("Request was cancelled");
}
}See the full API documentation for complete details.
Check out the examples directory:
MIT