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

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

Мы можем добавить колонку с помощью с помощью следующей команды в MySQL:
Да, возможно не стоит
Дальше, в связи с этим изменениями, у нас поменялась и сама модель нашего приложения.
Теперь у нас по-другому выглядит конструктор экземпляра в начале нашей модели, у него добавилось новое свойство:
Операцию по созданию дела в Модели я изменил чисто косметически. А вот операции по получению дела по
Кроме него, небольшой мутации подвергся и запрос всех данных с таблицы. Для моего React — приложения не нужны
В методе update мы тоже теперь обновляем дело по
Как вы заметили, у меня здесь неполный код, потому что даже фигурные скобки не закрываются, но полный код я не вижу смысл приводить полностью. Если все-таки что-то непонятно, то можно посмотреть его здесь .
В контроллере мы экспортируем созданные нами в модели функции. Если в общем, то каждый раз мы валидизируем запрос проверяя не было ли отправлено пустое тело (если вы планируете трансформить API в открытое, то это must have, да и вообще это полезно). Дальнейшие действия отличаются, в зависимости от того, нужно ли чтение
Остальную часть кода я запихну под spoiler, потому что тестировать мы будем только вышеупомянутые функции. Однако остальная часть кода тоже рабочая, но она технически отличается только аргументами от того, что уже было озвучено:
Теперь переходим к самому сладкому и простому: описание роутинга, который потом надо не забыть проимпортировать потом в сам server.js. По описанным путям и методам мы сможем получать и добавлять данные. В нашей папке app создаем подпапку routes, в которой нам нужно файл
После этого открываем наш файл
В большинстве подобных статей в конце тестируют API с помощью POSTMAN. Это и вправду отличный инструмент, который вам стоит освоить, если планируете хоть немного заниматься профессионально разработкой API (и не столь важно, для какой платформы и на каком языке). Если вы закончили написание вашего приложения, и на забыли включить вашу базу данных, то теперь можно и запустить само приложение:
Если вы совсем новичок, то вам будет интересно услышать о пакете
Теперь будет куда проще отлаживать возникающие ошибки. Но мы собирались протестировать это в реальном React-приложении.
У меня есть под рукой простое react- todo, которое внимательный читатель мог заметить в статье деплоя react приложения на Heroku этого блога. Однако я не будут переписывать весь бэк под наше react — приложение (хотя это совсем не сложно, но я не хочу чрезмерно удлинять статью). Поэтому я запущу React приложение на встроенном сервере
Теперь у нас есть дела, которые подтягиваются из базы данных:

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

На получение всех дел наше приложение работает. Однако в React — приложении пока не описаны методы ни удаления, ни добавления дел. Чтобы упростить cors — запросы, вам стоит добавить в node-приложении обслуживание js-билда на React, и тогда расписать методы добавления дел по
Всем спасибо за внимание. По традиции, несколько полезных ссылок:
Немного интересного из документации express по поводу работы middleware, в частности body-express
Писать асинхронные запросы на React без axios уже совсем не модно
И немного про настраивание cors в Express, раз о нем зашла речь

Всем еще раз привет. Мы возвращаемся к созданию приложения на 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, раз о нем зашла речь
akamajoris
Просьба больше не учить людей делать код, подверженный XSS и SQL Inj. Хватит.