TypeScript Application#

This guide shows how to build a TypeScript application using Node.js and npm.

The source code for the example app can be found in the tests/npm-app/ folder of the CCF git repository.

Prerequisites#

The following tools are assumed to be installed on the development machine:

  • Node.js

  • npm

Folder layout#

The sample app has the following folder layout:

$ tree --dirsfirst npm-app
npm-app
├── src
│   └── endpoints
│       ├── all.ts
│       ├── crypto.ts
│       ├── partition.ts
│       └── proto.ts
├── app.json
├── package.json
├── rollup.config.js
└── tsconfig.json

It contains these files:

Note

Rollup requires exactly one entry-point module. The src/endpoints/all.ts module serves that purpose and re-exports all endpoint handlers from the other files in the same folder. Keeping endpoint handlers in separate modules and referencing those directly in app.json allows for fine-grained control over which other modules are loaded, per endpoint. This in turn may improve load time and/or memory consumption, for example if not all endpoints share the same npm package dependencies.

Dependencies#

The sample uses several runtime and development packages (see package.json). One of them is the ccf-app package. This package references the current branch’s version of the ccf-app package using file:. To test against a published version you should adjust the version number accordingly:

"@microsoft/ccf-app": "~1.0.0",

Now you can continue with installing all dependencies:

$ npm install

Endpoint handlers#

An endpoint handler, here named abc, has the following structure:

import * as ccfapp from "@microsoft/ccf-app";

interface AbcRequest {
    ...
}

interface AbcResponse {
    ...
}

export function abc(request: ccfapp.Request<AbcRequest>): ccfapp.Response<AbcResponse> {
    // access request details
    const data = request.body.json();

    // process request
    // ...

    // return response
    return {
        body: ...,
        headers: ...,
        statusCode: ...
    }
}

AbcRequest and AbcResponse define the JSON schema of the request and response body, respectively. If an endpoint has no request or response body, the type parameters of ccfapp.Request/ccfapp.Response can be omitted.

As an example, the /partition endpoint of the sample app is implemented as:

import * as _ from "lodash-es";

import * as ccfapp from "@microsoft/ccf-app";

type PartitionRequest = any[];
type PartitionResponse = [any[], any[]];

export function partition(
  request: ccfapp.Request<PartitionRequest>,
): ccfapp.Response<PartitionResponse> {
  // Example from https://lodash.com.
  let arr = request.body.json();
  return { body: _.partition(arr, (n) => n % 2) };
}

Here, the request body is a JSON array with elements of arbitrary type, and the response body is an even/odd partitioning of those elements as nested JSON array. The example also shows how an external library, here lodash, is imported and used.

Warning

Even though request body schemas can be defined as part of the OpenAPI metadata, CCF does not validate incoming request data against those schemas. It is up to the application to perform any necessary validation.

Tip

See the ccf-app package API documentation for how to access the Key-Value Store and other CCF functionality. Although not recommended, instead of using the ccf-app package, all native CCF functionality can also be directly accessed through the ccf global variable.

Metadata#

App metadata is stored in an app.json file in the root of the app project. It is copied as-is to the dist/ folder during the build step. The file follows the metadata format used by app bundles.

Note that module paths must be relative to the dist/src/ folder and end with .js instead of .ts.

Conversion to an app bundle#

Preparing the app for deployment means converting it to CCF’s native JavaScript application format, an app bundle. This involves the following steps:

  • transform TypeScript into JavaScript,

  • transform bare imports (lodash) into relative imports (./node_modules/lodash/lodash.js),

  • transform old-style CommonJS modules into native JavaScript modules, and

  • store all files according to the app bundle folder structure.

For this, the sample app relies on the TypeScript compiler and rollup. Rollup also offers tree shaking support to avoid deploying unused modules. See package.json and rollup.config.js for details.

The conversion command is invoked with

$ npm run build

The app bundle can now be found in the dist/ folder and is ready to be deployed.

Deployment#

After the app was converted to an app bundle, it can be wrapped into a proposal and deployed. See the Deployment section of the app bundle page for further details.

A note on CommonJS modules#

The sample project uses the @rollup/plugin-commonjs package to automatically convert npm packages with CommonJS modules to native JavaScript modules so that they can be used in CCF.

For some packages this conversion may fail, for example when the package has circular module dependencies. If that is the case, try one of the following suggestions:

  1. Check if there is a JavaScript module variant of the package and use that instead. These are also named ES or ECMAScript modules/packages.

  2. Check if there is a known work-around to fix the conversion issue. Chances are you are not the only one experiencing it.

  3. Check if the npm package contains a browser bundle and try to import that instead. For example, this works for protobuf.js: import protobuf from 'protobufjs/dist/protobuf.js'.

  4. Manually wrap a browser bundle of the package without using npm. This may be needed if the browser bundle is not part of the npm package, although this is uncommon.

Manually wrapping a browser bundle (step 4) means copying the bundle source code in a module file and surrounding it with module boiler-plate. This may look something like:

let exports = {}, module = {exports};

// REPLACE this comment with the content of the bundle.

export default module.exports;

If the bundle uses only global exports instead of CommonJS/Node.js exports, then the module should look something like:

// REPLACE this comment with the content of the bundle.

// Adjust this to match the globals of the package.
export {ExportA, ExportB};