typed-rpc
Lightweight JSON-RPC solution for TypeScript projects that comes with the following features and non-features:
- π©βπ§ Service definition via TypeScript types
- π JSON-RPC 2.0 protocol
- π΅οΈ Full IDE autocompletion
- πͺΆ Tiny footprint (< 1kB)
- π Support for Deno and edge runtimes
- π« No code generation step
- π« No batch requests
- π« No other transports other than HTTP(S)
- π« No runtime type-checking
- π« No IE11 support
Usage
Create a service in your backend and export its type, so that the frontend can access type information:
// server/myService.ts
export const myService = {
hello(name: string) {
return `Hello ${name}!`;
},
};
export type MyService = typeof myService;
Note Of course, the functions in your service can also be
async
.
Create a server with a route to handle the API requests:
// server/index.ts
import express from "express";
import { rpcHandler } from "typed-rpc/express";
import { myService } from "./myService.ts";
const app = express();
app.use(express.json());
app.post("/api", rpcHandler(myService));
app.listen(3000);
Note You can also use typed-rpc in servers other than Express. Check out to docs below for examples.
On the client-side, import the shared type and create a typed rpcClient
with it:
// client/index.ts
import { rpcClient } from "typed-rpc";
// Import the type (not the implementation!)
import type { MyService } from "../server/myService";
// Create a typed client:
const client = rpcClient<MyService>("http://localhost:3000/api");
// Call a remote method:
console.log(await client.hello("world"));
Thatβs all it takes to create a type-safe JSON-RPC API. π
Demo
You can play with a live example over at StackBlitz:
Advanced Usage
Accessing the request
Sometimes itβs necessary to access the request object inside the service. A common pattern is to define the service as class
and create a new instance for each request:
export class MyServiceImpl {
/**
* Create a new service instance for the given request headers.
*/
constructor(private headers: Record<string, string | string[]>) {}
/**
* Echo the request header with the specified name.
*/
async echoHeader(name: string) {
return this.headers[name.toLowerCase()];
}
}
export type MyService = typeof MyServiceImpl;
Then, in your server, pass a function to rpcHandler
that creates a service instance with the headers taken from the incoming request:
app.post(
"/api",
rpcHandler((req) => new MyService(req.user))
);
Sending custom headers
A client can send custom request headers by providing a getHeaders
function:
const client = rpcClient<MyService>(apiUrl, {
getHeaders() {
return {
Authorization: auth,
};
},
});
Note The
getHeaders
function can also beasync
.
CORS credentials
To include credentials in cross-origin requests, pass credentials: 'include'
as option.
Support for other runtimes
The generic typed-rpc/server
package can be used with any server (or edge) runtime.
Here is an example for a Cloudflare Worker:
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, service);
return event.respondWith(
new Response(JSON.stringify(data), {
headers: {
"content-type": "application/json;charset=UTF-8",
},
})
);
},
};
Runtime type checking
Warning Keep in mind that
typed-rpc
does not perform any runtime type checks.
This is usually not an issue as long as your service can handle this gracefully. If you want, you can use a library like io-ts or ts-runtime to make sure that the arguments you receive match the expected type.
React hooks
While typed-rpc
itself does not provide any built-in UI framework integrations,
you an pair it with react-api-query,
a thin wrapper around TanStack Query.
License
MIT