Пошук уроків, статей та іншого контенту
Розберіть відмінності клієнтської й серверної перевірки та побудуйте надійну двоетапну валідацію даних.
Валідація — це перевірка, чи відповідають вхідні дані очікуваним правилам. У вебзастосунку її зазвичай виконують на двох рівнях:
клієнтська валідація — у браузері, до надсилання запиту;
серверна валідація — на сервері, після отримання запиту.
Ці рівні доповнюють один одного, але не замінюють.
Клієнтська валідація покращує взаємодію з користувачем, а серверна захищає застосунок і дані незалежно від того, як саме сформовано запит.
Клієнтська перевірка виконується в браузері. У React вона зазвичай запускається:
під час натискання кнопки надсилання форми;
під час зміни значення поля;
після втрати полем фокусу.
Наприклад, форма може одразу повідомити користувача, що:
поле обов’язкове;
email має неправильний формат;
пароль недостатньо довгий;
значення двох полів не збігаються.
Клієнтська валідація:
дає швидкий зворотний зв’язок;
не потребує мережевого запиту для простих перевірок;
зменшує кількість некоректних запитів до сервера;
покращує доступність і зручність форми.
Клієнтський код не можна вважати довіреним:
користувач може вимкнути JavaScript;
запит можна сформувати вручну через DevTools, curl або інший клієнт;
код браузера можна змінити;
клієнтська перевірка може бути обійдена або застаріти.
Тому клієнтська валідація — це передусім інструмент зручності, а не захисту.
Сервер отримує дані від клієнта й перевіряє їх перед виконанням будь-яких важливих дій:
збереженням у базу даних;
створенням облікового запису;
оплатою;
зміною прав доступу;
надсиланням даних до іншої системи.
Сервер має перевіряти всі дані, навіть якщо браузер уже виконав таку саму перевірку.
Серверна валідація повинна перевіряти:
наявність обов’язкових полів;
типи значень;
довжину рядків;
формат email та інших значень;
допустимий діапазон чисел;
взаємозв’язок між полями;
унікальність даних у базі;
права користувача на виконання операції.
Сервер також не повинен безпосередньо довіряти значенням, які надходять від клієнта. Наприклад, поле role: "admin" у запиті не має автоматично надавати користувачу права адміністратора.
| Ознака | Клієнтська валідація | Серверна валідація | |---|---|---| | Де виконується | У браузері | На сервері | | Основна мета | Зручність користувача | Безпека й цілісність даних | | Чи можна обійти | Так | Не повинна бути обійдена | | Потребує запиту | Не обов’язково | Так | | Має бути обов’язковою | Ні | Так |
У реальному застосунку потрібні обидва рівні:
клієнт швидко показує очевидні помилки;
сервер повторно перевіряє дані перед довіреною операцією.
Розглянемо форму реєстрації. Вона перевіряє:
ім’я;
email;
пароль;
підтвердження пароля.
Клієнтська перевірка не надсилає запит, якщо локальні правила порушені. Але навіть успішна локальна перевірка не означає, що сервер прийме дані.
import { useState } from "react";
const initialValues = {
name: "",
email: "",
password: "",
passwordConfirmation: "",
};
function validate(values) {
const errors = {};
const name = values.name.trim();
const email = values.email.trim();
if (!name) {
errors.name = "Введіть ім’я";
} else if (name.length < 2) {
errors.name = "Ім’я має містити щонайменше 2 символи";
}
if (!email) {
errors.email = "Введіть email";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.email = "Введіть коректний email";
}
if (!values.password) {
errors.password = "Введіть пароль";
} else if (values.password.length < 8) {
errors.password = "Пароль має містити щонайменше 8 символів";
}
if (!values.passwordConfirmation) {
errors.passwordConfirmation = "Повторіть пароль";
} else if (values.password !== values.passwordConfirmation) {
errors.passwordConfirmation = "Паролі не збігаються";
}
return errors;
}
export default function App() {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [status, setStatus] = useState("");
function handleChange(event) {
const { name, value } = event.target;
setValues((currentValues) => ({
...currentValues,
[name]: value,
}));
setErrors((currentErrors) => ({
...currentErrors,
[name]: undefined,
}));
setStatus("");
}
async function handleSubmit(event) {
event.preventDefault();
const clientErrors = validate(values);
if (Object.keys(clientErrors).length > 0) {
setErrors(clientErrors);
setStatus("");
return;
}
setStatus("Надсилання...");
try {
const response = await fetch("/api/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: values.name.trim(),
email: values.email.trim(),
password: values.password,
passwordConfirmation: values.passwordConfirmation,
}),
});
const result = await response.json();
if (!response.ok) {
setErrors(result.errors ?? {});
setStatus("");
return;
}
setValues(initialValues);
setErrors({});
setStatus("Реєстрацію успішно завершено");
} catch {
setStatus("Не вдалося з’єднатися із сервером");
}
}
return (
<main>
<h1>Реєстрація</h1>
<form onSubmit={handleSubmit} noValidate>
<label>
Ім’я
<input
name="name"
value={values.name}
onChange={handleChange}
aria-invalid={Boolean(errors.name)}
/>
</label>
{errors.name && <p role="alert">{errors.name}</p>}
<label>
Email
<input
name="email"
type="email"
value={values.email}
onChange={handleChange}
aria-invalid={Boolean(errors.email)}
/>
</label>
{errors.email && <p role="alert">{errors.email}</p>}
<label>
Пароль
<input
name="password"
type="password"
value={values.password}
onChange={handleChange}
aria-invalid={Boolean(errors.password)}
/>
</label>
{errors.password && <p role="alert">{errors.password}</p>}
<label>
Підтвердження пароля
<input
name="passwordConfirmation"
type="password"
value={values.passwordConfirmation}
onChange={handleChange}
aria-invalid={Boolean(errors.passwordConfirmation)}
/>
</label>
{errors.passwordConfirmation && (
<p role="alert">{errors.passwordConfirmation}</p>
)}
<button type="submit">Зареєструватися</button>
{status && <p role="status">{status}</p>}
</form>
</main>
);
}Функція validate є чистою функцією:
отримує значення форми;
не змінює стан React;
повертає об’єкт помилок;
не виконує мережевих запитів.
Об’єкт помилок має вигляд:
{
email: "Введіть коректний email",
password: "Пароль має містити щонайменше 8 символів"
}Для доступності помилки пов’язані з полями через:
role="alert" для повідомлень про помилки;
aria-invalid для позначення некоректного поля;
зрозумілі підписи label.
Атрибут noValidate вимикає стандартну браузерну валідацію форми, щоб застосунок використовував власні повідомлення. Це не вимикає серверну перевірку.
Нижче наведено мінімальний HTTP-сервер на Node.js без додаткових бібліотек. Він повторює основні правила клієнта та додає серверне правило: email existing@example.com уже зареєстрований.
Цей приклад можна запустити командою node server.js. Він очікує POST-запити на /api/register.
const http = require("node:http");
const PORT = 3000;
function validateRegistration(data) {
const errors = {};
if (!data || typeof data !== "object") {
return { form: "Некоректне тіло запиту" };
}
const name = typeof data.name === "string" ? data.name.trim() : "";
const email = typeof data.email === "string" ? data.email.trim() : "";
const password =
typeof data.password === "string" ? data.password : "";
const passwordConfirmation =
typeof data.passwordConfirmation === "string"
? data.passwordConfirmation
: "";
if (!name) {
errors.name = "Введіть ім’я";
} else if (name.length < 2) {
errors.name = "Ім’я має містити щонайменше 2 символи";
}
if (!email) {
errors.email = "Введіть email";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.email = "Введіть коректний email";
} else if (email.toLowerCase() === "existing@example.com") {
errors.email = "Цей email уже зареєстрований";
}
if (!password) {
errors.password = "Введіть пароль";
} else if (password.length < 8) {
errors.password = "Пароль має містити щонайменше 8 символів";
}
if (!passwordConfirmation) {
errors.passwordConfirmation = "Повторіть пароль";
} else if (password !== passwordConfirmation) {
errors.passwordConfirmation = "Паролі не збігаються";
}
return errors;
}
function sendJson(response, statusCode, body) {
response.writeHead(statusCode, {
"Content-Type": "application/json; charset=utf-8",
});
response.end(JSON.stringify(body));
}
const server = http.createServer((request, response) => {
if (request.method !== "POST" || request.url !== "/api/register") {
sendJson(response, 404, {
message: "Маршрут не знайдено",
});
return;
}
let body = "";
request.on("data", (chunk) => {
body += chunk;
// Обмежуємо розмір тіла запиту
if (body.length > 100_000) {
request.destroy();
}
});
request.on("end", () => {
let data;
try {
data = JSON.parse(body);
} catch {
sendJson(response, 400, {
errors: {
form: "Тіло запиту має бути коректним JSON",
},
});
return;
}
const errors = validateRegistration(data);
if (Object.keys(errors).length > 0) {
sendJson(response, 422, {
errors,
});
return;
}
// Тут у реальному застосунку відбувалося б збереження даних
sendJson(response, 201, {
message: "Користувача створено",
});
});
});
server.listen(PORT, () => {
console.log(`Сервер запущено на http://localhost:${PORT}`);
});Для помилок валідації часто використовують статус 422 Unprocessable Content. Він означає, що сервер зрозумів запит, але дані не відповідають правилам.
Інші типові ситуації:
400 — некоректний формат запиту, наприклад пошкоджений JSON;
401 — користувач не автентифікований;
403 — користувач не має потрібних прав;
409 — конфлікт, наприклад email уже існує;
500 — внутрішня помилка сервера.
Точний вибір статусу залежить від API, але формат помилок має бути передбачуваним для клієнта.
Клієнтська форма повинна вміти показувати помилки, які виникли лише на сервері. Наприклад, браузер не може надійно перевірити, чи існує email у базі даних.
Сервер може відповісти:
{
"errors": {
"email": "Цей email уже зареєстрований"
}
}У React така відповідь потрапляє до:
if (!response.ok) {
setErrors(result.errors ?? {});
setStatus("");
return;
}Тому користувач побачить серверну помилку біля відповідного поля так само, як і локальну.
Не слід покладатися лише на текст статусу HTTP або на загальне повідомлення. Якщо сервер повертає структурований об’єкт помилок, клієнт може точно визначити, яке поле потрібно підсвітити.
Клієнт і сервер часто мають однакові базові правила. Це створює ризик розбіжностей:
на клієнті пароль має містити 8 символів, а на сервері — 10;
клієнт приймає email у верхньому регістрі, а сервер відхиляє його;
одне поле перейменували на клієнті, але не змінили на сервері.
Щоб зменшити такі проблеми:
Визначайте правила явно.
Використовуйте однакові назви полів.
Повторюйте критичні перевірки на сервері.
Не копіюйте серверну бізнес-логіку в браузер без потреби.
Якщо архітектура дозволяє, виносьте просту схему в спільний модуль, який може виконуватися і на клієнті, і на сервері.
Навіть за наявності спільної схеми сервер все одно залишається остаточним джерелом довіри.
Локальні правила зазвичай синхронні:
const errors = validate(values);Вони не потребують мережі. Натомість перевірка унікальності email є асинхронною:
клієнт надсилає запит;
сервер звертається до бази;
сервер повертає результат.
Навіть якщо клієнт окремо перевірив доступність email, сервер має повторити цю перевірку під час реєстрації. Між перевіркою доступності та фактичним збереженням інший запит може зареєструвати такий самий email.
Тому остаточне правило унікальності повинне гарантуватися не лише кодом валідації, а й обмеженням у базі даних.
Валідація не замінює інші механізми безпеки, але є їхньою необхідною частиною.
Під час обробки форми:
не довіряйте значенням із браузера;
не зберігайте пароль у відкритому вигляді;
не повертайте зайві внутрішні деталі помилок;
обмежуйте розмір вхідного запиту;
перевіряйте типи, довжини та формати на сервері;
не використовуйте дані користувача в небезпечному SQL або HTML без належної обробки.
Повідомлення для користувача мають бути зрозумілими, але не повинні розкривати зайву інформацію про внутрішню будову сервера.
Надійний сценарій виглядає так:
Користувач заповнює форму.
React виконує клієнтську валідацію.
Якщо є помилки, запит не надсилається.
Якщо локальних помилок немає, React надсилає дані на сервер.
Сервер парсить і перевіряє запит.
Якщо дані некоректні, сервер повертає структуровані помилки.
React показує серверні помилки у формі.
Якщо перевірка успішна, сервер виконує операцію.
React показує результат користувачу.
Клієнтська перевірка скорочує непотрібні запити, а серверна забезпечує правильність рішення на довіреній стороні.
Користувач може надіслати запит без React або змінити дані перед відправленням.
Правильно: повторювати всі важливі перевірки на сервері.
Якщо клієнт приймає дані, а сервер відхиляє їх через інші правила, форма здається непередбачуваною.
Правильно: узгоджувати базові правила та повертати зрозумілі серверні помилки.
Непорожній рядок ще не означає коректне значення. Наприклад, " " після обрізання пробілів є порожнім.
Правильно: перевіряти тип, нормалізувати значення та перевіряти формат.
Повідомлення «Форма некоректна» не пояснює, що саме потрібно виправити.
Правильно: повертати помилки, пов’язані з конкретними полями, якщо це безпечно.
Стан даних на сервері може змінитися після локальної перевірки.
Правильно: сервер має перевіряти унікальність під час самої операції.
Кнопка може залишитися заблокованою після мережевої помилки або відповіді сервера.
Правильно: окремо керувати станами надсилання, успіху та помилки й завжди повертати форму до працездатного стану.
Клієнтська валідація виконується в браузері та покращує взаємодію з користувачем.
Серверна валідація є обов’язковою, оскільки клієнтському коду не можна довіряти.
Однакові правила потрібно узгоджувати між клієнтом і сервером.
Сервер має перевіряти типи, формати, обмеження, бізнес-правила та права доступу.
React повинен обробляти як локальні, так і серверні помилки.
Перевірка унікальності та інші залежні від стану системи правила мають виконуватися на сервері.
Надійна форма використовує двоетапну валідацію: швидку клієнтську та остаточну серверну.