3-step process to deploying SPA and API monorepo using Heroku postbuild
Build and serve a SPA + API monorepo on Heroku using Heroku postbuild. Includes a one-click deploy reference implementation.
Someone on Stackoverflow asked a question about how to run npm run build
and have the backend serve the frontend app as part of the Heroku deploy pipeline. In other words, how to deploy a frontend-backend monorepo on Heroku. As I had pieced the solution together when I was porting Lindy to Heroku, I wrote a brief answer. This post is a more detailed guide to setting everything up.
The basic principles behind building the frontend app as part of your deploy and subsequently serving it via the backend app boil down to:
- using correct buildpacks on Heroku;
- instructing Heroku's build process to compile the frontend app before compiling the app slug and caching dependencies for subsequent builds
- ensuring that the compiled frontend assets end up in a directory where the backend app expects them.
1. Using correct buildpacks
Heroku will automatically detect the buildpack to use using certain heuristics. E.g if your app is a vanilla Python app, Heroku will automatically detect the correct buildpack (Python) for your app.
However, since we're also trying to compile the frontend assets, we are going to have to tell Heroku to use the JavaScript buildpack in addition to the buildpack for your backend language.
Adding a buildpack is pretty straightfoward, you can do it either via the Heroku CLI or via the Heroku Dashboard. Here's what the buildpacks for Lindy look like. Note: the ffmpeg buildpack is specific to Lindy, you likely don't need it.
2. Use Heroku postbuild to compile the JavaScript app and cache dependencies
The next step is to compile the frontend app as part of the build process on Heroku. Add a package.json
to the root of your project if you don't have one already. The two key things needed in package.json
are the instructions for building your frontend app and caching the node_modules
directory.
For instance, if your frontend app is located in client/
, here's what package.json
in the project root should look like (other keys omitted).
Heroku will automatically run the specified command(s) in heroku-postbuild
— in this instance yarn install
and yarn run build
. Swap those out as needed for npm
. It'll also cache the node_modules
in the client/
directory as per cacheDirectories
directive.
3. Ensuring compiled files end up in the right place
It's good practice to .gitignore compiled files. Your build step should output the compiled-for-production files (i.e main.{js,css}
) into a directory where your backend will know where to expect them, both in development and in production.
For instance, with Lindy, in development, main.{js,css}
are being continuously compiled by Rollup as the source files are updated. In production, Rollup will compile, tree-shake and uglify main.js
, and purge and minify main.css
into the same directory as in development. In other words, the production app will find the relevant files in the same directory as in development, except in production they're compiled for production by the Heroku build process.
This is what a typical directory structure in such a scenario might look like. Roughly speaking, the build step in package.json
compiles files from client/src
into server/static
, which is where the server will know to look for them.
⌞ package.json
⌞ client
⌞ src
⌞ node_modules
⌞ ...
⌞ server
⌞ static
⌞ src
⌞ ...
Reference implementation
An example repository implementing the pointers in this guide, with plain JavaScript bundled with Rollup, served by a Flask app, is available on Github.