typed-rpc

npm bundle size

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:

Open in 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 be async.

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