Пошук уроків, статей та іншого контенту
Додасте перевірку типів, обов’язкових полів і бізнес-правил для вхідних даних API.
API отримує дані від зовнішнього клієнта, тому не можна припускати, що вони:
мають правильний тип;
містять усі обов’язкові поля;
відповідають очікуваному формату;
дотримуються правил предметної області.
Навіть якщо перевірка вже є у фронтенді, її потрібно повторювати на сервері. Клієнтський код можна обійти, змінити або замінити іншим клієнтом.
Валідація має відбуватися на межі застосунку — одразу після отримання HTTP-запиту та перетворення JSON у JavaScript-значення.
JSON підтримує такі типи даних:
рядок;
число;
логічне значення;
масив;
об’єкт;
null.
Наприклад, поле quantity може бути числом, але для замовлення недостатньо перевірити лише це. Значення 2.5 теж є числом, хоча кількість товару має бути цілим числом.
typeof value === "string";
typeof value === "number";
Array.isArray(value);
Number.isInteger(value);Для чисел краще використовувати Number.isInteger() або додаткові перевірки, а не тільки typeof value === "number".
Відсутнє поле та порожній рядок — це різні випадки:
const data = {};
data.name === undefined; // true
data.name === ""; // falseДля текстових полів часто потрібно перевіряти і тип, і те, що після видалення пробілів рядок не порожній:
typeof data.name === "string" && data.name.trim() !== "";Значення null також не слід вважати коректним значенням для обов’язкового поля.
Формат визначає, як саме має виглядати значення. Наприклад:
електронна пошта повинна містити @;
дата повинна мати узгоджений формат;
ідентифікатор може мати певну довжину;
рядок може містити лише дозволені символи.
Такі перевірки не повинні бути надмірно складними. Для кожного поля потрібно визначити практичні вимоги API.
Бізнес-правила описують не тип даних, а допустимість значення у конкретній системі.
Наприклад:
кількість товару має бути від 1 до 100;
замовлення повинно містити хоча б один товар;
товари в одному замовленні не повинні повторюватися;
для доставки кур’єром адреса є обов’язковою;
спосіб доставки повинен мати одне з дозволених значень.
Зручно повертати всі помилки одним масивом, а не зупинятися на першій:
{
"errors": [
{
"field": "customerEmail",
"message": "Має бути коректною електронною поштою"
},
{
"field": "items[0].quantity",
"message": "Має бути цілим числом від 1 до 100"
}
]
}Поле field допомагає клієнту зрозуміти, де саме сталася помилка. Для вкладених об’єктів і масивів варто використовувати зрозумілий шлях, наприклад items[0].quantity.
Для помилок вхідних даних API зазвичай використовують HTTP-статус 400 Bad Request.
Нижче наведено валідатор для такого запиту:
{
"customerEmail": "olena@example.com",
"deliveryMethod": "courier",
"address": "вул. Хрещатик, 1",
"items": [
{
"productId": "keyboard-01",
"quantity": 2
}
]
}Вимоги до даних:
customerEmail — обов’язковий рядок у форматі електронної пошти;
deliveryMethod — "pickup" або "courier";
address — обов’язкова для "courier";
items — непорожній масив;
productId — непорожній рядок;
quantity — ціле число від 1 до 100;
один і той самий товар не може повторюватися в замовленні.
const http = require("node:http");
const crypto = require("node:crypto");
function validateOrder(input) {
const errors = [];
if (
input === null ||
typeof input !== "object" ||
Array.isArray(input)
) {
return {
valid: false,
errors: [
{
field: "body",
message: "Тіло запиту має бути JSON-об'єктом"
}
]
};
}
const { customerEmail, deliveryMethod, address, items } = input;
if (typeof customerEmail !== "string" || customerEmail.trim() === "") {
errors.push({
field: "customerEmail",
message: "Поле є обов'язковим і має бути непорожнім рядком"
});
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(customerEmail)) {
errors.push({
field: "customerEmail",
message: "Має бути коректною електронною поштою"
});
}
const allowedDeliveryMethods = new Set(["pickup", "courier"]);
if (!allowedDeliveryMethods.has(deliveryMethod)) {
errors.push({
field: "deliveryMethod",
message: "Має бути pickup або courier"
});
}
if (address !== undefined && typeof address !== "string") {
errors.push({
field: "address",
message: "Має бути рядком"
});
}
if (deliveryMethod === "courier") {
if (typeof address !== "string" || address.trim() === "") {
errors.push({
field: "address",
message: "Є обов'язковим для доставки кур'єром"
});
}
}
if (!Array.isArray(items)) {
errors.push({
field: "items",
message: "Має бути масивом"
});
} else if (items.length === 0) {
errors.push({
field: "items",
message: "Має містити хоча б один товар"
});
} else {
const productIds = new Set();
items.forEach((item, index) => {
const fieldPrefix = `items[${index}]`;
if (
item === null ||
typeof item !== "object" ||
Array.isArray(item)
) {
errors.push({
field: fieldPrefix,
message: "Має бути об'єктом"
});
return;
}
if (
typeof item.productId !== "string" ||
item.productId.trim() === ""
) {
errors.push({
field: `${fieldPrefix}.productId`,
message: "Є обов'язковим непорожнім рядком"
});
} else if (productIds.has(item.productId)) {
errors.push({
field: `${fieldPrefix}.productId`,
message: "Товар не може повторюватися в одному замовленні"
});
} else {
productIds.add(item.productId);
}
if (
!Number.isInteger(item.quantity) ||
item.quantity < 1 ||
item.quantity > 100
) {
errors.push({
field: `${fieldPrefix}.quantity`,
message: "Має бути цілим числом від 1 до 100"
});
}
});
}
return {
valid: errors.length === 0,
errors
};
}
function sendJson(response, statusCode, data) {
response.writeHead(statusCode, {
"Content-Type": "application/json; charset=utf-8"
});
response.end(JSON.stringify(data));
}
const server = http.createServer((request, response) => {
const url = new URL(request.url, "http://localhost");
if (request.method !== "POST" || url.pathname !== "/orders") {
sendJson(response, 404, {
error: "Маршрут не знайдено"
});
return;
}
let rawBody = "";
request.on("data", (chunk) => {
rawBody += chunk;
// Обмежуємо максимальний розмір тіла запиту одним мегабайтом.
if (Buffer.byteLength(rawBody, "utf8") > 1_000_000) {
sendJson(response, 413, {
error: "Тіло запиту занадто велике"
});
request.destroy();
}
});
request.on("end", () => {
if (response.writableEnded) {
return;
}
let body;
try {
body = JSON.parse(rawBody);
} catch {
sendJson(response, 400, {
errors: [
{
field: "body",
message: "Тіло запиту має містити коректний JSON"
}
]
});
return;
}
const result = validateOrder(body);
if (!result.valid) {
sendJson(response, 400, {
errors: result.errors
});
return;
}
const order = {
id: crypto.randomUUID(),
...body,
createdAt: new Date().toISOString()
};
sendJson(response, 201, {
order
});
});
});
server.listen(3000, () => {
console.log("Сервер запущено на http://localhost:3000");
});Збережіть код у файлі server.js і запустіть:
node server.jsКоректний запит:
curl -X POST http://localhost:3000/orders \
-H "Content-Type: application/json" \
-d '{
"customerEmail": "olena@example.com",
"deliveryMethod": "courier",
"address": "вул. Хрещатик, 1",
"items": [
{
"productId": "keyboard-01",
"quantity": 2
}
]
}'Сервер поверне статус 201 Created.
Запит із помилками:
curl -X POST http://localhost:3000/orders \
-H "Content-Type: application/json" \
-d '{
"customerEmail": "wrong-email",
"deliveryMethod": "courier",
"items": [
{
"productId": "keyboard-01",
"quantity": 0
},
{
"productId": "keyboard-01",
"quantity": 2
}
]
}'Результатом буде відповідь зі статусом 400 і кількома описами помилок:
{
"errors": [
{
"field": "customerEmail",
"message": "Має бути коректною електронною поштою"
},
{
"field": "address",
"message": "Є обов'язковим для доставки кур'єром"
},
{
"field": "items[0].quantity",
"message": "Має бути цілим числом від 1 до 100"
},
{
"field": "items[1].productId",
"message": "Товар не може повторюватися в одному замовленні"
}
]
}Для API з JSON-тілом зручно використовувати такий порядок:
Перевірити HTTP-метод і маршрут.
Прочитати тіло запиту.
Розібрати JSON через JSON.parse().
Обробити помилку некоректного JSON.
Перевірити тип кореневого значення.
Перевірити обов’язкові поля та типи.
Перевірити формат і бізнес-правила.
Повернути 400, якщо дані некоректні.
Виконати основну операцію лише після успішної валідації.
Важливо не створювати замовлення, не записувати дані в базу та не виконувати бізнес-операцію до завершення перевірок.
Валідація відповідає на питання: «Чи є значення допустимим?»
Нормалізація змінює значення до узгодженого вигляду. Наприклад:
const normalizedEmail = customerEmail.trim().toLowerCase();Це різні операції. У простих випадках їх можна виконувати поруч, але бажано чітко розуміти, де дані перевіряються, а де змінюються.
Не слід автоматично виправляти помилкові значення. Наприклад, перетворення рядка "10" на число 10 може бути доречним лише тоді, коли це прямо передбачено контрактом API. Інакше клієнт може надсилати неправильні типи та не помічати проблеми.
Для валідації тіла запиту використовуйте 400 Bad Request.
Приклад поділу:
400 — JSON синтаксично неправильний або поля не відповідають вимогам;
404 — маршрут не знайдено;
413 — тіло запиту перевищує дозволений розмір;
201 — ресурс успішно створено.
Валідаційні помилки не повинні повертатися зі статусом 500. Статус 500 означає внутрішню помилку сервера, а некоректні дані клієнта є очікуваним сценарієм.
Клієнтська валідація покращує зручність користування, але не захищає API. Сервер повинен самостійно перевіряти кожен вхідний запит.
typeofПеревірка typeof value === "number" пропустить:
дробове число, якщо потрібне ціле;
NaN;
число за межами допустимого діапазону.
Для конкретних правил використовуйте Number.isInteger(), перевірку діапазону та інші відповідні умови.
Такої перевірки недостатньо:
if (body.name) {
// ...
}Вона не пояснює, чи є значення рядком і чи не складається воно лише з пробілів. Перевіряйте тип і вміст окремо.
Якщо повернути лише першу помилку, клієнту доведеться робити кілька однакових запитів для виправлення всіх полів. Масив помилок робить API зручнішим.
Коли перевірки розкидані по обробнику запиту, його складно тестувати та підтримувати. Винесення валідації в окрему функцію дозволяє перевіряти її незалежно від HTTP-сервера.
Не можна частково зберігати дані, а потім перевіряти решту полів. Спочатку потрібно отримати повний результат валідації, і лише за valid: true продовжувати обробку.
Валідуйте дані на сервері незалежно від перевірок на клієнті.
Перевіряйте типи, обов’язковість, формат і бізнес-правила.
Для помилок вхідних даних повертайте 400 Bad Request.
Повертайте масив помилок із полями та зрозумілими повідомленнями.
Для вкладених даних використовуйте шляхи на кшталт items[0].quantity.
Не виконуйте основну операцію до успішного завершення валідації.
Виносьте логіку перевірки в окремі функції, щоб її було простіше тестувати та повторно використовувати.