Feb 23, 2019

ts-rpc-http is a set of utilities that makes it extremely easy to make an RPC API using NodeJS with TypeScript. The basic idea is you start with your models defined in a single TypeScript file that can be shared between your backend and your frontend. Then with that single file and ts-rpc-http, you can generate a client automatically as well as jsonSchemas to perform automatic request validation:

npm i ts-rpc-http
npx ts-generate --model src/models.ts --clients clients.ts --schemas

Of course, you still need to create the server and the model file, but that isn’t too hard give the type safety we can get through ts-rpc-http out of the box.

GitHub / NPM

  Coverage Status  

View the repo on GitHub, it is open source and under the MIT license. Install via NPM as shown above.

Motivation

The motivation of this set of utilities is to make it extremely simple to create RPC APIs in TypeScript with a single source of truth for the API contract. Then with that contract, generating clients automatically that provide additional type safety at the client layer. This sort of type safety and code generation makes rapid prototyping and iteration a breeze with a TypeScript stack like MongoDB/NodeJS/React.

Type Hints

By using ts-rpc-http, you get a lot of nice type hints for all of your service method definitions, making it extremely clear what sort of JSON body you are receiving and throwing errors if you return the wrong response type per the service definition. See below for some examples of in editor type hints:

As you can see, the syntax is identical to Express because ts-rpc-http is just a small wrapper over Express. But what we get instead is a much clearer understanding of the request type as well as the response type (seen below).

This makes writing the service code extremely easy as TypeScript will yell at you if you try to return a malformed object or even the wrong type of object completely:

Example Model

You define the model using the RequestResponse type defined by ts-rpc-http, which will enable a lot of nice features for us later one when we create the server. All service definitions should extend the RPCService interface and specify the name of the service through its generic argument as seen below. Having this interface as a heritage clause allows us to pick up your service definitions easily via the AST. Feel free to have multiple service definitions that each extend the RPCService interface and multiple clients will be generated for you.

import { RequestResponse, RPCService } from 'ts-rpc-http/requestResponse';

export interface createTodoRequest {
  description: string;
}

export interface Todo {
  id: string;
  description: string;
  dateCreated: Date;
}

export interface ServiceDefinition extends RPCService<"Todo"> {
  createTodo: RequestResponse<createTodoRequest, Todo>;
  createTodoAsync: RequestResponse<createTodoRequest, Todo>;
}

Example Server

Creating the server following our model definition is extremely easy and supports both synchronous workflows as well as async/await as demonstrated below.

import { ServiceDefinition } from './models';
import { Server } from 'ts-rpc-http/server';

const server = new Server<ServiceDefinition>();

//validate schemas per the schema folder (optional)
server.validateSchemas('/src/schema');

server.rpc('createTodoAsync', async (req, res) => {
  //do some async stuff
  const description: string = await new Promise((resolve, reject) => {
    return resolve('promise description' + req.body.description);
  });

  res.status(200).send({
    id: 'random-id',
    dateCreated: new Date(),
    description,
  });
});

server.rpc('createTodo', (req, res) => {
  res.status(200).send({
    id: 'random-id',
    dateCreated: new Date(),
    description: req.body.description,
  });
});

server
  .start()
  //no need to log this as it is done by the server automatically with the port
  .then(r => console.log('Started server...'))
  .catch(e => new Error(e));

JSONSchemas may be generated for your service definition automatically if you choose, and the validation itself is opt-in. So if you’d prefer to write your own custom validation, you may. By defining properties as required or optional within TypeScript itself, you also get a decent amount of flexibility as far as which properties are checked with the automatic validation.

Generated Client

Below is a very simple example of a client that is generated with ts-rpc-http, giving you an idea of the type safety you get at the client layer. Imagine the time saved if you had 10 routes, or 20 routes.

import Client from 'ts-rpc-http/client';
import {
  ServiceDefinition,
  createTodoRequest,
  Todo,
} from './models';

export class TodoClient {
  private client: Client<ServiceDefinition>;
  constructor(baseURL: string) {
    this.client = new Client(baseURL);
  }

  public createTodo = async (
    body: createTodoRequest,
    token?: string
  ): Promise<Todo> => this.client.call('createTodo', body, token);

}