Урок 11

Валидация и обработка ошибок в API

API-роуты Next.js должны возвращать корректные коды и понятные ошибки.

Базовый ответ

import { NextResponse } from 'next/server';

export async function GET() {
    const posts = await db.posts.findMany();
    return NextResponse.json(posts);
}

Коды статусов

return NextResponse.json(data);                    // 200
return NextResponse.json(data, { status: 201 });   // 201 Created
return new NextResponse(null, { status: 204 });    // 204 No Content
return NextResponse.json({ error: 'Not found' }, { status: 404 });
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
return NextResponse.json({ errors: {...} }, { status: 422 });
return NextResponse.json({ error: 'Server error' }, { status: 500 });

Валидация с Zod

import { z } from 'zod';
import { NextRequest, NextResponse } from 'next/server';

const PostSchema = z.object({
    title: z.string().min(1).max(255),
    body: z.string().min(10),
    published: z.boolean().optional(),
});

export async function POST(request: NextRequest) {
    const body = await request.json();
    const parsed = PostSchema.safeParse(body);

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

    const post = await db.posts.create(parsed.data);
    return NextResponse.json(post, { status: 201 });
}

Единый формат ответа

// lib/api-response.ts
export function ok<T>(data: T, status = 200) {
    return NextResponse.json({ data }, { status });
}

export function fail(message: string, status = 400, errors?: unknown) {
    return NextResponse.json({ error: message, errors }, { status });
}

Использование:

import { ok, fail } from '@/lib/api-response';

export async function GET() {
    const posts = await db.posts.findMany();
    return ok(posts);
}

export async function POST(request: NextRequest) {
    const parsed = PostSchema.safeParse(await request.json());
    if (!parsed.success) {
        return fail('Validation failed', 422, parsed.error.flatten().fieldErrors);
    }

    const post = await db.posts.create(parsed.data);
    return ok(post, 201);
}

Обработка исключений

Оберни логику в try/catch:

export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
    try {
        const { id } = await params;
        const post = await db.posts.find(Number(id));

        if (!post) return fail('Post not found', 404);

        return ok(post);
    } catch (error) {
        console.error('API error:', error);
        return fail('Internal server error', 500);
    }
}

Обёртка для обработчика

type Handler<T> = (request: NextRequest, context: T) => Promise<NextResponse>;

export function withErrorHandling<T>(handler: Handler<T>): Handler<T> {
    return async (request, context) => {
        try {
            return await handler(request, context);
        } catch (error) {
            console.error(error);

            if (error instanceof z.ZodError) {
                return fail('Validation failed', 422, error.flatten().fieldErrors);
            }

            if (error instanceof AuthError) {
                return fail('Unauthorized', 401);
            }

            return fail('Internal server error', 500);
        }
    };
}

Использование:

export const GET = withErrorHandling(async (request) => {
    const posts = await db.posts.findMany();
    return ok(posts);
});

Аутентификация

import { auth } from '@/lib/auth';

export async function POST(request: NextRequest) {
    const session = await auth();
    if (!session) return fail('Unauthorized', 401);

    // ...
}

Rate limiting

import { Ratelimit } from '@upstash/ratelimit';

const ratelimit = new Ratelimit({
    redis: Redis.fromEnv(),
    limiter: Ratelimit.slidingWindow(10, '10 s'),
});

export async function POST(request: NextRequest) {
    const ip = request.headers.get('x-forwarded-for') ?? 'anonymous';
    const { success } = await ratelimit.limit(ip);

    if (!success) return fail('Too many requests', 429);

    // ...
}

CORS

export async function OPTIONS() {
    return new NextResponse(null, {
        status: 204,
        headers: {
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
            'Access-Control-Allow-Headers': 'Content-Type, Authorization',
        },
    });
}

Логирование

export async function POST(request: NextRequest) {
    const start = Date.now();

    try {
        // логика
    } finally {
        console.log(`${request.method} ${request.url} — ${Date.now() - start}ms`);
    }
}

Задание

  1. Создай lib/api-response.ts с ok и fail
  2. Оберни API-роут в withErrorHandling
  3. Добавь Zod-валидацию для POST
  4. Проверь: невалидные данные → 422, не найдено → 404, ошибка → 500

Что дальше

Следующий курс: «Деплой Next.js» — сборка, продакшен, мониторинг.