1. Базовые HTTP-статусы: что возвращать и когда
Хорошая обработка ошибок — это когда ваш API всегда возвращает понятный и ожидаемый ответ, даже если что-то пошло не так. Это позволяет:
- Клиенту (браузеру, мобильному приложению) корректно реагировать на ошибку.
- Разработчику быстро находить и исправлять баги.
- Пользователю получать дружелюбные сообщения (а не "сервер упал, держитесь!").
В мире HTTP есть целая система кодов статусов. Вот самые важные для REST API (и не только):
| Код | Название | Когда использовать |
|---|---|---|
|
|
Всё хорошо, успешный ответ |
|
|
Успешно создан новый ресурс |
|
|
Успешно, но без тела (например, DELETE) |
|
|
Неправильные данные от клиента |
|
|
Необходима авторизация |
|
|
Нет прав на действие |
|
|
Ресурс не найден |
|
|
Конфликт (например, дублирование) |
|
|
Ошибка валидации данных |
|
|
Неожиданная ошибка на сервере |
Важно:
Старайтесь возвращать именно тот статус, который отражает суть проблемы. Если пользователь отправил некорректные данные — это 400 или 422, если сервер не смог обработать запрос из-за ошибки в коде — это 500.
2. Как возвращать статус-коды и сообщения в Next.js Route Handlers
В Next.js (App Router, Route Handlers) для формирования ответа используется объект NextResponse. Он позволяет не только отправлять тело ответа (JSON, текст), но и устанавливать статус-код, заголовки и т.д.
Простейший пример
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json(
{ message: 'Всё хорошо!' }, // тело ответа
{ status: 200 } // статус-код
);
}
Пример с ошибкой
export async function GET() {
return NextResponse.json(
{ error: 'Ресурс не найден' },
{ status: 404 }
);
}
Пример с валидацией
export async function POST(request) {
const body = await request.json();
if (!body.name || typeof body.name !== 'string') {
return NextResponse.json(
{ error: 'Поле name обязательно и должно быть строкой' },
{ status: 400 }
);
}
// ...дальше логика создания ресурса
return NextResponse.json({ message: 'Создано!' }, { status: 201 });
}
3. Универсальная схема обработки ошибок: try/catch
В реальной жизни ошибки могут случаться где угодно: не только при валидации, но и при работе с базой данных, сторонними API, файловой системой и т.д. Чтобы ловить такие ошибки, используйте try/catch.
Пример с try/catch
export async function GET() {
try {
// Здесь может быть сложная логика: запрос к БД, файловой системе и т.д.
throw new Error('Что-то пошло не так!');
// Если всё хорошо:
// return NextResponse.json({ data: ... }, { status: 200 });
} catch (err) {
// Логируем ошибку для себя (можно в консоль, можно в Sentry и т.п.)
console.error('Ошибка сервера:', err);
// Возвращаем клиенту понятное сообщение и статус 500
return NextResponse.json(
{ error: 'Внутренняя ошибка сервера' },
{ status: 500 }
);
}
}
Важно!
Никогда не отправляйте клиенту текст настоящей ошибки (err.message), если не уверены, что там нет чувствительных данных. Лучше возвращать "человеческое" сообщение, а детали ошибки логировать на сервере.
4. Типовые сценарии: примеры для CRUD API
Давайте разберём несколько типовых кейсов для REST API: чтение, создание, обновление, удаление.
Получение ресурса (GET)
export async function GET(request, { params }) {
const { id } = params;
// Допустим, мы ищем пользователя по id
const user = await findUserById(id);
if (!user) {
return NextResponse.json(
{ error: 'Пользователь не найден' },
{ status: 404 }
);
}
return NextResponse.json(user, { status: 200 });
}
Создание ресурса (POST) с валидацией
export async function POST(request) {
try {
const body = await request.json();
// Примитивная валидация
if (!body.email || !body.password) {
return NextResponse.json(
{ error: 'Email и пароль обязательны' },
{ status: 400 }
);
}
// Проверяем, не занят ли email
const exists = await findUserByEmail(body.email);
if (exists) {
return NextResponse.json(
{ error: 'Пользователь с таким email уже существует' },
{ status: 409 }
);
}
// Создаём пользователя
const newUser = await createUser(body);
return NextResponse.json(newUser, { status: 201 });
} catch (err) {
console.error('Ошибка при создании пользователя:', err);
return NextResponse.json(
{ error: 'Не удалось создать пользователя' },
{ status: 500 }
);
}
}
Обновление и удаление (PUT/PATCH/DELETE)
Принцип тот же: валидируем входные данные, проверяем, существует ли объект, и ловим все неожиданные ошибки.
5. Формат сообщений об ошибках: best practices
Какой формат лучше всего использовать?
Обычно удобнее всего возвращать JSON с ключом error (или message), чтобы клиенту было удобно парсить ответ. Можно добавить и дополнительную информацию (например, код ошибки, поле, к которому относится ошибка).
Стандартный вариант:
{
"error": "Пользователь не найден"
}
Более подробный вариант:
{
"error": "Ошибка валидации",
"details": [
{ "field": "email", "message": "Неверный формат email" }
]
}
Если используете валидацию через zod или другую библиотеку, обычно она возвращает массив ошибок — это удобно для фронтенда, чтобы отобразить пользователю все проблемы разом.
6. Практика: добавляем обработку ошибок в мини-приложение
Допустим, у нас есть простое API для списка задач (todos). Давайте реализуем обработку ошибок для добавления новой задачи.
// app/api/todos/route.js
import { NextResponse } from 'next/server';
let todos = [];
export async function POST(request) {
try {
const body = await request.json();
if (!body.title || typeof body.title !== 'string') {
return NextResponse.json(
{ error: 'Поле title обязательно и должно быть строкой' },
{ status: 400 }
);
}
// Проверим, нет ли уже такой задачи
if (todos.some(todo => todo.title === body.title)) {
return NextResponse.json(
{ error: 'Такая задача уже существует' },
{ status: 409 }
);
}
const newTodo = { id: todos.length + 1, title: body.title };
todos.push(newTodo);
return NextResponse.json(newTodo, { status: 201 });
} catch (err) {
console.error('Ошибка при добавлении задачи:', err);
return NextResponse.json(
{ error: 'Не удалось добавить задачу' },
{ status: 500 }
);
}
}
7. Как обрабатывать ошибки на клиенте
Клиенту (например, вашему React-приложению) важно правильно реагировать на статусы и сообщения:
const response = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ title: '' }),
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {
const data = await response.json();
alert(`Ошибка: ${data.error}`);
} else {
const todo = await response.json();
// Обновляем список задач
}
8. Типичные ошибки при обработке ошибок и статус-кодов
Ошибка №1: всегда возвращается 200 OK, даже если произошла ошибка.
Это сбивает с толку клиента: он думает, что всё хорошо, а на самом деле нет. Всегда ставьте правильный статус-код!
Ошибка №2: отправка технической информации клиенту.
Никогда не возвращайте клиенту stack trace или подробности внутренней ошибки — это может быть небезопасно.
Ошибка №3: отсутствие try/catch вокруг асинхронного кода.
Если не обернуть код в try/catch, ошибка "упадёт" наружу, а клиент получит 500 без объяснения.
Ошибка №4: не логируются ошибки на сервере.
Если не писать ошибки в консоль (или в систему логирования), вы не узнаете, что что-то пошло не так.
Ошибка №5: не возвращается понятное сообщение об ошибке.
Пользователь не понимает, что случилось, и пишет гневные письма в поддержку.
Ошибка №6: неверный статус-код для ошибки валидации.
Например, возвращают 500 вместо 400 или 422 — это мешает фронтенду корректно обрабатывать ошибки.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ