typed-rpc
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
Philosophy
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.
Basic Usage
Server-Side
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.
Client-Side
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. π
Demo
Play with a live example on StackBlitz:
Advanced Usage
Accessing the Incoming Request
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);
});
Sending Custom Headers
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
.
Aborting Requests
Abort requests by passing the Promise to client.$abort()
:
const client = rpcClient<HelloService>(url);
const res = client.hello("world");
client.$abort(res);
Error Handling
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.
CORS Credentials
Include credentials in cross-origin requests with credentials: 'include'
.
Custom Transport
Use a different transport mechanism by specifying custom transport:
const client = rpcClient<MyService>({
transport: async (req: JsonRpcRequest, abortSignal: AbortSignal) => {
return {
error: null,
result: {
/* ... */
},
};
},
});
Websockets
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"
})
});
Support for Other Runtimes
typed-rpc/server
can be used with any server framework or edge runtime.
Fastify
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);
});
Deno
Example with Deno in this repository.
Next.js
Example with Next.js:
Cloudflare Workers
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" },
});
},
};
Support for Non-JSON Types
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 });
Runtime Type Checks
typed-rpc
does not perform runtime type checks. Consider pairing it with type-assurance for added safety.
React Hooks
Pair typed-rpc
with react-api-query for UI framework integration.
Changelog
6.1.1
- Add back
"main"
and"module"
entry points inpackage.json
in addition to the exports map.
6.1.0
- Built-in support for websockets.
- Pluggable request ID generator with better default (date + random string)
6.0.0
- 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.
License
MIT