DEV Community

Cover image for A Step By Step Guide to Updating Your Legacy Node.js App in 2024 (v10 to v22)
Jerome Choo
Jerome Choo

Posted on

A Step By Step Guide to Updating Your Legacy Node.js App in 2024 (v10 to v22)

Here at Diffbot, we enjoy hacking away at fun new app concepts. Not all of them turn out great, but Crawly was (and still is) a hit. It passively generates around 15% of inbound organic traffic to Diffbot. It's a shame the poor thing hasn't been owned or maintained for over 8 years. Frankly, it's a miracle this thing still builds.

This week, at the unanimous behest of Diffbot support and dev ops, I take up the challenge of getting this little app patched. The good news is that I did in 2 days. The bad news is that it was still a miserable experience, even for a tiny hackathon project.

In the hopes of helping a fellow dev, I've organized my notes into a step by step guide. I'll point out some places where I got stuck, but in general will try to stick to the things that worked. Because every project is unique, your mileage will vary. But at least you won't be starting with this useless Google search result.

Google search results for

How Crawly is Built

Crawly is an Express app built on the bones of hackathon-starter, a boilerplate package for Node apps. Surprisingly, this project is still alive and well maintained in 2024. But there's no guarantee that the project still uses the same dependencies (it doesn't), and there's a likelihood that even more dependencies would be added (there were). So I abandoned any plans to merge the latest in.

Crawly's main dependencies are express@4.13.3, mongodb@3.6.3, jade@1.11.0 (templating), passport@0.3.2, and request@2.67.0. It also included mocha and phantomjs for testing.

Bootstrap@3.3.5 is vendorized and compiled at build time.

The app is dockerized for deployment. Running off a node:10 image.

Some notable omissions —

  • No webpack. Bundling dependencies was still pretty cutting edge in 2014. Instead, Express middleware compiled dependencies like SCSS to CSS files at run time. (ew)
  • No tests. There're some assertions here and there. But I'm not surprised. This was a hackathon project after all.

Step 1: Upgrade Node v10 to v22

The keystone upgrade. We'll switch Node to v22 and reinstall node modules.

$ nvm use 22.5.1
Enter fullscreen mode Exit fullscreen mode

Delete your node_modules folder, delete package-lock.json, then run npm i

A wall of terminal errors, see below for reproduction of the important bits

Get used to these messages. We're going to see a lot of it. For now, we can ignore all of the deprecation warnings and focus on the errors, specifcally —

npm ERR! code 7
npm ERR! path ......./node_modules/kerberos
npm ERR! command failed
npm ERR! command sh- c prebuild-install || node-gyp rebuild
Enter fullscreen mode Exit fullscreen mode

What's going on here — one of the modules, kerberos, is prevented from running a prebuild-install step.

Modules with a pre/post install step will often fail if their expected version of Node is not available. To get past this, we need to update these packages first to a version that does support Node v22.

Tip: If you're dockerizing your app, pick a node image and use the version of node it uses. See Step 4: Docker up.

Step 2: Update dependencies in package.json
I used npm-check-updates. It works pretty well!

$ npx npm-check-updates --format group
Enter fullscreen mode Exit fullscreen mode

This will generate an output like this.

4 sections of packages in package.json — Patch, Minor, Major, Major version zero

I took the easy road and updated them all at once. Who knows? Could work.

$ npx npm-check-updates -u
$ npm i
Enter fullscreen mode Exit fullscreen mode

A wall of terminal errors, see below for reproduction of the important bits

I love how useless these deprecation warnings are. Half of them are nested dependencies and it's impossible to identify the parent dependency without successfully installing it.

It appears we were able to get past the kerberos preinstall error this time. But node-sass is still failing a post-install build step.

npm ERR! code 1
npm ERR! path /Users/jc/Diffbot/diffbot-crawler/node_modules/node-sass
npm ERR! command failed
npm ERR! command sh -c node scripts/build.js
Enter fullscreen mode Exit fullscreen mode

I've run into this issue before. node-sass is deprecated and replaced by sass.

We'll remove node-sass and the corresponding node-sass-middleware packages from package.json, replacing it with the latest version of sass.

$ npm uninstall --save node-sass node-sass-middleware
$ npm install --save sass
Enter fullscreen mode Exit fullscreen mode

npm install works!

xcellent! We're on our way. Before we move forward, let's be sure to update our import references in the code.

Because node-sass has been completed replaced with sass, we'll run a simple find and replace in the project for node-sass and node-sass-middleware to ensure all imports for this package are replaced.

-  var sass = require('node-sass-middleware');
+  var sass = require('sass');
Enter fullscreen mode Exit fullscreen mode

Step 3: npm run start

All required dependencies are installed, despite copious deprecation warnings. Let's try to get this app running.

$ npm run start
Enter fullscreen mode Exit fullscreen mode

Terminal error output from npm run start. Class constructor MongoStore cannot be invoked without 'new'

var MongoStore = require('connect-mongo')(session);
                                         ^
TypeError: Class constructor MongoStore cannot be invoked without 'new'
Enter fullscreen mode Exit fullscreen mode

A class constructor error with connect-mongo. This is the first of our build errors. Likely, they won't be the same as ours. But they're all pretty googlable. I'll share the ones we ran into.

I resolved this MongoStore constructor error with a simple syntax change outlined in this StackOverflow answer.

Terminal error output from npm run start. TypeError: dotenv.load is not a function

dotenv.load({ path: '.env' });
       ^
TypeError: dotenv.load is not a function
Enter fullscreen mode Exit fullscreen mode

Another easy syntax change. I followed the latest recommended syntax.

-  dotenv.load({ path: '.env' });
+  var dotenv = require('dotenv').config();
Enter fullscreen mode Exit fullscreen mode

Terminal error output from npm run start. TypeError: sass is not a function

app.use(sass({
        ^
TypeError: sass is not a function
Enter fullscreen mode Exit fullscreen mode

Sass is back. Remember that express middleware I mentioned earlier? When we removed node-sass-middleware, we also removed the shortcut function that converted SCSS files to CSS files for us in Express.

These days, it's a little heavy to compile SCSS at run time. Most web projects will use a tool like webpack to compile and inject references to stylesheets at build time. All that is served at run time are lightweight static files.

I'm not in the mood to install and generate a webpack config to patch a tiny legacy app. The middleware wasn't doing much. The only output was a single main.css file generated from a main.scss file.

We'll write a simple script to do this.

/**
 * Compiles main.scss to main.css
 */
const compileScss = () => {
  const scssFilePath = path.join(__dirname, 'public', 'css', 'main.scss');
  const cssFilePath = path.join(__dirname, 'public', 'css', 'main.css');

  // Check if the SCSS file exists
  if (fs.existsSync(scssFilePath)) {
    // Compile SCSS to CSS
    try {
      let result = sass.compile(scssFilePath, {
        sourceMap: true,
        style: "expanded"
      })

      if (result.css) {
          // Ensure the output directory exists
          fs.mkdirSync(path.dirname(cssFilePath), { recursive: true });
          // Write the compiled CSS to a file
          fs.writeFileSync(cssFilePath, result.css);
      }
      else {
        console.error("Couldn't generate main.css. See compileScss in app.js.")
      }
    }
    catch (e) {
      console.log(e)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

We'll stick this function in app.listen() so it runs once when express starts.

/**
 * Start Express server.
 */
app.listen(app.get('port'), function() {
  compileScss()
  console.log('Express server listening on port %d in %s mode', app.get('port'), app.get('env'));
});
Enter fullscreen mode Exit fullscreen mode

Onwards!

Terminal error output from npm run start. TypeError: expressValidator is not a function

app.use(expressValidator());
        ^
TypeError: expressValidator is not a function
Enter fullscreen mode Exit fullscreen mode

Another syntax issue. We'll get rid of this package altogether. It was included as part of hackathon-starter, but the original project owner never wrote any validation logic with it.

npm uninstall --save express-validator
Enter fullscreen mode Exit fullscreen mode

Terminal error output from npm run start. Pointing to CSS class

Express started! But we're not out of the woods yet. The compileScss() function we wrote failed to build the final CSS file. The .404 parsing error above is a simple fix. Class names cannot start with numerals.

While we're still seeing a bunch of deprecated package and syntax warnings, the app works. So

This is fine dog meme. The dog's face is replaced with a screenshot of 99+ console errors.

Step 4: Docker up

Our last major dependency to work out is Docker, and by extension, the separate but dependent mongodb container.

The Dockerfiles are pretty straight forward. Let's get the easy stuff out of the way.

# FROM node:10
FROM node:22-bullseye-slim
Enter fullscreen mode Exit fullscreen mode
# image: mongo:4.4
image: mongodb/mongodb-community-server:5.0.24-ubuntu2004
Enter fullscreen mode Exit fullscreen mode

Note that instead of simply going with :latest I chose specific images to make it less likely to break inadvertently on future version bumps.

The node:22-bullseye-slim image runs Node v22.5.1. In hindsight I would've figured this out before I got the app running on my local machine. But it's not a breaking change to go from 20.3.1 to 22.5.1.

$ nvm install 22.5.1
$ nvm use 22.5.1
Enter fullscreen mode Exit fullscreen mode

package.json

...
  "engines": {
    "node": "22.5.x"
  },
...
Enter fullscreen mode Exit fullscreen mode

For good measure we'll also create a .nvmrc file.

v22.5.1
Enter fullscreen mode Exit fullscreen mode

Drew, our dev ops lead, has thankfully maintained our docker setup over time. So there wasn't much in the way of Docker updates to make.

My initial docker compose up led to an infinitely restarting mongo container with an AuthenticationFailed error. There wasn't clear guidance anywhere, but I was able to fix this by adding authSource=admin to the mongodb URL (StackOverflow).

Screenshot of Crawly's landing page

Isn't she beautiful? Except...

Gif of clicking the form submit button leading to a 404

From the console —

/home/node/app/node_modules/mongoose/lib/model.js:545
 throw new MongooseError('Model.prototype.save() no longer accepts a callback');
       ^
MongooseError: Model.prototype.save() no longer accepts a callback
Enter fullscreen mode Exit fullscreen mode

Alas, callbacks out. Async in. But it was way too much work to refactor the code to use async functions, so I migrated all callbacks to use promises instead. (StackOverflow)

The was the single most time consuming step of this entire project. Every model CRUD function had to be updated. This means going through all the model functions listed in the Mongoose docs, finding them in the project, and replacing the callbacks with promises.

Here's an example of one.

// crawl.save(function(err) {
//     if (err) {
//         // Error
//     }
//     else {
//         // Success
//     }
// })

crawl.save()
.then(() =>{
    // Success
})
.catch((err) => {
    // Error
});
Enter fullscreen mode Exit fullscreen mode

We have lift off.

Crawly's form submit success page

Final Notes

Keep in mind that despite how I've laid this guide out, my actual experience involved over 50 open tabs of research and comfort hugs with my dog. And it wasn't even a huge project!

If you've been given the unfortunate task of updating a legacy project, I hope this helps.

Top comments (0)