typed-rpc

Lightweight JSON-RPC solution for TypeScript projects. It has beed designed for use-cases where you control both client and server and your client is the only API consumer you need to support. Therefore it comes with the following features and tradeoffs:

  • Service definition via TypeScript interfaces
  • No code generation step
  • Uses ES6 proxies (sorry, no IE11 support)
  • No runtime type-checking
  • Uses the JSON-RPC 2.0 protocol
  • No batch requests
  • HTTP(S) as only transport

Usage

Interface shared between client and server:

export interface MyService {
  hello(name: string): Promise<string>;
}

Client code:

import { rpcClient } from "typed-rpc";
import { MyService } from "../shared/MyService";

const client = rpcClient<MyService>("http://localhost:3000/api");

async function greet() {
  const greeting = await client.hello("world");
  console.log(greeting);
}

Server code:

import express from "express";
import { rpcHandler } from "typed-rpc/server";
import { MyService } from "../shared/MyService";

class MyServiceImpl implements MyService {
  async hello(name: string) {
    return `Hello ${name}!`;
  }
}

const app = express();
app.use(express.json());

app.post("/api", rpcHandler(new MyServiceImpl()));

app.listen(3000);

Advanced Usage

Accessing the request

Sometimes it's necessary to access the request object inside the service. This can be done by passing a ServiceFactory, e.g. a function that creates a service for each request:

app.post(
  "/api",
  rpcHandler((req) => new MyServiceImpl(req.user))
);

Excluding methods

If you don't want to proxy all methods to the server you can provide a second argument with local method overrides:

const client = rpcClient<MyService>(apiUrl, {
  hello() {
    return "override";
  },
});
const result = await client.hello("world");
// result === "override"

Custom headers

You can set custom request headers by providing a getHeaders function:

const client = rpcClient<MyService>(apiUrl, {
  getHeaders() {
    return {
      Authorization: auth,
    };
  },
});

Mixins

Sometimes it can be useful to mix in additional methods, for example to configure the custom headers:

const config = {
  auth: "",
  setAuth(auth: string) {
    this.auth = auth;
  },
  getHeaders() {
    if (!this.auth) return;
    return {
      Authorization: this.auth,
    };
  },
};

const client = rpcClient<MyService, typeof config>(apiUrl, config);
client.setAuth("secret");

License

MIT