@deepkit/rpc

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

Features

Runtime Types
Use just regular TypeScript for RPC functions definition.
Validation
Built-In validation for parameters using just TypeScript types.
Streaming
RxJS Observable, Subject, and BehaviorSubject natively supported.
Error Forwarding
Thrown errors are serialized and forwarded to the client.
Progress Tracking
Built-in upload/download progress tracking.
Peer to Peer
Peer to peer communication using Deepkit Broker.
Dependency Injection
All controller with access to the dependency injection container.
Transport Agnostic
Fast binary protocol with WebSocket, TCP and in-memory for SSR out of the box.
Classes
Full support of classes including constructor.

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 client and server.

The server implements actions using the @rpc decorator. The client imports the type only and can call all the actions as if they were local functions.

import { DeepkitClient } from '@deepkit/rpc';
import type { MyController } from './server.ts';
import { User } from './shared/models';

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

const user = await main.getUser(42);
console.log(user.username);
console.log(user instanceof User); //true
import { rpc } from '@deepkit/rpc';
import { User } from './shared/models';

@rpc.controller('/main')
class MyController  {

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

Class Controllers

RPC actions are simple class methods, decorated with @rpc.action(). Parameter and return type are automatically serialized and validated thanks to Deepkit Runtime Types.

You can even share your database models from Deepkit ORM and return them directly in your RPC action.

import { Positive } from '@deepkit/type';
import { rpc } from '@deepkit/rpc';
import { User } from './shared/models';

@rpc.controller('/main')
class MyController  {
    @rpc.action()
    async getUser(id: number & Positive): 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, classes, and custom entities are serialized automatically back and forth using a flexible and fast binary protocol.

Nominal class supports is fully supported, so you can use the same class name in different contexts.

@rpc.controller('/main')
class MyController {
    @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 invalid requests won't go through.

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.

import { Positive, Maximum, 
    Exclude, MaxLength } from "@deepkit/type";

@rpc.controller('/main')
class MyController {
    @rpc.action()
    async getUser(
        id: number & Positive & Maximum<10_000>
    ): Promise<User> {
        return new User(id);
    }

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

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

Streaming

Streaming data to the client shouldn't be hard. That's why Deepkit RPC supports RxJS Observable, Subject, and BehaviorSubject natively. Just return an 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.

import { DeepkitClient } from '@deepkit/rpc';
import type { MyController } from './server.ts';

const client = new DeepkitClient('localhost');
const main = client.controller<Controller>('/main');

const sensorData = await main.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 main.getChatChannel('general');
const sub = chat.subscribe((next) => {
    console.log('message', next);
});

//completes the whole Subject, triggering its TearDown
sub.unsubscribe();
import { Observable } from "rxjs";

@rpc.controller('/main')
class MyController {
    protected sensorData = new Subject<number>();

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

    @rpc.action()
    getChatChannel(
        name: string
    ): Observable<{ user: string, message: string }> {
        return new Observable((observer) => {
           const id = setInterval(() => {
              observer.next({user: 'Peter', message: 'Hello'});
          }, 1000);
           
           return {unsubscribe() {
               clearInterval(id);
           }};
        });
    }
}

Error Forwarding

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

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

A security layer allows to rewrite errors (e.g. to remove the error stack trace or hide sensitive messages).

import { MyCustomError } from "@deepkit/type";

const client = new DeepkitClient('localhost');
const ctrl = client.controlle<Controller>('/main');

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

try {
    await ctrl.customError();
} catch (error) {
    error instanceof MyCustomError; //true
}
@entity.name('@error/my-custom')
class MyCustomError extends Error {
    codes: string[] = [];
}
@rpc.controller('/main')
class MyController {
    @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;
    }
}

Progress Tracking

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

Multiple uploads and downloads can be tracked at the same time.

const client = new DeepkitClient('localhost');
const main = client.controller<Controller>('/main');

const progress = ClientProgress.track();
progress.upload.subscribe(progress => {
    console.log('upload progress',
        progress.upload.total, progress.current,);
});
await main.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 main.downloadFile('file.zip');

Questions & Answers

No answer for your question? Ask a question or see all questions.

See all questionsAsk a question

Examples

Made in Germany