DEV Community

loading...
Cover image for A Minimal Node.js, Express, & Babel Setup

A Minimal Node.js, Express, & Babel Setup

neightjones profile image Nate Jones ・12 min read

Let’s set up a basic Node.js / Express.js API that uses Babel. Babel will ‘transpile’ our ES2015+ code and module syntax to older-style code for compatibility purposes. I’ll use a basic Express API as the example, add in absolute imports via Babel, and briefly discuss whether we even need Babel in 2020 for our Node setup.

In Part 2 of this minimal setup (coming soon!), I’ll add in setup for eslint and prettier, and show you how to get them to play nicely together. Then we’ll update some settings in VS Code to finish the process.

The final repository can be found here:
neightjones/node-babel-template
*This template creates a basic Node.js / Express.js API using babel. It also sets up nice defaults for eslint and…*github.com

1. Node Version & Express Skeleton

First, let’s take care of our Node version. I like to use nvm to manage my Node versions across different projects. Please follow the install instructions they provide.

We’ll use the “Current” release on nodejs.org, which is 15.4.0 as of the time of this writing. Run these 2 commands:

nvm install 15.4.0
nvm alias default 15.4.0
Enter fullscreen mode Exit fullscreen mode

This installs Node version 15.4.0 to our list of nvm versions we have on our machine (run nvm ls to see which versions you have). We’ll one more piece of nvm configuration soon, but let’s move on to the code.

To get our code off the ground, we’ll create a new project with the Express application generator (run in my fresh repo node-babel-template):

npx express-generator .
Enter fullscreen mode Exit fullscreen mode

This tool generates a super simple Express api for us (bin/www is the entry file, app.js sets up the Express app, and there are a couple simple route handlers in the routes directories).

How can we specify which Node version we want to use with our project?

  1. Create a file at the root of the project called .nvmrc and simply put 15.4.0 in the file. In your terminal, in the project root directory, type nvm use — this command tells nvm to look for the .nvmrc file and use the version specified

  2. In your package.json, add a section called engines that looks like this:

// ... other parts
  "engines": {
    "node": "15.X"
  }
// ... other parts
Enter fullscreen mode Exit fullscreen mode

This portion of package.json specifies to build tools and others that the code is meant to work on the specified version. You could also specify something more generic like “at least Node 15,” but this works for now.

Before we start making changes, let’s make sure things are working as expected. Run the initial npm install to install the packages, then run the already-defined npm start script that the Express generator put in our package.json for us. The server should be listening (on port 3000 by default), and the generator made a ‘home’ route for us http://localhost:3000 — visit that in your browser and you should see the Express welcome message.

Let’s make a couple quick changes that’ll simplify the next steps — create a src directory at the root of our project and move these things into it: bin, public, routes, views, and app.js… this will break the current npm start script but we’re going to replace that anyway. Secondly, change the file www (in src/bin) to www.js.

Now let’s start turning this into a Babel project.

2. Basic Babel Setup

npm install --save-dev [@babel/core](http://twitter.com/babel/core) [@babel/cli](http://twitter.com/babel/cli) [@babel/preset-env](http://twitter.com/babel/preset-env) [@babel/node](http://twitter.com/babel/node)
Enter fullscreen mode Exit fullscreen mode
  • @babel /core gives us the Babel core compiler

  • @babel /cli gives us command line tools

  • @babel /preset-env is one of the official presets available through Babel. What is a preset? Babel works through a series of plugins, each of which define transformations that Babel applies to your code. You could run Babel without any plugins involved, in which case it’ll spit out the same exact code you started with. Say you find this plugin — *@babel/plugin-transform-arrow-functions *and set it up in your Babel config. That’s great because now you can use the es6 arrow function syntax and Babel will transpile it back to normal function syntax for you. BUT — you don’t want to manage all of these rules for es6 and beyond! Luckily, Babel presets include lots of these rules — *babel preset-env *will have all you need to use the latest and greatest syntax

  • @babel /node works just like the Node cli itself, but of course runs the Babel process, too. So, instead of running e.g. node index.js to run the Node process, you can use babel-node index.js (in development… in production, you’ll build transpiled code through Babel and run a normal Node process… you’ll see soon in our package.json scripts)

We’ll be back to package.json soon, but first let’s make a simple Babel config file that Babel will recognize when it runs and will act accordingly. Create a new file at the root level of your project called .babelrc.json, and give it the following contents:

{
  "presets": [
    "@babel/preset-env"
  ]
}
Enter fullscreen mode Exit fullscreen mode

With our core Babel packages installed and .babelrc.json set up, let’s update our npm scripts. In the scripts section of package.json, remove the start command that Express generator made for us, and add these new ones:

// ... other parts  
"scripts": {
    "dev": "babel-node ./src/bin/www.js",
    "clean": "rm -rf dist",
    "build": "npm run clean && babel ./src --out-dir dist --copy-files",
    "prod": "node ./dist/bin/www.js",
  }
// ... other parts
Enter fullscreen mode Exit fullscreen mode

Looking at each one:

  • dev — using our @babel /node package we installed, this is an easy way to do local development. Just like using node, but takes care of Babel tranpilation for us

  • clean — the build command (next) outputs the result of the Babel build in a dist folder… this simply deletes that built directory so we can start fresh each time

  • build — run the babel process on our source files so that we have a dist directory containing our transpiled code, ready to be run in production with normal node

  • prod — assuming we’ve built our code with the build command, we can now run it with node

Test out our new scripts

dev: As a sanity check, we should be able to use our dev command immediately. We don’t have any code yet that needs to be transformed, because the code generated by Express generator doesn’t use ES2015+ syntax, but that’s okay… we still *can *use babel-node to run what we have.

Run npm run dev and everything should work just like before.

build: Let’s make sure we can build our code with Babel. Run npm run build and you’ll see a dist directory created with transpiled files (of course we still don’t have much to transpile, yet).

prod: finally npm run prod will use normal node to run the entry file that sits inside the dist directory — run that and see that you can still visit localhost:3000 in your browser.

One more Babel-related item…

We set up our build process above to leverage Babel with its preset-env to transform syntax of ES2015+ code (which we’ll write soon) back to older-style syntax so that it runs across more environments. An example of transforming syntax is converting an arrow function const x = () => {} to a normal function function x() {}. However, the concept of a polyfill is slightly different… a polyfill is a piece of code that actually uses primitives of an older target version of the language to add* ***features the language so it’s compatible with our newer code. For example, the fetch call we often use in web development. There’s no concept of transforming syntax from ES2015+ fetch to something older, but rather a polyfill is written to **add a compatible fetch call. This article does a great job explaining in more depth.

So, for our purposes, how do we make sure that the correct things are polyfilled for us? This Babel documentation tells us that @babel /polyfill is deprecated in favor of using its two main constituent libraries directly: core-js and regenerator-runtime. Let’s install these 2:

npm install --save core-js regenerator-runtime
Enter fullscreen mode Exit fullscreen mode

Then, as they suggest in the Babel docs, add these as the first 2 lines in src/bin/www (after #!/user/bin/env node):

import 'core-js/stable';
import 'regenerator-runtime/runtime';
Enter fullscreen mode Exit fullscreen mode

You should still be able to run npm run dev and visit your page in the browser. In fact, we just introduced our first ES2015+ code (ES Module import syntax)! Since our code still works, this means that babel-node in our dev script is working properly. If you changed that script to node ./src/bin/www.js, it would fail and say “Cannot use import statement outside a module,” so we know Babel (in combination with preset-env) is doing its job.

Next, we’ll update our Express generator code to use modern syntax.

3. Update Code to Modern Syntax

Remember all of the code can be found in the repo linked at the top, but here are the main updates we’ll be making in our boilerplate files:

  1. Convert to ES Modules (export, export default and import syntax rather than Common JS module.exports and require syntax)

  2. Switch to const variables (block-scoped) instead of var variables

  3. Use arrow functions

The resulting files that we started with from our Express generator now look like this:

www.js

#!/user/bin/env node
import 'core-js/stable';
import 'regenerator-runtime/runtime';

/**
 * Module dependencies.
 */

import http from 'http';
import app from '../app';

/**
 * Normalize a port into a number, string, or false.
 */
const normalizePort = (val) => {
  const port = parseInt(val, 10);

  if (Number.isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
};

/**
 * Get port from environment and store in Express.
 */

const port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

/**
 * Create HTTP server.
 */

const server = http.createServer(app);

/**
 * Event listener for HTTP server "error" event.
 */
const onError = (error) => {
  if (error.syscall !== 'listen') {
    throw error;
  }

  const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(`${bind} requires elevated privileges`);
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(`${bind} is already in use`);
      process.exit(1);
      break;
    default:
      throw error;
  }
};

/**
 * Event listener for HTTP server "listening" event.
 */
const onListening = () => {
  const addr = server.address();
  const bind = typeof addr === 'string' ? `pipe ${addr}` : `port ${addr.port}`;
  console.log(`Listening on ${bind}`);
};

/**
 * Listen on provided port, on all network interfaces.
 */
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
Enter fullscreen mode Exit fullscreen mode

app.js

import createError from 'http-errors';
import express from 'express';
import path from 'path';
import cookieParser from 'cookie-parser';
import logger from 'morgan';

import indexRouter from './routes/index';
import usersRouter from './routes/users';

const app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use((req, res, next) => {
  next(createError(404));
});

// error handler
app.use((err, req, res) => {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

export default app;
Enter fullscreen mode Exit fullscreen mode

index.js

import express from 'express';
const router = express.Router();

/* GET home page. */
router.get('/', (req, res, next) => {
  res.render('index', { title: 'Express' });
});

export default router;
Enter fullscreen mode Exit fullscreen mode

routes/users.js

import express from 'express';
const router = express.Router();

/* GET users listing. */
router.get('/', (req, res, next) => {
  res.send('respond with a resource');
});

export default router;
Enter fullscreen mode Exit fullscreen mode

Re-run npm run dev and you’ll see that everything still works perfectly. Again, we have all this new syntax running through babel-node, which, using preset-env, already triggers all the transforms we need.

How can we double-check things are working as expected? Let’s test out our build command now that we’re relying on Babel to transpile our code. Run npm run build and open up dist/routes/index.js — this is our transpiled index route file that we updated above. It’ll look like this:

index.js

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports["default"] = void 0;

var _express = _interopRequireDefault(require("express"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

var router = _express["default"].Router();
/* GET home page. */


router.get('/', function (req, res, next) {
  res.render('index', {
    title: 'Express'
  });
});
var _default = router;
exports["default"] = _default;
Enter fullscreen mode Exit fullscreen mode

There’s a bunch going on here, but based on our syntax updates, take note of 2 things:

  • Since we switched to an ES Module style export default router, a bunch of the transpiled code is responsible for making that compatible with an older environment

  • On line 16, we can see the arrow function we had made was switched back to a normal function declaration

We’re all set with our Babel pipeline! We can write any code with ES2015+ syntax that’s covered by preset-env and know it’ll be transpiled properly. npm run prod can successfully use normal node to run your code built in dist.

Bonus: Absolute Imports

I always like setting up absolute imports right from the start. Do you ever wind up writing imports in your code like ../../../directoryX/thing? With absolute imports, we can create names for any directory we want and use those in an ‘absolute’ sense — that previous path could be reduced to e.g. directoryX/thing (note no leading dot or slashes). This is easy to do using a Babel plugin.

Let’s install the plugin with:

npm install --save-dev babel-plugin-module-resolver
Enter fullscreen mode Exit fullscreen mode

Check out babel-plugin-module-resolver here. As it says, it ‘allows you to add new “root” directories that contain your modules.’ The setup is nice and simple.

First, update your .babelrc.json to look like this:

{
  "presets": [
    "@babel/preset-env"
  ],
  "plugins": [
    ["module-resolver", {
      "alias": {
        "#routes": "./src/routes",
      }
    }]
  ]
}
Enter fullscreen mode Exit fullscreen mode

You’ll see we added a new plugins section, where we use our new plugin. Most importantly, see the alias object. This is where we can make up whatever names we’d like as aliases to use in our import statements throughout our code. As a sample, you see that #routes is now an alias for our routes directory under src. The # character isn’t required, but I’ve seen others use it as an easy way to see in your code that you’re using a custom alias.

With our new alias, head back to your src/app.js file. We have two imports in here for our routes:

import indexRouter from './routes/index';
import usersRouter from './routes/users';
Enter fullscreen mode Exit fullscreen mode

These imports are very straightforward so you wouldn’t necessarily need / want to use the aliases here, but let’s do it for the example nonetheless. Now they’ll look like this (notice no leading dot or slash):

import indexRouter from '#routes/index';
import usersRouter from '#routes/users';
Enter fullscreen mode Exit fullscreen mode

Restart your Node server and things will work just as before. Notice this is just a dev dependency — when you run npm run build and look at dist/app.js, you’ll see that Babel changes those absolute imports back to relative require statements.

Final Thought — Do you Need Babel in 2020?

It’s become a tough call in my mind whether it’s worth using a Babel setup with Node at this point. Check out this site that tracks the language features for various versions of Node (this article used 15.4.0).

What about ES Modules? Since Node 13.2.0, ES Modules have been available without any experimental flags. This means you can use import / export module syntax now in Node if you add type: "module" to your package.json files (or use .mjs file extensions). However, there are a couple of small gotchas to note:

  • As LogRocket mentions (this is as of March 2020), *“ES modules is still tagged experimental since the feature is not fully ready for production environments,” *and they also note some compatibility issues between ES Modules and CommonJS Modules (the latter isn’t an issue in Babel, which transforms back to CommonJS Modules)

  • In Node’s documentation here, you can see some more notes about ES Module subtleties. For example, you need to include file extensions, __dirname is out of scope, JSON file imports change, etc.

More generally in those same docs, you can see some pieces are still experimental. Nonetheless, support seems to largely be here.

So, do you need to bother with Babel for your Node setup? I think either decision is fine at this point. Personally, I prefer to stick with the Babel workflow in this article for now, but maybe that’ll change in the coming months.

  1. I want to stick with the syntax I’m used to for imports (especially so I can use the same syntax in e.g. a Create React App app)

  2. I like the Babel plugin ecosystem — I can keep using plugins like babel-plugin-module-resolver we looked at above. Plugins make it all pretty flexible

  3. The Node / Babel workflow is pretty mature at this point so you can find plenty of resources and Q&As on it online

Check out Part 2 of this series here (coming soon!), where we configure ESlint & Prettier in the project to help with syntax and style.

This post was originally published on Sapling

Discussion (0)

pic
Editor guide