📜 ⬆️ ⬇️

We write nice Node.js-API using async / await and Firebase database

We already talked about the basics of working with async / await in Node.js, and how using this new mechanism allows us to make the code better. Today we will talk about how to create, using async / await, RESTful API, interacting with the Firebase database. Particular attention is paid to how to write a beautiful, convenient and understandable asynchronous code. You can say goodbye to callback hell right now.



In order to work through this material, you need to have Node.js and Firebase Admin SDK installed. If this is not the case, here is the official guide to setting up your work environment

Data recording


In the examples, we will consider fragments of API code, which is the server part of an application designed to work with words. The user of the application, finding a new word and wanting to save it in order to learn later, can save it to the database. When it comes to memorizing a word, it can be queried from the base.
')
Create a POST endpoint that will save words to the database:

//  const admin = require('firebase-admin'); const express = require('express'); //  const db = admin.database(); const router = express.Router(); //   router.use(bodyParser.json()); // API router.post('/words', (req, res) => {  const {userId, word} = req.body;  db.ref(`words/${userId}`).push({word});  res.sendStatus(201); }); 

Everything is arranged very simply, without excesses. We accept the user ID ( userId ) and the word ( word ), then save the word in the collection of words .

However, even in such a simple example, something is missing. We forgot about error handling. Namely, we return the status code 201 even if the word in the database could not be saved.

Add error handling to our code:

 // API router.post('/words', (req, res) => { const {userId, word} = req.body; db.ref(`words/${userId}`).push({word}, error => {   if (error) {     res.sendStatus(500);     //       , ,  Sentry   } else {     res.sendStatus(201);   } }; }); 

Now that the endpoint returns the correct status codes, the client can output the appropriate message to the user. For example, something like: "The word was successfully saved", or: "The word could not be saved, try again."

If you feel insecure while reading code written using the capabilities of ES2015 +, take a look at this guide.

Reading data


So, we have already written something into the Firebase database. Let's try reading now. First, this is what the GET endpoint will look like, created using the traditional promise approach:

 // API router.get('/words', (req, res) => { const {userId} = req.query; db.ref(`words/${userId}`).once('value')   .then( snapshot => {     res.send(snapshot.val());   }); }); 

Here, in order not to overload the example, error handling is omitted.

While the code does not look so complicated. Now let's look at the implementation of the same using async/await :

 // API router.get('/words', async (req, res) => { const {userId} = req.query; const wordsSnapshot = await db.ref(`words/${userId}`).once('value'); res.send(wordsSnapshot.val()) }); 

Here, again, there is no error handling. Pay attention to the async , added before the parameters (res, req) arrow function, and to the await keyword, which precedes the expression db.ref() .

The db.ref() method returns a promise. This means that we can use await to “suspend” the execution of the script. The await can be used with any promises.

The res.send() method, located at the end of the function, will only be called after the db.ref() promise db.ref() .

All this is good, however, you can truly appreciate the beauty of solutions using async/await , in cases where you need to chain several asynchronous requests into a chain.

Let's say you need to sequentially start a certain number of asynchronous functions:

 const example = require('example-library'); example.firstAsyncRequest() .then( fistResponse => {   example.secondAsyncRequest(fistResponse)     .then( secondResponse => {       example.thirdAsyncRequest(secondResponse)         .then( thirdAsyncResponse => {           //           });     }); }); 

Not very good work. Such constructions are also called “pyramids of horror” (pyramid of doom). And if we add error handling here ...

Now we rewrite this code using async/await :

 const example = require('example-library'); const runDemo = async () => { const fistResponse = await example.firstAsyncRequest(); const secondResponse = await example.secondAsyncRequest(fistResponse); const thirdAsyncRequest = await example.thirdAsyncRequest(secondResponse); }; runDemo(); 

There are no horrors here now. Moreover, all expressions with the await keyword can be wrapped into a single try/catch to handle any errors:

 const example = require('example-library'); const runDemo = async () => { try {   const fistResponse = await example.firstAsyncRequest();   const secondResponse = await example.secondAsyncRequest(fistResponse);   const thirdAsyncRequest = await example.thirdAsyncRequest(secondResponse); } catch (error) {   //   } }; runDemo(); 

This code looks quite decent. Now let's talk about parallel queries and async/await .

Parallel requests and async / await


What if you need to read multiple records from the database at the same time? In fact, there is nothing particularly difficult here. It is enough to use the Promise.all() method to execute queries in parallel:

 // API router.get('/words', async (req, res) => { const wordsRef = db.ref(`words`).once('value'); const usersRef = db.ref(`users`).once('value'); const values = await Promise.all([wordsRef, usersRef]); const wordsVal = values[0].val(); const userVal = values[1].val(); res.sendStatus(200); }); 

Notes on working with Firebase


When creating an API endpoint that will return what is retrieved from the Firebase database, try not to return the entire snapshot.val() . This can cause problems with parsing JSON on the client.

For example, on the client side there is this code:

 fetch('https://your-domain.com/api/words') .then( response => response.json()) .then( json => {   //   }) .catch( error => {   //   }); 

What will be in the snapshot.val() returned by Firebase can be either a JSON object or a null value if no records were found. If you return null , then json.response() in the above code will json.response() error, as it will try to null , which is not an object, to parse.

To protect against this, you can use Object.assign() to always return the object to the client:

 // API router.get('/words', async (req, res) => { const {userId} = req.query; const wordsSnapshot = await db.ref(`words/${userId}`).once('value'); //  res.send(wordsSnapshot.val()) //  const response = Object.assign({}, snapshot.val()); res.send(response); }); 

Results


As you can see, the async / await construction helps to avoid the hell of callbacks and other troubles, making the code clearer and making it easier to handle errors. If you want to take a look at a real project built using Node.js and the Firebase database, Vocabify is the application developed by the author of this material. It is designed to memorize new words.

Dear readers! Do you use async / await in your Node.js projects?

Source: https://habr.com/ru/post/332768/


All Articles