Write isomorphic code in MeteorJs
Tags: webdev, meteor, javascript
Published: 2023-06-30 11:16:04

[MeteorJs](https://meteor.com/?utm_source=jkuester&utm_medium=online&utm_campaign=Q2-2022-Ambassadors) (short "Meteor") is a fullstack JavaScript framework with isomorphic capabilities: you can write code once and use it on the server and client similar.

This is a great advantage in shipping code faster but could also leak server-code to the client or [bloat the client bundle](https://dev.to/jankapunkt/meteor-browser-bundle-and-node-stubs-beware-what-you-import-342f).

Fortunately Meteor's bundling tools allow for exact code splitting: a module will only get bundled for a certain architecture realm (server, client) if it's imported/required within that realm.

## How to determine, if a module will get bundled?

The realms are defined by the entry point modules. Check your `package.json` for the following entry:

```json
{
  ...
  "meteor": {
    "mainModule": {
      "client": "client/main.js",
      "server": "server/main.js"
    },
    "testModule": "tests/main.js"
  },
  ...
}
```

Every import, starting from these entry points will cause the bundler to add the imported module to the server or client bundle or both. One exception, though is using [dynamic imports](https://docs.meteor.com/packages/dynamic-import.html?utm_source=jkuester&utm_medium=online&utm_campaign=Q2-2022-Ambassadors) but we won't cover that today.

## Splitting isomorphic code

First of all, we should talk about the term "isomorphic". The term itself is somewhat imprecise and [is sometimes used synonymous with "universal JavaScript"](https://en.wikipedia.org/wiki/Isomorphic_JavaScript). However, our goal is not always a universal module, since we deal with different dependencies in each environment. You will see this in the upcoming example.

If we talk about code structure then we may use the term "isomorphic" to describe code, that **has the same signature  and represents the (mostly) same behavior in all environments**.

Let's take a look at a simple example:

```js
export const SHA512 = {}

/**
 * Creates a new SHA512 hash from a given input
 * @param {string} input
 * @returns {Promise.<String>}
 */
SHA512.create = async () => {}
```

This module has the same signature and general behavior on the server and the client. It creates a SHA512 hash from a given input. Now, let's implement this code on the server using the [NodeJs-builtin crypto module](https://nodejs.org/api/crypto.html) first:

```js
...

if (Meteor.isServer) {
  SHA512.create = async input => {
    import crypto from 'crypto'
    return  crypto
      .createHash('sha512')
      .update(input, 'utf8')
      .digest('base64')
  }
}
```

For the client we use the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) instead. By doing so we avoid any expensive importing or stubbing of the `crypto` module.

```js
...

if (Meteor.isClient) {
  SHA512.create = async input => {
    const encoder = new TextEncoder()
    const data = encoder.encode(input)
    const hash = await window.crypto.subtle.digest({ name: 'SHA-512' }, data)
    const buffer = new Uint8Array(hash)
    return window.btoa(String.fromCharCode.apply(String, buffer))
  }
}
```

## Improve readability

The examples above are minimal and readability drastically declines if larger modules get involved. Therefore, we need to write an abstraction that helps to improve readability.

Fortunately, this is as simple as it can get:

```js
import { Meteor } from 'meteor/meteor'

export const isomorphic = ({ client, server }) => {
  if (Meteor.isClient && client) return client()
  if (Meteor.isServer && server) return server()
}
```

With this wrapper we can now define a single variable or property and immediately assign it's value, based on a function that only gets executed, if the bundler runs for the specific architecture realm. Let's apply this wrapper to our `SHA512` module.


```js
export const SHA512 = {}

/**
 * Creates a new SHA512 hash from a given input
 * @method
 * @async
 * @param {string} input
 * @returns {Promise.<string>}
 */
SHA512.create = isomorphic({
  server () {
    return async input => {
      import crypto from 'crypto'
      return crypto
        .createHash('sha512')
        .update(input, 'utf8')
        .digest('base64')
    }
  },
  client () {
    return async input => {
      const encoder = new TextEncoder()
      const data = encoder.encode(input)
      const hash = await window.crypto.subtle.digest({ name: 'SHA-512' }, data)
      const buffer = new Uint8Array(hash)

      return window.btoa(String.fromCharCode.apply(String, buffer))
    }
  }
})
```

## Run the code

Now let's run the code on the server and the client. You can use the following code snipped and place it in your entry points (usually `'server/main.js'` and `'client/main.js'`):

```js
import { SHA512 } from '../imports/SHA512'

Meteor.startup(async () => {
  const input = 'isomorphic code rocks'
  const output = await SHA512.create(input)
  console.debug(input, '=>', output)
})
```

On both architectures it will produce the exact same console output:

```
isomorphic code rocks => NfxQJL4a58eszCB64Fi0DRvolnEhABf9x4fVZsMTH6BF296uTdK2MYBbUJzLqHJIuUTLzAJqhzfZlAcEuCuZSQ==
```

If you are in doubt, whether this really uses the defined parts in `server` and `client` I suggest you to run your debuggers on the server and the client and inspect the running code. You will see that in each architecture there is only the architecture-specific code running.

Hooray! You wrote an isomorphic module with architecture specific implementation! :tada: :tada: :tada:


---
## About me

I regularly publish articles here on dev.to about **Meteor** and **JavaScript**.

You can also find (and contact) me on [GitHub](https://github.com/jankapunkt/), [Twitter](https://twitter.com/kuester_jan) and [LinkedIn](https://www.linkedin.com/in/jan-kuester/).

If you like what you are reading and want to support me, you can [sponsor me on GitHub](https://github.com/jankapunkt) or [send me a tip via PayPal](https://paypal.me/kuesterjan).

Keep up with the latest development on Meteor by visiting [their blog*](https://blog.meteor.com/?utm_source=jkuester&utm_medium=online&utm_campaign=Q2-2022-Ambassadors) and if you are the same into Meteor like I am and want to show it to the world, you should check out the [Meteor merch store*](https://shop.meteor.com/?utm_source=jkuester&utm_medium=online&utm_campaign=Q2-2022-Ambassadors).