Xcoding with Alfian

Software Development Videos & Tutorials

Serverless node.js REST API with Google Cloud Function & Firestore

Alt text

Serverless Application Architecture has been gaining massive popularity in recent years because as a software engineer we do not have to worry about configuration, deployment, and scalability of the application. Instead of spending time doing those activities, we can focus our time on building and testing the application logic for better user experience.

In this article, i am going to focus on demonstrating how to build a simple CRUD node.js Express REST API using Google Cloud Function and store the data using Firestore NoSQL database. Both of them are fully serverless and provided by Google Cloud as service with free tier.

What we will build:

  1. Express API to create, read, update, and delete (CRUD) note to Firestore database.
  2. Integrating our Express app to Google Cloud Function as HTTP Trigger.
  3. Deploying to Google Cloud Platform with Google Cloud CLI.

Building Express Router for Note CRUD

ur note API is a straightforwared Express router object with methods for GET all notes, GET single note by ID, POST to create note, PUT to update note by ID, and DELETE to delete note by ID.

To access Firestore we import admin from firebase-admin module that we add in our package.json NPM dependency. Because our app runs on Google Cloud Platform with the same project as our Firestore, we can just initialize the admin with default credential. If we run our app in other server, we can create and import JSON credential and pass it to initalize the Firestore. We store the firestore db object in db constant to access later.

How do Firestore model and store data?, according to Google Firestore documentation:

Following Cloud Firestore’s NoSQL data model, you store data in documents that contain fields mapping to values. These documents are stored in collections, which are containers for your documents that you can use to organize your data and build queries.

To sums it up, basically Firestore stores JSON document inside a collections, the JSON type can be String, Number, Nested objects. The cool things about Firestore we can also store subcollections within documents to build hierarchical data structures and it scales as the database grows.

Create Note

For our note app we will store the note inside collection we called “notes” and our document JSON will only contain text with the value of String.

const ref = await db.collection('notes').add({
    text: 'Hello i am a note text'
});

We can access the collection by passing the name of the collection, then we can use the add method passing the JSON data we want to add. This automatically generates unique ID to our document.

Querying Note

To query all our notes document inside the notes collection we simply call get() on collection object. It returns a snapshot of document reference that we can iterate.

const noteSnapshot = await db.collection('notes').get();
const notes = [];
noteSnapshot.forEach((doc) => {
    notes.push({
       id: doc.id,
       data: doc.data()
    });
});

To query a single note document inside the collection we can use the doc method passing the ID of the document.

const noteRef = await db.collection('notes').doc(id).get();

While the query is not as powerful as mongoDB, Firestore provide many quite powerful complex queries such as compound queries and many others. See the documentation about querying complex data.

Update Note

To update note we can use the doc method passing the ID and set method passing the data. We are passing the merge true options so if the document has another fields we are not overriding another fields, just the text field.

const ref = await db.collection('notes').doc(id).set({
    text: 'I am an updated text!'
}, {
    merge: true 
});

Delete Note

To delete note we can use the doc method passing the ID and delete method.

await db.collection('notes').doc(id).delete();

Here is the snippet of all the code inside our Router:

import express, { Router, Request } from 'express';
import admin from 'firebase-admin';

admin.initializeApp({
  credential: admin.credential.applicationDefault()
});
const db = admin.firestore();

const router = Router()
router.get('/', async (req, res, next) => {
    try {
        const noteSnapshot = await db.collection('notes').get();
        const notes = [];
        noteSnapshot.forEach((doc) => {
            notes.push({
                id: doc.id,
                data: doc.data()
            });
        });
        res.json(notes);
    } catch(e) {
        next(e);
    }
});

router.get('/:id', async(req, res, next) => {
    try {
        const id = req.params.id;
        if (!id) throw new Error('id is blank');
        const note = await db.collection('notes').doc(id).get();
        if (!note.exists) {
            throw new Error('note does not exists');
        }
        res.json({
            id: note.id,
            data: note.data()
        });
    } catch(e) {
        next(e);
    }
})

router.post('/', async (req, res, next) => {
    try {
        const text = req.body.text;
        if (!text) throw new Error('Text is blank');
        const data = { text };
        const ref = await db.collection('notes').add(data);
        res.json({
            id: ref.id,
            data
        });
    } catch(e) {
        next(e);
    }
});

router.put('/:id', async (req, res, next) => {
    try {
        const id = req.params.id;
        const text = req.body.text;
        if (!id) throw new Error('id is blank');
        if (!text) throw new Error('Text is blank');
        const data = { text };
        const ref = await db.collection('notes').doc(id).set(data, { merge: true });
        res.json({
            id,
            data
        });
    } catch(e) {
        next(e);
    }
});

router.delete('/:id', async (req, res, next) => {
    try {
        const id = req.params.id;
        if (!id) throw new Error('id is blank');
        await db.collection('notes').doc(id).delete();
        res.json({
            id
        });
    } catch(e) {
        next(e);
    }
});

export default router;

The Express Application

The Express app is very simple and straightforward, it assign some middlewares like cors, express 4.1+ json parser, and error handler. We also import our note Router and assign it to the /api route.

import express from 'express';
import cors from 'cors';
import route from './routes/index';
import methodOverride from 'method-override';

const app = express()
app.use(cors({ origin: true }));
app.use(express.json());
app.use('/api', route);
app.use(methodOverride())
app.use((err, req, res, next) => {
    res.status(400).json({
        error: err.message });
});

export default app;

Integrating to Google Cloud Function index.js File

To deploy our Express app to Google Cloud Platform, we need to create an index.js file that will be used by the Google Cloud CLI as an entrypoint when deploying the function to GCP. We need to export all the function we want to expose, the function must accept 2 parameters, which are Request and Response object, they are compatible with Express request and response object so we can import our Express app and use it right away. It is assigned to a note constant because each function deployed must have a name.

import app from './app';const note = App;
export { note }

One important note, Google Cloud Function currently only runs on Node .JS 6.1+ environment so we can’t deploy ES7 based source because Node 6 does not support many features like async await. Make sure to transpile all of the sources using Babel transpiler before deploying.

Deploying to Google Cloud Function From Local Machine

Make sure to Install Google Cloud CLI and beta components because Cloud Function is still in Beta, then type:

$ gcloud beta functions deploy note --trigger-http...
httpsTrigger: url: https://us-central1-alfianlosari-cd236.cloudfunctions.net/note
...

It will take about 2 minutes to deploy, after it completes, the url to access the function will be provided or you can access it from the Google Cloud Console. In this case to access the API we call https://us-central1-alfianlosari-cd236.cloudfunctions.net/note/api. We can also access the Google StackDriver Logger and Error Reporting from the Console to see all the log activities and error traces on our API.

There are many ways that we can use to deploy our functions, that you can refer in the deploying documentation.

Conclusion

I hope this article serves as a guide for all of you that want to deploy a simple backend service without knowledge of dev-ops and its complexity. You can access all the source code in the project Github Repository.

Serverless and Microservices are really the future for backend development because of the simplicity and rapid development productivity. There are still many improvements that need to be addressed, but i really believe Serverless is the future!.