Пошук уроків, статей та іншого контенту
Розберете структуру Request і Response та обробку параметрів, заголовків і статусів у серверних обробниках.
У Next.js серверний обробник маршруту — це функція, яка отримує об’єкт Request і повертає об’єкт Response.
У App Router обробники розміщують у файлі route.ts або route.js:
app/
└── api/
└── users/
└── route.tsНазва експортованої функції відповідає HTTP-методу:
GET — отримання даних;
POST — створення даних;
PUT — повне оновлення;
PATCH — часткове оновлення;
DELETE — видалення.
Мінімальний обробник має такий вигляд:
export async function GET(request: Request) {
return new Response("Hello, Next.js");
}Request містить дані вхідного HTTP-запиту:
URL;
метод;
заголовки;
тіло запиту;
cookies та іншу службову інформацію.
Response описує відповідь сервера:
тіло відповіді;
статус;
заголовки;
тип вмісту.
Об’єкт Request має властивість url. Для роботи з нею зручно створити об’єкт URL:
export async function GET(request: Request) {
const url = new URL(request.url);
const search = url.searchParams.get("search");
const page = url.searchParams.get("page") ?? "1";
return Response.json({
search,
page: Number(page),
});
}Для запиту:
/api/users?search=anna&page=2результат буде таким:
{
"search": "anna",
"page": 2
}Метод get() повертає рядок або null, якщо параметр відсутній.
Для параметрів, які можуть повторюватися, використовуйте getAll():
const tags = url.searchParams.getAll("tag");Запит:
/api/articles?tag=nextjs&tag=typescriptдасть:
["nextjs", "typescript"]Не варто використовувати request.url для ручового розбору рядка. Об’єкт URL коректно обробляє кодування символів, відсутні параметри та повторювані значення.
Метод доступний через request.method:
export async function POST(request: Request) {
if (request.method !== "POST") {
return Response.json(
{ error: "Метод не підтримується" },
{ status: 405 }
);
}
return Response.json({ message: "POST-запит оброблено" });
}Зазвичай додатково перевіряти метод не потрібно, оскільки Next.js викликає окрему функцію для кожного експортованого методу. Наприклад, POST-запит обробляє функція POST.
Власна перевірка може бути корисною, якщо метод впливає на внутрішню логіку або обробник передається в іншу функцію.
Заголовки доступні через властивість request.headers, яка має тип Headers.
Для отримання конкретного заголовка використовуйте get():
export async function GET(request: Request) {
const authorization = request.headers.get("authorization");
const contentType = request.headers.get("content-type");
return Response.json({
authorization,
contentType,
});
}Назви заголовків нечутливі до регістру:
request.headers.get("Authorization");
request.headers.get("authorization");Ці виклики повернуть те саме значення.
Щоб перевірити наявність заголовка, використовуйте has():
if (!request.headers.has("authorization")) {
return Response.json(
{ error: "Потрібна авторизація" },
{ status: 401 }
);
}Значення заголовка може бути відсутнім, тому get() може повернути null. Це потрібно враховувати під час перевірок.
Тіло запиту читається асинхронно. Для JSON використовуйте request.json():
export async function POST(request: Request) {
const body = await request.json();
return Response.json({
received: body,
});
}Якщо клієнт надіслав:
{
"name": "Anna",
"email": "anna@example.com"
}з обробника можна отримати:
body.name;
body.email;Метод читання тіла залежить від формату даних:
request.json() — JSON;
request.text() — звичайний текст;
request.formData() — дані HTML-форми;
request.arrayBuffer() — бінарні дані.
Тіло запиту не можна прочитати кілька разів одним і тим самим способом. Наприклад, після await request.json() повторний виклик request.json() спричинить помилку, оскільки потік тіла вже прочитано.
export async function GET() {
return new Response("Сервер працює");
}Для JSON зручно використовувати Response.json():
export async function GET() {
return Response.json({
message: "Сервер працює",
});
}Response.json() автоматично створює JSON-відповідь і встановлює заголовок Content-Type: application/json.
Статус передається другим аргументом у Response або Response.json():
export async function GET() {
return Response.json(
{ message: "Ресурс створено" },
{ status: 201 }
);
}Найпоширеніші статуси:
200 OK — запит успішно виконано;
201 Created — ресурс створено;
204 No Content — успішна відповідь без тіла;
400 Bad Request — некоректні дані запиту;
401 Unauthorized — відсутня або некоректна автентифікація;
403 Forbidden — доступ заборонено;
404 Not Found — ресурс не знайдено;
405 Method Not Allowed — метод не підтримується;
500 Internal Server Error — внутрішня помилка сервера.
Статус потрібно використовувати відповідно до результату операції. Наприклад, помилку валідації не слід повертати зі статусом 200.
Заголовки відповіді можна передати в об’єкті headers:
export async function GET() {
return Response.json(
{ message: "OK" },
{
status: 200,
headers: {
"Cache-Control": "no-store",
"X-Request-Source": "api",
},
}
);
}Також можна створити об’єкт Headers окремо:
export async function GET() {
const headers = new Headers();
headers.set("Content-Type", "application/json");
headers.set("X-Request-Source", "api");
return new Response(
JSON.stringify({ message: "OK" }),
{
status: 200,
headers,
}
);
}Для JSON зазвичай простіше використовувати Response.json(), оскільки він сам встановлює правильний тип вмісту.
Query-параметри передаються після ?, а параметри маршруту є частиною структури URL.
Для маршруту:
app/api/users/[id]/route.tsзапит:
/api/users/42містить параметр id зі значенням "42".
У сучасних версіях Next.js параметри контексту route handler потрібно отримати асинхронно:
type RouteContext = {
params: Promise<{
id: string;
}>;
};
export async function GET(
request: Request,
context: RouteContext
) {
const { id } = await context.params;
return Response.json({
userId: id,
});
}Параметри маршруту завжди надходять як рядки. Якщо значення має бути числом, його потрібно явно перетворити та перевірити:
const userId = Number(id);
if (!Number.isInteger(userId) || userId <= 0) {
return Response.json(
{ error: "Некоректний ідентифікатор користувача" },
{ status: 400 }
);
}Нижче наведено обробник для маршруту app/api/users/[id]/route.ts. Він:
отримує параметр id із URL;
читає query-параметр details;
перевіряє заголовок authorization;
читає JSON із тіла PATCH-запиту;
повертає різні статуси для успішних і помилкових сценаріїв.
type RouteContext = {
params: Promise<{
id: string;
}>;
};
type UpdateUserBody = {
name?: string;
email?: string;
};
export async function GET(
request: Request,
context: RouteContext
) {
const { id } = await context.params;
const userId = Number(id);
if (!Number.isInteger(userId) || userId <= 0) {
return Response.json(
{ error: "Некоректний ідентифікатор користувача" },
{ status: 400 }
);
}
const url = new URL(request.url);
const details = url.searchParams.get("details") === "true";
const user = {
id: userId,
name: "Anna",
email: "anna@example.com",
};
if (!details) {
return Response.json({
id: user.id,
name: user.name,
});
}
return Response.json(user);
}
export async function PATCH(
request: Request,
context: RouteContext
) {
const { id } = await context.params;
const userId = Number(id);
if (!Number.isInteger(userId) || userId <= 0) {
return Response.json(
{ error: "Некоректний ідентифікатор користувача" },
{ status: 400 }
);
}
const authorization = request.headers.get("authorization");
if (!authorization) {
return Response.json(
{ error: "Заголовок Authorization є обов'язковим" },
{ status: 401 }
);
}
let body: UpdateUserBody;
try {
body = await request.json();
} catch {
return Response.json(
{ error: "Тіло запиту має бути коректним JSON" },
{ status: 400 }
);
}
if (body.name !== undefined && body.name.trim() === "") {
return Response.json(
{ error: "Ім'я не може бути порожнім" },
{ status: 400 }
);
}
return Response.json(
{
id: userId,
updated: body,
},
{
status: 200,
headers: {
"Cache-Control": "no-store",
},
}
);
}Приклад запиту до GET:
/api/users/42?details=trueПриклад запиту до PATCH:
PATCH /api/users/42
Authorization: Bearer token
Content-Type: application/json
{
"name": "Anna Kovalenko"
}У реальному застосунку після валідації тут виконувалося б оновлення в базі даних. Структура Request і Response при цьому залишалася б такою самою.
Дані з URL, заголовків і тіла запиту надходять від клієнта, тому їм не можна безумовно довіряти.
Мінімальна валідація зазвичай містить:
перевірку наявності обов’язкових значень;
перевірку типу;
перевірку допустимого діапазону;
перевірку формату;
повернення зрозумілого статусу та повідомлення про помилку.
Наприклад:
export async function POST(request: Request) {
let body: unknown;
try {
body = await request.json();
} catch {
return Response.json(
{ error: "Некоректний JSON" },
{ status: 400 }
);
}
if (
typeof body !== "object" ||
body === null ||
!("name" in body) ||
typeof body.name !== "string"
) {
return Response.json(
{ error: "Поле name має бути рядком" },
{ status: 400 }
);
}
return Response.json(
{
name: body.name.trim(),
},
{ status: 201 }
);
}Тип unknown безпечніший за any: перед використанням значення потрібно перевірити його структуру.
Якщо ресурс не знайдено, потрібно повернути 404:
export async function GET(
request: Request,
context: {
params: Promise<{ id: string }>;
}
) {
const { id } = await context.params;
const user = null;
if (!user) {
return Response.json(
{ error: `Користувача ${id} не знайдено` },
{ status: 404 }
);
}
return Response.json(user);
}Не слід повертати 200 з тілом на кшталт { error: "Не знайдено" }, якщо операція фактично завершилася помилкою пошуку. HTTP-статус дозволяє клієнту правильно визначити результат запиту без аналізу тексту повідомлення.
Помилки під час читання JSON або роботи із зовнішніми ресурсами потрібно обробляти явно.
export async function GET() {
try {
const data = await loadData();
return Response.json(data);
} catch {
return Response.json(
{ error: "Не вдалося отримати дані" },
{ status: 500 }
);
}
}
async function loadData() {
return {
items: [],
};
}Клієнту не варто повертати внутрішні деталі винятку, наприклад текст помилки бази даних або стек викликів. Такі дані можуть містити службову інформацію.
await під час читання тілаНеправильно:
const body = request.json();request.json() повертає Promise, а не готовий об’єкт.
Правильно:
const body = await request.json();Неправильно:
const page = url.searchParams.get("page");
const nextPage = page + 1;Якщо page дорівнює "2", результатом буде "21", оскільки це рядок.
Правильно:
const page = Number(url.searchParams.get("page") ?? "1");
const nextPage = page + 1;Після перетворення все одно потрібно перевірити, що отримане число коректне.
200 для помилкиНеправильно:
return Response.json({
error: "Користувача не знайдено",
});Така відповідь має статус 200.
Правильно:
return Response.json(
{ error: "Користувача не знайдено" },
{ status: 404 }
);Якщо клієнт надіслав не JSON або пошкоджений JSON, request.json() завершиться помилкою. Для публічних обробників цей виклик бажано виконувати в try...catch.
У URL:
/api/users/42?details=true42 — динамічний параметр маршруту id;
details=true — query-параметр.
Вони отримуються по-різному:
const { id } = await context.params;
const url = new URL(request.url);
const details = url.searchParams.get("details");Параметр id має тип string, навіть якщо в URL він виглядає як число:
const { id } = await context.params;Для арифметики або перевірки числового ідентифікатора використовуйте Number(id) і перевіряйте результат.
Request містить URL, метод, заголовки та тіло вхідного запиту.
Query-параметри отримують через new URL(request.url).searchParams.
Заголовки читають за допомогою request.headers.get().
JSON-тіло читають асинхронно через await request.json().
Параметри динамічного маршруту надходять через context.params.
У сучасних версіях Next.js context.params у route handler потрібно очікувати через await.
Response.json() зручно використовувати для JSON-відповідей.
Статус передають у другому аргументі відповіді.
Помилки валідації, відсутність авторизації та відсутні ресурси мають отримувати відповідні HTTP-статуси.
Дані з Request завжди потрібно перевіряти перед використанням.