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 (at) gmail (dot) com