A lesson learned with Heroku, Node, Babel and Javascript ES6
November 14, 2017
I have a node-es6-heroku-boilerplate on Github for the final output of the discussion below.
Intro
Heroku is one of the more popular PaaS (Platform as a Service), cloud-based hosting services. It’s perfect for all levels of experienced developers because of it’s ease of use and low costs, as well as getting all sorts of server-side systems up and running in no time at all, including Node. This post will give a short intro for Heroku and Node, and go on to the main reason for me writing this post, which was because of a painful evening trying to get Javascript ES6 imports working on a Node-Heroku setup. There’s also a valuable lesson I learned which I’ll explain later.
Heroku has a few major competitors such as Digital Ocean and AWS for which you can read all about the comparisons in detail here and here respectively. I use Heroku for starter projects or for developing small side projects as it’s easy and free for their basic plan! You get one free dyno which is basically a service container to run your app, and when/if you do want to expand your project and launch it in production, it’s easy to upgrade and you only pay for that you use in temrs of per dyno/month.
I’m from a PHP background where my shared or dedicated servers were managed us at indigoRiver, and any build process had to be set up manually also. This was usually done with Git post-recieve hooks as described in another post I did a while back (this post will be updated shortly). This process of setting up Gulp server-side tasks and other bash script magic is pretty long winded, although it’s pretty solid once it’s working. With Heroku though, this process is bullet-proof, and so easy to set up with the extensive official documentation. It will run any processes you require such as npm install
to get your Node packages installed, and also running any npm scripts to do any transpiling, tests, etc.
The process of pushing a project to Heroku
I’ll quickly go over the process used to push a project to the Heroku cloud service.
- Create a Heroku account if you don’t have one, and create a new app from the Dashboard.
- Follow the instructions and select whether you want a Node JS system, or whatever back-end type you like.
- The entire deployment process is done via Git, which is explained in the docs, but be sure to add your sensitive data to a .gitignore file in a config file or environment variables.
- Add your secret variables to the Heroku admin area in the “settings” tab.
- Connect your Heroku account to your local machines CLI by installing the Heroku CLI tool and typing
heroku login
, enter your credentials, and log in. - Follow some more instructions as outlined on this page, and away you go!
ES6 modules support with Node on Heroku
Heroku has worked pretty smoothly for me every time I’ve used it, better than when I try to run my own post-recieve Git hooks to perform server-side Gulp tasks anyway. I’ve always written Node using the supported language set (require instead of imports, vars instead of let/const and old style functions) because I didn’t want to rely on another build step with Babel or Webpack, but this time I wanted to use ES6+ on my Heroku project with Babel.
I thought it would be straight forward (and in the end it was) but I spent hours one evening bashing my head against my desk wondering why the ES6 imports were not working. Like I said, in the end the answer was really stupid and simple, but I learned a valuable lesson which I’ll share with y’all.
ES6 module attempts…
So ES6 modules were not working. When I pushed up to Heroku I got the following error:
So Here are the steps I took:
- Fiddled with my heroku-postbuild… a lot!
//attempt 1
...
//attempt 24
"heroku-postbuild": "npm install && npm run start",
- Installed a load of babel devDependencies to see if one was not working:
"devDependencies": {
"babel": "6.23.0",
"babel-cli": "6.26.0",
"babel-preset-es2015": "6.24.1",
"babel-preset-node6": "11.0.0",
"babel-preset-stage-0": "6.24.1",
"babel-preset-env": "1.6.0",
...
}
- Kept trying different variations of my start script with different presets to be run after npm had finished installing all the packages:
//attempt 1
"start": "babel-node --presets es2015 server.js"
//attempt 2
"start": "babel-node --presets node6 server.js"
//attempt 3
"start": "./node_modules/.bin/babel-node server.js",
//attempt 4 - .babelrc
{
"presets": ["env"]
}
- Set heroku CLI config to use devDependencies:
{% highlight bash %} heroku config:set NPM_CONFIG_PRODUCTION=false
* Removed/updated values randomly in the package.json file
* Messed with the Procfile:
```bash
//attempt 1
node server.js
//attempt 2
npm run start
- Removed and re-added the Node buildpack from Heroku dashboard
Everytime I pushed it would crash instantly, and show me weird messages like it was running node server.js
instead of running it with node-babel
.
Dang it!
After trying out a bunch of different variations of all the above configuration options, I finally figured out that it was not working because my heroku-postbuild
was running a start script as well as the Procfile also running the start script. When starting out on this project I thought the start script had to run twice because that’s how my previous projects were set out.
After looking back, I realised that all my attempts to get the ES6 modules working was stuff I’d copy and pasted from Stack Overflow answers answered in the last 4 years, bits from my old projects, and guessing games because I assumed it would be easy to do. So what did I learn from this?
READ THE OFFICIAL DOCUMENTATION
One upside to all this faffing around though was that I stumbled upon some good features of Heroku and Babel which I didn’t know existed. This includes:
heroku restart
- restart the existing dyno in case it crashed or cached.heroku config:set NPM_CONFIG_PRODUCTION=false
- in the CLI, this is actually needed in order for npm to install the devDependencies from the package.json file.- Usage of babel-preset-env which is now recommended by the team at Babel. It can be used in replacement of babel-preset-es2015, babel-preset-es2016 etc, and automatically matches features and plugins based on the configuration you supply to it in the .babelrc file. If you leave it like I did with
... "presets": ["env"] ...
in your .babelrc file, it will cover all of ES6+. heroku run bash
- enables direct access to the Bash CLI on the remote Heroku instance! So helpful to get an overview of files, processes etc.
Final output
So I finally got it working, and went back to tidy up/remove any unwanted packages and code I’d frankensteined together to fix the issue which I kind of didn’t really have in the first place. Here’s the final setup:
//package.json
...
"engines": {
"node": "6.9.0"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"server": "nodemon server.js --exec babel-node", //for spinning up dev server which updates using Nodemon
"heroku-postbuild": "npm install", //called automatically when application is build on Heroku server
"start": "babel-node server.js" //for running on the Heroku server - called in the Procfile
},
...
"devDependencies": {
"babel": "6.23.0",
"babel-cli": "6.26.0",
"babel-preset-env": "1.6.0",
"nodemon": "1.11.0"
}
//.babelrc
{
"presets": ["env"]
}
//Procfile
web: npm run start
Next steps
Although I’ve used babel-node
to run the Node stuff, it’s recommended that it’s done differently for production use to stop the code from being re-compiled each time the server is spun up. Done “the recommended way”, the code is pre-compiled to save time and server resource.
Take a look at my node-es6-heroku-boilerplate on Github for more of a view!
Senior Engineer at Haven