1. Введение
В Next.js 15, если вы работаете с App Router, Route Handlers позволяют описывать обработчики для каждого из HTTP-методов прямо в одном файле. Это выглядит очень элегантно: вы экспортируете отдельные асинхронные функции с именами в верхнем регистре — GET, POST, PUT, DELETE.
Базовый шаблон Route Handler
// app/api/todos/route.ts
export async function GET(request) {
// обработка GET-запроса
}
export async function POST(request) {
// обработка POST-запроса
}
export async function PUT(request) {
// обработка PUT-запроса
}
export async function DELETE(request) {
// обработка DELETE-запроса
}
Каждая функция будет вызвана только тогда, когда клиент отправит запрос с соответствующим методом. То есть, если браузер делает fetch('/api/todos', { method: 'POST' }), будет вызвана функция POST.
Интересный факт:
В Next.js 13/14 Route Handlers были доступны только в папке /app/api/, а начиная с Next.js 15 — их можно использовать и для других маршрутов, не только внутри API.
2. Пример: Простое CRUD API для задач
Давайте сделаем мини-API для управления списком задач (todos). Для простоты будем хранить данные в массиве в памяти (в реальном проекте — в базе данных, но для понимания API этого достаточно).
Шаг 1: Файл Route Handler
Создайте файл app/api/todos/route.ts:
// app/api/todos/route.ts
let todos = [
{ id: 1, text: 'Купить хлеб', done: false },
{ id: 2, text: 'Выучить Next.js', done: false }
];
// Получить список задач
export async function GET(request) {
return Response.json(todos);
}
// Добавить новую задачу
export async function POST(request) {
const body = await request.json();
const newTodo = {
id: Date.now(),
text: body.text,
done: false
};
todos.push(newTodo);
return Response.json(newTodo, { status: 201 });
}
// Обновить задачу (например, отметить как выполненную)
export async function PUT(request) {
const body = await request.json();
const todo = todos.find(t => t.id === body.id);
if (!todo) {
return Response.json({ error: 'Not found' }, { status: 404 });
}
todo.text = body.text ?? todo.text;
todo.done = body.done ?? todo.done;
return Response.json(todo);
}
// Удалить задачу
export async function DELETE(request) {
const { id } = await request.json();
const index = todos.findIndex(t => t.id === id);
if (index === -1) {
return Response.json({ error: 'Not found' }, { status: 404 });
}
const deleted = todos.splice(index, 1)[0];
return Response.json(deleted);
}
Разбор кода
- GET возвращает весь массив задач.
- POST ожидает тело запроса с полем text, создает новую задачу и возвращает её.
- PUT ищет задачу по id и обновляет её поля.
- DELETE ищет задачу по id и удаляет её из массива.
В реальности: Если бы сервер перезапускался, все задачи бы пропадали, потому что массив заново инициализируется. Для постоянного хранения нужен бы был файл или база данных.
3. Как отправлять запросы с разными методами
С помощью браузера вы можете отправить только GET (обычная ссылка или загрузка страницы). Для остальных методов — используйте JavaScript (например, fetch), Postman или curl.
Примеры fetch-запросов
Получить список задач (GET)
fetch('/api/todos')
.then(res => res.json())
.then(data => console.log(data));
Добавить задачу (POST)
fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: 'Сделать домашку' })
})
.then(res => res.json())
.then(data => console.log(data));
Обновить задачу (PUT)
fetch('/api/todos', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: 1, done: true })
})
.then(res => res.json())
.then(data => console.log(data));
Удалить задачу (DELETE)
fetch('/api/todos', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: 1 })
})
.then(res => res.json())
.then(data => console.log(data));
Шутка программиста:
"DELETE-запросы — как удаление файлов на рабочем столе: иногда кажется, что удалил, а оно просто переехало в корзину!"
(В нашем случае — если сервер перезапустить, всё вернётся!)
4. Полезные нюансы
Как Next.js выбирает обработчик по методу
Запрос приходит на URL /api/todos:
- Если это GET — вызывается функция GET.
- Если POST — вызывается POST.
- Если PUT — вызывается PUT.
- Если DELETE — вызывается DELETE.
Если вы не определили функцию для какого-то метода (например, PATCH), сервер вернёт ошибку 405 Method Not Allowed. Это удобно: вы явно указываете, что поддерживаете, а что нет.
Аргумент request: что внутри?
В каждую функцию приходит объект request — это стандартный Request из Fetch API. В нём есть:
- request.method — метод запроса (GET, POST и т.д.)
- request.url — полный URL запроса
- request.headers — заголовки
- request.json() — асинхронный метод для получения тела запроса как объекта (только если Content-Type: application/json)
- request.text() — получить тело как строку
Полезный совет:
Не забывайте, что чтение тела (request.json()) — асинхронное и может быть вызвано только один раз за запрос.
Response и его методы
В Route Handler вы всегда возвращаете экземпляр Response (или используете удобную обёртку Response.json). Это позволяет:
- Установить статус ответа (например, 200, 201, 404, 500)
- Вернуть данные в формате JSON, текст или другой контент
- Указать заголовки (например, CORS, Content-Type)
return new Response('Hello!', { status: 200, headers: { 'X-Custom': 'value' } });
Для JSON-ответа используйте удобную функцию:
return Response.json({ message: 'ok' }, { status: 201 });
Как обрабатывать параметры запроса
Если ваш API работает с коллекцией (например, /api/todos), часто нужно работать с конкретным элементом (например, /api/todos/42). Для этого создают динамические сегменты маршрута ([id]/route.ts). Но если вы хотите поддерживать "универсальный" обработчик, можно передавать id в теле запроса (как выше), либо через query-параметры.
Пример: получение id из query
export async function GET(request) {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return Response.json(todos);
}
const todo = todos.find(t => t.id === Number(id));
if (!todo) {
return Response.json({ error: 'Not found' }, { status: 404 });
}
return Response.json(todo);
}
5. Типичные ошибки при работе с HTTP-методами в Route Handlers
Ошибка №1: Не реализован нужный метод.
Если вы забыли экспортировать функцию, например, PUT, а клиент отправляет PUT-запрос, сервер вернёт 405 Method Not Allowed. Проверьте, что экспортируете все нужные методы.
Ошибка №2: Неправильная обработка тела запроса.
Если вы не вызываете await request.json(), а сразу пытаетесь читать поля (request.body.text), получите undefined или ошибку. Используйте только асинхронные методы (await request.json(), await request.text()).
Ошибка №3: Ошибка типов данных.
Если клиент отправляет не JSON, а, например, form-data, а вы вызываете request.json(), получите ошибку парсинга. Всегда проверяйте Content-Type или используйте try/catch.
Ошибка №4: Не возвращаете статус ответа.
Если всегда возвращаете 200 OK, даже при ошибках (например, не найден id), клиенту сложно понять, что что-то пошло не так. Используйте правильные коды: 201 для создания, 404 для "не найдено", 400 для "плохой запрос".
Ошибка №5: Использование одного Route Handler для всех операций.
Иногда проще разделить обработчики по разным файлам (например, для /api/todos и /api/todos/[id]), чтобы не усложнять логику внутри одного файла.
Ошибка №6: Неочевидная мутация данных.
В нашем примере массив todos живёт только в памяти. В реальных приложениях нужно использовать базы данных или хотя бы файлы, чтобы данные не терялись при перезапуске сервера.
Ошибка №7: Не обрабатываете асинхронные ошибки.
Если при чтении тела запроса или в логике возникает ошибка, не забывайте возвращать пользователю информативное сообщение и правильный статус.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ