Как стать автором
Обновить
831.71
OTUS
Цифровые навыки от ведущих экспертов

Создаем CRUD API на Express и MySQL: часть вторая

Время на прочтение7 мин
Количество просмотров7.1K
Всем привет. В преддверии старта курса «Разработчик Node.js», хотим поделиться продолжением материала, который был написан нашим внештатным автором.





Всем еще раз привет. Мы возвращаемся к созданию приложения на Node.js и MySQL для небольшого todo — приложения на Node.js для React. С прошлого раза я немного пересмотрел структуру нашего приложения, и теперь решил добавить дополнительную колонку в базу данных под названием inner_key, которая будет необходима нам для отрисовки уникальных ключей для каждого отдельного дела (в списке повторяющихся элементов React нужен уникальный ключ на каждый элемент для его обработки в Virtual DOM. Если это по прежнему вызывает у вас вопросы, вам стоит изучить эту статью).



Мы можем добавить колонку с помощью с помощью следующей команды в MySQL:

  ALTER TABlE TODO ADD inner_key varchar(100);

Да, возможно не стоит timestamp (а именно с помощью Date.now() я создаю ключи для своего дела в React) складывать в колонку varchar, но я надеюсь, что это несложно будет поправить, если кто-то будет заниматься конечно оптимизацией нашего приложения. С другой стороны, я не собираюсь использовать timestamp для работы со временем в моем приложении, мне нужно только уникальное значение. Так что пока это не несет никакой проблемы.

Небольшие обновления в работе нашей модели


Дальше, в связи с этим изменениями, у нас поменялась и сама модель нашего приложения.
Теперь у нас по-другому выглядит конструктор экземпляра в начале нашей модели, у него добавилось новое свойство:

const Deal = function(deal) {
  this.text = deal.text;
  this.inner_key = deal.inner_key;
};


Операцию по созданию дела в Модели я изменил чисто косметически. А вот операции по получению дела по id пришлось довольно сильно поменять, потому что мы теперь получаем дело по inner_key. Единственное, я не стал менять параметры res dealId, но в принципе, это не сильно режет читабельность кода:

Deal.findById = (dealId, result) => {
  sql.query(`SELECT * FROM TODO WHERE inner_key = '${dealId}'`, (err, res) => {

	// здесь обработка ошибок, не вижу смысла ее дублировать

    if (res.length) {
      console.log("найдено дело: ", res[0]);
      result(null, res[0]);
      return;
    }
    // если вдруг не удалось найти
    result({ kind: "not_found" }, null);
  });
};

Кроме него, небольшой мутации подвергся и запрос всех данных с таблицы. Для моего React — приложения не нужны id из базы данных, мне нужен inner_key. Поэтому немного поменялся и сам запрос:

Deal.getAll = result => {
   const queryAll = "SELECT text, inner_key FROM TODO";
  sql.query(queryAll, (err, res) => {
// обработка ошибок

В методе update мы тоже теперь обновляем дело по innerkey, и то же самое происходит и в методе удаления:

Deal.updateById = (inner_key, deal, result) => {
  const queryUpdate = "UPDATE TODO SET text = ? WHERE inner_key = ?";
  sql.query(
    queryUpdate,
    [deal.text, inner_key],
    (err, res) => {

//мощная обработка ошибок
      }
      //отправка данных 
     //Дальше идет удаление 
  const queryDelete = "DELETE FROM TODO WHERE inner_key = ?";
  sql.query(queryDelete, inner_key, (err, res) => {
	// обработка ошибок
    }

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

Создание своего Контроллера


В контроллере мы экспортируем созданные нами в модели функции. Если в общем, то каждый раз мы валидизируем запрос проверяя не было ли отправлено пустое тело (если вы планируете трансформить API в открытое, то это must have, да и вообще это полезно). Дальнейшие действия отличаются, в зависимости от того, нужно ли чтение res.id для выполнения запроса, или это не уточненный запрос. В методе findAll я оставил разрешающие все заголовки ответа для того, чтобы упростить тестирование своего API.

    
const Deal = require("../models/deal.model.js");

//Создаем и сохраняем новое дело
exports.create = (req, res) => {
  //  Валидизируем запрос
  if (!req.body) {
    res.status(400).send({
      message: "У нас не может не быть контента"
    });
  }

  // создание своего дела

  const deal = new Deal({
    text: req.body.text,
    inner_key: req.body.inner_key
    // у нашего дела будет текст и внутренний id, который будет использоваться как 
    // ключ для элементов в React
  });


  Deal.create(deal, (err, data) => {
    if (err)
      res.status(500).send({
        message:
          err.message || "Произошла ошибка во время выполнения кода"
      });
    else res.send(data);
  });
};

// Получение всех пользователей из базы данных
exports.findAll = (req, res) => {
  Deal.getAll((err, data) => {
    if (err)
      res.status(500).send({
        message:
          err.message || "Что-то случилось во время получения всех пользователей"
      });
    else 
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Headers', 'origin, content-type, accept');
    // я оставлю заголовки, получаемые с сервера, в таком виде, но конечно в реальном продакшене лучше переписать под конкретный origin
    // ну или вы делаете open API какой-нибудь, тогда делаете что хотите
    res.send(data);
  });
};

Остальную часть кода я запихну под spoiler, потому что тестировать мы будем только вышеупомянутые функции. Однако остальная часть кода тоже рабочая, но она технически отличается только аргументами от того, что уже было озвучено:

остальная часть кода
//  Найти одно дело по одному inner_id
exports.findOne = (req, res) => {
  Deal.findById(req.params.dealId, (err, data) => {
    if (err) {
      if (err.kind === "not_found") {
        res.status(404).send({
          message: `Нет дела с id ${req.params.dealId}.`
        });
      } else {
        res.status(500).send({
          message: "Проблема с получением пользователя по id" + req.params.dealId
        });
      }
    } else res.send(data);
  });
};

// Обновление пользователя по inner_id
exports.update = (req, res) => {
  // валидизируем запрос
  if (!req.body) {
    res.status(400).send({
      message: "Контент не может быть пустой"
    });
  }

// обновление дела по "айди" - на самом деле inner_key
  Deal.updateById(
    req.params.dealId,
    new Deal(req.body),
    (err, data) => {
      if (err) {
        if (err.kind === "not_found") {
          res.status(404).send({
            message: `Не найдено дело с id ${req.params.dealId}.`
          });
        } else {
          res.status(500).send({
            message: "Error updating deal with id " + req.params.dealId
          });
        }
      } else res.send(data);
    }
  );
};

// удалить дело по inner_key
exports.delete = (req, res) => {
  Deal.remove(req.params.dealId, (err, data) => {
    if (err) {
      if (err.kind === "not_found") {
        res.status(404).send({
          message: `Не найдено дело с ${req.params.dealId}.`
        });
      } else {
        res.status(500).send({
          message: "Не могу удалить дело с " + req.params.dealId
        });
      }
    } else res.send({ message: `дело было успешно удалено` });
  });
};

// Удалить все дела из таблицы
exports.deleteAll = (req, res) => {
  Deal.removeAll((err, data) => {
    if (err)
      res.status(500).send({
        message:
          err.message || "Что-то пошло не так во время удаления всех дел"
      });
    else res.send({ message: `Все дела успешно удалены` });
  });
};

  


Роутинг


Теперь переходим к самому сладкому и простому: описание роутинга, который потом надо не забыть проимпортировать потом в сам server.js. По описанным путям и методам мы сможем получать и добавлять данные. В нашей папке app создаем подпапку routes, в которой нам нужно файл deals.routes.js. В нем нужно написать следующее:

module.exports = app => {
 //импортируем наш контроллер, что бы можно было передать им функции по запросу

    const deals = require("../controllers/deal.controller.js");
  
    // Создание нового дела по методу post
    app.post("/deals", deals.create);
  
    // Получение всех дел сразу
    app.get("/deals", deals.findAll);
  
    //Получение отдельного дела по id (на самом деле в запросе должен inner_key), но я не стал это менять
    app.get("/deal/:dealId", deals.findOne);
  
    // обновить дело по id
    // здесь тоже самое про inner_key
    app.put("/deal/:dealId", deals.update);
  
    //Удалить дело по id
    app.delete("/deal/:dealId", deals.delete);
  
    // Удалить сразу все дела
    app.delete("/deals", deals.deleteAll);
  };
 

После этого открываем наш файл server.js и добавляем над прослушкой порта следующее:

  require("./app/routes/deals.routes.js")(app);

 

Непосредственно использование нашего API


В большинстве подобных статей в конце тестируют API с помощью POSTMAN. Это и вправду отличный инструмент, который вам стоит освоить, если планируете хоть немного заниматься профессионально разработкой API (и не столь важно, для какой платформы и на каком языке). Если вы закончили написание вашего приложения, и на забыли включить вашу базу данных, то теперь можно и запустить само приложение:

<node server.js>

Если вы совсем новичок, то вам будет интересно услышать о пакете nodemon, который перезапускает ваше приложение, если произошли изменения в файлах проекта:

  npm i nodemon -g
  nodemon server.js

Теперь будет куда проще отлаживать возникающие ошибки. Но мы собирались протестировать это в реальном React-приложении.

У меня есть под рукой простое react- todo, которое внимательный читатель мог заметить в статье деплоя react приложения на Heroku этого блога. Однако я не будут переписывать весь бэк под наше react — приложение (хотя это совсем не сложно, но я не хочу чрезмерно удлинять статью). Поэтому я запущу React приложение на встроенном сервере create-react-app под 3000 портом, а наше приложение работает под 5003 портом. Пускай теперь на нашем пути и стоят труднопроходимые CORS, мы легко сможем получить наши данные хотя бы все todo. Пример работы наших запросов я почти полностью взял с официальной документации React, которая посвящена fetch запросам. Запросы в React приложениях обычно осуществляются после рендеринга компонента в componentDidMount, и именно в нем нужно осуществлять запросы к удаленным ресурсам:

   class App extends Component{
    constructor(){
      super()
      this.state ={
        error: null,
        isLoaded: false,
        items:[],
        currentItem: {text:"первое дело", inner_key:"firstItem"}
        
      }
    }
    componentDidMount() {
      fetch("http://localhost:5003/deals")
        .then(res => res.json())
        .then(
          (result) => {
            this.setState({
              //если и произошла загрузка, тогда мы активируем наш компонент
              isLoaded: true,
              items: result
            });
          },
          // Примечание: важно обрабатывать ошибки именно здесь, а не в блоке catch(),
          // чтобы не перехватывать исключения из ошибок в самих компонентах.
          (error) => {
            this.setState({
              isLoaded: true,
              error
            });
          }
        )
    }

Теперь у нас есть дела, которые подтягиваются из базы данных:



Все работает и в самом приложении:



На получение всех дел наше приложение работает. Однако в React — приложении пока не описаны методы ни удаления, ни добавления дел. Чтобы упростить cors — запросы, вам стоит добавить в node-приложении обслуживание js-билда на React, и тогда расписать методы добавления дел по inner_id, их удаления и обновления.

Всем спасибо за внимание. По традиции, несколько полезных ссылок:

Немного интересного из документации express по поводу работы middleware, в частности body-express
Писать асинхронные запросы на React без axios уже совсем не модно
И немного про настраивание cors в Express, раз о нем зашла речь
Теги:
Хабы:
+6
Комментарии1

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS