Intro to the easiest DB ever, Firestore

Gaute Meek Olsen
7 min readNov 18, 2018

--

Let me introduce you to the easiest database in the world. It’s called Firestore and it lives in the cloud. It’s really easy to get started as all coding is done on the front end, but it also scales really well as it is a product by Firebase from Google.

Set up

Feel free to follow along with the coding as I will show you how it works. If you get stuck on where to put the code, the full source code is provided at the bottom.

First, go to the Firebase console and spend the 3 clicks to create a project. Then inside the project, click Database in the navigation menu and Create database button (test mode is good for now). We will deal with securing our database later. Actually, that is all the setup needed, you now have a working NoSQL database.

I will walk you through how to read, write, update and delete to your database from the web. But you can see their documentation for IOS, Android, Java, Python, Node.js, Go, PHP, C#, and Ruby. (That’s their support as I’m writing this, I won’t be shocked if they add more language and platform support later)

Create an HTML file to get started.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Title</title>
</head>
<body>
<script>
/*Your JavaScript code goes here*/
</script>
</body>
</html>

Then we need to add Firebase to our web application. The simplest way is to go to the project overview in the Firebase console, click the web symbol above the text “Add an app to get started”, then just copy the code and paste it into your HTML file.

We are now ready to create a reference to our database.

const db = firebase.firestore();

Firestore saves our data in collections which contains documents. Collections can also have sub-collections. Let’s say we have the collections families and cities. Let's create references for our collections.

const families = db.collection('families');
const cities = db.collection('cities');

Adding data

We are going to use the add method to add families and set method to add cities.

const addFamily = surname => {
families.add({surname});
}
const addCity = (city, population) => {
const pop = parseInt(population);
cities.doc(city).set({pop})
.then(() => console.log('Document successfully written!'))
.catch(() => console.error('Error writing document: ', error));
}

The differences are that set uses the city as the ID, while add autogenerates an ID. Also if you use the set method and a document with the same ID already exists, it will be overwritten unless you add the { merge: true } option. Notice that then and catch are not required.

Now we need some HTML.

<label for="family-name">Surname:</label>
<input type="text" id="family-name">
<button id="family-button">Add</button>
<br>
<label for="city">City:</label>
<input type="text" id="city">
<label for="population">Population:</label>
<input type="text" id="population">
<button id="city-button">Add</button>

And some event listeners for our buttons.

document.querySelector('#family-button').addEventListener('click', () => {
const name = document.querySelector('#family-name').value;
addFamily(name);
});
document.querySelector('#city-button').addEventListener('click', () => {
const city = document.querySelector('#city').value;
const population = document.querySelector('#population').value;
addCity(city, population);
});

That is all you need to save data to your database, no need to create tables or schemas beforehand. If the collection doesn’t exist they will be created. You can see and administrate your data through the Firebase console (big bonus).

Getting data

Get a single object

This is how we retrieve the population of a single city from Firestore. data() returns our object.

const populationCity = async city => {
const doc = await cities.doc(city).get();
if (doc.exists) {
return doc.data().pop;
} else {
return 'city not found';
}
}

Let’s add HTML.

<label for="search-city">Population for city:</label>
<input type="text" id="search-city">
<button id="population-button">Add</button>
<span id="result-population"></span>

And JavaScript to use our populationCity method.

document.querySelector('#population-button').addEventListener('click', async () => {
const city = document.querySelector('#search-city').value;
const population = await populationCity(city);
document.querySelector('#result-population').textContent = population;
});

Getting multiple objects

If we want more objects from a collection we can use the get method on our cities reference. But we also have the option to filter, order, and limit our query. Let’s get the 3 biggest cities with over 1 million in population.

const getCities = async () => {
const querySnapshot = await cities.where('pop', '>=', 1000000).orderBy('pop', 'desc').limit(3).get();
let list = [];
querySnapshot.forEach(doc => list.push({name: doc.id, pop: doc.data().pop}));
return list;
}

Time for some HTML

<label>3 big cities:</label><button id="get-big">Get cities</button><div id="big-result"></div>

and JavaScript to use our function.

document.querySelector('#get-big').addEventListener('click', async () => {
const res = await getCities();
const bigDiv = document.querySelector('#big-result');
res.forEach(c => {
const p = document.createElement('p');
p.textContent = `${c.name}, ${c.pop}`;
bigDiv.appendChild(p);
});
});

Getting real-time updates

To get real-time updates to either a single document or a collection we will use the onSnapshot method. Let’s get all the cities.

cities.onSnapshot(snapshot => {
snapshot.docChanges().forEach(change => {
switch(change.type){
case 'added':
cityAdded({name: change.doc.id, pop: change.doc.data().pop});
break;
case 'modified':
cityUpdated({name: change.doc.id, pop: change.doc.data().pop});
break;
case 'removed':
cityRemoved(change.doc.id);
break;
}
})
})

We use the docChanges method to know what change happened. If a document was added, modified or removed. Now let’s show the cities.

<label>All cities</label>
<div id="all-cities"></div>

JavaScript with the three methods to populate our div.

const citiesDiv = document.querySelector('#all-cities');
const cityAdded = city => {
const p = document.createElement('p');
p.textContent = `${city.name}, ${city.pop}`;
p.id = city.name;
citiesDiv.appendChild(p);
}
const cityUpdated = city => {
document.querySelector('#' + city.name).textContent = `${city.name}, ${city.pop}`;
}
const cityRemoved = name => {
document.querySelector('#' + name).remove();
}

You can use the addCity method for an already existing city to trigger an update. We haven’t covered deletion yet, but you can do it from the developer console if you want to test it.

Updating data

Updating data is simple, just get the document you would like to change. Then call the update method with only the data you would like to change.

const updatePopulation = (city, pop) => {
cities.doc(city).update({
pop: pop
})
}

We would like the option to add 100 to a city population.

const b = document.createElement('button');
b.textContent = '+100';
b.onclick = () => updatePopulation(city.name, city.pop + 100);
p.appendChild(b);

This should be added inside the cityAdded function we created earlier.

Transactions

What if the city administrators had opened the application and clicked the increase button the exact same time? In that case, we need to use a transaction to avoid a race condition.

const populationTransaction = (city, inc) => {
const cityRef = cities.doc(city);
db.runTransaction(transaction => {
return transaction.get(cityRef).then(sfdoc => {
const newPop = sfdoc.data().pop + inc;
transaction.update(cityRef, {pop: newPop});
})
})
}

Then update our cityAdded function.

//b.onclick = () => updatePopulation(city.name, city.pop + 100);
b.onclick = () => populationTransaction(city.name, 100);

If you spam click the button before it disappears you will see that the transaction is working.

Remember that in a transaction you need to do the read (data()) before the write (update). Also if your transaction contains multiple writes and one fails, all writes will be rolled back.

Deleting data

Deleting data is simple.

const deleteCity = city => {
cities.doc(city).delete();
}

Adding a button inside our cityAdded function to call deleteCity.

const d = document.createElement('button');
d.textContent = 'delete';
d.onclick = () => deleteCity(city.name);
p.appendChild(d);

Sub-collections

If you need nested data, you could just have a list of objects inside your document. But then you need to retrieve the whole list every time you query your document. A better way which provides more flexibility is to use sub-collections.

Let's add a demo functions, so you get the idea.

const addDemoFamily = async () => {
/*demo family object*/
const demoFamily = {
surname: 'Smith',
members: [
{
name: 'Joe',
role: 'Child',
favouriteFood: ['Pizza', 'Fish', 'Pancakes']
},
{
name: 'Lucy',
role: 'Mother',
favouriteFood: ['Soup', 'Pizza']
},
{
name: 'Amy',
role: 'Child',
favouriteFood: ['Icecream']
}
]
};
/*Adding a family document*/
docRef = await families.add({surname: demoFamily.surname});
/*Getting our sub-collection*/
const memberRef = docRef.collection('members');
/*Adding documents to our sub-collection through a batch*/
const batch = db.batch();
for(let i=0; i<demoFamily.members.length; i++){
batch.set(memberRef.doc('' + i), demoFamily.members[i]);
}
await batch.commit();
/*Reading our saved data with a query*/
const querySnapshot = await memberRef.where('role', '==', 'Child').get();
let list = [];
querySnapshot.forEach(doc => list.push(doc.data().name));
alert('Only children: ' + list);
}

We have used a batched write to add all family members, to do the writing in one commit call. Then we need a button to trigger the function.

<button onclick="addDemoFamily()">Add demo family</button>

Visit your Firebase console to see how your data is added and structured.

Conclusion

I have now covered most of the Firestore functionality. I hope you see how easy it is. No need to set up a server which needs to connect to a database and provide an API. And we get a lot of functionality such as offline support, real-time updates, and an administration user interface.

But what about security? If all this happens from the client, then our data cannot be safe. Security should always be done in the backend. This is where Firestore rules and Firebase authentication plays its part. Follow my next article to see how you can secure your database and customize who and which actions are allowed.

Thank you for reading!

Feel free to follow me on Twitter at @GauteMeekOlsen

Check out my next article Firestore and Security for how to secure your Firestore data on the server side.

All code

--

--