Урок 08

Route Handlers продвинуто

Динамические параметры

// app/api/posts/[id]/route.ts
export async function GET(
    request: Request,
    { params }: { params: Promise<{ id: string }> }
) {
    const { id } = await params;
    const post = await db.post.findUnique({ where: { id: Number(id) } });

    if (!post) {
        return Response.json({ error: 'Not found' }, { status: 404 });
    }

    return Response.json(post);
}

Query-параметры

export async function GET(request: Request) {
    const { searchParams } = new URL(request.url);
    const page = Number(searchParams.get('page') ?? 1);
    const limit = Number(searchParams.get('limit') ?? 10);

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

    return Response.json({ data: posts, page, limit });
}

Тело запроса

export async function POST(request: Request) {
    const body = await request.json();

    const post = await db.post.create({ data: body });
    return Response.json(post, { status: 201 });
}

FormData

export async function POST(request: Request) {
    const formData = await request.formData();
    const file = formData.get('file') as File;
    const title = formData.get('title') as string;

    // обработка

    return Response.json({ ok: true });
}

Заголовки

export async function GET(request: Request) {
    const auth = request.headers.get('authorization');

    if (!auth?.startsWith('Bearer ')) {
        return Response.json({ error: 'Unauthorized' }, { status: 401 });
    }

    const token = auth.slice(7);
    const user = await verifyToken(token);

    return Response.json({ user });
}

Cookies

import { cookies } from 'next/headers';

export async function GET() {
    const cookieStore = await cookies();
    const token = cookieStore.get('token')?.value;

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

    return Response.json({ token });
}

// Установка
cookieStore.set('token', 'value', {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 7,
});

Валидация через Zod

import { z } from 'zod';

const CreatePostSchema = z.object({
    title: z.string().min(3),
    body: z.string().min(10),
    published: z.boolean().optional(),
});

export async function POST(request: Request) {
    const json = await request.json();
    const result = CreatePostSchema.safeParse(json);

    if (!result.success) {
        return Response.json(
            { errors: result.error.flatten().fieldErrors },
            { status: 422 }
        );
    }

    const post = await db.post.create({ data: result.data });
    return Response.json(post, { status: 201 });
}

CORS

const corsHeaders = {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
};

export async function OPTIONS() {
    return new Response(null, { status: 204, headers: corsHeaders });
}

export async function GET() {
    const data = await db.post.findMany();
    return Response.json(data, { headers: corsHeaders });
}

Стриминг

export async function GET() {
    const encoder = new TextEncoder();

    const stream = new ReadableStream({
        async start(controller) {
            for (let i = 0; i < 10; i++) {
                controller.enqueue(encoder.encode(`data: ${i}\n\n`));
                await new Promise((r) => setTimeout(r, 100));
            }
            controller.close();
        },
    });

    return new Response(stream, {
        headers: {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive',
        },
    });
}

SSE для real-time.

Кеширование

export async function GET() {
    const data = await db.post.findMany();

    return Response.json(data, {
        headers: {
            'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
        },
    });
}

revalidate

export const revalidate = 60;

Webhooks

import crypto from 'crypto';

export async function POST(request: Request) {
    const body = await request.text();
    const signature = request.headers.get('x-signature');

    const expected = crypto
        .createHmac('sha256', process.env.WEBHOOK_SECRET!)
        .update(body)
        .digest('hex');

    if (signature !== expected) {
        return Response.json({ error: 'Invalid signature' }, { status: 401 });
    }

    const event = JSON.parse(body);
    // обработка

    return Response.json({ received: true });
}

Rate limiting

const requests = new Map<string, number[]>();

export async function GET(request: Request) {
    const ip = request.headers.get('x-forwarded-for') ?? 'unknown';
    const now = Date.now();
    const window = 60_000;
    const limit = 100;

    const timestamps = (requests.get(ip) ?? []).filter((t) => now - t < window);

    if (timestamps.length >= limit) {
        return Response.json({ error: 'Too many requests' }, { status: 429 });
    }

    timestamps.push(now);
    requests.set(ip, timestamps);

    // обработка
}

В продакшене — Redis или специализированные сервисы.

Итоги

  • params — промис
  • searchParams через URL
  • Response.json для ответа
  • Zod для валидации
  • CORS через заголовки
  • SSE для real-time
  • Webhooks с проверкой подписи