Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

@creditkarma/thrift-client

creditkarma48.8kApache-2.01.0.4TypeScript support: included

Thrift client library for NodeJS written in TypeScript.

thrift, typescript, rpc, microservices, http client, framework

readme

Thrift Server

A set of libraries for building microservices in Node.js, written in TypeScript.

As the name would suggest these libraries use Apache Thrift for service-to-service communication with RPC. All libraries come ready with distributed tracing through Zipkin.

The available libraries:

Note: Thrift Server is young and will still be undergoing some breaking changes before v1.0.0. The rule of thumb before 1.0.0 is that minor releases will potentially include breaking changes, but patch versions will not.

Development

To build and run thrift-server locally you can follow these steps.

First, clone the repo:

$ git clone https://github.com/creditkarma/thrift-server.git

Because thrift-server is a mono-repo containing multiple libraries, we use lerna to manage inter-dependencies. Running common npm commands at the project root will run these commands in all the packages.

Install dependencies and build libraries:

$ cd thrift-server
$ npm install
$ npm run build

To see things working you can run npm test:

$ npm test

Building a Working Application

Let's make a quick working application with the included libraries.

We'll do this step by step:

  • Create a project
  • Install dependencies
  • Define our service
  • Run codegen on our Thrift IDL
  • Create a service
  • Create a service client
  • Make service calls with our client

Create a Project Directory

We need a place to build things:

$ mkdir thrift-example
$ cd thrift-example

Next initialize our workspace:

$ git init
$ npm init

I create directories for our source code and our Thrift IDL:

$ mkdir thrift
$ mkdir src

Setting up TypeScript

I'm going to be using TypeScript so I'm going to add a tsconfig.json file to my project root.

That file looks like this:

{
    "compilerOptions": {
        "target": "es6",
        "module": "commonjs",
        "moduleResolution": "node",
        "sourceMap": true,
        "declaration": true,
        "rootDir": "./src",
        "outDir": "./dist",
        "noEmitOnError": true,
        "strict": true,
        "noUnusedLocals": true,
        "pretty": true
    },
    "exclude": [
        "node_modules",
        "dist"
    ]
}

I'm also going to add some scripts to by package.json to build the TypeScript:

"scripts": {
    // ...
    "prebuild": "rm -rf dist",
    "build": "tsc",
    // ...
}

Install Dependencies

Because Thrift Server is developed with TypeScript and recommended usage is with TypeScript, all Thrift Server libraries define dependencies as peer dependencies to avoid type collisions.

$ npm install --save-dev typescript
$ npm install --save-dev @creditkarma/thrift-typescript
$ npm install --save @creditkarma/thrift-server-core
$ npm install --save @creditkarma/thrift-server-hapi
$ npm install --save @creditkarma/thrift-client
$ npm install --save request
$ npm install --save @types/request
$ npm install --save hapi
$ npm install --save @types/hapi

Example Service

Our Thrift service contract looks like this:

service Calculator {
  i32 add(1: i32 left, 2: i32 right)
  i32 subtract(1: i32 left, 2: i32 right)
}

I save this file in my project as thrift/calculator.thrift.

Generating Service Code

We generate TypeScript from our Thrift IDL using thrift-typescript. In my package.json I add something like this:

v0.9.x of this library requires thrift-typescript v3.x v0.7.x - 0.8.x of this library requires thrift-typescript v2.x

"scripts": {
    // ...
    "codegen": "thrift-typescript --target thrift-server --sourceDir thrift --outDir src/generated",
    // ...
}

No we can can generate our service interfaces by:

$ npm run codegen

If everything went well there should now be a new file at src/generated/calculator.ts.

Every generated service exports 3 common types (others may be exported on a service-to-service basis). In the <service-name>.ts file service becomes a namespace so there is now a namespace in calculator.ts called Calculator. The 3 types I mentioned are nested in this namespace.

The three common types:

  • IHandler: An interface for the service methods
  • Processor: A class that is constructed with an object of type IHandler. This handles decoding service requests and encoding service responses.
  • Client: A class that provides the public interface for consumers. This handles encoding service requests and decoding service responses.

Creating a Service

To create a Thrift service you need to first choose you Node Http server library. Thrift Server supports either Express or Hapi. For this example we are using Hapi, but the Express usage is almost identical.

No matter which option you choose Thrift support is added to the chosen server via plugin/middleware.

We need a new file. I'm calling mine src/server.ts.

The code to implement this service is pretty straight-forward:

import * as Hapi from 'hapi'
import { ThriftServerHapi } from '@creditkarma/thrift-server-hapi'
import { Calculator } from './generated/calculator'

const PORT: number = 8080

const server = new Hapi.Server()

server.connection({ port: PORT })

/**
 * Implementation of our Thrift service.
 *
 * Notice the second parameter, "context" - this is the Hapi request object,
 * passed along to our service by the Hapi Thrift plugin. Thus, you have access to
 * all HTTP request data from within your service implementation.
 */
const serviceHandlers: Calculator.IHandler<Hapi.Request> = {
    add(left: number, right: number, context?: express.Request): number {
        return left + right
    },
    subtract(left: number, right: number, context?: express.Request): number {
        return left - right
    },
}

const processor: Calculator.Processor<Hapi.Request> = new Calculator.Processor(serviceHandlers)

/**
 * Register the Thrift plugin.
 *
 * This plugin adds a route to your server for handling Thrift requests. The path
 * option is the path to attach the route handler to and the handler is the
 * Thrift service processor instance.
 */
server.register(ThriftServerHapi<Calculator.Processor>({
    path: '/thrift',
    thriftOptions: {
        serviceName: 'calculator-service',
        handler: processor,
    }
}), err => {
    if (err) {
        throw err
    }
})

/**
 * Start your hapi server
 */
server.start((err) => {
    if (err) {
        throw err
    }
    server.log('info', `Thrift service running on port ${PORT}`)
})

Creating a Service Client

Creating a service client is similarly not that difficult.

I'm adding the following code to a file called src/client.ts.

import {
    createHttpClient,
    RequestOptions,
} from '@creditkaram/thrift-client'

import { Calculator } from './codegen/calculator'

// Create Thrift client
const thriftClient: Calculator.Client<RequestOptions> = createHttpClient(Calculator.Client, {
    serviceName: 'calculator-service',
    hostName: 'localhost', // The host of the service to connect to
    port: 8080, // The port of the service to connect to
    requestOptions: {} // RequestOptions to pass to got
})

The thrift-client library uses Request as its underlying Http client. You will notice in the sample code the requestOptions parameter. This is optional and is passed through to the Request instance. This can be used to handle things like serving Thrift with TLS.

Making Service Calls

Okay, so we have a service and a client, let's see this thing in action. To do that we're going to setup a simple web server in front of our Thrift client.

Because we're already using Hapi, let's add this to our src/client.ts file:

import {
    createHttpClient,
    RequestOptions,
} from '@creditkaram/thrift-client'

import * as Hapi from 'hapi'

import { Calculator } from './codegen/calculator'

// Create Thrift client
const thriftClient: Calculator.Client<RequestOptions> = createHttpClient(Calculator.Client, {
    serviceName: 'calculator-service',
    hostName: 'localhost',
    port: 8080,
    requestOptions: {} // RequestOptions to pass to got
})

const server = new Hapi.Server({ debug: { request: ['error'] } })

const PORT = 9000
server.connection(PORT)

server.route({
    method: 'GET',
    path: '/add/{left}/{right}',
    handler(request: Hapi.Request, reply: Hapi.ReplyWithContinue) {
        thriftClient.add(request.params.left, request.params.right)
            .then((response: RecommendationsResponse) => {
                reply(response)
            })
            .catch((err: any) => {
                reply(err)
            })
    },
})

server.start((err: any) => {
    if (err) {
        throw err
    }
    server.log('info', `Web server running on port ${PORT}`)
})

I'm also going to add a file src/index.ts that will start the service and the client.

import { fork } from 'child_process'

const clientProc = fork('./client.js')
const serverProc = fork('./server.js')

function exit(code: number) {
    clientProc.kill()
    serverProc.kill()
    process.exitCode = code
}

process.on('SIGINT', () => {
    console.log('Caught interrupt signal')
    exit(0)
})

Back in package.json I'm going to add another script to start our app:

"scripts": {
    // ...
    "start": "npm run codegen && npm run build && node dist/index.js",
    // ...
}

Finally, we can:

$ npm start

And:

$ curl http://localhost:9000/add/5/6

Contributing

For more information about contributing new features and bug fixes, see our Contribution Guidelines. External contributors must sign Contributor License Agreement (CLA)

License

This project is licensed under Apache License Version 2.0

changelog

Change Log

All notable changes to this project will be documented in this file. See Conventional Commits for commit guidelines.

0.17.0 (2020-04-02)

BREAKING CHANGES

  • Upgrade to using Hapi v20 (ad1c86). This requires services to operate on node > v12.

0.16.1 (2020-05-20)

Features

  • Injectable request object(370a0c)

0.16.0 (2020-01-21)

BREAKING CHANGES

  • Updated int64 to handle comparision between versions not sharing same prototype (80a5dd).

0.15.0 (2019-10-02)

BREAKING CHANGES

  • Updated hapi to the latest version to address snyk vulnerability, moving hapi to @hapi/hapi (fa3543)

0.14.6 (2019-09-05)

Features

  • Added blacklist to thrift-client (1cdf0a) (469af4)
  • Added runtime check for types into binary protocol(9e5c1e)

0.14.0 (2019-08-09)

BREAKING CHANGES

  • Support for stripping header structs (60e84c)

0.13.5 (2019-03-28)

Bug fixes

  • Correct annotation types for thrift client (14a95a)

0.12.5 (2019-01-14)

Bug fixes

  • Correct an issue when path is incorrectly constructed (0377e1a)

0.12.0 (2019-01-11)

Features

  • Add support to thrift-client and thrift-server-hapi to support endpoint-per-method (d85c58)

0.11.0 (2018-11-27)

BREAKING CHANGES

  • Splits plugins/middleware into their own packages (f5f344)

The APIs for plugins is unchanged. It's just that the plugins are no longer included with their given client/server.

For example if you want to include Zipkin tracing for thrift-client you know have to also install the zipkin-client-filter.

npm install --save @creditkarma/thrift-server-core
npm install --save @creditkarma/thrift-client
npm install --save @creditkarma/thrift-zipkin-core
npm install --save @creditkarma/zipkin-client-filter

And then use it as such:

import {
    createHttpClient,
} from '@creditkaram/thrift-client'

import {
    ThriftClientZipkinFilter,
} from '@creditkarma/thrift-client-zipkin-filter'

import { Calculator } from './codegen/calculator'

const thriftClient: Calculator.Client<ThriftContext<CoreOptions>> =
    createHttpClient(Calculator.Client, {
        hostName: 'localhost',
        port: 8080,
        register: [ ThriftClientZipkinFilter({
            localServiceName: 'calculator-client',
            remoteServiceName: 'calculator-service',
            tracerConfig: {
                endpoint: 'http://localhost:9411/api/v1/spans',
                sampleRate: 0.1,
            }
        }) ]
    })

Features

0.10.0 (2018-11-16)

BREAKING CHANGES

  • Cleaned up format of Zipkin options (de5601f)

Features

  • Allow users to provide their own logging function (30f3793)

0.9.0 (2018-10-11)

BREAKING CHANGES

  • Update to use TypeScript v3 (cd33cd)

  • thrift-server-hapi: Update to use Hapi 17 (3844d4)

Features

  • thrift-server-core: Add types for annotations (22804d)

0.8.0 (2018-08-16)

BREAKING CHANGES

  • Change the client plugin API so that the handler function expects two arguments (d4f38e)

The big change here is that the client middle ware object changed from this:

interface IThriftMiddlewareConfig<Options> {
    methods?: Array<string>
    handler(data: Buffer, context: ThriftContext<Options>, next: NextFunction<Options>): Promise<IRequestResponse>
}

To this:

interface IThriftClientFilterConfig<Options> {
    methods?: Array<string>
    handler(request: IThriftRequest<Options>, next: NextFunction<Options>): Promise<IRequestResponse>
}

The new IThriftRequest object contains the outgoing payload on the data property and the user-passed context on the context property. Beyond this more data about the outgoing request is present, specifically the uri and the service methodName.

0.6.2 (2018-03-13)

Bug Fixes

0.6.1 (2018-03-13)

Bug Fixes

  • thrift-server-hapi: Fix naming of plugin (d288caf)

0.6.0 (2018-03-13)

Features

  • thrift-server-hapi: Allow passing auth strategy to hapi plugin (#55) (f84e73b)

0.5.1 (2018-03-12)

Bug Fixes

  • Update peerDependencies (752728c)

0.5.0 (2018-03-12)

Bug Fixes

  • dynamic-config: use typescript compiler to load typescript configs (#48) (e4fe62b)
  • thrift-client: fix linting error (46e8727)
  • thrift-client: Update test server with facotry function (6608ef4)
  • Have zipkin plugin generate root traceid if none (09acbd6)

Features

  • thrift-server-express: Add factory to create thrift servers (2d37def)
  • thrift-server-hapi: Add factory to create thrift servers (#49) (372765b)
  • Add Zipkin plugins (33eeb24)
  • Support distributed tracing with zipkin (377e0c6)