Develop Node app in ES6 without Nodemon and Babel
Tags: node, babel, nodemon, rollup
Published: 2020-01-08 06:25:46

I recently started a new Node.js project, and as a habit, I began by installing two familiar packages: `nodemon` and `babel`. The purpose was that I needed a way to hot reload my app while writing it in ES6 Module.

A tool we have gotten to know since the beginning of time for hot reloading is `nodemon`.

Also, since the default configuration of Node.js only supports common.js, we need a way to transpile our code back to common.js. Surely, support for ES6 modules in Node is behind `--experimental-modules` and requires `.mjs` extension (which is intrusive in my opinion). 

(The latest major version of Node.js has already allowed us to use ES6 modules without the tag, but this does not seem to be backported to previous majors, and using it still requires extension)

Most tutorials will suggest Babel for the job. However, I think it is way too much for our purpose (Babel is more suitable to be used for browsers). It also removes the benefits of using ES6 (tree shaking).

## Rollup to the rescue

Introducing [Rollup](https://github.com/rollup/rollup).

> Rollup is a module bundler for JavaScript which compiles small pieces of code into something larger and more complex, such as a library or application.

(note: *application*)

Get started by installing `rollup` as a dev dependency.

```sh
yarn add rollup -D
// or
npm i rollup --save-dev
```

Next, create `rollup.config.js`. (You may use ES6 in this file)

```javascript
export default {
  input: 'api/server.js',
  output: {
    file: 'bundle.js',
    format: 'cjs',
  },
};
```

By this config, we are taking our `api/server.js` (or wherever your main script is), and output a CommonJS version of it.

Even though it is CommonJS after all, the exported file has gone through [treeshaking](https://github.com/rollup/rollup#tree-shaking). Also, since everything is compiled into one file, our code may run a bit faster in Node by eliminating the need to require different modules.

Just for reference, here is my `api/server.js`, written in ES6.

```javascript
import next from 'next';
import { createServer } from 'http';
import apolloServer from './apollo-server';
import app from './app';
import { connect as connectMongoDB } from './db/mongo';

// http
const port = process.env.PORT || '3000';
const httpServer = createServer();

// next
const nextApp = next({ dev: process.env.NODE_ENV !== 'production' });
const nextHandle = nextApp.getRequestHandler();

// apollo
apolloServer.applyMiddleware({ app, path: '/api' });
apolloServer.installSubscriptionHandlers(httpServer);

async function start() {
  await connectMongoDB();
  await nextApp.prepare();
  app.all('*', nextHandle);
  httpServer.on('request', app.handler);
  httpServer.listen({ port }, () => {
    console.log(`🚀  Apollo API ready at :${port}${apolloServer.graphqlPath}`);
    console.log(
      `🚀  Apollo WS ready at :${port}${apolloServer.subscriptionsPath}`
    );
  });
}

start();
```

### Hot reloading

To achieve the functionality of `nodemon`, we add a rollup plugin called [`@rollup/plugin-run`](https://github.com/rollup/plugins/tree/master/packages/run).
> 
> 🍣 A Rollup plugin which runs your bundles in Node once they're built.
> 
> Using this plugin gives much faster results compared to what you would do with nodemon.

(In my experience using it, this plugin **is** faster than `nodemon`)

```sh
yarn add @rollup/plugin-run -D
// or
npm i @rollup/plugin-run --save-dev
```

(We will import the above package in `rollup.config.js`, which may be complained by `eslint`, you can either `eslint-disable` the warning or add the package as a regular dependency).

Back in `rollup.config.js`:

```javascript
import run from '@rollup/plugin-run';

export const roll = rollup;

const dev = process.env.NODE_ENV !== 'production';

export default {
  input: 'api/server.js',
  output: {
    file: 'bundle.js',
    format: 'cjs',
  },
  plugins: [
    dev && run(),
  ],
};
```

We import `@rollup/plugin-run` and include it in `plugins`. Notice that this will only run in development (by checking `process.env.NODE_ENV`).

### Add scripts to package.json

```json
{
  "scripts": {
    "start": "node bundle.js",
    "build": "NODE_ENV=production rollup -c",
    "dev": "rollup -c -w",
  }
}
```

Our `start` script simply runs the output `bundle.js`.

Our `build` script runs `rollup` setting `NODE_ENV` to production. (you may need `cross-env` in Windows)

Our `dev` call `rollup`. The flag `-c` means using our config file `rollup.config.js`. The flag `-w` rebuilds our bundle if the source files change on disk. In fact, `@rollup/plugin-run` does not do hot-reloading but only runs the Node process every time `rollup` recompile.

### What about .env

We often use `.env` in development. `@rollup/plugin-run` allows us to execute an argument. In `rollup.config.js`, edit our `run()` function.

```javascript
run({
  execArgv: ['-r', 'dotenv/config'],
})
```

This allows us to do `node -r` (--require) `dotenv/config`. This usage can be seen [here](https://github.com/motdotla/dotenv#preload).

### Integrate Babel

Even though we do not use Babel to transpile `import/export` to `require/module.exports`, there are cases when we still need it. For example, I use it for `@babel/plugin-proposal-optional-chaining`, which enables optional chaining (this proposal is 🔥 btw).

The plugin we need is [`rollup-plugin-babel`](https://github.com/rollup/rollup-plugin-babel)

```sh
yarn add -D @babel/core rollup-plugin-babel
// or
npm i --save-dev @babel/core rollup-plugin-babel 
```

We can now add it to `rollup.config.js`.

```javascript
import run from '@rollup/plugin-run';
import babel from 'rollup-plugin-babel';

const dev = process.env.NODE_ENV !== 'production';

export default {
  input: 'api/server.js',
  output: {
    file: 'bundle.js',
    format: 'cjs',
  },
  plugins: [
    babel(),
    dev &&
      run({
        execArgv: ['-r', 'dotenv/config'],
      }),
  ],
};
```

The default configuration of `rollup-plugin-babel` will read from `.babelrc`. However, if you are like me, who has `.babelrc` not for the node server but for framework such as React or Next.js, you can opt out. Doing so by edit `babel()`:

```javascript
babel({
  babelrc: false,
  plugins: ['@babel/plugin-proposal-optional-chaining'],
})
```

That's it!