Writing Mongo Realm serverless functions with TypeScript
Tags: mongodb, realm, serverless, typescript
Published: 2020-12-10 23:55:50

Mongo Realm serverless functions don't support TypeScript natively, this article describes how to create an environment that allows writing functions using TypeScript.

The process of enabling TS support consist of the following steps:

1. 🖲 Moving configuration files to Github repository
2. 🏗 Creating a TypeScript build chain
3. 🔩 Add type declarations
4. 🚜 Enabling Github Automatic Deployments
5. 🐕 Enforce consistency with husky hooks
6. 🧪 (Optional) Add CI/CD workflow using Github Actions

Mongo Realm doesn't support TypeScript natively, and we don't want to change that. Instead, we leverage the beauty of TypeScript––the fact that TS produces JavaScript files during the transpilation process. That output JS files can be easily consumed by Mongo Realm. In other words, adding support for TypeScript comes down to structuring our codebase, and adding one extra step to our build chain.

## 🖲 Moving configuration files to Github repository

Mongo Realm allows exporting/importing all project configuration files, which is great for versioning and collaborative development. To automate the process, we can use Github Automatic Deployment to sync our codebase with the Mongo Realm application.

First, we need to create a Github repo (we use handy [gh cli tool](https://cli.github.com/), but you can create it manually on Github website)

```bash
gh repo create <name-of-the-repo>
cd <name-of-the-repo>
```

Now, make sure you have installed and authenticated `real-cli`, if not, follow [these steps](https://docs.mongodb.com/realm/deploy/realm-cli-reference#std-label-install-realm-cli).

```bash
# download your realm app configuration files
realm-cli export \
 --app-id <realm-app-id> \ 
 --output myRealmApp \
 --for-source-control

# move downloaded content and remove empty directory
mv myRealmApp/* . && rm -r myRealmApp

# commit all changes to GitHub repo
git add .
git commit -m "initial commit"
git push
```

## 🏗 Creating TypeScript buildchain

Now we need to add a buildchain for producing JavaScript files from our TypeScript sources. We create a `src/` directory that keeps our TypeScript version of the project source files.

It's important to match the directories structure of [Mongo Realm application](https://docs.mongodb.com/realm/deploy/application-configuration-files#directory-structure),

```bash
mkdir src
cp -r functions src
```

If we want to create a function called `testFunc` we will create a directory `src/functions/testFunc` with `source.ts` and `config.json` files.

```typescript
// src/functions/testFunc/source.ts
exports = (test: string) => {
  return test.toUpperCase()
}

// src/functions/testFunc/config.json
{
  "name": "testFunc",
  "private": false
}
```

Now, we need to transpile these source files to JavaScript equivalent. We create a Nodejs project at a project root level and add typescript packages.

```bash
npm init -y
npm i -D typescript @types/node
npx tsc --init
```

In `tsconfig.json` we specify

```json
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "./",
    "rootDir": "./src",
    "resolveJsonModule": true,
    "moduleResolution": "node",
  },
  "include": ["src/**/*", "src/**/*.json"],
}
```

Mongo Realm supports ES6+ so we set the `target` accordingly. Merging functions from src to root level is achieved with `outDir` and `rootDir`. Resolving JSONs is required because we want our `config.json` files to be copied from `src` to `outDir` directory. 

Now we are ready to execute transpilation with `npx tsc`

```bash
$ tree functions

functions
└── testFunc
    ├── config.json
    └── source.js

$ cat functions/testFunc/source.js

exports = function (test) {
    return test.toUpperCase();
};
```

At this point, our JavaScript function is generated and ready to be deployed.

## 🔩 Add type declarations

Realm does not allow us to use ES Modules––therefore we can not import types, what we can do instead is to create type declarations that are automatically consumed by TypeScript compiler.

For instance, to add support for global `context` object, create `/src/types/realm.d.ts` file with the following content

```typescript
/// <reference types="mongodb" />

type Services = {
  (name: "mongodb-atlas") : import("mongodb").MongoClient;
  // add your other services here
  (name: string) : any;
}
declare global {
   namespace context {
      const services: {
         get: Services;
      };
      const user: {
         id: string;
         type: "normal" | "server" | "system";
         data: object;
         custom_data: object;
         identities: any[];
      };
      const functions: {
         execute: (name: string, ...args: any[]) => any;
      };
      const environment: {
         tag: string;
      };
      const values: {
         get: (value) => any;
      };
   }
}
```

Instead of using import syntax, we use the `/// <reference` directive for importing mongodb type declarations.

Lastly, install mongodb package

```bash
npm i mongodb
```

And enjoy the strict typing of `context` object.

```typescript
// src/functions/testFunc/source.ts
exports = (test: string) => {
  const client: MongoClient = context.services.get("mongo-atlas")
  // TypeScript correctly detect MongoClient type.
  return test.toUpperCase()
}
```

## 🚜 Enabling Github Automatic Deployments

We want our Github repo to be the single source of truth, to achieve that, we need Realm to sync with our repo. Fortunately, it's easy with the [Github Automatic Deployments](https://docs.mongodb.com/realm/deploy/deploy-automatically-with-github) feature. 
Now each time, you make a push to your repo, the changes will be automatically deployed to your Realm application.

## 🐕 Enforce consistency with husky hooks

Right now, you have to remember to transpile your TypeScript source code before each push to the Github repo. If you forget about it, the changes won't take effect, since your `/functions` directory contains out-dated JavaScript files. Luckily, [husky](https://typicode.github.io/husky/#/) comes to the rescue––it allows to set hook actions that will be triggered before each commit/push.

```typescript
npm i -D husky
npx husky install
npx husky add pre-commit "npx tsc && git add functions"
```

Now each time we execute `git commit`, husky will execute `tsc` for us, making sure that the `src/` and `/functions` are synchronized before pushing a change to the repo––and so, deploying to Realm app.

## 🧪 (Optional) Add CI/CD workflow using Github Actions

As the icing on the cake, we can add CI/CD automation workflow. Since there is already a great resource on how to achieve it, I will just leave the link to the repo [https://github.com/mongodb-developer/SocialStats](https://github.com/mongodb-developer/SocialStats).

I personally prefer Github Actions instead of Jenkins, if you are interested in achieving the same workflow using GH Actions, please let me know in the comments, and I will extend this article.

# 🧑‍🏫 Conclusions
The Mongo Realm serverless functions infrastructure is still in the early stage, in contrast to e.g. Firebase (Google Cloud Functions) that supports TypeScript functions natively. Yet, in this article, we've shown that by preparing the development environment it is possible.
Personally, I believe that in the future `realm-cli` will handle this whole process for us (similarly how `firebase` cli does).

I hope this article was useful and saved you some time. 👋