Статтю опубліковано англійською мовою.
Міграція API Express у обробники маршрутів Next.js App Router
Дізнайтеся, як перетворити маршрути Express, середовище проміжного оброблення та шаблони даних на Next.js App Router із використанням серверних компонентів та аспекти розгортання.
Коли міграція на Next.js справді має сенс (а коли — ні)
App Router в Next.js стає розумним вибором, коли ваш проект відповідає принаймні двом з цих умов:
- Швидкість початкового завантаження чи видимість у пошукових системах є критично важливими, і ви наразі використовуєте односторінковий додаток, який отримує дані від сервера Express після завантаження сторінки.
- Ваша система розгортання вже орієнтована на Vercel чи подібні платформи або ви готові використовувати таку інфраструктуру — функції App Router, пов’язані з обробкою даних на краю мережі та безсерверною архітектурою, передбачають саме таке середовище розгортання.
- Ваш додаток на Express переважно відображає сторінки та обробляє стандартні операції CRUD, замість того щоб керувати постійними з’єднаннями, фоновими завданнями чи ресурсоємними процесами.
Міграція не рекомендується, коли:
- Ваш бекенд виконує значну кількість завдань, що виходять за межі простого відображення сторінок — черги повідомлень, заплановані завдання, кінцеві точки gRPC чи постійні з’єднання через сокети. Обробники маршрутів у Next.js не можуть замінити спеціалізовану службу бекенду; ймовірно, ви будете використовувати Next.js як фронтенд-шар, залишаючи позаду окрему службу Express чи Node.
- Ви сильно покладаєтесь на вже існуючу екосистему middleware Express — спеціалізовані провайдери автентифікації, бібліотеки обмеження кількості запитів чи інтеграції для моніторингу — які вимагали б повної перереалізації заради мінімальної вигоди.
- Ваше додаток складається переважно з автентифікованих, інтерактивних інтерфейсів панелі керування з обмеженими вимогами до пошукових систем — основні переваги App Router (стрімове серверне генерування, оптимізація пошуку, статичне генерування) майже не мають значення в цьому сценарії, тоді як інвестиції у навчання залишаються значними.
Рекомендований підхід: збережіть ваш Express API як авторитетне джерело даних та бізнес-правил, а потім використовуйте Next.js як шар для відображення контенту та обробки запитів з боку фронтенду. Спочатку мігруйте користувацький інтерфейс та маршрути, орієнтовані на читання даних, залишивши без змін внутрішні сервіси, які активно використовуються для запису даних. Цей посібник слідує саме цій стратегії міграції.
Пов’язування маршрутів Express з обробниками маршрутів Next.js
Обробники маршрутів, розташовані у app/api/**/route.ts, є найбільш прямим замінником визначень маршрутів у Express. Концептуальна зміна полягає у тому, що замість об’єктів req та res ви отримуєте об’єкт Request та повертаєте об’єкт Response (або використовуєте NextResponse для додаткової зручності), а кожен метод HTTP стає окремою експортованою функцією замість виклику методу router.get().
Розгляньмо стандартний маршрут Express, який отримує та створює замовлення:
// express: routes/orders.ts
import ⟨0⟩ from "express";
import ⟨1⟩ from "../db";
import ⟨2⟩ from "../middleware/auth";
const router = Router(); router.get("/api/orders", requireAuth, async (req, res) => {
const userId = req.user.id;
const orders = await db.order.findMany({ where: ⟨3⟩ });
res.json(⟪4⟩);
}); router.post("/api/orders", requireAuth, async (req, res) => {
const ⟨5⟩ = req.body;
if (!items?.length) {
return res.status(400).json(⟪6⟩);
}
const order = await db.order.create({
data: ⟨7⟩,
});
res.status(201).json(⟪8⟩);
}); export default router;
Відповідна реалізація обробника маршруту:
// app/api/orders/route.ts
import ‹0› from „next/server“;
import ‹1› from „@/lib/db“;
import ‹2› from „@/lib/auth“;
export async function GET(req: NextRequest) {
const user = await getSessionUser(req);
if (!user) {
return NextResponse.json(‹3›, ‹4›);
} const orders = await db.order.findMany({ where: ‹5› });
return NextResponse.json(‹6›);
}export async function POST(req: NextRequest) {
const user = await getSessionUser(req);
if (!user) {
return NextResponse.json(‹7›, ‹8›);
} const body = await req.json();
if (!body.items?.length) {
return NextResponse.json(‹9›, { status: 400 }); } const order = await db.order.create({
data: { userId: user.id, items: body.items },
});
return NextResponse.json({ order }, { status: 201 });
}
Під час перекладу шаблонів Express у обробники маршрутів варто звернути увагу на кілька деталей:
Динамічні сегменти використовують синтаксис, заснований на папках, замість префіксів у вигляді двокрапки. Маршрут Express у форматі /api/orders/:id стає app/api/orders/[id]/route.ts. Параметр надходить як другий аргумент у функцію-обробник: GET(req, { params }: { params: Promise<{ id: string }> }). У поточних версіях Next.js параметри надсилаються у вигляді обіцянки, тому її необхідно очекувати перед отриманням значення.
Не існує ланцюга проміжних засобів для кожної маршруту. Проміжний засіб requireAuth від Express перетворюється або на спільну функцію-засіб, яка викликається на початку кожного обробника (як показано у вищенаведеному прикладі), або, що краще, на логіку всередині middleware.ts (що розглядається у наступному розділі), щоб окремі обробники маршрутів не знали про проблеми автентифікації.
Розбір тіла запиту вимагає явних викликів — потрібно писати await req.json(), а не покладатися на express.json(). Автоматичний розбір не відбувається, що насправді покращує зрозумілість: ви уникаєте несподіваних обмежень щодо розміру тіла, які накладає глобальний проміжний засіб, налаштований місяцями тому і про який забули.
Обробники маршрутів залишаються звичайними функціями Node чи Edge. Якщо ви перевіряли вхідні дані за допомогою zod у своїх маршрутах Express, цей код перевірки передається без змін.
Компоненти сервера проти існуючих у вас патернів клієнтського рендерингу у React
Цей аспект більше, ніж будь-який інший, здивовує команди. У архітектурі Express плюс React кожен компонент за замовчуванням є клієнтським компонентом: він рендерується у браузері, а коли йому потрібні дані, він викликає ваш API з useEffect або через бібліотеку для отримання даних, таку як React Query.
// Старий підхід: React, який обробляється на клієнті, взаємодіє з Express
function OrderList() {
const [orders, setOrders] = useState<Order[] | null>(null);
useEffect(() => {
fetch("/api/orders", { credentials: "include" })
.then((r) => r.json())
.then((data) => setOrders(data.orders));
}, []); if (!orders) return <Spinner />;
return (
<ul>
{orders.map((o) => (
<li key={o.id}>{o.id} — ${o.total}</li>
))}
</ul>
);
}
App Router змінює цей стандарт. Кожен компонент є серверним компонентом, якщо ви не вказали інше, що означає, що він виконується на сервері, безпосередньо отримує доступ до вашої бази даних чи сервісів та ніколи не надсилає свій JavaScript на клієнт. Для даних, які належать до конкретної сторінки, ви повністю може не використовувати API-шляхи.
і запитайте про це безпосередньо:// app/orders/page.tsx — Компонент сервера, без „use client“
import { db } from "@/lib/db";
import { getSessionUser } from "@/lib/auth";
import { redirect } from "next/navigation";
export default async function OrdersPage() {
const user = await getSessionUser();
if (!user) redirect("/login"); // прямий доступ до БД, без fetch, без стану завантаження, без додаткових витрат від клієнтського пакету
const orders = await db.order.findMany({
where: { userId: user.id }rver надсилає HTML, який вже містить дані. Ви додаєте "use client" лише тоді, коли компонент потребує інтерактивності: стану, ефектів, обробників подій або API, доступних лише в браузері:// app/orders/OrderFilters.tsx
"use client";
import React from "react";
import { useRouter, useSearchParams } from "next/navigation";export function OrderFilters() {
const router = useRouter();
const params = useSearchParams();
const [status, setStatus] = useState(params.get("status") ?? "all"); function apply(next: string) {
setStatus(next);
const url = new URLSearchParams(params);
url.set("status", next);
router.push(`/orders?${url.toString()}`);
} return (
<select value={status} onChange={(e) => apply(e.target.value)}>
<option value="all">Усі</option>
<option value="pending">У процесі</option>
<option value="shipped">Відправлено</option>
</select>
);
}
/code> Практична рекомендація для команд: використовуйте "use client" якомога нижче в дереві компонентів. Зберігайте ваші сторінки та макети у вигляді серверних компонентів; застосовуйте цю директиву лише до листкових компонентів, яким дійсно потрібна інтерактивність. Якщо ви з привички додаватимете "use client" до кожного компонента (що трапляється під час перенесення односторінкового додатку без переосмислення архітектури), ви не отримаєте жодних переваг від App Router та у кінцевому підсумку матимете гіршу модель розуміння структури, ніж у початковій конфігурації.
Заміна середовища автентифікації Express на середовище Next.js
У Express середовище автентифікації виконується для кожного маршруту всередині процесу Node.js. Next.js надає файл middleware.ts, який перехоплює запити на рівні межі перед тим, як вони досягнуть будь-якої сторінки чи обробника, що виступає архітектурним еквівалентом:
// стара версія: middleware/auth.ts (Express)
import jwt from „jsonwebtoken“;
export function requireAuth(req, res, next) {
const token = req.cookies.session;
if (!token) return res.status(401).json([„0“]); try {
req.user = jwt.verify(token, process.env.JWT_SECRET!);
next();
} catch {
res.status(401).json([„1“]);
}
}
// middleware.ts — знаходиться у корені проекту
import „2“ from „next/server“;
import „3“ from „jose“; // сумісний з Edge, на відміну від jsonwebtoken
const PROTECTED_PREFIXES = [„/dashboard“, „/orders“, „/api/orders“]; export async functionion middleware(req: NextRequest) {
const isProtected = PROTECTED_PREFIXES.some((p) =>
req.nextUrl.pathname.startsWith(p)
);
if (!isProtected) return NextResponse.next(); const token = req.cookies.get("session")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", req.url));
} try {
const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
const { payload } = await jwtVerify(token, secret); // передати перевірений ідентифікатор користувача далі через заголовок запиту
const headers = new Headers(req.headers);
headers.set("x-user-id", String(payload.sub));
return NextResponse.next({ request: { headers } });
} catch {
return NextResponse.redirect(new URL("/login", req.url));
}
}export const config = {
matcher: ["/dashboard/:path*", "/orders/:path*", "/api/orders/:path*"],
};
Два
Проблеми, які постійно заважають командам під час міграції:- За замовчуванням мідлвейр працює на середовищі Edge runtime, а не на Node.js. Будь-яка бібліотека, яка залежить від основних модулів Node.js —
jsonwebtoken, типові клієнти баз даних — буде не працювати або функціонувати некоректно. Використовуйте альтернативи, придатні для роботи в Edge:joseє стандартним вибором для операцій з JWT. Перенесіть усі запити до баз даних у Server Components або Route Handlers, де є повне середовище Node.js; ніколи не намагайтеся виконувати їх у мідлвейрі.
SELECT * FROM sessions WHERE id = ?. Обмежуйте логіку мідлвейру швидкими, безстановими операціями, такими як перевірка підпису токена. Відкладіть питання авторизації — „Чи може цей користувач переглядати це замовлення?“ — на компонент сторінки або сам обробник, де повністю доступні можливості для роботи з базою даних та API Node.js.Патерни отримання даних: серверні компоненти проти вашого поточного підходу з викликами API
Більшість архітектур Express разом із React дотримуються передбачуваної послідовності: компонент завантажується, отримує дані з вашого API, API запитує базу даних, JSON надсилається назад до браузера, компонент оновлюється. Два кроки в мережі — від клієнта до сервера, від сервера до бази даних — передають інформацію, яка вже була у сервера.
За допомогою App Router операції читання у Server Components скорочуються до одного кроку: сервер запитує базу даних та безпосередньо передає клієнту потік HTML із результатом, як показано у коді OrdersPage. Для операцій запису існують два стандартні підходи: Route Handlers, коли потрібен традиційний API (наприклад, публічний REST-інтерфейс), або Server Actions для змін, викликаних власними формами та інтерфейсом користувача.
Server Actions найбільше відрізняються від конвенцій Express. Ви визначаєте функцію, яка виконується на сервері, та запускаєте її безпосередньо з форми, не створюючи жодного явного API-шляху:
// app/orders/actions.ts
"use server";
import { db } from "@/lib/db";
import { getSessionUser } from "@/lib/auth";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";export async function createOrder(formData: FormData) {
const user = await getSessionUser();
if (!user) redirect("/login"); const itemId = formData.get("itemId");
if (typeof itemId !== "string" || !itemId) {
throw new Error("Необхідно вказати itemId");
} await db.order.create({
data: { userId: user.id, items: [{ itemId, qty: 1 }] },
}); // оновити сторінку замовлень із свіжими даними з сервера — не потрібно повторного завантаження на клієнті
revalidatePath("/orders");
}
// app/orders/NewOrderForm.tsx
import ⟨0⟩ from „./actions“;
export function NewOrderForm() {
return (
<form action=⟪1⟩>
<input type="text" name="itemId" placeholder="ID товару" required />
<button type="submit">Створити замовлення</button>
</form>
);
}
Вибір між діями сервера та обробниками маршрутів:
Дія сервера — це внутрішній виклик функції з вашого інтерфейсу користувача до сервера; обробник маршруту — це справжній кінець HTTP-запиту. Це розрізнення має значення, коли ви вирішуєте, яка схема підходить для певної зміни даних.
Використовуйте Route Handler, коли кінцева точка має залишатися стабільною та доступною ззовні цього додатку Next.js — з мобільного клієнта, через інтеграцію з третіми сторонами або публічний API. Route Handler надають вам чіткі URL-шляхи, HTTP-дії та контракт, який ви контролюєте та версіонуєте.
Використовуйте Server Action, коли зміни виникають з ваших власних форм та інтерактивних компонентів. Server Action вимагають менше шаблонного коду та автоматично оновлюють кешовані дані за допомогою revalidatePath або revalidateTag, усуваючи необхідність ручного оновлення кешу після виклику fetch. Вони не призначені для зовнішніх користувачів чи гарантій зворотної сумісності; розглядайте їх як внутрішні віддалені виклики процедур.
Практичний тест: якщо ви хочете задокументувати кінцеву точку для особи поза вашою командою, зробіть її обробником маршрутів. Якщо вона існує лише для підтримки кнопки чи форми у вашому інтерфейсі, тоді серверна дія буде простішою.
Відмінності у розгортанні (Vercel проти ECS/EB)
Команди, звиклі до розгортання Express на ECS чи Elastic Beanstalk, стикаються з фундаментальними архітектурними змінами при використанні Next.js на Vercel. Додаток Express працює як процес Node з довгим терміном існування: один процес обробляє багато запитів, підтримує активні з’єднання з базою даних та характеризується передбачуваними показниками використання пам’яті та часу запуску.
Next.js на Vercel розгортає сторінки та обробники маршрутів як окремі безсерверні або функції на краю мережі. Кожна з цих функцій запускається незалежно, працює в межах власних обмежень щодо виконання та не користується спільним пулом постійних з’єднань до бази даних, на відміну від одного процесу Express. Якщо ви створите клієнт Prisma так само, як це робили в Express, умова обмеження кількості з’єднань до бази даних буде порушена під навантаженням, оскільки кожне викликання функції може створювати власне з’єднання.
// lib/db.ts — необхідний шаблон для серверлесс-Prisma
import { PrismaClient } from "@prisma/client";
const globalForPrisma = global as unknown as { prisma: PrismaClient };// повторне використання клієнта під час багаторазових викликів замість створення нового щоразу
export const db =
globalForPrisma.prisma ??
new PrismaClient({
// використовувати стрінг підключення з пулом (наприклад, PgBouncer / Prisma Accelerate / RDS Proxy)
datasources: { db: { url: process.env.DATABASE_URL_POOLED } },
});if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;
Цей шаблон дозволяє повторно використовувати клієнта під час багаторазових викликів замість створення нової інстанції щоразу при отриманні запиту.
Якщо ви розгортаєте Next.js у ECS або Elastic Beanstalk замість Vercel, фреймворк працює як традиційний сервер Node. Виконайте команду next build, потім next start, або налаштуйте режим автономного виведення, щоб отримати більш легкі об’єкти Docker. Ви зберігаєте переваги довго працюючих процесів, але втрачаєте автоматичну мережу Vercel, поступову генерацію статичних файлів у CDN та можливість розгортання прев’ю без налаштувань. Вам доведеться самостійно налаштовувати ці функції або погодитися з їх відсутністю. Це обґрунтований інженерний вибір, а не компроміс; багато команд використовують Next.js у ECS саме тому, що вже мають власну інфраструктуру та не хочуть розділяти хостинг між двома постачальниками.
Виділіть час на два додаткові завдання міграції: змінні середовища (Next.js вимагає префіксу NEXT_PUBLIC_ для будь-якої змінної, яка є доступною для браузера — перевірте кожну змінну, яку ви передаєте під час збірки React) та налаштування під час збірки та під час виконання (значення, вбудовані у статичний експорт, поводяться інакше, ніж значення, які читаються під час запиту, на відміну від одного процесу Express, де все є під час виконання).
Чек-лист міграції та поширені помилки
Виконуйте ці кроки у послідовності:
- Розгорніть додаток Next.js паралельно з вашим сервером Express — не торкайтеся існуючого додатку під час початкової налаштування.
- Спочатку перетворіть сторінки, які активно використовують читання даних та оптимізовані для пошукових систем — ці сторінки найбільше виграють від використання Server Components та мають мінімальний ризик під час міграції.
middleware.ts за допомогою бібліотеки для перевірки токенів, сумісної з середовищем виконання Edge.next build && next start) — next dev приховує певні помилки, які з’являються лише у режимі продакшну, наприклад, порушення меж між сервером та клієнтом.Поширені помилки під час міграції:
- Імпорт коду, призначеного лише для сервера, у компонент клієнта. Коли компонент з позначкою
"use client"імпортує щось, що має доступ доfs, клієнтського з’єднання до бази даних чи конфіденційних даних, процес збірки або завершується невдачею, або — що ще небезпечніше — конфіденційна інформація потрапляє до JavaScript-коду клієнта. Встановіть пакетserver-only, щоб спричинити помилку під час збірки, замість того щоб таємно витікати облікові дані.
revalidatePath або revalidateTag після виконання дії сервера. Без явної перевірки інтерфейс відображає застарілі дані після змін, оскільки компоненти сервера можуть знаходитися у кеші."use client". Звички, успадковані з односторінкових додатків, є основною причиною того, чому додаток Next.js не досягає кращої продуктивності, ніж оригінал.Підсумок
Якщо ви все ще не впевнені, почніть з обмеженого експерименту: виберіть одну сторінку з інтенсивним читанням, важливу для SEO, з вашого поточного додатку, перебудуйте її як серверний компонент, який запитуватиме вашу базу даних або безпосередньо викликатиме ваш існуючий API Express, та розгорніть її за маршрутом, яким не керує сервер Express. Порівняйте час отримання першого байта та розмір пакету перед міграцією інших сторінок. Після підтвердження ефективності цього підходу перемістіть механізми автентифікації до файлу middleware.ts, а потім поступово перетворюйте найбільш використовувані процеси змін на Server Actions — поступова міграція є безпечнішою та практичнішою, ніж повне переписування. Команди, які стикаються з проблемами, — це ті, що мігрують весь кодовий базис раніше, ніж з’ясують, де саме серверні компоненти справді скорочують обсяг роботи, а де вони лише додають новий концептуальний рівень на вже функціональну систему.
Пов’язана література
- Створення інтерфейсу пошуку в React із чітким розподілом обов’язків щодо стану — Розділяйте події, похідні значення, терміни життя запитів та відображення даних, щоб правильна відповідь надходила на правильний запит.
- Дебагування RAG шляхом знаходження першої стадії, яка зазнала невдачі — Розділяйте проблеми в джерелах даних, процесі їх отримання, генерації та експлуатації, щоб кожен експеримент вирішував чітко визначену проблему.
- Роль мосту TypeScript 6 на шляху до нативного компілятора TS 7 — Дізнайтеся, як TypeScript 6 оновлює стандартні налаштування, механізми розрішення модулів та синтаксис імпорту, щоб підготувати кодові бази до швидшого компілятора TypeScript 7, заснованого на Go.