1. Сложная логика в Route Handlers
Route Handler — это не просто точка входа для запроса. Это место, где может происходить всё, что угодно:
- Агрегация данных из разных источников (например, из нескольких файлов, баз данных, внешних API).
- Валидация и преобразование данных.
- Сложные фильтрации, сортировки, постобработка.
- Обработка транзакций, параллельных запросов, кэширование.
- Генерация сложных ответов (например, объединённых структур, отчётов, вложенных объектов).
- Реализация бизнес-логики, которую сложно выразить одной строкой.
Главное правило:
Route Handler — не место для "гигантского супа" из кода. Хорошая практика — выносить отдельные части логики в функции, сервисы, утилиты. Но для учебных целей мы рассмотрим примеры прямо в handler'ах, чтобы вы увидели, как это работает "изнутри".
2. Пример 1: Агрегация данных из нескольких источников
Задача
Допустим, у нас есть два файла с данными (users.json и tasks.json). Нам нужно реализовать API-эндпоинт, который возвращает список пользователей с их задачами.
users.json:
[
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" }
]
tasks.json:
[
{ "id": 1, "userId": 1, "title": "Buy milk" },
{ "id": 2, "userId": 1, "title": "Read book" },
{ "id": 3, "userId": 2, "title": "Write code" }
]
Route Handler: /app/api/users-with-tasks/route.ts
import { NextResponse } from "next/server";
import fs from "fs/promises";
import path from "path";
export async function GET() {
// Пути к файлам
const usersPath = path.join(process.cwd(), "data", "users.json");
const tasksPath = path.join(process.cwd(), "data", "tasks.json");
// Читаем оба файла параллельно
const [usersRaw, tasksRaw] = await Promise.all([
fs.readFile(usersPath, "utf-8"),
fs.readFile(tasksPath, "utf-8"),
]);
const users = JSON.parse(usersRaw);
const tasks = JSON.parse(tasksRaw);
// Агрегируем задачи к пользователям
const result = users.map((user: any) => ({
...user,
tasks: tasks.filter((task: any) => task.userId === user.id),
}));
return NextResponse.json(result);
}
Что тут происходит?
- Сначала параллельно читаем оба файла (быстрее, чем по очереди).
- Парсим оба JSON.
- Для каждого пользователя ищем его задачи.
- Возвращаем объединённую структуру.
Пример ответа:
[
{
"id": 1,
"name": "Alice",
"tasks": [
{ "id": 1, "userId": 1, "title": "Buy milk" },
{ "id": 2, "userId": 1, "title": "Read book" }
]
},
{
"id": 2,
"name": "Bob",
"tasks": [
{ "id": 3, "userId": 2, "title": "Write code" }
]
}
]
Полезный паттерн:
Promise.all — отличный способ параллелить независимые операции (например, чтение файлов или запросы к разным API).
3. Пример 2: Сложная валидация и агрегация ошибок
Задача
Создать эндпоинт для регистрации пользователя, который:
- Проверяет уникальность email.
- Проверяет сложность пароля.
- Возвращает все ошибки разом, а не только первую.
users.json:
[
{ "id": 1, "email": "alice@example.com", "name": "Alice" }
]
Route Handler: /app/api/register/route.ts
import { NextResponse } from "next/server";
import fs from "fs/promises";
import path from "path";
function validatePassword(password: string) {
const errors = [];
if (password.length < 8) errors.push("Пароль слишком короткий");
if (!/[A-Z]/.test(password)) errors.push("Нет заглавной буквы");
if (!/\\d/.test(password)) errors.push("Нет цифры");
return errors;
}
export async function POST(request: Request) {
const { email, password, name } = await request.json();
const usersPath = path.join(process.cwd(), "data", "users.json");
const usersRaw = await fs.readFile(usersPath, "utf-8");
const users = JSON.parse(usersRaw);
const errors = [];
// Проверка уникальности email
if (users.some((u: any) => u.email === email)) {
errors.push("Пользователь с таким email уже существует");
}
// Проверка пароля
errors.push(...validatePassword(password));
if (errors.length > 0) {
return NextResponse.json({ errors }, { status: 400 });
}
// Добавляем пользователя (в реальном проекте — хешируйте пароль!)
const newUser = { id: Date.now(), email, name };
users.push(newUser);
await fs.writeFile(usersPath, JSON.stringify(users, null, 2), "utf-8");
return NextResponse.json({ message: "Регистрация успешна", user: newUser });
}
Фишки:
- Сбор всех ошибок в массив, отправка их клиенту.
- Простая, но наглядная валидация пароля.
- Проверка уникальности email.
- Имитация добавления пользователя в JSON-файл.
В реальных проектах:
Валидацию лучше выносить в отдельные библиотеки (например, zod или yup), а пароли — никогда не хранить в открытом виде!
4. Пример 3: Фильтрация, сортировка и пагинация на сервере
Задача
Реализовать эндпоинт /api/tasks, который:
- Позволяет фильтровать задачи по userId.
- Позволяет сортировать по полю (title или id).
- Поддерживает пагинацию: page, pageSize.
Route Handler: /app/api/tasks/route.ts
import { NextRequest, NextResponse } from "next/server";
import fs from "fs/promises";
import path from "path";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const userId = searchParams.get("userId");
const sortBy = searchParams.get("sortBy") || "id";
const page = Number(searchParams.get("page") || 1);
const pageSize = Number(searchParams.get("pageSize") || 10);
const tasksPath = path.join(process.cwd(), "data", "tasks.json");
const tasksRaw = await fs.readFile(tasksPath, "utf-8");
let tasks = JSON.parse(tasksRaw);
// Фильтрация
if (userId) {
tasks = tasks.filter((t: any) => String(t.userId) === userId);
}
// Сортировка
tasks = tasks.sort((a: any, b: any) => {
if (sortBy === "title") return a.title.localeCompare(b.title);
return a.id - b.id;
});
// Пагинация
const total = tasks.length;
const start = (page - 1) * pageSize;
const paged = tasks.slice(start, start + pageSize);
return NextResponse.json({
page,
pageSize,
total,
tasks: paged,
});
}
Пример запроса:
/api/tasks?userId=1&sortBy=title&page=2&pageSize=1
Что происходит:
- Парсим параметры из URL.
- Фильтруем задачи по userId, если нужно.
- Сортируем по выбранному полю.
- Делаем срез (slice) для нужной страницы.
- Возвращаем результат с метаданными.
Фишка:
Такой паттерн легко масштабируется — можно добавить любую бизнес-логику фильтрации и сортировки.
5. Пример 4: Параллельные запросы к внешним API
Задача
Реализовать эндпоинт /api/weather-and-news, который одновременно получает погоду и новости с двух разных внешних API и возвращает их вместе.
Route Handler: /app/api/weather-and-news/route.ts
import { NextResponse } from "next/server";
export async function GET() {
// Примеры внешних API (используйте свои ключи и настоящие API!)
const weatherUrl = "https://api.example.com/weather?city=Moscow";
const newsUrl = "https://api.example.com/news?topic=world";
// Для учебного примера — просто имитация fetch
const [weather, news] = await Promise.all([
fetch(weatherUrl).then(res => res.json()).catch(() => ({ error: "Нет погоды" })),
fetch(newsUrl).then(res => res.json()).catch(() => ({ error: "Нет новостей" })),
]);
return NextResponse.json({ weather, news });
}
Фишки:
- Используем Promise.all для одновременного запроса.
- Обработка ошибок отдельных API (если один упал, второй всё равно возвращается).
- Универсальный паттерн для агрегации данных из разных сервисов.
6. Пример 5: Вложенная бизнес-логика и транзакции
Задача
Реализовать эндпоинт /api/transfer, который переводит "баллы" с одного пользователя на другого.
Требования:
- Проверить, что оба пользователя существуют.
- Проверить, что у отправителя хватает баллов.
- Выполнить перевод как атомарную операцию (в рамках одного запроса).
users.json:
[
{ "id": 1, "name": "Alice", "points": 100 },
{ "id": 2, "name": "Bob", "points": 25 }
]
Route Handler: /app/api/transfer/route.ts
import { NextResponse } from "next/server";
import fs from "fs/promises";
import path from "path";
export async function POST(request: Request) {
const { fromId, toId, amount } = await request.json();
const usersPath = path.join(process.cwd(), "data", "users.json");
const usersRaw = await fs.readFile(usersPath, "utf-8");
const users = JSON.parse(usersRaw);
const sender = users.find((u: any) => u.id === fromId);
const receiver = users.find((u: any) => u.id === toId);
const errors = [];
if (!sender) errors.push("Отправитель не найден");
if (!receiver) errors.push("Получатель не найден");
if (sender && sender.points < amount) errors.push("Недостаточно баллов");
if (errors.length > 0) {
return NextResponse.json({ errors }, { status: 400 });
}
// Атомарно обновляем баллы (на практике — делать через транзакции в БД!)
sender.points -= amount;
receiver.points += amount;
await fs.writeFile(usersPath, JSON.stringify(users, null, 2), "utf-8");
return NextResponse.json({
message: `Успешный перевод ${amount} баллов от ${sender.name} к ${receiver.name}`,
sender: { id: sender.id, points: sender.points },
receiver: { id: receiver.id, points: receiver.points }
});
}
Важное замечание:
В реальных приложениях для подобных операций обязательно используйте транзакции на уровне базы данных! Работа с файлами — только для учебных целей.
7. Пример 6: Генерация отчётов и вложенных структур
Задача
Нужно вернуть отчёт по пользователям: сколько у каждого задач, сколько выполнено, сколько не выполнено.
tasks.json:
[
{ "id": 1, "userId": 1, "done": true },
{ "id": 2, "userId": 1, "done": false },
{ "id": 3, "userId": 2, "done": true }
]
Route Handler: /app/api/users-task-report/route.ts
import { NextResponse } from "next/server";
import fs from "fs/promises";
import path from "path";
export async function GET() {
const usersPath = path.join(process.cwd(), "data", "users.json");
const tasksPath = path.join(process.cwd(), "data", "tasks.json");
const [usersRaw, tasksRaw] = await Promise.all([
fs.readFile(usersPath, "utf-8"),
fs.readFile(tasksPath, "utf-8"),
]);
const users = JSON.parse(usersRaw);
const tasks = JSON.parse(tasksRaw);
const report = users.map((user: any) => {
const userTasks = tasks.filter((t: any) => t.userId === user.id);
const total = userTasks.length;
const done = userTasks.filter((t: any) => t.done).length;
const notDone = total - done;
return {
id: user.id,
name: user.name,
totalTasks: total,
done,
notDone,
};
});
return NextResponse.json(report);
}
Результат:
[
{ "id": 1, "name": "Alice", "totalTasks": 2, "done": 1, "notDone": 1 },
{ "id": 2, "name": "Bob", "totalTasks": 1, "done": 1, "notDone": 0 }
]
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ