Пошук уроків, статей та іншого контенту
Навчитеся повертати зрозумілі помилки API, обирати HTTP-статуси та не розкривати зайві внутрішні деталі.
Помилка API — це не лише проблема на сервері. Це ще й відповідь клієнту, який має зрозуміти:
що сталося;
чи потрібно повторити запит;
чи треба змінити дані;
чи має користувач пройти автентифікацію;
чи є проблема тимчасовою або остаточною.
Невдала відповідь:
{
"error": "Something went wrong"
}Ще гірша відповідь:
{
"error": "SequelizeConnectionError: password authentication failed for user..."
}Перша відповідь не дає клієнту достатньо інформації, а друга розкриває внутрішні деталі сервера.
Зазвичай API повертає помилку в однаковому форматі:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "Користувача не знайдено"
}
}Поле code зручно використовувати в програмному коді клієнта, а message — показувати користувачу або використовувати для логування.
Статус має описувати результат запиту, а не внутрішню причину помилки.
400 Bad RequestЗапит має неправильний формат:
пошкоджений JSON;
відсутнє обов’язкове поле;
параметр має неправильний формат.
400 Bad RequestНаприклад:
{
"error": {
"code": "INVALID_JSON",
"message": "Тіло запиту має містити коректний JSON"
}
}401 UnauthorizedКористувач не автентифікований або не надав коректні облікові дані.
403 ForbiddenКористувач автентифікований, але не має права виконувати операцію.
404 Not FoundЗапитаний ресурс не існує.
{
"error": {
"code": "USER_NOT_FOUND",
"message": "Користувача не знайдено"
}
}409 ConflictЗапит конфліктує з поточним станом даних. Наприклад, користувач із таким email вже існує.
422 Unprocessable EntityСинтаксис запиту коректний, але значення не проходять перевірку бізнес-правил.
На практиці 400 і 422 іноді використовують взаємозамінно. Важливіше, щоб API послідовно дотримувався обраного підходу.
500 Internal Server ErrorНепередбачена помилка на сервері:
база даних недоступна;
помилка в коді;
зовнішній сервіс повернув несподіваний результат.
Клієнту не потрібно повідомляти точну внутрішню причину такої помилки.
Корисно розділяти помилки на два типи.
Це ситуації, які є частиною нормальної роботи API:
користувача не знайдено;
дані не пройшли перевірку;
email уже використовується;
користувач не має доступу.
Такі помилки можна безпечно повернути клієнту разом із відповідним HTTP-статусом.
Це помилки, яких API не очікує:
виняток у бізнес-логіці;
помилка підключення до бази даних;
помилка сторонньої бібліотеки;
звернення до властивості undefined.
Такі помилки потрібно:
записати в серверні логи;
повернути клієнту загальне повідомлення;
не відправляти stack trace, SQL-запит, змінні середовища або секрети.
Замість того щоб передавати статус і код окремо в кожній частині програми, можна створити спеціальний клас помилки:
class ApiError extends Error {
constructor(statusCode, code, message) {
super(message);
this.name = 'ApiError';
this.statusCode = statusCode;
this.code = code;
this.isOperational = true;
}
}Тепер очікувану помилку можна створити так:
throw new ApiError(
404,
'USER_NOT_FOUND',
'Користувача не знайдено'
);Властивість isOperational позначає помилки, які можна безпечно повернути клієнту. Для звичайної помилки JavaScript такої властивості не буде.
Обробник помилок має бути централізованим. Це допомагає:
повертати однаковий формат відповідей;
не дублювати код;
приховувати внутрішні деталі;
централізовано записувати помилки в логи.
Нижче наведено повний приклад API без сторонніх бібліотек. Він використовує вбудований модуль node:http.
const http = require('node:http');
const { URL } = require('node:url');
class ApiError extends Error {
constructor(statusCode, code, message) {
super(message);
this.name = 'ApiError';
this.statusCode = statusCode;
this.code = code;
this.isOperational = true;
}
}
const users = [
{
id: 1,
name: 'Олена',
email: 'olena@example.com'
}
];
function sendJson(response, statusCode, body) {
response.writeHead(statusCode, {
'Content-Type': 'application/json; charset=utf-8'
});
response.end(JSON.stringify(body));
}
function sendError(response, error) {
const isApiError = error instanceof ApiError;
const statusCode = isApiError ? error.statusCode : 500;
const code = isApiError ? error.code : 'INTERNAL_SERVER_ERROR';
const message = isApiError
? error.message
: 'Внутрішня помилка сервера';
if (!isApiError) {
console.error('Непередбачена помилка:', error);
}
sendJson(response, statusCode, {
error: {
code,
message
}
});
}
function readJson(request) {
return new Promise((resolve, reject) => {
let body = '';
request.on('data', (chunk) => {
body += chunk;
});
request.on('end', () => {
if (body.length === 0) {
resolve({});
return;
}
try {
resolve(JSON.parse(body));
} catch {
reject(new ApiError(
400,
'INVALID_JSON',
'Тіло запиту має містити коректний JSON'
));
}
});
request.on('error', reject);
});
}
function findUserById(id) {
return users.find((user) => user.id === id);
}
async function handleRequest(request, response) {
const requestUrl = new URL(
request.url,
`http://${request.headers.host || 'localhost'}`
);
const path = requestUrl.pathname;
if (request.method === 'GET' && path === '/users') {
sendJson(response, 200, { data: users });
return;
}
const userPathMatch = path.match(/^\/users\/([^/]+)$/);
if (request.method === 'GET' && userPathMatch) {
const id = Number(userPathMatch[1]);
if (!Number.isInteger(id) || id <= 0) {
throw new ApiError(
400,
'INVALID_USER_ID',
'Ідентифікатор користувача має бути додатним цілим числом'
);
}
const user = findUserById(id);
if (!user) {
throw new ApiError(
404,
'USER_NOT_FOUND',
'Користувача не знайдено'
);
}
sendJson(response, 200, { data: user });
return;
}
if (request.method === 'POST' && path === '/users') {
const body = await readJson(request);
if (typeof body.name !== 'string' || body.name.trim() === '') {
throw new ApiError(
422,
'INVALID_NAME',
'Поле name має бути непорожнім рядком'
);
}
if (
typeof body.email !== 'string' ||
!body.email.includes('@')
) {
throw new ApiError(
422,
'INVALID_EMAIL',
'Поле email має містити коректну email-адресу'
);
}
const emailAlreadyUsed = users.some(
(user) => user.email === body.email
);
if (emailAlreadyUsed) {
throw new ApiError(
409,
'EMAIL_ALREADY_EXISTS',
'Користувач із таким email уже існує'
);
}
const user = {
id: users.length + 1,
name: body.name.trim(),
email: body.email
};
users.push(user);
sendJson(response, 201, { data: user });
return;
}
throw new ApiError(
404,
'ROUTE_NOT_FOUND',
'Маршрут не знайдено'
);
}
const server = http.createServer((request, response) => {
handleRequest(request, response).catch((error) => {
if (response.headersSent) {
response.end();
return;
}
sendError(response, error);
});
});
server.listen(3000, () => {
console.log('API запущено на http://localhost:3000');
});Запустіть файл:
node server.jsПриклад успішного запиту:
curl http://localhost:3000/users/1Відповідь:
{
"data": {
"id": 1,
"name": "Олена",
"email": "olena@example.com"
}
}Якщо користувача не існує:
curl http://localhost:3000/users/99API поверне статус 404:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "Користувача не знайдено"
}
}Приклад помилки валідації:
curl \
-X POST \
-H "Content-Type: application/json" \
-d '{"name":"","email":"wrong-email"}' \
http://localhost:3000/usersУ цьому випадку сервер поверне 422 і повідомлення про перше невалідне поле.
Внутрішні помилки потрібно логувати на сервері:
console.error(error);А клієнту повертати безпечну відповідь:
{
"error": {
"code": "INTERNAL_SERVER_ERROR",
"message": "Внутрішня помилка сервера"
}
}Не слід повертати клієнту:
error.stack;
SQL-запити;
назви внутрішніх файлів;
змінні середовища;
токени;
паролі;
повні повідомлення сторонніх сервісів;
структуру бази даних.
Повідомлення помилки може змінюватися залежно від середовища, але формат відповіді бажано залишати стабільним.
Наприклад, у режимі розробки розробнику може бути корисним stack trace, але в production його потрібно приховувати:
function getPublicMessage(error) {
const isProduction = process.env.NODE_ENV === 'production';
if (error instanceof ApiError) {
return error.message;
}
if (isProduction) {
return 'Внутрішня помилка сервера';
}
return error.message;
}Навіть у development не варто бездумно показувати клієнту секрети. Логи сервера та відповідь API — це різні канали для різної аудиторії.
Якщо запит містить кілька неправильних полів, одного повідомлення може бути недостатньо. У такому випадку можна додати список помилок:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Перевірте дані запиту",
"details": [
{
"field": "name",
"message": "Поле name є обов’язковим"
},
{
"field": "email",
"message": "Поле email має некоректний формат"
}
]
}
}Такий формат дозволяє клієнтському застосунку показати помилку біля відповідного поля форми.
Головне — не змішувати технічні деталі з повідомленнями для користувача. Наприклад, назву поля можна повернути, але внутрішній SQL-запит — ні.
У Node.js помилки в асинхронному коді потрібно явно передавати до обробника.
Для async-функції це зазвичай роблять через try...catch або через Promise.catch():
async function processRequest(request, response) {
try {
const result = await loadDataFromService();
sendJson(response, 200, {
data: result
});
} catch (error) {
sendError(response, error);
}
}У сервері з прикладу цю роль виконує:
handleRequest(request, response).catch((error) => {
sendError(response, error);
});Якщо не обробити відхилений Promise, помилка може залишитися непоміченою або спричинити нестабільну поведінку процесу.
Важливо також не відправляти дві відповіді для одного запиту. Після виклику response.end() відповідь уже завершена. Саме тому в обробнику перевіряється response.headersSent.
HTTP-статус допомагає клієнту вирішити, що робити далі.
Зазвичай:
400, 401, 403, 404, 409, 422 не потрібно автоматично повторювати без зміни запиту;
500 іноді можна повторити, але причину слід аналізувати;
502, 503, 504 часто означають тимчасову проблему із зовнішнім сервісом або інфраструктурою.
Автоматичні повтори мають бути обережними. Повторення POST може створити дублікати, якщо операція вже була виконана, але відповідь не дійшла до клієнта.
200 для невдалого запитуНе варто повертати:
200 OKразом із тілом:
{
"success": false,
"message": "Користувача не знайдено"
}Клієнту доведеться аналізувати тіло відповіді замість стандартного HTTP-статусу. Для відсутнього ресурсу використовуйте 404.
500 для всіх проблемЯкщо користувач надіслав неправильні дані, це не помилка сервера. Використовуйте 400 або 422.
Статус 500 призначений для ситуацій, коли сервер не зміг коректно виконати правильний запит.
error.message без перевіркиПовідомлення непередбаченої помилки може містити:
внутрішній шлях до файлу;
назву таблиці;
частину SQL-запиту;
службові параметри;
дані стороннього сервісу.
Повертайте оригінальне повідомлення лише для відомих і безпечних помилок API.
Stack trace корисний під час налагодження, але він розкриває структуру застосунку. Зберігайте його в логах, а не в JSON-відповіді API.
Якщо один endpoint повертає:
{
"message": "Помилка"
}а інший:
{
"errors": [
"Помилка"
]
}клієнту складніше обробляти відповіді. Визначте єдиний формат і використовуйте його для всіх маршрутів.
Якщо клієнт отримує лише 500, але сервер нічого не записує в логи, діагностувати проблему буде складно. Логуйте щонайменше:
текст помилки;
stack trace;
HTTP-метод;
маршрут;
час виникнення;
ідентифікатор запиту, якщо він використовується.
Не додавайте до логів паролі, токени та інші секрети.
Обробляйте помилки централізовано.
Використовуйте HTTP-статус, який відповідає суті проблеми.
Для очікуваних помилок повертайте стабільні коди та зрозумілі повідомлення.
Для непередбачених помилок використовуйте 500 Internal Server Error.
Логуйте внутрішні деталі на сервері, але не відправляйте їх клієнту.
Не повертайте stack trace, SQL-запити, секрети та внутрішні шляхи.
Дотримуйтеся єдиного формату JSON-відповідей.
Обробляйте помилки асинхронних операцій через try...catch або .catch().
Не відправляйте більше однієї відповіді для одного HTTP-запиту.