Lightweight JSON-RPC solution for TypeScript projects with the following features:
- 👩‍🔧 Service definition via TypeScript types
- 📜 JSON-RPC 2.0 protocol
- 🕵️ Full IDE autocompletion
- 🪶 Tiny footprint
- 🏝️ Optional support for non-JSON types
- đźšš Support for custom transports
- 🔌 Optional websocket support
- 🌎 Support for Deno and edge runtimes
- đźš« No code generation step
- đźš« No dependencies
- đźš« No batch requests
- đźš« No runtime type-checking
- đźš« No IE11 support
- 🥱 No fancy project page, just this README
typed-rpc focuses on core functionality, keeping things as simple as possible. The library consists of just two files: one for the client and one for the server.
You'll find no unnecessary complexities like middlewares, adapters, resolvers, queries, or mutations. Instead, we offer a generic package that is request/response agnostic, leaving the wiring up to the user.
First, define your typed service. This example shows a simple service with a single method:
// server/myService.ts
export const myService = {
hello(name: string) {
return `Hello ${name}!`;
},
};
export type MyService = typeof myService;Tip: Functions in your service can also be
async.
Create a server route to handle API requests:
// server/index.ts
import express from "express";
import { handleRpc } from "typed-rpc/server";
import { myService } from "./myService.ts";
const app = express();
app.use(express.json());
app.post("/api", (req, res, next) => {
handleRpc(req.body, myService)
.then((result) => res.json(result))
.catch(next);
});
app.listen(3000);Note:
typed-rpccan be used with servers other than Express. Check out the docs below for examples.
Import the shared type and create a typed rpcClient:
// client/index.ts
import { rpcClient } from "typed-rpc";
import type { MyService } from "../server/myService";
const client = rpcClient<MyService>("/api");
console.log(await client.hello("world"));Once you start typing client. in your IDE, you'll see all your service methods and their signatures suggested for auto-completion. 🎉
Play with a live example on StackBlitz:
Define the service as a class to access request headers:
export class MyServiceImpl {
constructor(private headers: Record<string, string | string[]>) {}
async echoHeader(name: string) {
return this.headers[name.toLowerCase()];
}
}
export type MyService = typeof MyServiceImpl;Create a new service instance for each request:
app.post("/api", (req, res, next) => {
handleRpc(req.body, new MyService(req.headers))
.then((result) => res.json(result))
.catch(next);
});Clients can send custom headers using a getHeaders function:
const client = rpcClient<MyService>({
url: "/api",
getHeaders() {
return { Authorization: auth };
},
});Tip: The
getHeadersfunction can also beasync.
Abort requests by passing the Promise to client.$abort():
const client = rpcClient<HelloService>(url);
const res = client.hello("world");
client.$abort(res);In case of an error, the client throws an RpcError with message, code, and optionally data. Customize errors with RpcHandlerOptions or provide an onError handler for logging.
For internal errors (invalid request, method not found), the error code follows the specs.
Include credentials in cross-origin requests with credentials: 'include'.
Use a different transport mechanism by specifying custom transport:
const client = rpcClient<MyService>({
transport: async (req: JsonRpcRequest, abortSignal: AbortSignal) => {
return {
error: null,
result: {
/* ... */
},
};
},
});Typed-rpc comes with an alternative transport that uses websockets:
import { websocketTransport } from "typed-rpc/ws";
import
const client = rpcClient<MyService>({
transport: websocketTransport({
url: "wss://websocket.example.org"
})
});typed-rpc/server can be used with any server framework or edge runtime.
Example with Fastify:
import { handleRpc } from "typed-rpc/server";
fastify.post("/api", async (req, reply) => {
const res = await handleRpc(req.body, new Service(req.headers));
reply.send(res);
});Example with Deno in this repository.
Example with Next.js:
Example with Cloudflare Workers:
import { handleRpc } from "typed-rpc/server";
import { myService } from "./myService";
export default {
async fetch(request: Request) {
const json = await request.json();
const data = await handleRpc(json, myService);
return new Response(JSON.stringify(data), {
headers: { "content-type": "application/json;charset=UTF-8" },
});
},
};Configure a transcoder like superjson for non-JSON types.
On the client:
import { serialize, deserialize } from "superjson";
const transcoder = { serialize, deserialize };
const client = rpcClient<DateService>({
url: "/my-date-api",
transcoder,
});On the server:
import { serialize, deserialize } from "superjson";
const transcoder = { serialize, deserialize };
handleRpc(json, dateService, { transcoder });typed-rpc does not perform runtime type checks. Consider pairing it with type-assurance for added safety.
Pair typed-rpc with react-api-query for UI framework integration.
- Ensure the RpcClient is not treated as a promise. See #33
- Add back
"main"and"module"entry points inpackage.jsonin addition to the exports map.
- Built-in support for websockets.
- Pluggable request ID generator with better default (date + random string)
- Services can now expose APIs with non-JSON types like Dates, Maps, Sets, etc. by plugging in a transcoder like superjson.
- Previously, typed-rpc only shipped a CommonJS build in
/liband Deno users would directily consume the TypeScript code in/src. We now use pkgroll to create a hybrid module in/distwith both.mjsand.cjsfiles. - We removed the previously included express adapter to align with the core philosopy of keeping things as simple as possible.
MIT