Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «API маршрути (обробники маршрутів) у Next.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Обробники маршрутів (Route Handlers)** у Next.js - це файли `route.ts` у директорії `app`, які експортують функції HTTP методів для створення серверних API-ендпоінтів. ```tsx // app/api/users/route.ts export async function GET() { const users = await db.user.findMany() return NextResponse.json(users) } ``` **Ключове:** Route Handlers - для публічних API і вебхуків; Server Actions - для мутацій із власного UI.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Обробники маршрутів (Route Handlers)** - це файли `route.ts` у директорії `app`, які експортують функції з іменами HTTP методів і створюють серверні API-ендпоінти у Next.js. ## Теорія ### TL;DR - Файл `route.ts` експортує функції `GET`, `POST`, `DELETE` тощо, кожна з яких відповідає HTTP методу - За угодою вони знаходяться в `app/api/`, але можна розміщувати де завгодно в `app` - GET-обробники без аргументу `request` кешуються автоматично; всі інші запускаються динамічно - Route Handlers - для публічних API, вебхуків і зовнішніх інтеграцій - Server Actions - коли мутація запускається з форми або кнопки у власному UI ### Швидкий приклад ```tsx // app/api/users/route.ts import { NextResponse } from 'next/server' import { db } from '@/lib/db' export async function GET() { const users = await db.user.findMany() return NextResponse.json(users) // статус 200 за замовчуванням } export async function POST(request: Request) { const body = await request.json() const user = await db.user.create({ data: body }) return NextResponse.json(user, { status: 201 }) } ``` Дві функції, два HTTP методи. Увесь патерн. ### Динамічні маршрути Додай `[id]` до назви папки і Next.js передає цей сегмент другим аргументом обробника: ```tsx // app/api/users/[id]/route.ts export async function GET( request: Request, { params }: { params: { id: string } } ) { const user = await db.user.findUnique({ where: { id: params.id } }) if (!user) { return NextResponse.json({ error: 'Not found' }, { status: 404 }) } return NextResponse.json(user) } export async function DELETE( request: Request, { params }: { params: { id: string } } ) { await db.user.delete({ where: { id: params.id } }) return new Response(null, { status: 204 }) // тіло не потрібне } ``` ### Читання даних запиту Обробники маршрутів використовують стандартний Web API `Request`. Ніяких специфічних для Next.js абстракцій: ```tsx // app/api/search/route.ts export async function GET(request: Request) { const { searchParams } = new URL(request.url) const query = searchParams.get('q') if (!query) { return NextResponse.json({ error: 'Параметр запиту відсутній' }, { status: 400 }) } const results = await db.post.findMany({ where: { title: { contains: query, mode: 'insensitive' } } }) return NextResponse.json(results) } ``` Для кук і заголовків є хелпери з `next/headers`: ```tsx import { cookies, headers } from 'next/headers' export async function GET() { const token = cookies().get('session')?.value const userAgent = headers().get('user-agent') return NextResponse.json({ token, userAgent }) } ``` ### Поведінка кешування Кешування - місце де найчастіше виникає плутанина. GET-обробник без аргументу `request` Next.js розглядає як статичний і кешує під час білду. Щойно ти читаєш щось із запиту, маршрут стає динамічним. ```tsx // Кешується - обчислюється під час білду export async function GET() { const res = await fetch('https://api.example.com/posts') return NextResponse.json(await res.json()) } // Не кешується - читає дані запиту export async function GET(request: Request) { const { searchParams } = new URL(request.url) // запускається при кожному зверненні } ``` Явне управління кешуванням через конфіг сегменту: ```tsx export const dynamic = 'force-dynamic' // завжди динамічний export const revalidate = 300 // кеш на 5 хвилин, потім оновлення ``` ### Route Handlers vs Server Actions | | Route Handlers | Server Actions | |---|---|---| | Призначення | API-ендпоінти | Мутації в UI | | HTTP методи | GET, POST, PUT, DELETE, PATCH... | Тільки POST | | Викликається з | Будь-якого HTTP клієнта (fetch, curl, мобільні застосунки) | Тільки з React компонентів | | Прогресивне покращення | Ні | Так (працює зі звичайними HTML-формами) | | Коли обирати | Публічний API, вебхуки, інтеграції | Форми, кнопки у власному UI | Якщо зовнішній сервіс має викликати твій ендпоінт, використовуй Route Handler. Якщо тільки твій UI, Server Actions прибирають багато шаблонного коду. ### Типові помилки **1. Немає await для params у Next.js 15** Починаючи з Next.js 15, `params` став Promise. Без `await` отримаєш `undefined`: ```tsx // Не працює в Next.js 15 export async function GET( _: Request, { params }: { params: { id: string } } ) { console.log(params.id) // undefined в Next.js 15 } // Правильно export async function GET( _: Request, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params console.log(id) } ``` **2. Думати що GET завжди динамічний** GET-обробник без `request` кешується. Якщо повертаєш свіжі дані з бази, але забув `export const dynamic = 'force-dynamic'`, користувачі бачитимуть застарілий результат після деплою. **3. Розміщення `route.ts` і `page.tsx` в одному сегменті** ``` app/users/page.tsx <- сторінка app/users/route.ts <- обробник маршруту в тій же папці = конфлікт ``` Next.js кине помилку. Перенеси обробник в `app/api/users/route.ts`. **4. Відсутня обробка помилок** ```tsx // Погано - необроблена помилка бази стає 500 з порожнім тілом export async function GET() { const users = await db.user.findMany() return NextResponse.json(users) } // Краще export async function GET() { try { const users = await db.user.findMany() return NextResponse.json(users) } catch { return NextResponse.json({ error: 'Database error' }, { status: 500 }) } } ``` ### Де зустрічається на практиці - Публічні REST API для мобільних застосунків і сторонніх клієнтів - Обробники вебхуків: Stripe, GitHub, Clerk - OAuth callback-обробники - Ендпоінти для завантаження файлів - Тригери cron-задач від зовнішніх планувальників (Vercel Cron, GitHub Actions) ### Питання на співбесіді **Q:** Чи можна викликати Route Handler із серверного компонента (Server Component)? **A:** Технічно так, але це зайвий HTTP round-trip. Server Component і так виконується на сервері, тому простіше звернутися до бази або сервісу напряму. **Q:** Що буде, якщо експортувати функцію з невалідним іменем HTTP методу з `route.ts`? **A:** Next.js просто проігнорує її. Розпізнаються тільки `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD` і `OPTIONS`. **Q:** Як поширити логіку авторизації між кількома Route Handlers без дублювання? **A:** Два варіанти: `middleware.ts` для захисту цілих груп маршрутів до того як запит дійде до обробника, або функція-обгортка що перевіряє сесію і загортає кожен обробник окремо. **Q:** У чому різниця між `NextResponse` і нативним `Response`? **A:** `NextResponse` розширює нативний `Response` хелперами: `.json()`, маніпуляції з куками, перенаправлення. Для простих відповідей `new Response(body, { status: 200 })` теж підійде. ## Приклади ### Базовий CRUD ендпоінт ```tsx // app/api/posts/route.ts import { NextResponse } from 'next/server' import { db } from '@/lib/db' export async function GET() { const posts = await db.post.findMany({ orderBy: { createdAt: 'desc' } }) return NextResponse.json(posts) } export async function POST(request: Request) { const { title, content } = await request.json() if (!title) { return NextResponse.json({ error: 'Заголовок обов\'язковий' }, { status: 400 }) } const post = await db.post.create({ data: { title, content } }) return NextResponse.json(post, { status: 201 }) } ``` GET повертає пости відсортовані за датою. POST перевіряє вхідні дані перед записом у базу і повертає 201 при успіху. ### Обробник вебхука з перевіркою підпису ```tsx // app/api/webhooks/stripe/route.ts import { headers } from 'next/headers' import Stripe from 'stripe' const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!) export async function POST(request: Request) { const body = await request.text() // сирий рядок, не JSON const signature = headers().get('stripe-signature')! let event: Stripe.Event try { event = stripe.webhooks.constructEvent( body, signature, process.env.STRIPE_WEBHOOK_SECRET! ) } catch { return new Response('Невалідний підпис', { status: 400 }) } if (event.type === 'checkout.session.completed') { // обробка успішного платежу } return new Response(null, { status: 200 }) } ``` Stripe надсилає сире тіло, тому читати потрібно через `request.text()`. Якщо спробуєш `request.json()`, перевірка підпису буде провалюватись щоразу. ### Захищений ендпоінт із перевіркою сесії ```tsx // app/api/profile/route.ts import { cookies } from 'next/headers' import { verifySession } from '@/lib/auth' import { NextResponse } from 'next/server' import { db } from '@/lib/db' export async function GET() { const token = cookies().get('session')?.value if (!token) { return NextResponse.json({ error: 'Не авторизовано' }, { status: 401 }) } const session = await verifySession(token) if (!session) { return NextResponse.json({ error: 'Невалідна сесія' }, { status: 401 }) } const user = await db.user.findUnique({ where: { id: session.userId } }) return NextResponse.json(user) } ``` Читаємо куку, перевіряємо, потім отримуємо користувача. Два рівні перевірки перед тим як дані покинуть сервер.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.