Luciano Serruya Aloisi

Deploying full-stack applications with Firebase

This note was firstly meant to be part of this other note, but I then decided to write it as a separate note, so please read the first one before continuing

Deployment

We have our Vue app and our Express server, both written in TypeScript and working seamlessly in development. But what about deployment? How do we deploy this app? We can't just build our Vue app and deploy it in a static files hosting serving such as Netlify or Firebase Hosting because it makes HTTP requests to a /api endpoint, so we need to deploy our Express back-end as well. For this tutorial, we are going to use Firebase Hosting for our SPA (it only consists in a single HTML file and some JavaScript bundle files), and Cloud Functions for Firebase for our backend.

Setting up Firebase

First and foremost, we need to have the firebase-cli installed in our machine (npm install -g firebase-tools). Once installed, initialize firebase in your project by running firebase init inside your project's directory. Select both Hosting and Functions. Create a new Firebase project or use an existing one, you choose (I have a playground project that I use for playing around with Firebase and GCP).

Select TypeScript when prompted what language will you use to write your Cloud Functions, No to TSLint, and Yes to installing dependencies.

For Firebase Hosting, use your dist directory as your public directory (dist is automatically created by Vue when we generate our production bundle), Yes when prompted to configure it as a single-page app, and Yes when prompted to overwrite index.html (or No, doesn't matter).

Okay awesome, we have now set up Firebase Hosting and Cloud Functions for Firebase in our projects. You will see a brand new directory called functions - that's where your Cloud Functions' source code is in. If you open functions/src/index.ts, you will see a commented-out HTTPS Cloud Function. If you'd like to try it out, uncomment it and run npm run serve while inside of your functions directory. This will transpile your TS code and spin up the Firebase emulators in port 4000 by default for you to test locally your functions or your Firebase Hosting project.

What is really cool about HTTPS-based Cloud Functions is that you can pass your whole Express app to handle any incoming request - and that's what we are going to do now! As we don't want to install Express once again in our functions directory, we are going to create and export a new function in src/server/index.ts that will create a express object, configure it with our routes, and return it. Your src/server/index.ts file should look like this

import express, { Express } from "express";

export interface APIIndexRouteResponse {
  message: string;
}

export function configureServer(app: Express) {
  app.get("/api", (_, res) => {
    const response = { message: "Hello world!" } as APIIndexRouteResponse;
    res.json(response);
  });
}

export function createAndConfigureServer(): Express {
  const app = express();
  configureServer(app);
  return app;
}

Great! We can now call this new function in our functions/src/index.ts file to get a configured express object to handle HTTP requests

// functions/src/index.ts

import * as functions from "firebase-functions";
import { createAndConfigureServer } from "../../src/server";

export const helloWorld = functions.https.onRequest(createAndConfigureServer());

If we now try to spin up Firebase emulators, it will fail telling us something about esModuleInterop and what have you. We can sort that out easily by setting "esModuleInterop": true in functions/tsconfig.json, as a compilerOptions setting. Remove functions/lib before trying again. If we now try once again, it still won't work! It now says something about a valid "main" entry in package.json. Yet another easy bug to fix, open functions/package.json and change "main": "lib/index.js" for "main": "lib/functions/src/index.js". Remove functions/lib, run npm run serve and it now works! You should see a JSON object with your message when navigating to http://localhost:5001/<FIREBASE_PROJECT_ID>/<REGION>/helloWorld/api (don't forget to hit the /api route! We are not handling any other route).

We are going to add some really handy npm-scripts in our base package.json to build our application: one for building our SPA, one for building our Cloud Functions, and one for building both.

// ./package.json
...
  "scripts": {
    "dev": "nodemon",
    "serve": "vue-cli-service serve",
    "build:app": "rm -rf dist && vue-cli-service build",
    "build:functions": "rm -rf functions/lib && npm --prefix functions run build",
    "build": "npm run build:app && npm run build:functions",
    "lint": "vue-cli-service lint"
  },

Finally, we are going to add one rewrite rule to our Firebase file for Firebase Hosting (firebase.json)

{
  "functions": {
    "predeploy": "npm --prefix \"$RESOURCE_DIR\" run build"
  },
  "hosting": {
    "public": "dist",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "rewrites": [
      {
        "source": "/api",
        "function": "helloWorld"
      },
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

We are telling Firebase Hosting to rewrite incoming request to /api to our backend, which is running in a Cloud Function called helloWorld (of course you could name it whatever you want, but bare in mind that you would have to update this file if you do so).

October 12 Update. I've recently realized that Firebase Hosting rewrites incoming requests to /api only, so if you had a /api/login route for example, you wouldn't be able to reach it via <YOUR_SITE>/api/login. To solve this issue, you would have to update your rewrite rule to also catch any subsequent path

"hosting": {
  "rewrites": [
    {
      "source": "/api{,/**}",
      "function": <YOUR_FUNCTION>
    }
  ]
}

And we are done setting up Firebase for our project! To test it locally before deploying, you can simply run firebase emulators:start, and navigate to localhost:4000 to see the Firebase Emulators Suite. Your SPA will be served by default on localhost:5000, and your Cloud Functions will be available on localhost:5001. If you are happy with what you see, deploy it with firebase deploy

Hope you liked it!

🐦 @LucianoSerruya

📧 lucianoserruya (at) gmail (dot) com