@minimall.io/json-rpc
v2.0.0
Published
Zero-dependency, transport-agnostic JSON-RPC 2.0 client and server for TypeScript.
Maintainers
Readme
@minimall.io/json-rpc
A zero-dependency, transport-agnostic JSON-RPC 2.0 client and server for TypeScript.
Table of contents
- Features
- Installation
- Quick start
- Examples
- Server
- Client
- Error classes
- Validators
- Design notes
- Migration from v1.0.0
- Development
- License
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.
Clientis parameterized by a single asynchronousTransportfunction;Serverreturns a pure asynchronousHandler. Any HTTP, WebSocket, worker, or stdio adapter can satisfy either contract. - In-process composition. The
Handlerreturned byServeris assignable toTransport, soClient(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) => Methodqualifies. - Transport-layer context passing. The server threads an opaque
contextvalue 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
JSONRPCBaseErrorsubclasses; the client rehydrates wire responses into the same classes forinstanceofdiscrimination 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-rpcRequires 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]) // => 3Examples
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:ServerandClientwired in-process through the Handler-as-Transport shortcut; two requests and a notification with no I/O layer in between.flat-methods.ts— the minimalResolver: 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, soimport * as actions from './actions.js'makes every exported function a wire method automatically.context.ts— per-request context forwarded from the transport to everyMethodcall: 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 (returnsundefined).method-errors.ts— signaling protocol-level errors from methods viaJSONRPCInvalidParamsError,JSONRPCServerError(codes-32099..-32000), andJSONRPCCustomError(codes outside the reserved range).batch.ts— client-side mixed request/notification batch: positional results, per-slot error instances, andnullentries for notification slots.typed-errors.ts—instanceofdiscrimination acrossJSONRPCInvalidResponseError, the five fixed error classes,JSONRPCServerError,JSONRPCCustomError, and theJSONRPCBaseErrorcatch-all.middleware.ts—ResolverandTransportmiddleware as plain function composition: auth + logging on the server, retry + metrics on the client.shared-types.ts— end-to-end type safety: a single sharedMethodstype drives a typed client wrapper and a typed implementation map, with param and result inference at every call site.http.ts— fetch-basedTransportand aRequest→Responseadapter 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 ofmethodon 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' })
// => undefinedThe 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 Requesterror response withid: null(§6). - A
Methodreturningundefinedis coerced toresult: nullon the wire (JSON has noundefined).
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) => MethodBecause 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
JSONRPCBaseErrorsubclass 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:
isJSONRPCNotificationrejects any object that has anidkey, evenid: undefined— matching the brandedid?: neveronJSONRPCNotification.isJSONRPCResponsesrequires a non-empty array; an empty array returnsfalse.
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
JSONRPCErrorCodewith an arbitrary integer fails at compile time, andJSONRPCServerError/JSONRPCCustomErroradditionally validate at runtime. - The client generates a fresh
crypto.randomUUID()id per request. Sharing a transport across multipleClientinstances is safe at the library level — the UUID ids never collide, so eachClientcorrelates 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 viaPromise.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
Handlerreturned byServeris assignable toTransport, enabling zero-I/O, same-process wiring:const client = Client(Server(resolver)). - Middleware is plain function composition.
ResolverandTransportare 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
Handleris forwarded opaquely to every invokedMethod, 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+notifycollapsed into one overload; pass{ notify: true }in the third argument to send a notification.batchRequest+batchNotifycollapsed into a single array overload. ACallmay setoptions: { 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 multipleClientinstances is safe. - Empty batch now throws
JSONRPCInvalidRequestErrorbefore any I/O. - Non-structured
params(string, number, boolean, null) are rejected client-side withJSONRPCInvalidParamsErrorbefore 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 to204 No Content. - Empty-array batch now produces a single
Invalid Requestresponse (spec §6), matching the spec's §7 empty-array example. - Mixed batches with invalid elements now produce per-element
Invalid Requesterrors for the invalid slots instead of one blanket error for the whole batch. - A method returning
undefinedis coerced toresult: nullon the wire (JSON has noundefined).
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
- Replace every
client.request(m, p)withclient(m, p). - Replace every
client.notify(m, p)withclient(m, p, { notify: true }). - Replace
client.batchRequest([...])/client.batchNotify([...])with a singleclient([...])call; setoptions: { notify: true }per entry where needed. - Update batch error handling: iterate the result array and check each slot with
instanceofinstead of wrapping the whole call intry/catch. - Replace
extends JSONRPCErrorwithextends JSONRPCBaseErrorwherever subclassed. - Rename imports of
errorFromResponse→errorFromJSONRPCError. - If
JSONRPCInvalidResponseError instanceof JSONRPCErrorwas relied upon, add a dedicated catch arm — it now extendsErrordirectly. - If the server's HTTP adapter returned
200 { "result": null }for notifications, switch to204 No Contentonundefined. - Delete any dead code that handled the literal
nullslot in batch response arrays — notification slots are now omitted. - Review custom error codes: application-defined codes outside
-32768..-32000now belong inJSONRPCCustomError, notJSONRPCServerError. - Update any custom
Transportimplementation to handleJSONRPCNotificationas a standalone input and mixed batches containing notifications. - Replace
import type { JSONRPCCall }withimport type { Call }; addoptions: { notify: true }per entry where per-call notification was needed. - If generic method wrappers relied on
ReturnType<Method>beingPromise<unknown>, adjust — the return type is nowunknown. - If the server callable was explicitly typed, annotate with the new
Handlertype.
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 examplesThe full publish gate is prepublishOnly: check → tsc → check:tests → check:examples → vitest run.
License
MIT © minimall.io
