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

@minimall.io/json-rpc

v2.0.0

Published

Zero-dependency, transport-agnostic JSON-RPC 2.0 client and server for TypeScript.

Readme

@minimall.io/json-rpc

A zero-dependency, transport-agnostic JSON-RPC 2.0 client and server for TypeScript.

Table of contents

Features

  • Zero dependencies. Nothing to audit downstream; the package ships with no runtime imports.
  • Full JSON-RPC 2.0 compliance. Single requests, batch requests, notifications, request/response id correlation, and every error code defined by the specification.
  • Transport-agnostic on both sides. Client is parameterized by a single asynchronous Transport function; Server returns a pure asynchronous Handler. Any HTTP, WebSocket, worker, or stdio adapter can satisfy either contract.
  • In-process composition. The Handler returned by Server is assignable to Transport, so Client(Server(resolver)) is a complete end-to-end wiring with no I/O layer.
  • Delegated method resolution. The server does not own a method registry — it defers lookup to a user-supplied Resolver. Flat object lookup, nested namespaces, authorization gates, lazily loaded modules — anything (methodPath: string) => Method qualifies.
  • Transport-layer context passing. The server threads an opaque context value from the transport boundary directly to every invoked method, carrying auth principals, tracing spans, or request-scoped state without module globals.
  • Unified dispatcher. One callable handles single requests, notifications, and mixed request/notification batch operations through overloads.
  • End-to-end typed errors. Server methods throw JSONRPCBaseError subclasses; the client rehydrates wire responses into the same classes for instanceof discrimination across the network boundary. Error codes are branded at the type level and range-checked at runtime.
  • Runtime validators for every wire shape. Type guards for requests, notifications, batches, responses, errors, ids, params, and version fields are exported for use in transport adapters, middleware, and custom handlers.

Installation

npm install @minimall.io/json-rpc

Requires Node.js 18 or later. The package is ESM-only ("type": "module").

Quick start

Because the Handler returned by Server is itself assignable to Transport, a Client and Server can be composed in-process with no I/O layer in between — the shortest end-to-end wiring is a single expression:

import {
  Client,
  type Dispatcher,
  JSONRPCMethodNotFoundError,
  type Method,
  type Resolver,
  Server,
} from '@minimall.io/json-rpc'

type Methods = Record<string, (...args: never[]) => unknown>

const methods: Methods = {
  add: async ([a, b]: [number, number]) => a + b,
  greet: async ({ name }: { name: string }) => `Hello, ${name}!`,
  log: async (_params: unknown): Promise<void> => {
    // side effect (e.g., audit log) — return value ignored for notifications
  },
}

const resolver: Resolver = (methodPath) => {
  if (!(methodPath in methods)) throw new JSONRPCMethodNotFoundError()
  return methods[methodPath] as Method
}

const client: Dispatcher = Client(Server(resolver))

await client('greet', { name: 'World' })                  // => 'Hello, World!'
await client('log', { event: 'demo' }, { notify: true })  // => undefined
await client('add', [1, 2])                               // => 3

Examples

The files below are self-contained, type-checked patterns under examples/. They are type-check fixtures (noEmit), not standalone scripts — each is exercised at runtime by a matching test under tests/examples/. To use a pattern in a consumer project, copy the body and swap the '../src/index.js' import for '@minimall.io/json-rpc'.

  • basic.ts — end-to-end hello world: Server and Client wired in-process through the Handler-as-Transport shortcut; two requests and a notification with no I/O layer in between.
  • flat-methods.ts — the minimal Resolver: a plain object lookup. Most apps that don't need namespacing can stop here.
  • namespace-resolver.ts — module-based routing: a reducer walks dotted method paths (products.inventory.check) against a nested namespace object, so import * as actions from './actions.js' makes every exported function a wire method automatically.
  • context.ts — per-request context forwarded from the transport to every Method call: authentication principals, role checks, tracing spans, database transactions, feature flags.
  • notifications.ts — spec §4.1 and §6 notification behavior: a single notification, a mixed batch (notification slot omitted from the response), and an all-notifications batch (returns undefined).
  • method-errors.ts — signaling protocol-level errors from methods via JSONRPCInvalidParamsError, JSONRPCServerError (codes -32099..-32000), and JSONRPCCustomError (codes outside the reserved range).
  • batch.ts — client-side mixed request/notification batch: positional results, per-slot error instances, and null entries for notification slots.
  • typed-errors.tsinstanceof discrimination across JSONRPCInvalidResponseError, the five fixed error classes, JSONRPCServerError, JSONRPCCustomError, and the JSONRPCBaseError catch-all.
  • middleware.tsResolver and Transport middleware as plain function composition: auth + logging on the server, retry + metrics on the client.
  • shared-types.ts — end-to-end type safety: a single shared Methods type drives a typed client wrapper and a typed implementation map, with param and result inference at every call site.
  • http.ts — fetch-based Transport and a RequestResponse adapter that runs unchanged on Cloudflare Workers, Bun, Deno, Node 18+, and Edge Functions.
  • websocket.ts — bidirectional RPC over a single socket: the same connection serves both outgoing client transport and incoming server handler, discriminated by the presence of method on each frame.

Server

Server returns a callable Handler.

export type Handler = (
  input: unknown,
  context?: unknown,
) => Promise<JSONRPCResponse | JSONRPCResponse[] | undefined>

It handles single requests, batch requests, notifications, structurally invalid payloads, and method-execution errors. Thrown JSONRPCBaseError subclasses are serialized into spec-compliant error responses; every other thrown value collapses to -32603 Internal error with no data field, preventing leakage of internal details.

const server: Handler = Server(resolver)

// Single request — returns JSONRPCSuccessResponse object.
const single = await server({ id: 1, jsonrpc: '2.0', method: 'add', params: [1, 2] })
// => { id: 1, jsonrpc: '2.0', result: 3 }

// Unknown-method request — returns JSONRPCErrorResponse object.
const unknownMethod = await server({ id: 1, jsonrpc: '2.0', method: 'missing' })
// => { error: { code: -32601, message: 'Method not found' }, id: 1, jsonrpc: '2.0' }

// Single notification — returns undefined.
const notification = await server({ jsonrpc: '2.0', method: 'log', params: { event: 'sign-in' } })
// => undefined

// Mixed batch — notification entries omitted from the response array.
const mixed = await server([
  { jsonrpc: '2.0', method: 'log', params: { event: 'sign-in' } },
  { id: 1, jsonrpc: '2.0', method: 'add', params: [3, 4] },
])
// => [{ id: 1, jsonrpc: '2.0', result: 7 }]

// All-notifications batch — returns undefined, not an empty array.
const allNotifications = await server([
  { jsonrpc: '2.0', method: 'log', params: { event: 'sign-in' } },
  { jsonrpc: '2.0', method: 'log', params: { event: 'sign-out' } },
])
// => undefined

// Unknown-method notification — silently swallowed; spec §4.1 requires no
// reply, so the resolver's Method not found throw never surfaces.
const unknownMethodNotification = await server({ jsonrpc: '2.0', method: 'missing' })
// => undefined

The server handler does not parse strings. It expects an already-parsed JSON value; translating malformed wire bytes into a -32700 Parse error response is the adapter's responsibility.

Return semantics:

  • Single request → a JSONRPCResponse (success or error).
  • Single notification → undefined (§4.1: the Server MUST NOT reply).
  • Batch request → an array of responses with notification slots omitted (§6).
  • Batch of only notifications → undefined (§6: the Server MUST NOT return an empty array).
  • Empty array batch → a single Invalid Request error response with id: null (§6).
  • A Method returning undefined is coerced to result: null on the wire (JSON has no undefined).

Context

Every handler invocation accepts an optional second argument that is forwarded unchanged to every method call. Typical uses include authentication principals, tracing spans, database transactions, feature flags, and request correlation ids. The library treats context as opaque; its shape is defined by the application.

type Context = {
  trace?: string
  user?: { id: string; role: 'admin' | 'user' }
}

// Methods read the context as an optional second parameter.
const whoami = async (_params: unknown, ctx?: Context) => ctx?.user?.id ?? null

// The transport builds a fresh context per incoming request.
const ctx: Context = {
  trace: req.headers['x-trace-id'],
  user: await authenticate(req),
}

const response = await server(payload, ctx)

Resolver

A Resolver is a single function that the Server depends on for method retrieval. Anything that maps a string to a Method function qualifies.

export type Method = (params: unknown, context?: unknown) => unknown
export type Resolver = (methodPath: string) => Method

Because Resolver is a single-function type, middleware (auth, logging) is plain function composition.

import {
  type Handler,
  JSONRPCCustomError,
  JSONRPCMethodNotFoundError,
  type Method,
  type Resolver,
  Server,
} from '@minimall.io/json-rpc'

import * as actions from './actions.js'

// Representative directory layout for the `./actions.js` namespace:
//
//   actions/
//     products/
//       inventory.ts   // export const check = async (...) => ...
//       index.ts       // export * as inventory from './inventory.js'
//     users/
//       profile.ts     // export const get = async (...) => ...
//       index.ts       // export * as profile from './profile.js'

type AuthContext = { user?: { id: string } }

type GenericMethod = (...args: never[]) => unknown

type Methods = {
  [key: string]: Methods | GenericMethod
}

type Reducer = Methods | GenericMethod | undefined

const reducer = (actions: Reducer, key: string): Reducer => {
  if (!actions) throw new JSONRPCMethodNotFoundError()
  if (typeof actions === 'function') throw new JSONRPCMethodNotFoundError()
  if (!Object.hasOwn(actions, key)) throw new JSONRPCMethodNotFoundError()
  return actions[key]
}

const namespaceResolver = (methods: Methods, methodPath: string): Method => {
  const path: string[] = methodPath.split('.')
  const method = path.reduce<Reducer>(reducer, methods)
  if (!method || typeof method !== 'function')
    throw new JSONRPCMethodNotFoundError()
  return method as Method
}

export const NamespaceResolver =
  (methods: Methods): Resolver =>
  (methodPath: string): Method =>
    namespaceResolver(methods, methodPath)

// Authorize using a context-provided user principal. Throws JSONRPCCustomError
// so the client receives a well-formed error response with a meaningful code.
export const withAuth =
  (next: Resolver): Resolver =>
  (methodPath) => {
    const method = next(methodPath)
    return async (params, ctx) => {
      const { user } = (ctx ?? {}) as AuthContext
      if (!user) throw new JSONRPCCustomError('Unauthorized', 401)
      return method(params, ctx)
    }
  }

export const server: Handler = Server(withAuth(NamespaceResolver(actions)))

const response = await server({ id: 1, jsonrpc: '2.0', method: 'products.inventory.check', params: { productId: 11 } }, ctx)

Protocol-level errors are signaled by throwing a JSONRPCBaseError subclass from the method. JSONRPCServerError covers codes -32099..-32000 (the implementation-defined server-error range); JSONRPCCustomError covers application-defined codes outside the reserved -32768..-32000 range. Both validate the code at construction time.

Client

Client returns a callable Dispatcher. The returned function carries two overloads:

export type Options = {
  notify?: boolean
}

export type Call = {
  method: string
  params?: unknown
  options?: Options
}

export type Dispatcher = {
  (method: string, params?: unknown, options?: Options): Promise<unknown>
  (calls: Call[]): Promise<unknown[]>
}

Requests and notifications:

const client: Dispatcher = Client(transport)

const sum = await client('add', [1, 2])
const user = await client('users.get', { id })

await client('log', { msg: 'user signed in' }, { notify: true })
await client('ping', undefined, { notify: true })

If params is not an array, object, or undefined, the client throws JSONRPCInvalidParamsError before calling the transport.

Batch operations. Pass an array of calls; results are returned positionally:

  • Request slot → the method's return value on success, or a JSONRPCBaseError subclass instance if that particular request failed.
  • Notification slot → null.

Per-request errors do not throw; they are returned at their positions. A whole-batch rejection (e.g., a server-level -32700 response with id: null) is thrown as the corresponding error class. A malformed response shape throws JSONRPCInvalidResponseError. An empty array throws JSONRPCInvalidRequestError without touching the transport.

Each outgoing request is stamped with a fresh crypto.randomUUID() id. Batch responses are reordered by id to match the caller's input order.

import { JSONRPCMethodNotFoundError } from '@minimall.io/json-rpc'

const results = await client([
  { method: 'add', params: [1, 2] },
  { method: 'audit.log', options: { notify: true }, params: { event: 'x' } },
  { method: 'users.get', params: { id: 'abc' } },
])

if (results[2] instanceof JSONRPCMethodNotFoundError) {
  // Handle the failed lookup without aborting the rest of the batch.
}

Typed error handling

Wire error codes are rehydrated into JSONRPCBaseError subclasses via errorFromJSONRPCError, enabling instanceof discrimination without manual code inspection:

import {
  JSONRPCBaseError,
  JSONRPCCustomError,
  JSONRPCInvalidParamsError,
  JSONRPCInvalidResponseError,
  JSONRPCMethodNotFoundError,
  JSONRPCServerError,
} from '@minimall.io/json-rpc'

try {
  await client('unknown.method')
} catch (error) {
  if (error instanceof JSONRPCInvalidResponseError) {
    // Transport-layer failure: malformed response, id mismatch, wrong version.
  } else if (error instanceof JSONRPCMethodNotFoundError) {
    // Method not found — -32601.
  } else if (error instanceof JSONRPCInvalidParamsError) {
    // Invalid params — -32602.
  } else if (error instanceof JSONRPCServerError) {
    // Server error — -32000 through -32099.
  } else if (error instanceof JSONRPCCustomError) {
    // Application-defined code outside the reserved range.
  } else if (error instanceof JSONRPCBaseError) {
    // Parse error (-32700), Invalid Request (-32600), Internal error (-32603), or reserved-but-unassigned codes.
  } else {
    throw error
  }
}

Every JSONRPCBaseError subclass exposes error.code and error.data for inspection — useful on JSONRPCServerError and JSONRPCCustomError, whose codes vary per instance. JSONRPCInvalidResponseError extends Error directly, not JSONRPCBaseError — it is a client-local transport failure (malformed response, id mismatch, wrong jsonrpc version) and never crosses the wire, so it needs a dedicated catch arm.

Transport

A Transport is the function that Client depends on to carry a request to the server and return the parsed response:

export type Transport = (
  request: JSONRPCRequest | JSONRPCNotification | JSONRPCBatch,
) => Promise<unknown>

The transport owns serialization, wire delivery, and response parsing. It receives a structured request object and must resolve with the parsed JSON value from the server — a JSONRPCResponse for a request, a JSONRPCResponse[] for a batch, or undefined when the server returns no body (notifications and all-notifications batches; HTTP adapters typically map this to 204 No Content).

import {
  Client,
  type Dispatcher,
  type Transport,
} from '@minimall.io/json-rpc'

const JSON_CONTENT_TYPE = 'application/json'

export const httpTransport =
  (
    url: string,
    init: { credentials?: RequestCredentials; headers?: HeadersInit } = {},
  ): Transport =>
  async (rpcRequest) => {
    const headers = new Headers(init.headers)
    headers.set('content-type', JSON_CONTENT_TYPE)
    const base: RequestInit = {
      body: JSON.stringify(rpcRequest),
      headers,
      method: 'POST',
    }
    const requestInit: RequestInit =
      init.credentials === undefined
        ? base
        : { ...base, credentials: init.credentials }
    const response = await fetch(url, requestInit)
    if (response.status === 204) return undefined
    return response.json()
  }

const client: Dispatcher = Client(httpTransport('/rpc'))

Error classes

All classes below extend JSONRPCBaseError (itself a subclass of Error) and carry code, message, and optional data — except JSONRPCInvalidResponseError, which extends Error directly and is client-local. All classes are exported from the package root.

| Class | Code | Purpose | | ------------------------------ | ------------------------- | -------------------------------------------------------------------------------------------- | | JSONRPCBaseError | any JSONRPCErrorCode | Base class; used directly for reserved-but-unassigned wire codes. | | JSONRPCParseError | -32700 | Malformed JSON on the wire. | | JSONRPCInvalidRequestError | -32600 | Payload is not a valid JSON-RPC request object. Also thrown client-side on an empty batch. | | JSONRPCMethodNotFoundError | -32601 | Method does not exist or is not callable. | | JSONRPCInvalidParamsError | -32602 | Invalid method parameters. | | JSONRPCInternalError | -32603 | Internal server error. The default for unrecognized throws. | | JSONRPCServerError | -32099..-32000 | Implementation-defined server errors. Runtime RangeError if out-of-range. | | JSONRPCCustomError | outside -32768..-32000 | Application-defined errors. Runtime RangeError if inside reserved range. | | JSONRPCInvalidResponseError | client-local | Extends Error directly. Malformed response, id mismatch, wrong version. |

Wire ↔ class conversion is round-trip symmetric. The error module exports three helpers:

import {
  errorFromJSONRPCError,
  errorToJSONRPCError,
  errorToJSONRPCErrorResponse,
  JSONRPCMethodNotFoundError,
} from '@minimall.io/json-rpc'

errorFromJSONRPCError({ code: -32601, message: 'Method not found' })
// -> JSONRPCMethodNotFoundError instance

errorToJSONRPCError(new JSONRPCMethodNotFoundError())
// -> { code: -32601, message: 'Method not found' }

errorToJSONRPCErrorResponse(new JSONRPCMethodNotFoundError(), 7)
// -> { error: { code: -32601, message: 'Method not found' }, id: 7, jsonrpc: '2.0' }

The data key is omitted from the serialized object when the error carries no data.

Validators

Validators for every JSON-RPC shape are exported for use in transport layers, middleware, and custom handlers. They act as TypeScript type guards, narrowing unknown to the corresponding typed shape at the call site.

import {
  isJSONRPCError,
  isJSONRPCErrorCode,
  isJSONRPCErrorResponse,
  isJSONRPCId,
  isJSONRPCNotification,
  isJSONRPCParams,
  isJSONRPCRequest,
  isJSONRPCResponse,
  isJSONRPCResponses,
  isJSONRPCSuccessResponse,
  isJSONRPCVersion,
} from '@minimall.io/json-rpc'

const payload: unknown = await request.json()

if (isJSONRPCRequest(payload)) {
  // payload is JSONRPCRequest — access fields without casting.
  const { id, method, params } = payload
}

| Validator | Narrows to | | ---------------------------- | --------------------------------------------------- | | isJSONRPCError | JSONRPCError | | isJSONRPCErrorCode | JSONRPCErrorCode | | isJSONRPCErrorResponse | JSONRPCErrorResponse | | isJSONRPCId | JSONRPCId | | isJSONRPCNotification | JSONRPCNotification | | isJSONRPCParams | unknown[] \| Record<string, unknown> \| undefined | | isJSONRPCRequest | JSONRPCRequest | | isJSONRPCResponse | JSONRPCResponse | | isJSONRPCResponses | JSONRPCResponse[] | | isJSONRPCSuccessResponse | JSONRPCSuccessResponse | | isJSONRPCVersion | '2.0' |

Two invariants worth noting:

  • isJSONRPCNotification rejects any object that has an id key, even id: undefined — matching the branded id?: never on JSONRPCNotification.
  • isJSONRPCResponses requires a non-empty array; an empty array returns false.

Design notes

  • Both halves are factory functions rather than classes. There is no lifecycle, registration API, or mutable configuration.
  • The server does not parse strings. It expects an already-parsed JSON value; parse errors (-32700) are the responsibility of the layer that decodes the wire format.
  • Error codes are branded at the type level — constructing any JSONRPCErrorCode with an arbitrary integer fails at compile time, and JSONRPCServerError / JSONRPCCustomError additionally validate at runtime.
  • The client generates a fresh crypto.randomUUID() id per request. Sharing a transport across multiple Client instances is safe at the library level — the UUID ids never collide, so each Client correlates its own responses correctly.
  • Batch responses preserve input order. On the client, every slot is positional — notification slots carry null. On the server, notification slots are omitted per §6 and the remaining request responses keep their relative input order. The server resolves handlers concurrently via Promise.all; the client reconstructs order via an id-keyed map.
  • An error thrown by a notification handler is silently swallowed — spec §4.1 requires no reply. Failures never surface on the wire; diagnostics must live inside the method body.
  • The Handler returned by Server is assignable to Transport, enabling zero-I/O, same-process wiring: const client = Client(Server(resolver)).
  • Middleware is plain function composition. Resolver and Transport are single-function types, so wrapping them with auth, logging, retries, or tracing requires no plugin system.
  • Context flows from the transport. The second argument to Handler is forwarded opaquely to every invoked Method, letting auth principals, tracing spans, or request-scoped state ride through without module globals.
  • The library is written in a functional style — pure functions, immutable types, composition over inheritance, and side effects confined to the transport and adapter edges.

Migration from v1.0.0

v2.0.0 is a breaking release, and v1.0.0 is deprecated on npm — upgrading is required to receive future updates. The wire format is unchanged (still JSON-RPC 2.0), but the public TypeScript API has been reshaped for stricter spec compliance, better batch ergonomics, and tighter type safety.

Client: callable dispatcher replaces the four-method object

// v1.0.0
const client = Client(transport)
await client.request('add', [1, 2])
await client.notify('log', { msg })
await client.batchRequest([{ method: 'add', params: [1, 2] }])
await client.batchNotify([{ method: 'log', params: { msg } }])

// v2.0.0
const client = Client(transport)
await client('add', [1, 2])
await client('log', { msg }, { notify: true })
await client([{ method: 'add', params: [1, 2] }])
await client([{ method: 'log', options: { notify: true }, params: { msg } }])
  • request + notify collapsed into one overload; pass { notify: true } in the third argument to send a notification.
  • batchRequest + batchNotify collapsed into a single array overload. A Call may set options: { notify: true } individually, so mixed request/notification batches are native.
  • Per-request errors in a batch are now returned as error instances at their positions, not thrown. One error no longer aborts the whole batch.
  • Request ids are now crypto.randomUUID() strings, not monotonic integers. Sharing a transport across multiple Client instances is safe.
  • Empty batch now throws JSONRPCInvalidRequestError before any I/O.
  • Non-structured params (string, number, boolean, null) are rejected client-side with JSONRPCInvalidParamsError before calling the transport.

Server: notifications now return undefined; batch slots are omitted

// v1.0.0
await server({ jsonrpc: '2.0', method: 'log' })
// -> null
await server([
  { jsonrpc: '2.0', method: 'log' },
  { id: 1, jsonrpc: '2.0', method: 'add', params: [3, 4] },
])
// -> [null, { id: 1, jsonrpc: '2.0', result: 7 }]

// v2.0.0
await server({ jsonrpc: '2.0', method: 'log' })
// -> undefined
await server([
  { jsonrpc: '2.0', method: 'log' },
  { id: 1, jsonrpc: '2.0', method: 'add', params: [3, 4] },
])
// -> [{ id: 1, jsonrpc: '2.0', result: 7 }]
// (notification slot is omitted per spec §6)
  • All-notifications batch returns undefined (not [null, null, ...]). HTTP transports translate this to 204 No Content.
  • Empty-array batch now produces a single Invalid Request response (spec §6), matching the spec's §7 empty-array example.
  • Mixed batches with invalid elements now produce per-element Invalid Request errors for the invalid slots instead of one blanket error for the whole batch.
  • A method returning undefined is coerced to result: null on the wire (JSON has no undefined).

Error hierarchy: base class renamed; spec-exact messages; new JSONRPCCustomError

| v1.0.0 | v2.0.0 | | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | JSONRPCError (base class) | JSONRPCBaseError (base class). JSONRPCError is now the wire-object type. | | 'Parse Error', 'Invalid Params', etc. | 'Parse error', 'Invalid params', etc. (spec §5.1, exact casing). | | error.name === 'JSONRPCError' | error.name === 'JSON-RPC 2.0 Error' | | errorFromResponse(wireError) | errorFromJSONRPCError(wireError) | | (none) | errorToJSONRPCError and errorToJSONRPCErrorResponse for serialization. | | JSONRPCServerError accepted any -32099..-32000 literal at compile time. | Same range, plus a runtime RangeError if the code is out of range. | | Non-standard codes fell through to JSONRPCServerError. | JSONRPCCustomError for codes outside -32768..-32000; unassigned reserved codes rehydrate to JSONRPCBaseError. | | JSONRPCInvalidResponseError extended JSONRPCError with code -32600. | JSONRPCInvalidResponseError extends Error directly — it is a client-local transport failure and never crosses the wire. |

Types and validators: additions, renames, and removals

| v1.0.0 | v2.0.0 | | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | JSONRPCRequest.id?: JSONRPCId (optional; same shape for request and notification) | JSONRPCRequest.id: JSONRPCId (required); new JSONRPCNotification type with id?: never. | | JSONRPCRequestBatch, JSONRPCResponseBatch | JSONRPCBatch (requests + notifications). No JSONRPCResponseBatch. | | JSONRPCResponse = success \| error \| null | JSONRPCResponse = success \| error; the server returns undefined for "no response" instead of embedding null in the union. | | JSONRPCErrorCode = literal union (-32600 \| ...) | Branded type union (JSONRPCParseErrorCode \| JSONRPCInvalidRequestErrorCode \| ...). Construction with invalid codes fails at compile time. | | Method = (params, context?) => Promise<unknown> | Method = (params, context?) => unknown. Sync methods are now permitted; the return type is widened, not narrowed. | | Transport = (request: JSONRPCRequest \| JSONRPCRequestBatch) => Promise<unknown> | Transport = (request: JSONRPCRequest \| JSONRPCNotification \| JSONRPCBatch) => Promise<unknown>. Custom transports must now handle standalone notifications and mixed batches containing them. | | (none) | New Handler type — the exported return type of Server, replacing the anonymous v1 callable. | | JSONRPCCall = Pick<JSONRPCRequest, 'method' \| 'params'> | Removed. Use the new Call type ({ method, params?, options? }) for batch calls — the options field carries per-call notify: true. | | isInt32, isJSONRPCServerErrorCode, isJSONRPCRequestBatch, isJSONRPCResponseBatch | All removed. isJSONRPCRequestBatch / isJSONRPCResponseBatch are superseded by per-element isJSONRPCRequest / isJSONRPCNotification checks and isJSONRPCResponses (non-empty response array). isInt32 and isJSONRPCServerErrorCode have no public replacement — range checks are now inline in the JSONRPCServerError / JSONRPCCustomError constructors. | | isJSONRPCId accepted numbers only in the int32 range. | isJSONRPCId accepts any finite number. |

Quick migration checklist

  1. Replace every client.request(m, p) with client(m, p).
  2. Replace every client.notify(m, p) with client(m, p, { notify: true }).
  3. Replace client.batchRequest([...]) / client.batchNotify([...]) with a single client([...]) call; set options: { notify: true } per entry where needed.
  4. Update batch error handling: iterate the result array and check each slot with instanceof instead of wrapping the whole call in try/catch.
  5. Replace extends JSONRPCError with extends JSONRPCBaseError wherever subclassed.
  6. Rename imports of errorFromResponseerrorFromJSONRPCError.
  7. If JSONRPCInvalidResponseError instanceof JSONRPCError was relied upon, add a dedicated catch arm — it now extends Error directly.
  8. If the server's HTTP adapter returned 200 { "result": null } for notifications, switch to 204 No Content on undefined.
  9. Delete any dead code that handled the literal null slot in batch response arrays — notification slots are now omitted.
  10. Review custom error codes: application-defined codes outside -32768..-32000 now belong in JSONRPCCustomError, not JSONRPCServerError.
  11. Update any custom Transport implementation to handle JSONRPCNotification as a standalone input and mixed batches containing notifications.
  12. Replace import type { JSONRPCCall } with import type { Call }; add options: { notify: true } per entry where per-call notification was needed.
  13. If generic method wrappers relied on ReturnType<Method> being Promise<unknown>, adjust — the return type is now unknown.
  14. If the server callable was explicitly typed, annotate with the new Handler type.

Development

npm install
npm run build           # tsc -> dist/
npm run dev             # tsc --watch
npm test                # vitest in watch mode
npm run test:run        # vitest single run
npm run test:coverage   # vitest run --coverage (v8, 100% thresholds)
npm run lint            # biome lint .
npm run check           # biome check . (format + lint + import sort)
npm run check:tests     # tsc -p tests
npm run check:examples  # tsc -p examples

The full publish gate is prepublishOnly: checktsccheck:testscheck:examplesvitest run.

License

MIT © minimall.io