DEV Community

Cover image for Beginner's Guide for Creating a Serverless REST API using NodeJS over Google Cloud Functions
Levi Velázquez
Levi Velázquez

Posted on • Updated on

Beginner's Guide for Creating a Serverless REST API using NodeJS over Google Cloud Functions

API REST using Google Cloud Functions (Serverless)

Serverless application has gained a lot of importance over time. It allows focussing on your app code/tests without worrying about configurations, deployment process or scalability.

We're going to create a function that will be exposed via rest URL. That function will be invoked every time a HTTP(S) request is received.

During execution, an express server will be summoned exposing our REST services.

What we are going to build?

  • Express API for CRUD services(create, read, update and delete) on a Firestore database.
  • Use Google Cloud Function to expose our Express server
  • Deploy our Google Cloud Function using Cloud CLI.

 Creating our firebase project

In order to create our first project, let's do it here. Select add project, project's name must be unique, let's use prefix github-ring-{github_user}, github-ring-levinm in my case. Be sure to select Firestore as our database.

image1

For creating our database, click on Develop>Database and select "start in test mode".

image2

Initializing our project locally

We need to install firebase using NPM.

npm install -g firebase-tools

Then, let's login into our firebase account.

firebase login
........... input credentials

Initialize the project

firebase init
........ select project

image3

It will prompt an interactive console.

  1. Select Functions and Hosting options.
  2. What language would you like to use to write Cloud Functions? TypeScript
  3. Do you want to use TSLint to catch probable bugs and enforce style? Yes
  4. Do you want to install dependencies with npm now? Yes
  5. What do you want to use as your public directory? Press enter to select public (it is the default option)
  6. Configure as a single-page app (rewrite all urls to /index.html)? No

We're ready, our firebase project was initialized.

Installing Express.js and dependencies

cd functions
npm install --save express body-parser 

Creating our Google Cloud Function

Open src/index.ts, it will be the entrypoint for our Express.js server

 Import main libraries

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import * as express from 'express';
import * as bodyParser from "body-parser";

Initialize firebase for accesing to its services

admin.initializeApp(functions.config().firebase);

Intialize Express.js server

const app = express();
const main = express();

Configure the server.

  • Let's add the path used for receiving the request.
  • Select JSON as our main parser for processing requests body.
main.use('/api/v1', app);
main.use(bodyParser.json());

 Export our function.

Last but not least, let's define our Google Cloud Function name, we are going to expose it using export. Our function will receive an express server object(this case main) which will be used for request processing. If you want more information regarding how it works, you can check this good answer on Stackoverflow

export const webApi = functions.https.onRequest(main);

Creating our first service

Let's expose a GET endpoint returning just a string.

app.get('/warm', (req, res) => {
    res.send('Calentando para la pelea');
})

Our src/index.ts file should look like this:


import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import * as express from 'express';
import * as bodyParser from "body-parser";

admin.initializeApp(functions.config().firebase);

const app = express();
const main = express();

main.use('/api/v1', app);
main.use(bodyParser.json());

export const webApi = functions.https.onRequest(main);

app.get('/warmup', (request, response) => {

    response.send('Warming up friend.');

})

 Deploying our function.

Before deploying it, we need to change our config file firebase.json as follows:

{
  "functions": {
    "predeploy": [
      "npm --prefix \"$RESOURCE_DIR\" run lint",
      "npm --prefix \"$RESOURCE_DIR\" run build"
    ]
  },
  "hosting": {
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "/api/v1/**",
        "function": "webApi"
      }
    ]
  }
}

This rule allows "to route" all requests sent through api/v1 to be served by webApi function (Our exported one).

Also, Google CLI installs Typescript v2 by default. So, we need to update our typescript version >=3.3.1. You can do it in functions.package.json.

  "devDependencies": {
    "tslint": "~5.8.0",
    "typescript": "~3.3.1"
  },

Re-install dependencies.

cd functions
npm install

We are ready for deploying.

firebase deploy
.....
✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/github-ring-levivm/overview
Hosting URL: https://github-ring-levivm.firebaseapp.com

If everything is ok, Hosting URL will be our Google Cloud Function endpoint.

 Testing our function

Let's send a GET request using CURL

$ curl -G "https://github-ring-levivm.firebaseapp.com/api/v1/warmup"
Warming up friend.

Rest API CRUD

Let's add our CRUD endpoints. We are going to manage fights information.

Create a record

First, let's initialize our database. We open our src/index.ts and add this after admin initialization


admin.initializeApp(functions.config().firebase);
const db = admin.firestore(); // Add this

In order to create a fight record, let's create POST /fights/ endpoint. Our fight record is going to have a winner, loser and title.

app.post('/fights', async (request, response) => {
  try {
    const { winner, loser, title } = request.body;
    const data = {
      winner,
      loser,
      title
    } 
    const fightRef = await db.collection('fights').add(data);
    const fight = await fightRef.get();

    response.json({
      id: fightRef.id,
      data: fight.data()
    });

  } catch(error){

    response.status(500).send(error);

  }
});
  • We get our post data using request.body
  • We use add() method to add a new fight, if the collection doesn't exist (our case), it will create it automatically.
  • In order to get the actual record data, we must use get() over the ref.
  • Return a json using response.json.

 Get a record

We create a GET /fights/:id endpoint in order to fetch a fight by id.

app.get('/fights/:id', async (request, response) => {
  try {
    const fightId = request.params.id;

    if (!fightId) throw new Error('Fight ID is required');

    const fight = await db.collection('fights').doc(fightId).get();

    if (!fight.exists){
        throw new Error('Fight doesnt exist.')
    }

    response.json({
      id: fight.id,
      data: fight.data()
    });

  } catch(error){

    response.status(500).send(error);

  }

});
  • We get the fight id using request.params.
  • We validate if the id is not blank.
  • We get the fight and check if it exists.
  • If fight doesn't exist we throw an error
  • If fight exists, we return the data.

 Get a record list

We create a GET /fights/ endpoint.

app.get('/fights', async (request, response) => {
  try {

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

    response.json(fights);

  } catch(error){

    response.status(500).send(error);

  }

});

  • We get a collection snapshot.
  • We iterate over every document and push the data into an array.
  • We return our fight list.

 Update a record

We must create a PUT /fights/:id endpoint in order to update a fight by id.

app.put('/fights/:id', async (request, response) => {
  try {

    const fightId = request.params.id;
    const title = request.body.title;

    if (!fightId) throw new Error('id is blank');

    if (!title) throw new Error('Title is required');

    const data = { 
        title
    };
    const fightRef = await db.collection('fights')
        .doc(fightId)
        .set(data, { merge: true });

    response.json({
        id: fightId,
        data
    })


  } catch(error){

    response.status(500).send(error);

  }

});
  • We get request data.
  • We validate the data
  • We update a record using set(data, merge: true). It means it is going to update only the fields passed on data parameter.

 Deleting a record.

For deleting a fight, we need to add an endpoint DELETE /fights/:id.


app.delete('/fights/:id', async (request, response) => {
  try {

    const fightId = request.params.id;

    if (!fightId) throw new Error('id is blank');

    await db.collection('fights')
        .doc(fightId)
        .delete();

    response.json({
        id: fightId,
    })


  } catch(error){

    response.status(500).send(error);

  }

});

  • We get the fight id.
  • We use delete() in order to delete a doc instance (Remember that firestore is database based on documents( "NoSQL" ))

Our src/index.ts file should looks like this

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import * as express from 'express';
import * as bodyParser from "body-parser";

admin.initializeApp(functions.config().firebase);
const db = admin.firestore(); // Add this

const app = express();
const main = express();

main.use('/api/v1', app);
main.use(bodyParser.json());

export const webApi = functions.https.onRequest(main);

app.get('/warmup', (request, response) => {

    response.send('Warming up friend.');

});

app.post('/fights', async (request, response) => {
  try {
    const { winner, losser, title } = request.body;
    const data = {
      winner,
      losser,
      title
    } 
    const fightRef = await db.collection('fights').add(data);
    const fight = await fightRef.get();

    response.json({
      id: fightRef.id,
      data: fight.data()
    });

  } catch(error){

    response.status(500).send(error);

  }
});

app.get('/fights/:id', async (request, response) => {
  try {
    const fightId = request.params.id;

    if (!fightId) throw new Error('Fight ID is required');

    const fight = await db.collection('fights').doc(fightId).get();

    if (!fight.exists){
        throw new Error('Fight doesnt exist.')
    }

    response.json({
      id: fight.id,
      data: fight.data()
    });

  } catch(error){

    response.status(500).send(error);

  }
});

app.get('/fights', async (request, response) => {
  try {

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

    response.json(fights);

  } catch(error){

    response.status(500).send(error);

  }

});

app.put('/fights/:id', async (request, response) => {
  try {

    const fightId = request.params.id;
    const title = request.body.title;

    if (!fightId) throw new Error('id is blank');

    if (!title) throw new Error('Title is required');

    const data = { 
        title
    };
    const fightRef = await db.collection('fights')
        .doc(fightId)
        .set(data, { merge: true });

    response.json({
        id: fightId,
        data
    })


  } catch(error){

    response.status(500).send(error);

  }

});

app.delete('/fights/:id', async (request, response) => {
  try {

    const fightId = request.params.id;

    if (!fightId) throw new Error('id is blank');

    await db.collection('fights')
        .doc(fightId)
        .delete();

    response.json({
        id: fightId,
    })


  } catch(error){

    response.status(500).send(error);

  }

});

 Testing

We deploy our function.

firebase deploy
....

We test all our endpoints.

# Testing create fight (POST /fights)
$ curl -d '{"winner":"levi", "losser":"henry", "title": "fight1"}' -H "Content-Type: application/json" -X POST "https://github-ring-levivm.firebaseapp.com/api/v1/fights/"

> {"id":"zC9QORei07hklkKUB1Gl","data":{"title":"fight1","winner":"levi","losser":"henry"}

# Testing  get a fight (GET /fight:id)
$ curl -G "https://github-ring-levivm.firebaseapp.com/api/v1/fights/zC9QORei07hklkKUB1wGl/"

>{"id":"zC9QORei07hklkKUB1Gl","data":{"winner":"levi","losser":"henry","title":"fight1"}}


# Testing get fights list (GET /fights/)
$ curl -G "https://github-ring-levivm.firebaseapp.com/api/v1/fights/"
> [{"id":"zC9QORei07hklkKUB1Gl","data":{"title":"fight1","winner":"levi","losser":"henry"}}]

# Testing update a fight (PUT /fights/:id)
$ curl -d '{"title": "new fight title"}' -H "Content-Type: application/json" -X PUT "https://github-ring-levivm.firebaseapp.com/api/v1/fights/zC9QORei07hklkKUB1Gl/"

> {"id":"zC9QORei07hklkKUB1Gl","data":{"title":"new fight title"}}

# Testing delete a fight (DELETE /fight/:id)
$ curl -X DELETE "https://github-ring-levivm.firebaseapp.com/api/v1/fights/zC9QORei07hklkKUB1Gl/"

> {"id":"zC9QORei07hklkKUB1Gl"}

And we are done, we have built our API Rest using Google Cloud Function (Serverless).

Note: You can check your database using Firestore interface within our Firebase console.

image4

If this was helpful, share it :).

If you like my content or it was helpful, you can motivate me to write more content by buying me a coffee

Latest comments (44)

Collapse
 
alextoul profile image
Alexandre Toulemonde • Edited

Since Jan 2020, we need extra settings to make the API calls public once deployed.

gcloud functions add-iam-policy-binding webApi \
--member="allUsers" \
--role="roles/cloudfunctions.invoker"

Collapse
 
projectcorrelationrelationship profile image
Project-Correlation-Relationship

Hi, Excellent tutorial. I using Windows 10, and I managed to get almost all the way through but after I run the firebase deploy, I am getting the following error

"debug] [2021-01-26T09:35:46.363Z] Error: EPERM: operation not permitted, scandir 'c:/Users/abc/AppData/Local/Application Data'

Any pointers please.

Collapse
 
gautham495 profile image
Gautham Vijayan

Phenomenal tutorial about firebase cloud functions.

Collapse
 
levivm profile image
Levi Velázquez

Glad u liked it.

Collapse
 
spock123 profile image
Lars Rye Jeppesen

Functions as a backend only works for the US region.

In other words, it's a no-go for people using firebase hosting, database, storage in other regions, as latency will be atrocious.

We had our entire backend rewritten as functions before we discovered this issue. Had to completely abandon Firebase functions because all our other Firebase assets are located in Europe. So we moved our backend elsewhere, unfortunately.

Just be aware guys: functions as an API for your frontend ONLY works for US region. We had 500ms latency for every request because of this.

Collapse
 
engmms profile image
engmms • Edited

Hi Levi
really very useful tutorial , thanks
I face this problem ( I am new with nodsJS and TypeScript

dev-to-uploads.s3.amazonaws.com/i/...

Collapse
 
levivm profile image
Levi Velázquez

That error is from your tslinter, you should add variable types or relax the your linter rules about errors.

Collapse
 
tfutada profile image
Takashi Futada

Thanks a lot. This helps a lot.

Collapse
 
madewithjavascript profile image
Made with Javascript

Super Helpful! using it on my project

Collapse
 
levivm profile image
Levi Velázquez

Glad to hear that.

Collapse
 
psidneyrobinson profile image
Sid Robinson • Edited

Hey there, I'm having a hard time getting this deployed. I've copied your index.ts file so I know the file is good, I just can't figure out why my code is breaking. I'm attaching a screenshot but not sure it's coming threw.

Collapse
 
levivm profile image
Levi Velázquez

I can't see the screenshot.

Collapse
 
psidneyrobinson profile image
Sid Robinson

Yeah.. not sure why it didn't work.. here is an url.. sidneyphillip.com/screenshot.png

Thread Thread
 
levivm profile image
Levi Velázquez

This is about your TS lint configuration, it isn't nothing about the platform itself.

Check this answer: stackoverflow.com/questions/430642...

Let me know if you need anything else.

Collapse
 
interpegasus profile image
Arturo

Hi Levi, Thanks for the tutorial. Currently, the API endpoints work fine after they are deployed.

However, when running the emulator locally, the API endpoints seem not to be recognized.

URL: mylocal_server/v1/warm or mylocal_server/crew-iq/us-central1...
Browser Output: 'Cannot GET /v1/warm'

Do you know what might cause the issue or what would be the local API endpoint?

$ firebase emulators:start --only functions
i emulators: Starting emulators: functions
✔ functions: Using node@10 from host.
✔ functions: Emulator started at 0.0.0.0:8080
i functions: Watching "/home/ubuntu/environment/Crew-IQ/functions" for Cloud Functions...
⚠ functions: The Cloud Firestore emulator is not running, so calls to Firestore will affect production.
✔ functions[webApi]: http function initialized (0.0.0.0:8080/crew-iq/us-central1/w...).
✔ All emulators started, it is now safe to connect.

Collapse
 
hungtranpg profile image
hungtranpg • Edited

Great tutorial!
Should we have only one large Express app to handle all requests? Or should we divide it to multi-app by functionally?
Because of some limits
cloud.google.com/functions/quotas

Collapse
 
calderaro profile image
Angel Calderaro • Edited

is there a reason to have 2 express apps? good post!

Collapse
 
levivm profile image
Levi Velázquez

It is a small trick to allow rewrite the base URL, also, one is for attaching to the function, the other one for handling the rest.

Collapse
 
kevinwong15 profile image
Little Stone

What is the advantage of having them as separate?
I believe the deployment to Firebase is still the same..

Thread Thread
 
levivm profile image
Levi Velázquez

Yes, it's the same, it's just a trick, one app handles the initial request, and the other the routing.

Collapse
 
akshay_pal01 profile image
Akshay Pal

Good, It is helpful.

Collapse
 
vardank37 profile image
vardank37

Hey Levi, Thank you for this tutorial. I just have one question, is it possible to get data in realtime using restful API?

Collapse
 
levivm profile image
Levi Velázquez

Yes, I mean, in this example you are getting the data in real-time or do you have other concept in mind ?

Collapse
 
vardank37 profile image
vardank37 • Edited

Here we are sending get and post requests but what I am asking for is something like firebase listeners. Like how can we get data automatically ( get data in frontend ) if something changes in the database without sending a request?

Thread Thread
 
levivm profile image
Levi Velázquez

Ah, I got it, no, this for request/response flow, but Firebase has a tool for that: Firebase Cloud Messaging, so, you can have channels/listeners and send data back and forth: firebase.google.com/docs/cloud-mes...

Collapse
 
thibaultwalterspieler profile image
Thibault Walterspieler

Hi,
First, thanks for this tutorial. I tried many things but I still get many "WebGL2RenderingContext" errors (x4) when deploying Firebase (firebase deploy).
thepracticaldev.s3.amazonaws.com/i...
I had followed at the letter your tutorial, but I'm still blocked.
Do you have any clue ?

Thank you again

Collapse
 
levivm profile image
Levi Velázquez • Edited

It seems to be something about your typescript, try to re-install it.

Collapse
 
thibaultwalterspieler profile image
Thibault Walterspieler

I finally success to manage this error by setting skipLibCheck: true in my tsconfig.json.
typescriptlang.org/docs/handbook/c...
Thank you anyway ;)

Collapse
 
prvnbist profile image
Praveen Bisht

Sweet, exactly what I was looking for. Thanks!

Collapse
 
levivm profile image
Levi Velázquez

Good, it helped you out.