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

от автора

Всем привет. В преддверии старта курса «Разработчик 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, раз о нем зашла речь

ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/491786/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *