Пройдемо на практиці найпоширеніші http-методи. Але спочатку проведемо деякі налаштування для коректної роботи.
Спершу створимо middleware, який дозволить нам отримувати дані з req.body. Якщо цей middleware не прописати, то нам буде з body приходити undefined, бо за замовчанням ми не маємо доступу до цих даних.
app.use(express.json());
Створимо проєкт такої структури:
project
├── index.js
└── usersdb.json
У файлі index.js будемо писати логіку нашого сервера. Поки ми не працюємо із базою даних у її ролі виступатиме файл usersdb.json. Поки що це порожній масив. А зберігатимемо в нього обʼєкти наших юзерів.
usersdb.json
[]
Створимо аналог сервісу REST API:
Метод
Маршрут
Операція
POST
/users
Створити користувача
GET
/users
Отримати всіх користувачів
GET
/users/<user_id>
Отримати користувача за id
PUT/PATCH
/users/<user_id>
Оновити користувача за id
DELETE
/users/<user_id>
Видалити користувача за id
Для генерування id юзера встановимо бібліотеку генерування унікального id .
terminal
npm i uuid
Зберемо кістяк нашого сервера на який будемо нарощувати функціонал.
index.js
// ===========LIBS=========== //
const express = require("express");
const uuid = require("uuid").v4;
const fs = require("fs").promises;
// ===========EXPRESS APP=========== //
const app = express();
// ===========MIDDLEWARE=========== //
app.use(express.json());
// Тут будемо писати наші кастомні middleware
// ===========CONTROLLERS=========== //
// Тут будемо писати наші контроллери
// ===========SERVER INIT=========== //
app.listen(3000, () => {
console.log("server is up and runned on port 3000");
});
Контроллери це по суті наші функції обробники запитів від користувача. В залежності від типу запиту і маршруту, якщо сервер виявляє збіг, то буде виконуватися ця функція.
У найпростішому розумінні наш контроллер повинен виконати такі дії:
провести валідацію даних, які прийшли
створити (укомплектувати) дані для подальшої обробки
опрацювати (синхронізувати) ці дані з базою даних
повернути відповідь на front-end
На цьому етапі для простоти розуміння пропустимо пункт валідації, вважаючи, що дані від користувача приходять валідні. Розглянемо пʼять вищезгаданих контролерів.
Створення користувача
Кожне звернення до бази даних, або роботи з файлами - це асинхронна операція, тому колбек-функція всередині контроллера має бути async. Крім того, використовуємо конструкцію try{} catch(){} для відловлювання помилок. Якщо виникає помилка, то можна вивести в консоль помилку і повернути на фронтенд статус 500 (свого роду збій на сервері).
Щодо основного коду:
Спочатку витягнемо отримані дані з об'єкта req його параметра body. В цьому обʼєкті передаються дані з фронтенду.
Створюємо новий обʼєкт користувача із отриманими даними і генеруємо унікальний id
Читаємо файл бази даних, парсимо його, пушимо нового користувача, і знову записуємо назад у файл
Формуємо відповідь для фронтенду. Передаємо статус 201 (успішна операція), а також json із повідомленням і обʼєктом новоствореного користувача.
index.js
/**
* CREATE USER: POST /users
*/
app.post("/users", async (req, res) => {
try {
const { name, year, email } = req.body;
// тут має бути валідація даних, які прийшли
//створюємо обʼєкт нового користувача, генеруємо для нього унікальний id
const newUser = { name, year, email, id: uuid() };
// зберігаємо користувача в базу даних (usersdb.json)
const usersDB = await fs.readFile("./usersdb.json");
const users = JSON.parse(usersDB);
users.push(newUser);
await fs.writeFile("./usersdb.json", JSON.stringify(users));
//надсилаємо відповідь на фронтенд
res.status(201).json({
msg: "User created",
user: newUser,
});
} catch (err) {
console.log(err);
res.sendStatus(500);
}
});
Перевіримо роботу нашого коду. Зробимо запит у програмі Postman (стрілками вказано порядок введення налаштувань):
Після натискання на кнопку Send ми отримуємо від нашого сервера відповідь:
Відкриємо наш файл usersdb.json, який виступає у ролі бази даних і переконуємося, що дані збережено:
Отримання всіх користувачів
Для отримання даних з сервера використовують метод GET.
Для перевірки роботи логіки додамо ще одного користувача і в окремій вкладці зробимо GET-запит.
Отримання користувача за id
Якщо у нашому запиті передається змінний параметр як-то <user_id>, то у контроллері в маршруті він вказується після двох крапок. І отримати цей параметр ми можемо взявши його зі змінної з такою ж назвою у обʼєкті req.params. Щоб отримати обʼєкт користувача за унікальним id, треба перебрати масив методом find.
index.js
/**
* GET ONE USER BY ID: GET /users/<user_id>
*/
app.get("/users/:userId", async (req, res) => {
try {
const { userId } = req.params;
const users = JSON.parse(await fs.readFile("./usersdb.json"));
const user = users.find((item) => item.id === userId);
res.status(200).json({
msg: "Success",
user,
});
} catch (err) {
console.log(err);
res.sendStatus(500);
}
});
Знаючи унікальний id користувача ми можемо отримати з бази даних інформацію про нього. Відкриємо нову вкладинку в Postman і виконаємо запит.
Рефакторинг коду (виносимо повторювану дію в middleware)
З таблички вище видно, що нам доведеться принаймні в кількох контролерах отримувати обʼєкт користувача за id. Щоб не дублювати код, доречно винести цей функціонал в middleware і він вже далі передаватиме цей обʼєкт користувача до наступного обробника.
Напишемо кастомний middleware:
Спільною властивістю для таких запитів є маршрут ("/users/:userId"). Тобто ця проміжна функція відпрацює, якщо буде збігатися маршрут.
Якщо збігу користувача за id не буде виявлено, то повернемо помилку 404, якщо користувача з таким id буде виявлено, то middleware передасть в req.user обʼєкт нашого користувача і передасть обробку наступній функції.
index.js
/**
* Сustom middleware: find user by id
*/
app.use("/users/:userId", async (req, res, next) => {
try {
const { userId } = req.params;
const users = JSON.parse(await fs.readFile("./usersdb.json"));
const user = users.find((item) => item.id === userId);
if (!user) {
return res.status(404).json({
msg: "User does not exist",
});
}
req.user = user;
next();
} catch (err) {
console.log(err);
res.sendStatus(500);
}
});
Проведемо рефакторинг нашого контролера. У ньому вже не потрібно звертатися до бази даних для отримання обʼєкта користувача, бо він міститься в req.user, тому і функцію можна робити не асинхронною і не використовувати try{} catch(){}.
index.js
/**
* GET ONE USER BY ID: GET /users/<user_id>
*/
app.get("/users/:userId", (req, res) => {
const { user } = req;
res.status(200).json({
msg: "Success",
user,
});
});
Перевіримо запити за id, який не існує і який існує:
Зміна даних користувача за id
Тут хоч і проходить middleware і повертає нам обʼєкт користувача, але функцію все одно треба робити асинхронною, бо у нас відбувається звернення до бази даних (запис файлу).
Проядок коду:
Спочатку створюємо оновлений обʼєкт користувача. Розпиляємо старий обʼєкт і отримані оновлені дані від фронтренду з req.body.
Витягаємо з бази даних (файлу) всіх користувачів. Створюємо новий масив і проходимо методом map старий масив. Якщо id не збігається - лишаємо старе значення, якщо id збігається - повертаємо оновлений обʼєкт користувача.
Змінимо дані якогось користувача за унікальним id:
Перевіримо чи змінились дані у базі даних (файлі):
Видалення користувача за id
Тут так само асинхронна функція, тому що ми звертаємося до файлу. Тут використовуємо метод filter. Відбираємо у новий масив всіх користувачів у яких не збігається id. І перезаписуємо файл. Таким чином збережуться всі користувачі, крім того, чий id прийшов з фронтенду.
index.js
/**
* DELETE USER BY ID: DELETE /users/<user_id>
*/
app.delete("/users/:userId", async (req, res) => {
try {
const { user } = req;
const users = JSON.parse(await fs.readFile("./usersdb.json"));
let updatedUsers = [];
updatedUsers = users.filter((item) => item.id !== user.id);
await fs.writeFile("./usersdb.json", JSON.stringify(updatedUsers));
// як варіант res.sendStatus(204);
res.status(200).json({
msg: "Success",
});
} catch (err) {
console.log(err);
res.sendStatus(500);
}
});
Перевіримо як проходить запит:
Перевіримо чи видалився контакт з файлу:
Як бачимо, всі контролери працюють. Елементарний REST API сервіс побудовано.
Весь код розглянутого сервера
index.js
// ===========LIBS=========== //
const express = require("express");
const uuid = require("uuid").v4;
const fs = require("fs").promises;
// ===========EXPRESS APP=========== //
const app = express();
// ===========MIDDLEWARE=========== //
app.use(express.json());
/**
* Сustom middleware: find user by id
*/
app.use("/users/:userId", async (req, res, next) => {
try {
const { userId } = req.params;
const users = JSON.parse(await fs.readFile("./usersdb.json"));
const user = users.find((item) => item.id === userId);
if (!user) {
return res.status(404).json({
msg: "User does not exist",
});
}
req.user = user;
next();
} catch (err) {
console.log(err);
res.sendStatus(500);
}
});
// ===========CONTROLLERS=========== //
/**
* CREATE USER: POST /users
*/
app.post("/users", async (req, res) => {
try {
const { name, year, email } = req.body;
// тут має бути валідація даних, які прийшли
//створюємо обʼєкт нового користувача, генеруємо для нього унікальний id
const newUser = { name, year, email, id: uuid() };
// зберігаємо користувача в базу даних (usersdb.json)
const usersDB = await fs.readFile("./usersdb.json");
const users = JSON.parse(usersDB);
users.push(newUser);
await fs.writeFile("./usersdb.json", JSON.stringify(users));
//надсилаємо відповідь на фронтенд
res.status(201).json({
msg: "User created",
user: newUser,
});
} catch (err) {
console.log(err);
res.sendStatus(500);
}
});
/**
* GET USERS LIST: GET /users
*/
app.get("/users", async (req, res) => {
try {
const users = JSON.parse(await fs.readFile("./usersdb.json"));
res.status(200).json({ msg: "Success", users });
} catch (err) {
console.log(err);
res.sendStatus(500);
}
});
/**
* GET ONE USER BY ID: GET /users/<user_id>
*/
app.get("/users/:userId", (req, res) => {
const { user } = req;
res.status(200).json({
msg: "Success",
user,
});
});
/**
* UPDATE USER BY ID: PATCH /users/<user_id>
*/
app.patch("/users/:userId", async (req, res) => {
try {
const { user } = req;
const { name, year, email } = req.body;
const updatedUser = { ...user, name, year, email };
const users = JSON.parse(await fs.readFile("./usersdb.json"));
let updatedUsers = [];
updatedUsers = users.map((item) => {
if (item.id !== updatedUser.id) return item;
if (item.id === updatedUser.id) return updatedUser;
});
await fs.writeFile("./usersdb.json", JSON.stringify(updatedUsers));
res.status(200).json({
msg: "Success",
user: updatedUser,
});
} catch (err) {
console.log(err);
res.sendStatus(500);
}
});
/**
* DELETE USER BY ID: DELETE /users/<user_id>
*/
app.delete("/users/:userId", async (req, res) => {
try {
const { user } = req;
const users = JSON.parse(await fs.readFile("./usersdb.json"));
let updatedUsers = [];
updatedUsers = users.filter((item) => item.id !== user.id);
await fs.writeFile("./usersdb.json", JSON.stringify(updatedUsers));
// як варіант res.sendStatus(204);
res.status(200).json({
msg: "Success",
});
} catch (err) {
console.log(err);
res.sendStatus(500);
}
});
// ===========SERVER INIT=========== //
app.listen(3000, () => {
console.log("server is up and runned on port 3000");
});
Коди статус-повідомлень:
Інформаційні 100 – 199
Успішні 200 – 299
Перенаправлення 300 - 399
Клієнтські помилки 400 – 499
Серверні помилки 500 - 599
Коди статус-повідомлень не обовʼязково знати всі на памʼять. Але Бажано основні знати:
200 - OK. Запит успішно опрацьовано.
201 - Created. Запит успішно виконано і в результаті було створено ресурс.
400 - Bad Request. Сервер не розуміє запит через неправильний синтаксис.
401 - Unauthorized. Для отримання запитаної відповіді потрібна автентифікація.
403 - Forbidden. У клієнта немає прав доступу до вмісту, тому сервер відмовляється дати відповідь.
404 - Not Found. Сервер не може знайти запитуваний ресурс.
500 - Internal Server Error. Сервер зіштовхнувся із ситуацією, яку він не знає як обробити.