JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /Примеры сложной серверной логики в Route Handlers

Примеры сложной серверной логики в Route Handlers

Модуль 4: Node.js, Next.js и Angular
12 уровень , 8 лекция
Открыта

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 }
]
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ