Валидация и обработка ошибок в 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`);
}
}
Задание
- Создай
lib/api-response.tsсokиfail - Оберни API-роут в
withErrorHandling - Добавь Zod-валидацию для POST
- Проверь: невалидные данные → 422, не найдено → 404, ошибка → 500
Что дальше
Следующий курс: «Деплой Next.js» — сборка, продакшен, мониторинг.