LIBRARY

RPC

@deepkit/rpc

The fastest way to connect your frontend with your backend or interconnect your micro-services with automatic serialization, validation, error forwarding, fully typsafe interface, and streaming capabilities using RxJS
— without code generation or config files, all TypeScript.

read documentation

How it works

You write your controller interface (actions) and models (parameter and return types) in a shared library/package that can be imported by frontend/client and server.

The server implements the interface using a simple TypeScript class and decorators.
The client consumes the interface with a client that automatically serializes and executes the action on the server.

Client

Angular, React, Vue, HTML5, Node.

import {DeepkitClient} from '@deepkit/rpc';
import {MyControllerInterface} from 'common';

//connect via WebSockets/TCP, or in-memory for SSR
const client = new DeepkitClient('localhost');
const ctrl = client.controller(MyControllerInterface);

const user = await ctrl.getUser(42);
console.log(user.username);
console.log(user instanceof User); //true

Server

Using TypeScript classes.

import {rpc} from '@deepkit/rpc';
import {MyControllerInterface, User} from 'common';

@rpc.controller(MyControllerInterface)
class MyController implements MyControllerInterface {
    @rpc.action()
    async getUser(id: number): Promise<User> {
        return new User(id);
    }
}

Common

In a common package shared with server and client you define your interface and models.

@entity.name('user')
export class User {
    username: string = '';
    constructor(public id: number) {}
}

export MyControllerInterface = Controller<MyControllerInterface>('my', [User]);
export interface MyControllerInterface {
     async getUser(id: number): Promise<User>;
}

Note: Both the client and server can register a controller, and both can consume a controller making the communication flow in both direction. This example demonstrates the simplest use-case where the server registers a controller and a client connecting via TCP/WebSockets consumes that controller.

Simple class methods

RPC actions are simple class methods, decorated with @rpc.action(). Parameter and return type are inferred as much as possible using Deepkit Type. Types that can't be inferred from TypeScript's emitDecoratorMetadata can be manually annotated using Deepkit Type. Fully type-safe since you import the TypeScript interface of your controller in your clients directly. You can even share your database models from Deepkit ORM and return them directly in your RPC action.

import {rpc} from '@deepkit/rpc';
import {MyControllerInterface, User} from 'common';

@rpc.controller(MyControllerInterface)
class MyController implements MyControllerInterface {
    @rpc.action()
    async getUser(id: number): Promise<User> {
        return new User(id);
    }

    @rpc.action()
    async labelUser(id: number, labels: string[]): Promise<{[name: string]: number}> {
        return labels.map(v => {
            return {v: 1};
        });
    }
}

Type serialization

Data types of action parameters and its return type like String, Number, Boolean, Date, arrays, typed arrays, objects, and custom entities are serialized automatically back and forth using a flexible and fast binary protocol.

class MyController implements MyControllerInterface {
    @rpc.action()
    async getUser(id: number): Promise<User> {
        return new User(id);
    }

    @rpc.action()
    hello(name: string): string {
        return 'Hello ' + name;
    }

    @rpc.action()
    async uploadFile(data: Uint8Array): Promise<boolean> {
        return true;
    }
}

Parameter validation

All parameters are automatically validated on the server side to make sure even malicious clients can't send invalid requests.

Use all available validators from Deepkit Type, or write your own validation functions.

When invalid parameters are sent a ValidationError object is thrown with detailed error code and message for each parameter, that can be nicely shown in the user interface.

class MyController implements MyControllerInterface {
    @rpc.action()
    async getUser(id: number & Positive & Maximum<10000>): Promise<User> {
        return new User(id);
    }

    @rpc.action()
    hello(name: string & Exclude<' '>): string {
        return 'Hello ' + name;
    }

    @rpc.action()
    async uploadFile(data: Uint8Array & MaxLength<1048000>): Promise<boolean> {
        return true;
    }
}

RxJS streaming

Streaming data to the client shouldn't be hard. That's why Deepkit RPC supports RxJS Observable, Subject, and BehaviorSubject natively.

Just return a Observable in an action and the client receives an observable as well, that forwards all emitted values — of course automatically serialized.

As soon as the client unsubscribes the Observable the same Observable is completed on the server side as well and triggers the unsubscribe callback.

All Observables and subscriptions are automatically closed when the client disconnects.

// client
const client = new DeepkitClient('localhost');
const ctrl = client.controller(MyControllerInterface);

const sensorData = await ctrl.sensorData();
const sub = sensorData.subscribe((next) => {
    console.log('sensor says', next);
});

//when done watching for data, but keeps Subject
//on server alive, for other subscribers.
sub.unsubscribe();

const chat = await ctrl.getChatChannel('general');
chat.subscribe((next) => {
    console.log('sensor says', next);
});

//completes the whole Subject, triggering its TearDown
chat.unsubscribe();
// server
class MyController implements MyControllerInterface {
    protected sensorData = new Subject<number>();

    @rpc.action()
    sensorData(): Subject<number> {
        // return already existing subjects
        return sensorData;
    }

    @rpc.action()
    getChatChannel(
        name: string
    ): Subject<{user: string, message: string} {
        const subject = new Subject();

        const interval = setInterval(() => {
            subject.next({
                user: 'bot', message: 'It is time!'
            });
        });
        //add tearDown function, when client closes
        subject.subscribe().add(() => {
            clearInterval(interval);
        });

        return subject;
    }

    @rpc.action()
    async subscribeNumbers(): Observable<number> {
        return new Observable((observer) => {
          console.log('new subscription!');
          observer.next(42);

          return {unsubscribe() {
            console.log('subscription ended');
          }};
        });
    }
}

Error forwarding

If an error is throwing in an action on the server, it is automatically serialized and forwarded to the client.

Custom error classes can also be used the same as with custom entity classes.

//client.ts
const client = new DeepkitClient('localhost');
const ctrl = client.controller(MyControllerInterface);

try {
    const sensorData = await ctrl.sensorData('a');
} catch (error) {
    error.message === 'No sensor a';
}

try {
    await ctrl.customError();
} catch (error) {
    error instanceof MyCustomError; //true
}
//common.ts
@entity.name('@error/my-custom')
class MyCustomError extends Error {
    codes: string[] = [];
}
//server.ts
class MyController implements MyControllerInterface {
    @rpc.action()
    getSensorData(name: string): Subject<number> {
        if (name === 'temp') return this.tempSensor;
        throw new Error(`No sensor ${name}`);
    }

    @rpc.action()
    customError(): string {
        const error = new MyCustomError();
        error.codes = ['a', 'b'];
        throw error;
    }
}

Peer2Peer

A client can register a peer controller under a global identifier which can be consumed by other clients. This allows to communicate from client to client without routing it manually as a pub/sub router (@deepkit/broker) or in-memory router is implemented out of the box.

A security layer allows to control whether a client can register as peer or consume peer controllers.

Security

You can configure an arbitrary token as login and control for each action whether a client is allowed to execute. A session system enables you to retrieve and store session related values.

Download/Upload progress

Bigger packages are chunked automatically and allow you to monitor the upload and download progress.

//client.ts
const client = new DeepkitClient('localhost');
const ctrl = client.controller(MyControllerInterface);

const progress = ClientProgress.track();
progress.upload.subscribe(progress => {
    console.log('upload progress',
        progress.upload.total, progress.current,);
});
await ctrl.uploadFile(new Uint8Array(1024*1024));

const progress2 = ClientProgress.track();
progress2.download.subscribe(progress => {
    console.log('download progress',
        progress2.download.total, progress2.download.current,
    );
});
const zip = await ctrl.downloadFile('file.zip');
//server.ts
class MyController implements MyControllerInterface {
    @rpc.action()
    uploadFile(data: Uint8Array): Promise<boolean> {
        return true;
    }

    @rpc.action()
    downloadFile(path: string): Promise<Uint8Array> {
        return new Uint8Array(1024*1024);
    }
}
Made in Germany