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
Швидкий приклад
// 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 передає цей сегмент другим аргументом обробника:
// 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 абстракцій:
// 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:
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 розглядає як статичний і кешує під час білду. Щойно ти читаєш щось із запиту, маршрут стає динамічним.
// Кешується - обчислюється під час білду
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)
// запускається при кожному зверненні
}Явне управління кешуванням через конфіг сегменту:
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:
// Не працює в 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. Відсутня обробка помилок
// Погано - необроблена помилка бази стає 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 ендпоінт
// 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 при успіху.
Обробник вебхука з перевіркою підпису
// 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(), перевірка підпису буде провалюватись щоразу.
Захищений ендпоінт із перевіркою сесії
// 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)
}Читаємо куку, перевіряємо, потім отримуємо користувача. Два рівні перевірки перед тим як дані покинуть сервер.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.