Skip to main content

API маршрути (обробники маршрутів) у Next.js

Обробники маршрутів (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 HandlersServer 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) }

Читаємо куку, перевіряємо, потім отримуємо користувача. Два рівні перевірки перед тим як дані покинуть сервер.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?