A complete server and client implementation of the awesome Twirp Specification witten in typescript.
Supported spec v7 and v8
Table of Contents:
Run the following to install the package
npm i twirp-ts -S
or
yarn add twirp-ts
Make sure you have protoc
or buf
installed.
Mac:
brew install protobuf
Linux:
apt-get install protobuf
Optional:
This plugin works with buf too, follow the link to see how to install it
twirp-ts relies on either ts-proto or protobuf-ts to generate protobuf message definitions
The protoc-gen-twirp_ts
is instead used to generate server
and client
code for twirp-ts
It is as simple as adding the following options in your protoc
command
PROTOC_GEN_TWIRP_BIN="./node_modules/.bin/protoc-gen-twirp_ts"
--plugin=protoc-gen-twirp_ts=${PROTOC_GEN_TWIRP_BIN} \
--twirp_ts_out=$(OUT_DIR)
Here's an example working command with the recomended flags:
using ts-proto (click to see)
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
using protobuf-ts (click to see)
PROTOC_GEN_TWIRP_BIN="./node_modules/.bin/protoc-gen-twirp_ts"
PROTOC_GEN_TS_BIN="./node_modules/.bin/protoc-gen-ts"
OUT_DIR="./generated"
protoc \
-I ./protos \
--plugin=protoc-gen-ts=$(PROTOC_GEN_TS_BIN) \
--plugin=protoc-gen-twirp_ts=$(PROTOC_GEN_TWIRP_BIN) \
--ts_opt=client_none \
--ts_opt=generate_dependencies \
--ts_out=$(OUT_DIR) \
--twirp_ts_opt="protobufts=true" \
--twirp_ts_out=$(OUT_DIR) \
./protos/*.proto
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);
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
},
});
server.withPrefix("/custom-prefix") // or false to remove it
http.createServer(server.httpHandler())
.listen(8080);
or you can pass it to the handler directly:
http.createServer(server.httpHandler({
prefix: "/custom-prefix",
})).listen(8080);
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> {
// ... implementation
},
});
const app = express();
app.post(server.matchingPath(), server.httpHandler());
app.listen(8000);
Note: if you want to change the default prefix use server.withPrefix()
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);
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)
}
}
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);
The package uses Semver Versioning system.
However, keep in mind that the code-generation plugin is tightly coupled to the twirp-ts library.
Make sure that whenever you update twirp-ts
you re-generate the server and client code. This make sure that the generated code will be using the updated library
MIT <3