Skip to content

A Client and Server implementation of the Twirp Protocol written in Typescript

Notifications You must be signed in to change notification settings

bhandfast/twirp-ts

 
 

Repository files navigation

Twirp-TS

A complete server and client implementation of the awesome Twirp Specification witten in typescript.

Supported spec v7 and v8


Getting Started

Run the following to install the package

npm i twirp-ts -S

or

yarn add twirp-ts

Server generation

This library relies on the awesome ts-proto to generate protobuf message definitions

The protoc-gen-twirp_ts is instead used to generated server and client code for twirp-ts

It is as simple as adding the following option in your protoc command

--twirp_ts_out=$(OUT_DIR)

Here's an example working command:

PROTOC_GEN_TWIRP_BIN="./node_modules/.bin/protoc-gen-twirp_ts"
PROTOC_GEN_TS_BIN="./node_modules/.bin/protoc-gen-ts_proto"

OUT_DIR="./generated"

protoc \
    -I ./protos \
    --plugin=protoc-gen-ts_proto=${PROTOC_GEN_TS_BIN} \
    --plugin=protoc-gen-twirp_ts=${PROTOC_GEN_TWIRP_BIN} \
    --ts_proto_opt=esModuleInterop=true \
    --ts_proto_opt=outputClientImpl=false \
    --ts_proto_out=${OUT_DIR} \
    --twirp_ts_out=${OUT_DIR} \
    ./protos/*.proto

Server Implementation

Once you've generated the server code you can simply start a server as following:

import * as http from "http";
import {TwirpContext} from "twirp-ts";
import {createHaberdasherServer} from "./generated/haberdasher.twirp";
import {Hat, Size} from "./generated/service";

const server = createHaberdasherServer({
    async MakeHat(ctx: TwirpContext, request: Size): Promise<Hat> {
        // Your implementation
    },
});

http.createServer(server.httpHandler())
    .listen(8080);

Path prefix

By default the server uses the /twirp prefix for every request. You can change or remove the prefix passing the prefix option to the handler

const server = createHaberdasherServer({
    async MakeHat(ctx: TwirpContext, request: Size): Promise<Hat> {
        // Your implementation
    },
});

http.createServer(server.httpHandler({
    prefix: "/custom-prefix", // or false to remove it
})).listen(8080);

Integrating with express

If you'd like to use express as your drop in solution to add more routes, or middlewares you can do as following:

const server = createHaberdasherServer({
    async MakeHat(ctx: TwirpContext, request: Size): Promise<Hat> {
        return Hat.fromPartial({
            name: "wooow",
        });
    },
});

const app = express();

app.use("/twirp", server.httpHandler({
    prefix: false,
}));

http.createServer(app).listen(8000);

it is important that you disable the prefix from the default handler if you provide one.

Server Hooks & Interceptors

Link to Spec

Interceptors are a form of middleware for Twirp requests. Interceptors can mutate the request and responses, which can enable some powerful integrations, but in most cases, it is better to use Hooks for observability at key points during a request. Mutating the request adds complexity to the request lifecycle.

Be mindful to not hide too much behind interceptors as with every middleware alike implementation is easy to increase complexity making it harder to reason about.

Example:

const server = createHaberdasherServer({
    // ...
});

async function exampleInterceptor(ctx: TwirpContext, req: any, next: Next) {
    console.log("Before response");

    const response = await next(ctx, req);

    console.log("After response");

    return response;
}

server.use(exampleInterceptor)

Server Hooks They provide callbacks for before and after the request is handled. The Error hook is called only if an error was returned by the handler.

A great place for metrics and logging

const server = createHaberdasherServer({
    // ...
});

const serverHooks: ServerHooks = {
    requestReceived: (ctx) => {
        console.log("Received");
    },
    requestRouted: (ctx) => {
        console.log("Requested");
    },
    requestPrepared: (ctx) => {
        console.log("Prepared");
    },
    requestSent: (ctx) => {
        console.log("Sent");
    },
    error: (ctx, err) => {
        console.log(err);
    }
};

server.use(serverHooks);

Errors

Link to Spec

The library comes with a built in TwirpError which is the default and standard error for all of your errors.

You can certainly create custom errors that extend a TwirpError

For Example:

import {TwirpError, TwirpErrorCode} from "twirp-ts";

class UnauthenticatedError extends TwirpError {
    constructor(traceId: string) {
        super(TwirpErrorCode.Unauthenticated, "you must login");
        this.withMeta("trace-id", traceId)
    }
}

Twirp Client

As well as the server you've also got generated client code, ready for you to use.
You can choose between JSON client and Protobuf client.

The generated code doesn't include an actual library to make http requests, but it gives you an interface to implement the one that you like the most.

Alternatively you can use the provided implementation based on node http and https package.

For example:

const jsonClient = new HaberdasherClientJSON(NodeHttpRPC({
    baseUrl: "http://localhost:8000/twirp",
}));

const protobufClient = new HaberdasherClientProtobuf(NodeHttpRPC({
    baseUrl: "http://localhost:8000/twirp",
}));

You can check the full example on how to integrate the client with axios

Here is a snippet:

const client = axios.create({
    baseURL: "http://localhost:8080/twirp",
})

const implementation: Rpc = {
    request(service, method, contentType, data) {
        return client.post(`${service}/${method}`, data, {
            responseType: contentType === "application/protobuf" ? 'arraybuffer' : "json",
            headers: {
                "content-type": contentType,
            }
        }).then(response => {
            return response.data
        });
    }
}

export const jsonClient = new HaberdasherClientJSON(implementation);
export const protobufClient = new HaberdasherClientProtobuf(implementation);

Licence

MIT <3

About

A Client and Server implementation of the Twirp Protocol written in Typescript

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages

  • TypeScript 99.1%
  • Other 0.9%