Smooth local Firebase development setup with Firebase emulator and Snowpack
Turns out it's not that simple to get local Firebase emulators up and running. I went through the process and documented it on the way.
Setting up Firebase for local development is not too hard, but it's pretty tedious to connect everything together. I had to go through this for one of my side projects and documented the steps on the way. Read on to find out how to start local Firebase firestore and functions emulator together with Snowpack with just one command.
Why Firebase?#
If you are thinking of doing a small POC or if you are on a tight budget it's hard to beat Firebase. You get everything you need out of the box. Storage, database, serverless functions, hosting, messaging plus a ton other stuff. And the best thing is that it won't break your bank.
Plus, you get a generous free quota and also the full power of Google Cloud Platform in case you need it.
Creating Snowpack project boilerplate#
I am using Snowpack with Svelte as examples, but the concepts of this setup can be applied to any web framework or bundler.
If you want to know more about Snowpack you can read my article - Snowpack with Svelte, Typescript and Tailwind CSS is a very pleasant surprise.
Let's start by setting up new Snowpack project and later we will add Firebase to the mix.
$ npx create-snowpack-app svelte-firebase --template @snowpack/app-template-svelte
$ cd svelte-firebase && npm start
You should now see our app's start page in the browser with the local dev server running on port 8080
.
Installing Firebase#
Next step we need to do is add Firebase to the mix.
Note: before continuing make sure you have a functioning local Java runtime environment since the Firebase emulators are built on top of Java.
To get a required firebase
CLI command we need to install firebase-tools. The easiest way is to install it globally with npm
.
$ npm i -g firebase-tools
There are other methods of installing Firebase CLI, here is more information.
Now we need to add Firebase to our project. For that we need to do two things.
Login to Firebase
Before we can use Firebase CLI we need to login to Firebase console. We can do that from the command line.
$ firebase login
Firebase will open a webpage for us in the browser where you can authenticate yourself.
Initializing Firebase
Before continuing we need to create a new Firebase project in the Firebase console, if you don't already have an existing one. There is also an option to create a new project straight from the Firebase CLI, but I found it a little glitchy. That's why I recommend to do it in the Firebase console instead.
Did you do it? Nice! We are now ready to add Firebase integration to our project.
$ firebase init
You will be presented with a few options.
Select Firestore and Emulators options by pressing Space key. Press Enter when done.
Next select Use existing project option and select our new Firebase project we created earlier in the Firebase console.
Accept the defaults for the rest of the options. Just say "Yes" to everything. We can always change it later.
If everything went smoothly, you will end up with the following new files in out directory.
# main firebase config
firebase.json
# firestore compound indexes config
firestore.indexes.json
# firestore seurity rules definitions
firestore.rules
# firebase project linking file
.firebaserc
The most important file is firebase.json
. It's a main config file which tells Firebase where to find stuff, what is enabled in the project and what local ports emulators should use.
{
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
},
"emulators": {
"functions": {
"port": 5001
},
"firestore": {
"port": 8080
},
"ui": {
"enabled": true
}
}
}
From the file above we can see that we will have two local emulators running - functions and Firestore. The Firestore emulator's port is a little problematic as it listens on the same port as Snowpack (8080).
Note: If you want to see what default ports are used for Firebase emulators see this page.
Let's add the Firebase start script to our package.json
so that we can start the Firebase emulators with npm CLI.
Add this row to the scripts
part in the package.json
.
"start:firebase": "firebase emulators:start"
Now we can start Firebase emulators with npm run start:firebase
command. Neat!
Firebase Emulator UI#
The output also tells that we have an emulator UI running on http://localhost:4000
.
If you visit that page you will see this page.
Each emulator has its own status card and the only active is the Firebase emulator which is running on port 8080
.
If you would like to know more about how Firebase emulator can be setup and used here is a link to the official documentation.
Adding Firebase functions#
We could have added Firebase functions support from the start, but I didn't do it on purpose just so I could show how you can add it later.
If you look at the terminal screenshot above, you saw that Firebase emulator complained that it couldn't find any functions.
Let's fix it.
$ firebase init functions
Choose Typescript and say no to the tslint part. We don't need it, because Typescript compiler will catch most of the errors for us. Plus tslint has been deprecated anyway.
Note: Firebase functions aka Google Cloud Functions support only Node.js v10. Well, Node.js v8 too, but my guess is that you don't want to use it. A more resent LTS Node version should work fine for local development, but that's something to keep in mind if you get any weird behaviour when deploying to live environment later.
As you can see Firebase initialized our Firebase functions project in the new functions
directory. It's actually a separate subproject with it's own package.json
and all.
If you look at our firebase.json
file, you will see the new section in it.
{
"functions": {
"predeploy": "npm --prefix \"$RESOURCE_DIR\" run build"
}
}
What is this you may ask and what is the $RESOURCE_DIR
environment variable? That's actually a Firebase functions predeploy hook and that variable is just an alias for word functions
, or more precise, it defaults to the functions
word and allows you to change the default location and name of your Firebase functions directory.
We might as well have written this.
{
"functions": {
"predeploy": "npm --prefix functions run build"
}
}
The predeploy hook's job is to build your functions the last time before deploying them to live environment.
Unfortunately it does not fire in the dev environment when we use our emulators. Let's try to start the Firebase emulator again.
That's because we haven't built our functions yet. The emulator expect to find the transpiled Javascript code in the functions/lib
directory and right now it's empty. We need to build our functions by executing the same command as in the predeploy hook - npm --prefix functions run build
, but before you do please edit the functions/src/index.ts
and uncomment the function body.
If you start the emulator now and go to the Firebase Emulator UI you will see that our functions emulator is now running too. If you click on the logs button you will see the function url.
If you visit the function URL you will get back a friendly "Hello from Firebase!" greeting back.
Automatic Firebase functions recompilation
Nice! But not quite. We still have a small problem. Every time we change the function code we need to rebuild it. Lucky us that Typescript compiler has a --watch
option!
To take advantage of it we can add the following row to our functions/package.json
scripts section.
"watch": "tsc --watch"
We can now run npm start watch
in our functions
project and Typescript will watch for file changes and recompile them every time they change.
Note: you can also run the command from our main project with npm run watch --prefix functions
.
Making everything run smoothly#
Alright, we can now run our main app, start the Firebase emulator and do automatic Firebase function recompilation. That alone requires three different terminals. It's there a better way?
Good news! There is. You see, there is a small NPM package called npm-run-all that will solve all our problems.
It's like a Swiss Army knife. One of the tools it has it the ability to run multiple npm scripts in parallel with its run-p
command. That's exactly what we need to start our Snowpack app, Firebase emulator and Typescript compiler at once.
No time to waste. Let's get straight to it.
First, add the package as a dependency to our project npm add -D npm-run-all
. Next, we need to remix our scripts
section in package.json
a bit.
{
"scripts": {
"start": "run-p dev start:*",
"build": "snowpack build",
"test": "jest",
"dev": "snowpack dev",
"start:firebase": "firebase emulators:start",
"start:functions": "npm run watch --prefix functions"
}
}
You can see that we replaced the start
property with the magic run-p dev start:*
command.
What it does is to execute all script passed as arguments in parallel. The star after the start:
is a pretty neat way to tell that all scripts prefixed with start:
should be run. Think of it as a glob function.
However, there is still a small problem with our setup. Both Snowpack and Firestore emulator use port 8080
. We need to change one of them to use a different port.
Let's change Snowpack's. We will run Snowpack on port 8000
instead. Open snowpack.config.json
and add a new devOptions section.
{
"extends": "@snowpack/app-scripts-svelte",
"devOptions": {
"port": 8000
},
"scripts": {},
"plugins": []
}
Now everything should start normally with only one command npm start
.
Isn't life wonderful?!
Using Firebase emulator in code#
Alright, we now have this new fancy setup, but how do we use Firestore in code? Not to worry! There are many ways to skin a cat. Here is a naïve one.
Add firebase.ts
to the src
directory with the following code.
// firebase.ts
import firebase from 'firebase/app';
import 'firebase/firebase-firestore';
import 'firebase/firebase-functions';
let firestore: firebase.firestore.Firestore | null = null;
let functions: firebase.functions.Functions | null = null;
// Naive implementation of Firebase init.
// For education purposes. Never store your config in source control!
const config = {
apiKey: 'your-firebase-key',
projectId: 'testing-firebase-emulators'
};
firebase.initializeApp(config);
const db = (): firebase.firestore.Firestore => {
if (firestore === null) {
firestore = firebase.firestore();
// Snowpack's env variables. Does now work in Svelte files
if (import.meta.env.MODE === 'development') {
// firebase.firestore.setLogLevel('debug');
firestore.settings({
host: 'localhost:8080',
ssl: false
});
}
}
return firestore;
};
const funcs = (): firebase.functions.Functions => {
if (functions === null) {
functions = firebase.app().functions();
if (import.meta.env.MODE === 'development') {
// tell Firebase where to find the Firebase functions emulator
functions.useFunctionsEmulator('http://localhost:5001');
}
}
return functions;
};
export { db, funcs };
Boom! We now have a basic Firebase setup that we can use in our code.
Using local Firebase functions and Firestore your code#
Let's use the new setup in our Svelte app. For the sake of examples to prove that everything works we will make a call to our helloWorld
Firebase function and create a simple TODO list backed by local Firestore.
Replace App.svelte
with the code below.
<!-- App.svelte -->
<script>
import { onMount } from 'svelte';
import { db, funcs } from './firebase';
import firebase from 'firebase/app';
import 'firebase/firebase-firestore';
let message = '';
let todo = '';
let todos = [];
// Firestore collection reference
let todoCollection = null;
// Firestore server timestamp function
const timestamp = firebase.firestore.FieldValue.serverTimestamp;
onMount(async () => {
// reference to our cloud function
const helloFn = funcs().httpsCallable('helloWorld');
const response = await helloFn();
// assign result to message variable
message = response.data.message;
// assign collection to a variable
todoCollection = db().collection('todos');
// create a firestore listener that listens to collection changes
const unsubscribe = todoCollection.orderBy('createdAt', 'desc').onSnapshot(ss => {
let docs = [];
// snapshot has only a forEach method
ss.forEach(doc => {
docs = [...docs, { id: doc.id, ...doc.data() }];
});
// replace todo variable with firebase snapshot changes
todos = docs;
});
// unsubscribe to Firestore collection listener when unmounting
return unsubscribe;
});
const submitHandler = async () => {
if (!todo) return;
// create new todo document
await todoCollection.add({ action: todo, createdAt: timestamp() });
todo = '';
};
</script>
<h2>Functions Emulator</h2>
<!-- result from the helloWorld Firebase function call -->
<p>{message}</p>
<h2>Firestore Emulator</h2>
<form on:submit|preventDefault={submitHandler}>
<input type="text" bind:value={todo} placeholder="Add new todo" />
<button type="submit">add</button>
</form>
{#if todos.length}
<ul>
{#each todos as todo (todo.id)}
<li>{todo.action}</li>
{/each}
</ul>
{:else}
<p>No todos. Please add one.</p>
{/if}
If you start the app now, you will get a CORS error from our Firebase HTTP function. That's expected because Firebase HTTP functions don't have CORS support built-in. We could add it to our cloud function, but there is a better way - Firebase Callable functions.
Fixing Firebase function error#
The fix is easy. We just need to change the type of our Firebase cloud function to callable. Firebase will then call it differently and we don't have to worry about CORS at all.
Change the code of our helloWorld
function to this.
// functions/src/index.ts
import * as functions from 'firebase-functions';
export const helloWorld = functions.https.onCall((data, context) => {
return { message: 'Hello from Firebase!' };
});
The object is returned as response.data
. That's pretty nice as we don't have to worry about the HTTP response/request at all. We just return a plain object and Firebase will take care of serialization for us.
What about Firebase auth?#
As of time of writing Firebase Authentication is not yet supported in the Firebase emulator, but it's hopefully coming very soon.
But not to worry, you can effectively mock it in your tests if you need to. There are a few different ways you can do it, but it's a little too long to explain here. Maybe in another article.
Testing Firebase#
I won't touch the subject of testing now, but 'firebase emulators:exec' is your friend here. With its help it possible to start the local Firebase emulators, run your tests, and then shut down the emulators.
Summary#
We should now have a pretty nice setup where we can fire up all the emulators and code with just one command. Mission accomplished!
Firebase have really matured during the last couple of years and if you want to do a quick prototype or have to build an internal app with storage needs please take another look at Firebase.
Here is the full code https://github.com/codechips/svelte-local-firebase-emulator-setup
Thanks for reading and I hope that you learned something new!