Пошук уроків, статей та іншого контенту
Порівняєте Server Actions і API Routes та визначите відповідний підхід для різних сценаріїв інтеграції.
У Next.js є два основні способи виконати серверну операцію:
Server Actions — серверні функції, які викликаються безпосередньо з React-компонентів або форм.
API Routes — HTTP-ендпоїнти, доступні через URL і HTTP-методи.
У сучасному App Router під API Routes зазвичай мають на увазі Route Handlers:
app/api/tasks/route.tsУ Pages Router використовують класичний формат:
pages/api/tasks.tsServer Actions і Route Handlers можуть виконувати схожу бізнес-логіку, але вони мають різні контракти та призначення.
Server Action — це асинхронна функція, яка виконується на сервері та позначена директивою "use server".
Окремий файл із Server Actions:
// app/tasks/actions.ts
"use server";
import { revalidatePath } from "next/cache";
type ActionState = {
error?: string;
success?: boolean;
};
export async function createTask(
_previousState: ActionState,
formData: FormData
): Promise<ActionState> {
const title = formData.get("title");
if (typeof title !== "string" || title.trim().length < 3) {
return {
error: "Назва завдання має містити щонайменше 3 символи",
};
}
const normalizedTitle = title.trim();
// Тут зазвичай викликають ORM або клієнт бази даних.
console.log("Створення завдання:", normalizedTitle);
revalidatePath("/tasks");
return {
success: true,
};
}Server Action можна передати як action для HTML-форми. Для відображення результату зручно використовувати useActionState:
// app/tasks/TaskForm.tsx
"use client";
import { useActionState } from "react";
import { createTask } from "./actions";
const initialState = {};
export function TaskForm() {
const [state, formAction, isPending] = useActionState(
createTask,
initialState
);
return (
<form action={formAction}>
<label htmlFor="title">Назва завдання</label>
<input
id="title"
name="title"
type="text"
minLength={3}
required
/>
<button type="submit" disabled={isPending}>
{isPending ? "Збереження..." : "Створити"}
</button>
{state.error && <p role="alert">{state.error}</p>}
{state.success && <p>Завдання створено</p>}
</form>
);
}Server Action:
виконується лише на сервері;
може напряму звертатися до бази даних, файлової системи або секретів;
може викликатися через <form action={...}>;
може повертати серіалізоване значення;
може викликати revalidatePath або revalidateTag;
не створює стабільний публічний HTTP-контракт для сторонніх клієнтів.
Директива "use server" не означає, що функція стає довільно доступним публічним API. Next.js створює внутрішній механізм виклику цієї функції, але його формат не слід використовувати як контракт для мобільного застосунку чи іншої зовнішньої системи.
Route Handler — це HTTP-обробник у файлі route.ts або route.js. Він може реалізовувати окремі HTTP-методи:
// app/api/tasks/route.ts
import { NextResponse } from "next/server";
type Task = {
id: string;
title: string;
};
export async function POST(request: Request) {
const body: unknown = await request.json();
if (
typeof body !== "object" ||
body === null ||
!("title" in body) ||
typeof body.title !== "string"
) {
return NextResponse.json(
{ error: "Поле title є обов'язковим" },
{ status: 400 }
);
}
const title = body.title.trim();
if (title.length < 3) {
return NextResponse.json(
{ error: "Назва завдання має містити щонайменше 3 символи" },
{ status: 422 }
);
}
const task: Task = {
id: crypto.randomUUID(),
title,
};
return NextResponse.json(task, { status: 201 });
}
export async function GET() {
return NextResponse.json({
tasks: [],
});
}Такий обробник доступний за адресою:
POST /api/tasks
GET /api/tasksЙого можна викликати з браузера, мобільного застосунку, іншого сервера або зовнішньої інтеграції:
const response = await fetch("/api/tasks", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Перевірити звіт",
}),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error);
}Route Handler має звичайний HTTP-контракт:
URL;
HTTP-метод;
заголовки;
cookies;
тіло запиту;
статус відповіді;
формат відповіді.
Тому він краще підходить для REST-подібних API, webhook-ів і взаємодії з клієнтами, які не є частиною React-інтерфейсу Next.js.
Server Action викликають як функцію або передають формі:
<form action={createTask}>
{/* поля форми */}
</form>Route Handler викликають через HTTP:
await fetch("/api/tasks", {
method: "POST",
});Server Action приховує мережевий шар від компонента. Route Handler, навпаки, робить HTTP-контракт явним.
Server Actions добре інтегровані з HTML-формами та FormData. Це зручно для операцій, які безпосередньо випливають із взаємодії користувача з інтерфейсом:
створення запису;
редагування профілю;
видалення елемента;
зміна налаштування;
надсилання форми.
Route Handler може обробляти будь-яку структуру запиту:
JSON;
FormData;
текст;
бінарні дані;
спеціальні заголовки.
Server Actions призначені передусім для коду всередині Next.js-застосунку.
Route Handler підходить для:
браузерного клієнта;
мобільного застосунку;
CLI;
іншого бекенда;
стороннього сервісу;
webhook-відправника.
У Route Handler можна явно повернути потрібний статус:
return NextResponse.json(
{ error: "Недостатньо прав" },
{ status: 403 }
);Server Action зазвичай повертає дані стану або виконує перенаправлення. Вона не є зручним інструментом для моделювання повного набору HTTP-відповідей для зовнішніх клієнтів.
Server Action добре поєднується з інвалідацією кешу після мутації:
revalidatePath("/tasks");Це дозволяє виконати зміну та повідомити Next.js, які дані потрібно повторно отримати.
У Route Handler кешування та інвалідація також доступні, але їх потрібно організувати явно в межах архітектури застосунку. Сам факт виклику HTTP-ендпоїнта не визначає, які сторінки або теги потрібно оновити.
операція викликається з вашого Next.js UI;
це мутація, пов’язана з формою або кнопкою;
не потрібен публічний API-контракт;
важливі простий виклик і типізація на межі компонента;
після операції потрібно інвалідувати кеш Next.js;
серверна логіка має залишатися внутрішньою для застосунку.
Приклад: користувач редагує назву завдання у внутрішній панелі.
endpoint має викликатися не лише Next.js-клієнтом;
потрібно підтримати мобільний застосунок або сторонній сервіс;
потрібні різні HTTP-методи та статус-коди;
реалізується webhook;
необхідні специфічні заголовки або формат відповіді;
API має бути окремим стабільним контрактом;
клієнти повинні використовувати fetch, curl або HTTP SDK.
Приклад: платіжний сервіс надсилає webhook після успішної оплати.
Не обов’язково дублювати бізнес-логіку в Server Action і Route Handler. Краще винести спільну операцію в серверний сервіс, а поверх нього створити різні адаптери.
// lib/tasks.ts
export type CreateTaskInput = {
title: string;
};
export async function saveTask(input: CreateTaskInput) {
// Тут може бути виклик бази даних.
return {
id: crypto.randomUUID(),
title: input.title,
};
}Server Action може перетворити FormData на вхідні дані сервісу:
// app/tasks/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { saveTask } from "@/lib/tasks";
export async function createTask(formData: FormData) {
const title = formData.get("title");
if (typeof title !== "string" || title.trim().length < 3) {
return { error: "Некоректна назва" };
}
await saveTask({ title: title.trim() });
revalidatePath("/tasks");
return { success: true };
}Route Handler може перетворити JSON на ті самі вхідні дані:
// app/api/tasks/route.ts
import { NextResponse } from "next/server";
import { saveTask } from "@/lib/tasks";
export async function POST(request: Request) {
const body: unknown = await request.json();
if (
typeof body !== "object" ||
body === null ||
!("title" in body) ||
typeof body.title !== "string" ||
body.title.trim().length < 3
) {
return NextResponse.json(
{ error: "Некоректна назва" },
{ status: 422 }
);
}
const task = await saveTask({
title: body.title.trim(),
});
return NextResponse.json(task, { status: 201 });
}У такій структурі:
адаптер відповідає за формат входу та виходу;
сервіс відповідає за бізнес-операцію;
Server Action і Route Handler не розходяться в поведінці;
бізнес-логіку не потрібно копіювати.
Server Action не звільняє від перевірки прав доступу. Будь-яку дію потрібно захищати на сервері:
визначити поточного користувача;
перевірити його роль або власність ресурсу;
перевірити вхідні дані;
лише після цього змінювати стан.
Не слід покладатися на:
приховані поля форми;
перевірки лише в Client Component;
кнопку, яка прихована для певної ролі;
значення, передане клієнтом як userId або role.
Те саме стосується Route Handler: URL може бути відомий будь-кому, тому endpoint повинен самостійно перевіряти автентифікацію та авторизацію.
Server Action не варто використовувати як контракт для мобільного клієнта або зовнішньої команди. Для цього створіть Route Handler із явними правилами щодо URL, методів, статусів і формату даних.
Перевірка доступу в інтерфейсі — лише частина UX. Кожна Server Action і кожен Route Handler повинні повторно перевіряти права на сервері.
Якщо одна й та сама операція доступна через форму та API, не копіюйте її реалізацію. Винесіть спільну логіку в серверний модуль і використовуйте його в обох адаптерах.
Клієнт має отримувати стабільну структуру результату. Для Server Action це може бути стан:
{ error: "Некоректні дані" }Для Route Handler — JSON разом із відповідним HTTP-статусом:
{
"error": "Некоректні дані"
}Не повертайте клієнту stack trace, SQL-помилки або секрети конфігурації.
Webhook-відправник очікує URL і HTTP-відповідь. Server Action не є зручним зовнішнім webhook endpoint. Для такого сценарію використовуйте Route Handler.
Server Actions — внутрішні серверні операції, тісно пов’язані з React-компонентами та формами.
Route Handlers — HTTP-ендпоїнти з явним контрактом для різних клієнтів і інтеграцій.
Server Actions зручні для мутацій у власному Next.js UI.
Route Handlers краще підходять для публічних API, мобільних клієнтів і webhook-ів.
Обидва підходи повинні виконувати серверну валідацію та перевірку доступу.
Спільну бізнес-логіку варто винести в окремий серверний сервіс, а Server Action і Route Handler використовувати як різні адаптери.