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 custom transports
  • 🌎 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

The philosophy of typed-rpc is to strictly focus on the core functionality and keep things as simple as possible. The whole library basically consists of two files, one for the client and one for the server.

You won't find any unnecessarily complex concepts like middlewares, adapters, resolvers, transformers, queries or mutations.

(If you want queries and mutations though, we've got you covered.)

And instead of having adapters for all the different servers, meta frameworks or edge runtimes, we provide a generic package that is request/response agnostic and leave the wiring up to the user.

Basic 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;

[!TIP] 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 the 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 incoming 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.headers))
);

Sending custom headers

A client can send custom request headers by providing a getHeaders function:

const client = rpcClient<MyService>({
  url: "http://localhost:3000/api",
  getHeaders() {
    return {
      Authorization: auth,
    };
  },
});

[!TIP] The getHeaders function can also be async.

Aborting requests

You can abort requests by passing the Promise to .$abort() like this:

const client = rpcClient<HelloService>(url);

const res = client.hello("world");
client.$abort(res);

Error handling

In case of an error, the client will throw an RpcError instance that has a message, code and optionally a data property.

When the service throws an error, these properties will be serialized to the client.

For internal errors (invalid request, method not found) the error code is set according to the specs.

CORS credentials

To include credentials in cross-origin requests, pass credentials: 'include' as option.

Custom transport

By default, the client uses the global fetch implementation to perform requests. If you want to use a different mechanism, you can specify custom transport:

const client = rpcClient<MyService>({
  transport: async (req: JsonRpcRequest, abortSignal: AbortSignal) => {
    return {
      error: null,
      result: {
        /* ... */
      },
    };
  },
});

Support for other runtimes

The generic typed-rpc/server package can be used with any server framework or (edge-) runtime.

Fastify

With Fastify, you would use typed-rpc like this:

import { handleRpc, isJsonRpcRequest } from "typed-rpc/server";

fastify.post("/api", async (req, reply) => {
  if (isJsonRpcRequest(req.body)) {
    const res = await handleRpc(req.body, new Service(req.headers));
    reply.send(res);
  }
});

Open in StackBlitz

Deno

ðŸĶ• You can also use typed-rpc in Deno like in this example.

[!NOTE] This package is also published under https://deno.land/x/typed_rpc

Next.js

Here's an example that uses typed-rpc in a Next.js project:

Open in StackBlitz

Clodflare Workers

In a Cloudflare Worker you can use typed-rpc like this:

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",
      },
    });
  },
};

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 type-assurance 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 can pair it with react-api-query, a thin wrapper around TanStack Query. A type-safe match made in heaven. 💕

License

MIT