Migrating to Netlify from Heroku

· 5 min read

When Heroku first launched in 2007, it was a pioneering way to build and deploy applications. It was one of the first services I remember focusing on the developer experience. Using their CLI tool, I could quickly create applications and extend functionality with their rich add-on architecture. Things stepped up for me when they launched Ruby support in 2009, and thanks to Heroku, I could rapidly build applications and deploy them to production. Heroku was what made the first iteration of NHSx possible in such a short space of time. They helped popularise patterns that would define how most people would build applications.

It was with great sadness that I saw the product go stale after the Salesforce acquisition. For some time, I’ve been severely unhappy with the service and stability (not to mention the OAuth incident and response time) but lacked time to migrate most of my applications away from it. I wished I hadn’t waited so long. Moving to Netlify was very straightforward, and in this post, I’ll highlight the steps I took to migrate stac.works to Netlify.

Using Netlify

Netlify has been gaining serious traction over the past few years, with many static sites migrating to its simplified deployment and hosting. My site uses Next.js, so your build steps will differ depending on your framework, but the concepts are the same.

Netlify can use a TOML file located at `netlify.toml` to declare your configuration, a different approach from Heroku, which preferences a JSON file named `app.json`. Both contain similar instructions for the platform to test and deploy your application.

Like Heroku, Netlify has a CLI tool that allows you to interact with the platform and run everything locally to ensure parity between what you see locally and what ends up deployed. This is their recommended approach to building apps on the platform.

Moving things over

Before doing anything, as I set the codebase up to target another platform, I disabled automatic deployment from GitHub on Heroku to avoid accidentally deploying changes to the wrong platform. As soon as I could verify Netlify deployed the site without any issues, I planned to move the domain over and spin down the Heroku app.

The first step was to ensure my application could run as a static export. This meant sanity checking my React components for any usage of server-side functionality. There was only one area where I used this functionality to redirect one static path to another, and this was in a redirect. The handler looked something like below.

const redirectRequest = (res, path) => {
  if (res) {
    res.writeHead(302, {
      Location: path
    });

    res.end();
    res.finished = true;
  } else {
    Router.replace(path);
  }
};

Netlify has a nifty way of setting redirects in the TOML config, so this was easy to replace.

[[redirects]]
  from = "/events"
  to = "/events/all-day-hey"

Next, I wanted to convert the Heroku platform configuration to Netlify’s. My Heroku `app.json` file looked something like this.

{
  "name": "stac-works",
  "description": "The main site for Software Consultancy, Stac.",
  "stack": "heroku-20",
  "buildpacks": [
    {
      "url": "heroku/nodejs"
    }
  ],
  "environments": {
    "test": {
      "scripts": {
        "test-setup": "npm run lint",
        "test": "npm run test"
      }
    }
  }
}

Converting this to the `netlify.toml` file was extremely simple.

[build]
  command = "npm run ci && npm run export"
  publish = "_site/"

I also had a `.nvmrc` file which declared the Node.js version. Heroku and Netlify detect this file to ensure the builds are run on the correct Node.js version.

My `package.json` file had some scripts declared to simplify the commands I used during the build (as well as set the export output path). I used the convenient `start-server-and-test` package to run the server and Cypress tests in parallel to ensure the integration tests could run against a server running the production build. This was important to me as I didn’t want to run linting and tests in isolation (as a GitHub action, for example). I needed the tests and linting to run in the same environment as the build.

{
  ...

  "engines": {
    "node": "16"
  },

  "scripts": {
    "build": "next build",
    "start": "next start",
    "export": "rm -rf _site && mkdir _site && npm run build && next export . -o _site",
    "lint": " eslint {*.js,**/*.js}",
    "test": "cypress run",
    "ci": "npm run lint && npm run ci:test",
    "ci:test": "start-server-and-test ci:start http://localhost:3000 test",
    "ci:start": "npm run build && npm run start"
  },

  ...
}

I created a branch to test everything out and set up Netlify to temporarily use this branch to ensure I could get builds running. Once things worked, I merged the branch into `main` and set this as the production branch. As I had moved to a different approach to running the application (from a Next.js server to a statically exported site), I could remove the `Procfile` that used to be required to run the application server on Heroku.

And from there, I could test the two applications side-by-side on both platforms. This allowed me to check for any issues with the build before migrating the domain over to Netlify. The process only took a few hours, and now I’ve got faster (and cheaper) builds running on a platform that has the support and investment for years to come.

If you’ve not tried Netlify yet, I recommend giving it a go. The developer experience is incredible, and the community is active and helpful. If you’re someone who prefers the CLI, get started with their CLI documentation on their documentation site.