Skip to main content
Deno 2 is finally here ๐ŸŽ‰๏ธ
Learn more

typed-rpc

npm bundle size

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 (< 1kB)
  • ๐Ÿšš Support for custom transports
  • ๐Ÿ๏ธ Optional support for non-JSON types
  • ๐ŸŒŽ 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:

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

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: {
        /* ... */
      },
    };
  },
});

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

Open in StackBlitz

Deno

Example with Deno in this repository.

Next.js

Example with Next.js:

Open in StackBlitz

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.

Whatโ€™s new in v6

  • 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