npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jmorrell/jsonrpc

v0.1.0

Published

Lightweight [JSON-RPC 2.0](https://www.jsonrpc.org/specification) library for TypeScript.

Readme

@jmorrell/jsonrpc

Lightweight JSON-RPC 2.0 library for TypeScript.

  • Spec-compliant JSON-RPC 2.0
  • Supports batched requests
  • Supports bidirectional communication
  • Works great with TypeScript
  • Transport-agnostic core
  • Zero dependencies
  • Designed for Cloudflare Workers

Influences

This library was influenced by the designs of:

Comparison with Cap'n Web

Cap'n Web is an object-capabilities RPC library from Kenton Varda. Beyond its support for object-capabilities, it is designed to integrate very nicely with TypeScript and Cloudflare Workers. I couldn't find a JSON-RPC library that worked as nicely, so I ~~stole its design~~ used Cap'n Web as inspiration for a JSON-RPC implementation.

Cap'n Web has a number of benefits over JSON-RPC

  • Object capabilities. You can pass functions and classes by reference. This is more than a feature, it completely changes how expressive your API can be, and enables patterns that are not possible with a more limited RPC protocol.
  • Pipelining. Invoke two methods and then pass their results into a third in one round-trip.
  • Support for non-JSON data types: ReadableStream, bigint, Date

So why would you ever want to use JSON-RPC instead?

Mainly it's boring and has been around for longer than a few months. The JSON-RPC 2.0 Spec was last updated in 2013. Even more impressive, you can sit down with a cup of coffee and read the whole thing. Your coffee might not even be cool enough to drink when you've finished.

A few more points:

  • Widely used in the Language Server Protocol which drives code-editing basically everywhere
  • Used in MCP spec
  • Easy to invoke with plain curl commands
  • Works well with browser dev tools
  • Many client implementations in many languages

Cap'n Web is strictly more powerful, and I look forward to seeing it grow and mature, but for many projects today JSON-RPC is a great fit.

Basic usage

Define a TypeScript interface

This serves as the service contract. Import it on both client and server for end-to-end type safety.

export type HelloService = {
  hello(name: string): string;
};

Client (HTTP)

import { newHttpBatchRpcSession } from "@jmorrell/jsonrpc";

const client = newHttpBatchRpcSession<HelloService>("https://example.com/api");

const result = await client.hello("World");

console.log(result);

Full autocompletion. Every method returns a Promise of its return type.

Server (Cloudflare Workers)

import { newWorkersRpcResponse } from "@jmorrell/jsonrpc";

export class HelloServiceImpl implements HelloService {
  hello(name: string) {
    return `Hello, ${name}!`;
  }
}

export default {
  async fetch(request: Request, env: Env) {
    let url = new URL(request.url);
    
    if (url.pathname === "/api") {
      return newWorkersRpcResponse(request, new HelloServiceImpl());
    }
    
    return new Response("Not found", { status: 404 });
  },
};

JSON-RPC Spec Support

This library complies with the JSON-RPC 2.0 Spec, however it intentionally leaves out support for two features:

Notifications

Only requests are allowed. You can use a request with a void return if you do not need a response.

Named arguments

In order to work nicely with TypeScript, we only accept positional arguments. Named params are rejected with -32602 Invalid params. Note that you can use one argment with an object as the first parameter to immitate using named arguments.

// Instead of this
example(a: string, b: string, c: string): string

// write your function like this:
example({ a: string, b: string, c: string}): string

HTTP client API

Auto-batching

Calls made in the same event loop turn are batched into a single HTTP request:

// These three calls produce ONE HTTP request with a JSON-RPC batch array.
const [sum, difference, product] = await Promise.all([
  client.add(1, 2),
  client.subtract(5, 3),
  client.multiply(2, 4),
]);

Batching uses setTimeout(0) -- all synchronous calls and microtasks (.then(), queueMicrotask) within the same turn are included.

Custom headers

const client = newHttpBatchRpcSession<MathService>({
  url: "https://example.com/api",
  getHeaders: () => ({ Authorization: `Bearer ${token}` }),
});

getHeaders can be async.

WebSocket RPC

newWebSocketRpcSession creates a typed RPC session over a WebSocket. Pass a URL string and it connects for you, or pass an existing WebSocket instance.

Client-to-server calls

import { newWebSocketRpcSession } from "@jmorrell/jsonrpc";
import type { ServerApi } from "./server";

const session = newWebSocketRpcSession<ServerApi>("wss://example.com/api");

const result = await session.remote.add(1, 2); // 3

Unlike the HTTP client, each call is an individual JSON-RPC message over the persistent connection -- no batching needed.

Bidirectional RPC

Both sides can call methods on each other. The client provides a local service object that the server can invoke:

import { newWebSocketRpcSession } from "@jmorrell/jsonrpc";
import type { ServerApi, ClientApi } from "./server";

// Local methods the server can call on us.
const localService: ClientApi = {
  onEvent(event) {
    console.log("Server pushed:", event);
  },
};

const session = newWebSocketRpcSession<ServerApi, ClientApi>("wss://example.com/api", localService);

// Call the server.
const result = await session.remote.add(1, 2);

On the server side (Cloudflare Workers), use newWorkersWebSocketRpcSession to get both the HTTP Response and the session handle:

import { newWorkersWebSocketRpcSession } from "@jmorrell/jsonrpc";

export default {
  async fetch(request: Request) {
    const { response, session } = newWorkersWebSocketRpcSession<ClientApi, typeof service>(
      request,
      service,
    );

    // Push events to the client.
    session.remote.onEvent({ message: "hello" });

    // Clean up when the client disconnects.
    session.onClose(() => {
      console.log("Client disconnected");
    });

    return response;
  },
};

Session lifecycle

// Register a close handler.
session.onClose(() => console.log("Connection closed"));

// Close the session (also closes the underlying WebSocket).
session.close();

// Supports Symbol.dispose for `using` declarations.
using session = newWebSocketRpcSession<ServerApi>("wss://example.com/api");

Calling a remote method after close rejects with "Session is closed". When the WebSocket drops unexpectedly, all pending calls reject and close handlers fire.

Server API

newWorkersRpcResponse(request, service, options?)

Convenience dispatcher for Cloudflare Workers. Routes based on the request:

  • POST -> HTTP batch handler (with CORS Access-Control-Allow-Origin: *)
  • WebSocket upgrade -> WebSocket RPC session
  • Anything else -> 400 Bad Request

newHttpBatchRpcResponse(request, service, options?)

HTTP handler. Takes a Request, returns a Response.

  • Non-POST requests get 405 Method Not Allowed with Allow: POST header
  • Invalid JSON returns a -32700 Parse error response
  • Notifications return 204 No Content
  • Everything else returns 200 with Content-Type: application/json

CORS is out of scope -- handle preflight before calling newHttpBatchRpcResponse, or use newWorkersRpcResponse which adds a permissive CORS header.

newWorkersWebSocketRpcResponse(request, service?, options?)

Fire-and-forget WebSocket handler for Cloudflare Workers. Creates a WebSocketPair, starts an RPC session as acceptor, and returns the 101 upgrade response. Use this when you don't need to call methods on the client.

newWorkersWebSocketRpcSession(request, service?, options?)

Same as above, but returns { response, session } so you can use session.remote to call methods on the client. See Bidirectional RPC above.

processRpc(body, service, options?)

Transport-agnostic core. Takes a parsed JSON body (not a Request), returns the response object(s) or null for notification-only requests. Use this if you need to handle the transport yourself.

import { processRpc } from "@jmorrell/jsonrpc";

ws.on("message", async (data) => {
  const body = JSON.parse(data);
  const result = await processRpc(body, myService);
  if (result !== null) ws.send(JSON.stringify(result));
});

Error handling

RpcError

When a remote method returns a JSON-RPC error, the client throws an RpcError with message, code, and optional data:

import { RpcError } from "@jmorrell/jsonrpc";

try {
  await client.divide(1, 0);
} catch (err) {
  if (err instanceof RpcError) {
    console.log(err.message); // "Division by zero"
    console.log(err.code); // -32000
  }
}

Internal errors follow the spec-defined error codes:

| Code | Meaning | | ------ | ---------------- | | -32700 | Parse error | | -32600 | Invalid request | | -32601 | Method not found | | -32602 | Invalid params | | -32603 | Internal error |

RpcProtocolError

Protocol-level issues (malformed messages, unknown response IDs, transport failures) are reported via the onError callback as RpcProtocolError instances. Each has a code string:

| Code | Meaning | | ----------------------- | --------------------------------------- | | PARSE_ERROR | Malformed JSON on transport | | INVALID_MESSAGE | Non-object message received | | UNROUTABLE_MESSAGE | Message is neither request nor response | | INVALID_RESPONSE | Response fails structural validation | | NULL_RESPONSE_ID | Response has null/undefined ID | | UNKNOWN_RESPONSE_ID | No pending call for response ID | | NOTIFICATION_RECEIVED | Unsupported notification received | | HANDLER_ERROR | Service method threw | | SEND_FAILED | Transport send threw |

License

MIT