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-rpc
can 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
getHeaders
function 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.
- Add back
"main"
and"module"
entry points inpackage.json
in 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
/lib
and Deno users would directily consume the TypeScript code in/src
. We now use pkgroll to create a hybrid module in/dist
with both.mjs
and.cjs
files. - We removed the previously included express adapter to align with the core philosopy of keeping things as simple as possible.
MIT