Skip to content

yariv/InferRPC

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

39 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

InferRPC Introduction

InferRPC is a TypeScript library that facilitates writing API clients and servers that communicate by a protocol described by a shared schema. InferRPC provides the following benefits:

  1. Compile-time type checking of both client and server code.
  2. Run-time validation of requests and responses to ensure they comply with the schema's basic types as well as extended validation rule (e.g. does this string represent a URL?)
  3. Zero code generation.
  4. Easy integration into any backend framework. Currently, Koa and NextJS are supported.
  5. Extensible to support arbitrary serialization protocols (only JSON is currently supported).

InferRPC uses Zod for expressing request/response schemas and for validating payloads.

Usage

To implement an API client and/or server using InferRPC, you typically start by implementing a static data structure describing the schema. InferRPC uses type inference to infer the request/response types from this data structure.

InferRPC schemas are TypeScript dictionaries mapping method names to request and response schema.

In TypeScript, a schema is a data structure that adheres to the following type signature:

import * as z from "zod";

type AbstractSchemaType = Record<
  string,
  { req: z.ZodType<any>; res: z.ZodType<any> }
>;

Here's an example schema with 2 methods:

  1. sayHi, which takes a name parameter and returns a string.
  2. divide, which takes two numbers and returns the result of dividing num1 by num2.
import * as z from "zod";

export const testSchema = {
  sayHi: {
    req: z.object({
      name: z.string(),
    }),
    res: z.string(),
  },
  divide: {
    req: z.object({
      num1: z.number(),
      num2: z.number(),
    }),
    res: z.number(),
  },
};

This example only uses basic types (strings and numbers) but Zod lets you express more complex validation rules, such as whether a string contains a date, a URL, or an email address.

Server Example

A complete Koa-based server for this schema can be implemented with the following code snippet:

import Koa from "koa";
import Router from "koa-router";
import { Server } from "net";
import { createKoaRoute } from "infer-rpc/koaAdapter";
import { ApiHttpError } from "infer-rpc/types";
import { testSchema } from "./testSchema";

const createServer = (port: number): Server => {
  const koa = new Koa();
  const apiRouter = new Router({
    prefix: pathPrefix,
  });

  createKoaRoute(apiRouter, testSchema, "divide", async ({ num1, num2 }) => {
    if (num2 === 0) {
      throw new ApiHttpError("Can't divide by 0", 400);
    }
    return num1 / num2;
  });

  createKoaRoute(apiRouter, testSchema, "sayHi", async ({ name }) => {
    return "Hi " + name;
  });

  koa.use(apiRouter.allowedMethods());
  koa.use(apiRouter.routes());
  return koa.listen(port);
};

InferRPC provides an alternative way to implement the server by inferring the interface derived from the schema. When you create a server that implements the inferred interface, the TypeScript compiler automatically checks for you that you've implemented all the required methods.

Here's an example snippet based on the code snippet above:

import { createKoaRoutes } from "infer-rpc/koaAdapter";
import type { InferInterace } from "infer-rpc/types";

...

const server: InferInterface<typeof testSchema> = {
  sayHi({ name }) {
    return "Hi " + name;
  },

  divide({ num1, num2 }) {
    if (num2 === 0) {
      throw new ApiHttpError("Can't divide by 0", 400);
    }
    return num1 / num2;
  },
};
createKoaRoutes(apiRouter, testSchema, server);

NextJS is also supported. This snippet shows to implement a NextJS API handler:

import { createNextHandler } from "InferRPC/nextAdapter"

export default createNextHandler(
      testSchema,
      "divide",
      async ({ num1, num2 }) => {
        if (num2 === 0) {
          throw new ApiHttpError("Can't divide by 0", 400);
        }
        return num1 / num2;
      }
    );
  });

Below are a few screenshots from VSCode highlighting the benefits of how InferRPC leverages TypeScript's type checking.

  • Your editor's auto-complete feature can help you choose a valid method name: Screen Shot 2021-03-10 at 9 33 39 AM

  • If you try to implement an invalid method name, you get an error: Screen Shot 2021-03-09 at 4 57 41 PM

  • If you try to add an invalid parameter name, you get an error: Screen Shot 2021-03-09 at 4 58 22 PM

  • If your method returns an invalid response type, you get an error: Screen Shot 2021-03-09 at 4 59 39 PM

Client Example

Implementing a client that adheres to the schema is easy. Here's an example:

const testClient = async () => {
  const client = new TypedHttpClient("http://localhost:3001/api", testSchema);
  const result = await client.call("divide", { num1: 10, num2: 2 });
  console.log(result);
};

The following screenshots show the benefits of InferRPC's static type checking.

  • Your editor can help you auto-complete the valid method names: Screen Shot 2021-03-09 at 3 43 25 PM

  • Auto-complete also works for parameters: Screen Shot 2021-03-09 at 3 44 28 PM

  • If you enter a wrong method name, you get an error: Screen Shot 2021-03-09 at 5 02 01 PM

  • If you enter a wrong parameter name, you get an error: Screen Shot 2021-03-09 at 5 02 30 PM

  • If you enter a wrong parameter type, you get an error: Screen Shot 2021-03-09 at 5 03 05 PM

  • The response type is checked as well: Screen Shot 2021-03-09 at 5 09 00 PM

  • If the client sends an invalid request to the server, the server will return a Zod validation error, as shown in this snippet (note that Zod lets you customize error messages if you want them to be more user friendly): Screen Shot 2021-03-09 at 5 26 58 PM

Peer-to-Peer Protocols

InferRPC also supports statically typed peer-to-peer protocols for usage with bi-directional streams such as WebSockets.

Peer-to-Peer protocols differ from client-server protocol in that they don't have a pre-defined request-response flow. Instead, each peer may send and receive messages at any time. This leads to a different schema definition:

type PeerSchema = Record<string, z.ZodType<any>>;

This schema is a mapping between method names and payloads, which can be any Zod type.

Here's an example, inspired by the client/server example above:

const schema1 = {
  divide: z.object({
    num1: z.number(),
    num2: z.number(),
  }),
  sayHi: z.object({
    name: z.string(),
  }),
};

const schema2 = {
  divideResult: z.number(),
  sayHiResult: z.string(),
};

To use InferRFC in a p2p application, create a Peer object. Its constructor takes 2 schemas: the schema for the incoming messages, the schema for the outgoing messages, and a listener for errors. Here's an example:

const listener: PeerListener = {
  onMissingHandler: (msgType) => {
    console.error("missing handler", msgType);
  },
  onParseError: (error) => {
    console.error("parse error", error);
  },
};

const peer1 = new Peer(schema1, schema2, listener);
const peer2 = new Peer(schema2, schema1, listener);

In this example, the two Peers are initialized. The Peers use opposite schemas for incoming and outgoing messages, but it's also possible for Peers to use the same schema if their protocol is symmetric.

The following snippet shows how to simulate bi-directional communication between peers:

peer1.setHandler("divide", async ({ num1, num2 }) => {
  peer2.onMessage(peer1.serialize("divideResult", num1 / num2));
});
peer1.setHandler("sayHi", async ({ name }) => {
  peer2.onMessage(peer1.serialize("sayHiResult", "Hi " + name));
});
peer2.setHandler("divideResult", async (num) => {
  console.log(num); // prints '2'
});
peer2.setHandler("sayHiResult", async (result) => {
  console.log(result); // prints "Hi Sarah";
});

peer1.onMessage(peer2.serialize("divide", { num1: 10, num2: 5 }));
peer1.onMessage(peer2.serialize("sayHi", { name: "Sarah" }));

Just as with the client/server examples above, InferRPC ensures types as statically checked and your IDE can auto-complete valid values for you.

  • Handler names are auto-completed: Screen Shot 2021-03-10 at 3 28 30 PM

  • So are handler parameters: Screen Shot 2021-03-10 at 3 25 23 PM

  • Invalid handler parameters cause compilation errors: Screen Shot 2021-03-10 at 3 25 43 PM

The same features work for serializing messages before they are sent over the wire.

  • Message names are auto-completed: Screen Shot 2021-03-10 at 3 26 19 PM

  • So are parameter names: Screen Shot 2021-03-10 at 3 26 38 PM

  • Invalid parameter types cause compilation errors: Screen Shot 2021-03-10 at 3 26 53 PM

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published