company_banner

Пишем симпатичные Node.js-API с использованием async/await и базы данных Firebase

Original author: Paul Breslin
  • Translation
Мы уже рассказывали об основах работы с async/await в Node.js, и о том, как использование этого нового механизма позволяет сделать код лучше. Сегодня поговорим о том, как создавать, используя async/await, RESTful API, взаимодействующие с базой данных Firebase. Особое внимание обратим на то, как писать красивый, удобный и понятный асинхронный код. Можете прямо сейчас попрощаться с адом коллбэков.



Для того, чтобы проработать этот материал, у вас должны быть установлены Node.js и Firebase Admin SDK. Если это не так — вот официальное руководство по настройке рабочей среды.

Запись данных


В примерах мы будем рассматривать фрагменты кода API, которое представляет собой серверную часть приложения, предназначенного для работы со словами. Пользователь приложения, найдя новое слово и желая сохранить его для того, чтобы позже выучить, может сохранить его в базу. Когда дело дойдёт до заучивания слова, его можно запросить из базы.

Создадим конечную точку POST, которая будет сохранять слова в базу данных:

// Зависимости
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);
});

Тут всё устроено очень просто, без излишеств. Мы принимаем идентификатор пользователя (userId) и слово (word), затем сохраняем слово в коллекции words.

Однако, даже в таком вот простом примере кое-чего не хватает. Мы забыли об обработке ошибок. А именно, мы возвращаем код состояния 201 даже в том случае, если слово в базу сохранить не удалось.

Добавим в наш код обработку ошибок:

// 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);
    }
  };
});

Теперь, когда конечная точка возвращает правильные коды состояний, клиент может вывести подходящее сообщение для пользователя. Например, что-то вроде: «Слово успешно сохранено», или: «Слово сохранить не удалось, попробуйте ещё раз».

Если вы неуверенно чувствуете себя, читая код, написанный с использованием возможностей ES2015+, взгляните на это руководство.

Чтение данных


Итак, в базу данных Firebase мы уже кое-что записали. Попробуем теперь чтение. Сначала — вот как будет выглядеть конечная точка GET, созданная с использованием традиционного подхода с применением промисов:

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

Тут, чтобы не перегружать пример, опущена обработка ошибок.

Пока код выглядит не таким уж и сложным. Взглянем теперь на реализацию того же самого с использованием 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())
});

Здесь, опять же, нет обработки ошибок. Обратите внимание на ключевое слово async, добавленное перед параметрами (res, req) стрелочной функции, и на ключевое слово await, которое предшествует выражению db.ref().

Метод db.ref() возвращает промис. Это означает, что тут мы можем задействовать await для того, чтобы «приостановить» выполнение скрипта. Ключевое слово await можно использовать с любыми промисами.

Метод res.send(), расположенный в конце функции, будет вызван только после того, как разрешится промис db.ref().

Всё это хорошо, однако, по-настоящему оценить красоту решений, использующих async/await, можно в случаях, когда нужно объединить в цепочку несколько асинхронных запросов.

Скажем, надо последовательно запустить некое количество асинхронных функций:

const example = require('example-library');

example.firstAsyncRequest()
  .then( fistResponse => {
    example.secondAsyncRequest(fistResponse)
      .then( secondResponse => {
        example.thirdAsyncRequest(secondResponse)
          .then( thirdAsyncResponse => {
            // Безумие продолжается
          });
      });
  });

Не очень-то хорошо получилось. Такие конструкции ещё называют «пирамидами ужаса» (pyramid of doom). А если сюда ещё добавить обработку ошибок…

Теперь перепишем этот код с использованием 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();

Никаких ужасов тут теперь нет. Более того, все выражения с ключевым словом await можно обернуть в один блок try/catch для обработки любых ошибок:

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();

Такой код выглядит вполне достойно. Теперь поговорим о параллельных запросах и async/await.

Параллельные запросы и async/await


Что если нужно одновременно прочитать из базы данных множество записей? На самом деле — ничего особенно сложного тут нет. Достаточно использовать метод Promise.all() для параллельного выполнения запросов:

// 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);
});

Примечания о работе с Firebase


Создавая конечную точку API, которая будет возвращать то, что получено из базы данных Firebase, постарайтесь не возвращать весь snapshot.val(). Это может вызвать проблемы с разбором JSON на клиенте.

Например, на стороне клиента есть такой код:

fetch('https://your-domain.com/api/words')
  .then( response => response.json())
  .then( json => {
    // Обработка данных
  })
  .catch( error => {
    // Обработка ошибок
  });

То, что будет в snapshot.val(), возвращённом Firebase, может оказаться либо JSON-объектом, либо значением null, если ни одной записи найти не удалось. Если возвратить null, тогда json.response() в вышеприведённом коде выдаст ошибку, так как он попытается этот null, не являющийся объектом, разобрать.

Чтобы от этого защититься, можно использовать Object.assign() для того, чтобы всегда возвращать клиенту объект:

// 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);
});

Итоги


Как видите, конструкция async/await помогает избежать ада коллбэков и прочих неприятностей, делая код понятнее и облегчая обработку ошибок. Если вы хотите взглянуть на реальный проект, построенный с применением Node.js и базы данных Firebase, вот Vocabify — приложение, которое разработал автор этого материала. Оно предназначено для запоминания новых слов.

Уважаемые читатели! Используете ли вы async/await в своих проектах на Node.js?
RUVDS.com
VDS/VPS-хостинг. Скидка 10% по коду HABR

Comments 6

    +3
    Не надо так демонизировать промисы, поверх которых async/await работает. Ваш пример с пирамидой ужаса можно записать так:

    const example = require('example-library');
    
    example.firstAsyncRequest()
    .then( fistResponse => example.secondAsyncRequest(fistResponse) )
    .then( secondResponse => example.thirdAsyncRequest(secondResponse) )
    .then( thirdAsyncResponse => // никакого безумия!!!!11 )
    //а теперь можно и глобальный catch на все три операции поставить
    .catch( err => handleError(err) );
    


    Пирамида ужаса это скорее про коллбеки.
    Это я к тому, что раз async/await уже в браузерах, то это не повод забыть про промисы, а наоборот, повод лучше в них разобраться ибо они никуда не уйдут.
      –1

      Ещё проще
      example.firstAsyncRequest()
      .then(example.secondAsyncRequest)
      и так далее…

      +4
      Сначала скептически отсесся к async / await, когда вышла спецификация. Но теперь проект полностью был переписан с этой технологией. И да, код действительно стал проще и читабельнее. Моя рекомендация — однозначно использовать.
        +1

        Зачем ради одной проверки на null копировать все поля? Не лучше ли было бы
        сделать так:


        // API
        router.get('/words', async (req, res) => {
          const {userId} = req.query;
          const wordsSnapshot = await db.ref(`words/${userId}`).once('value');
        
          // Еще лучше
          const response = snapshot.val() || {};
          res.send(response);
        });

        Касаемо асинхронных функций в Express роутере, попробуйте сделать так (я не
        думаю, что вы дождетесь ответа. Даже от обработчика ошибок):


        router.get('/words', async (req, res) => {
          noSuchMethod(); // <=
        
          res.send({});
        });
        
        router.use((err, req, res, next) => {
          console.error(err);
        
          err.sendStatuc(500);
        });

        А исправить это можно вполне легко:


        function isPromise(obj) {
          if (!obj || !obj.constructor) {
            return false;
          }
        
          if (obj.constructor.name === 'Promise'
            || obj.constructor.displayName === 'Promise') {
            return true;
          }
        
          return (typeof obj.then === 'function' || typeof obj.catch === 'function');
        }
        
        function wrap(fn) {
          const { length } = fn;
        
          if (length < 4) {
            return (req, res, next) => {
              const result = fn(req, res, next);
        
              return isPromise(result) ? result.catch(next) : result;
            };
          } else {
            return (err, req, res, next) => {
              const result = fn(err, req, res, next);
        
              return isPromise(result) ? result.catch(next) : result;
            };
          }
        };
        
        router.get('/words', wrap(async (req, res) => {
          noSuchMethod(); // <=
        
          res.send({});
        }));
        
        router.use((err, req, res, next) => {
          console.error(err); // Вот теперь сюда свалится ошибка
        
          err.sendStatuc(500);
        });
          0

          p.s. конечно же не err.sendStatuc(500);, а res.sendStatus(500);. К сожалению, кончилось время редактирования, и пишу комментарием.

          0
          спасибо, эта статья мне окончательно открыла глаза на async/await

          Only users with full accounts can post comments. Log in, please.