Next.jsJuniorCoding

Как настроить API-маршруты в App Router с помощью Route Handlers?

Route Handlers в App Router — файлы route.ts с именованными экспортами GET/POST/PUT/DELETE и т.д., работающие через стандартный Web API (NextRequest/NextResponse). Params в Next.js 15 — асинхронный Promise.

Route Handlers в Next.js App Router

Route Handlers — это аналог API-маршрутов из Pages Router, написанный по стандарту Web API (Request/Response). Файл route.ts внутри папки app/ создаёт HTTP-эндпоинт. Каждый именованный экспорт соответствует HTTP-методу.

Базовая структура

// app/api/hello/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(req: NextRequest) {
  return NextResponse.json({ message: 'Hello, World!' });
}

export async function POST(req: NextRequest) {
  const body = await req.json();
  return NextResponse.json({ received: body }, { status: 201 });
}

// Поддерживаемые методы: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS

Доступ к параметрам URL

// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(
  req: NextRequest,
  { params }: { params: Promise<{ id: string }> } // в Next.js 15 params — Promise
) {
  const { id } = await params;

  const user = await db.users.findUnique({ where: { id } });
  if (!user) {
    return NextResponse.json({ error: 'User not found' }, { status: 404 });
  }

  return NextResponse.json(user);
}

Работа с query-параметрами

// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(req: NextRequest) {
  const { searchParams } = req.nextUrl;
  const page = parseInt(searchParams.get('page') ?? '1');
  const limit = parseInt(searchParams.get('limit') ?? '20');
  const category = searchParams.get('category') ?? undefined;

  const products = await db.products.findMany({
    where: { category },
    skip: (page - 1) * limit,
    take: limit,
  });

  return NextResponse.json({ products, page, limit });
}

Работа с заголовками и cookies

// app/api/profile/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { cookies, headers } from 'next/headers';

export async function GET(req: NextRequest) {
  // Вариант 1: через req.cookies
  const token = req.cookies.get('auth_token')?.value;

  // Вариант 2: через next/headers (работает в любом серверном контексте)
  const cookieStore = await cookies();
  const userId = cookieStore.get('user_id')?.value;

  const headersList = await headers();
  const userAgent = headersList.get('user-agent');

  if (!userId) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const profile = await db.users.findUnique({ where: { id: userId } });
  return NextResponse.json(profile);
}

Обработка FormData и файлов

// app/api/upload/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function POST(req: NextRequest) {
  const formData = await req.formData();
  const file = formData.get('file') as File | null;
  const name = formData.get('name') as string;

  if (!file) {
    return NextResponse.json({ error: 'No file provided' }, { status: 400 });
  }

  const bytes = await file.arrayBuffer();
  const buffer = Buffer.from(bytes);

  // Сохраняем в S3/MinIO
  await s3.putObject({
    Bucket: process.env.S3_BUCKET!,
    Key: `uploads/${Date.now()}-${file.name}`,
    Body: buffer,
    ContentType: file.type,
  });

  return NextResponse.json({ success: true, name });
}

Streaming Response

// app/api/stream/route.ts
export async function GET() {
  const stream = new ReadableStream({
    async start(controller) {
      const encoder = new TextEncoder();
      const lines = ['Первая строка\n', 'Вторая строка\n', 'Готово\n'];

      for (const line of lines) {
        controller.enqueue(encoder.encode(line));
        await new Promise(resolve => setTimeout(resolve, 500));
      }
      controller.close();
    },
  });

  return new Response(stream, {
    headers: { 'Content-Type': 'text/event-stream' },
  });
}

Подводные камни

  • route.ts нельзя размещать рядом с page.tsx: в одной папке не может быть одновременно page.tsx и route.ts — они конфликтуют.
  • GET Route Handlers в Next.js 15 динамические по умолчанию: в отличие от Next.js 14, GET-маршруты больше не кэшируются автоматически. Добавьте export const dynamic = 'force-static' для кэширования.
  • params — Promise в Next.js 15: забытый await params даст [object Promise] вместо реального значения и сложные для отладки баги.
  • req.body нельзя читать дважды: после await req.json() тело запроса исчерпано — нельзя повторно вызвать req.formData() или req.text().
  • CORS требует ручной настройки: Route Handlers не добавляют CORS-заголовки автоматически — нужно явно добавить Access-Control-Allow-Origin и обработать OPTIONS-запрос.
  • Отсутствие middleware-like цепочки: нет встроенного аналога Express middleware — для общей логики (логирование, авторизация) создайте HOF-обёртку (withAuth(handler)).
  • Большие файлы зависают без стриминга: при загрузке больших файлов через req.arrayBuffer() весь файл буферизуется в памяти — для файлов > 10 MB используйте стриминг.

Common mistakes

  • Полагаться на встроенную CORS-настройку
  • Возвращать NextResponse.json({error}) без статуса 4xx/5xx
  • Кешировать персональные GET без dynamic = 'force-dynamic'
  • Использовать Route Handler там, где достаточно Server Action

What the interviewer is testing

  • Знает соответствие экспорт = метод
  • Использует zod/валидацию входа
  • Различает Edge и Node runtime
  • Понимает, когда выбирать handler vs Server Action

Sources

Related topics