Lightweight, type-safe RPC over HTTP and WebSockets.
The tRPC and Next.js Server Actions, but for everyone.
Lixnet is a tiny yet powerful TypeScript-first RPC library for teams who want a small, explicit RPC layer with excellent typing, without codegen or framework lock-in.
If you like the ergonomics of tRPC or Next.js Server Actions, but want something that:
- stays close to the Fetch platform (
Request/Response) - works in Bun / Next.js / Workers
- keeps the protocol simple and inspectable
Lixnet is a good fit.
It gives you:
- HTTP RPC via
fetch(LixnetServer+LixnetClient) - WebSocket events + callbacks (
LixnetPeer) - Optional Zod validation on the server
- Next.js-like
request.cookies()/request.headers()ergonomics inside handlers (without pulling in Next)
It’s designed to be minimal: no codegen, no schema registry, no client bundler magic.
- Quickstart (HTTP RPC)
- Framework adapters (server wiring)
- Server:
LixnetServer - Client:
LixnetClient - Handlers:
requestandresponse - WebSockets:
LixnetPeer - API reference
- Contributor docs
- License
Install:
bun add lixnet zodOther package managers:
pnpm add lixnet zod
npm i lixnet zodThis is the contract shared by server and client.
type Events = {
greet: (input: { name: string }) => Promise<string>;
};Register handlers with optional Zod schemas, then expose an HTTP endpoint that forwards the incoming Request to server.handle(...).
import { z } from "zod";
import { LixnetServer } from "lixnet";
type Events = {
greet: (input: { name: string }) => Promise<string>;
};
export const server = new LixnetServer<Events>({
debugLog: false,
});
server.on({
event: "greet",
schema: z.object({ name: z.string() }),
handler: async ({ name, request, response }) => {
// Optional: read request cookies/headers (Next-like API)
const userAgent = request.headers().get("user-agent");
if (userAgent) response.header("x-user-agent", userAgent);
return `Hello, ${name}!`;
},
});Lixnet uses the Fetch standard Request/Response, so wiring is always “forward Request into server.handle(...)”.
import { server } from "./rpc/server";
Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
if (req.method === "POST" && url.pathname === "/rpc") {
return server.handle(req);
}
return new Response("Not found", { status: 404 });
},
});export async function POST(req: Request) {
return server.handle(req);
}export default {
async fetch(req: Request) {
const url = new URL(req.url);
if (req.method === "POST" && url.pathname === "/rpc") {
return server.handle(req);
}
return new Response("Not found", { status: 404 });
},
};import { LixnetClient } from "lixnet";
type Events = {
greet: (input: { name: string }) => Promise<string>;
};
const client = new LixnetClient<Events>({ rpcUrl: "/api/rpc" });
const message = await client.call("greet", { name: "World" });If you need auth/cookies, pass fetch options:
await client.call(
"greet",
{ name: "World" },
{ credentials: "include", headers: { Authorization: "Bearer ..." } },
);server.on(...) accepts either one registration object or an array.
event: string key matching yourEventstypeschema(optional): Zod schema to validateinputhandler: receives validated input plus{ request, response }injections
Example registering multiple events:
server.on([
{
event: "greet",
schema: z.object({ name: z.string() }),
handler: async ({ name }) => `Hello, ${name}!`,
},
{
event: "health",
handler: async () => ({ ok: true as const }),
},
]);server.handle(request) expects the request body to be JSON:
{ "event": "someEvent", "input": { "...": "..." } }Responses are JSON and shaped as either:
{ "data": "..." }or
{ "error": "..." }new LixnetServer({ ... }) supports:
defaultHeaders: headers included on every response (unless deleted later)formatter: customize howLixnetResponsebecomes aResponselogger+debugLog: currently used for invalid JSON logging
Example:
const server = new LixnetServer<Events>({
debugLog: true,
defaultHeaders: {
"access-control-allow-origin": "*",
},
});client.call(event, input, options?):
- sends a
POSTwith{ event, input } - throws
Error(...)when the response contains{ error } - otherwise returns
{ data }’s value
const client = new LixnetClient<Events>({
rpcUrl: "https://api.example.com/rpc",
});Handler signature is:
- your validated input fields
- plus:
request: aRequestenhanced withrequest.cookies()andrequest.headers()response: aLixnetResponseyou can use to stage headers/cookies/status
Common operations:
- Set status:
response.code(201) - Set header:
response.header("X-Foo", "bar") - Delete header:
response.deleteHeader("x-foo") - Set cookie:
response.cookie("session", "abc", { httpOnly: true, path: "/" }) - Delete cookie:
response.deleteCookie("session", { path: "/" })
request.cookies() / request.headers() also support “read your writes” inside the handler because they merge staged response mutations.
LixnetPeer is a typed event dispatcher for WebSockets with:
- fire-and-forget events
- optional callbacks (request/response style over WS)
- optional chunking for large payloads (see contributor docs for protocol details)
Minimal sketch:
import { LixnetPeer } from "lixnet";
type ClientToServer = {
ping: (input: { t: number }) => Promise<{ ok: true }>;
};
type ServerToClient = {
notify: (input: { message: string }) => void;
};
const peer = new LixnetPeer<ServerToClient, ClientToServer>();
peer.on("ping", async ({ t, socket }) => {
return { ok: true };
});
// In your WS message handler:
// peer.handle({ data: event.data, socket: ws });const peer = new LixnetPeer<ServerToClient, ClientToServer>();
peer.setSocket(ws);
ws.addEventListener("message", (event) => {
peer.handle({ data: event.data, socket: ws });
});await peer.call(
"ping",
{ t: Date.now() },
{
callback: (result) => {
// result is typed as Awaited<ReturnType<ClientToServer["ping"]>>
console.log(result.ok);
},
},
);If messages can exceed your runtime’s WS frame limits, enable chunking:
peer.setTransmissionLimit(64_000); // bytes-ish (string length)
peer.setTransmissionChunksLimit(20); // currently stored, not enforced by senderFrom src/exports.ts:
LixnetServerLixnetClientLixnetPeerLixnetResponse
Types:
LXN_ServerClient_EventTypeLXNServerHandlerFunctionInputLXN_ServerClient_Request(alias ofLixnetRequest)LixnetRequest,LixnetCookies,LixnetHeaders
- Implementation + extension guide:
FOR_AGENTS.md
MIT