Артыкул апублікаваны на англійскай мове.
Пераклад Express API-ў у працоўнікі маршрутаў Next.js App Router
Дазвольце даклэ научыцца, як перакласті маршруты Express, сервіс-медіумы і шаблоны дадзейнаў у Next.js App Router з викорыстаннем серверных компанентоў, а таксама рассмотрыць аспекты развяртання.
Калі майграцыя на Next.js дзейсна мае сэнс (і калі — няма)
Next.js App Router стае розумным выборам, калі ваш проект адпавядае прынеймна двум з наступных умов:
- Швальна пачатковая загрузка або видавамасціваеся ў пошуковыя системы є критычна важным, і вы зараз выкарыстоўваете одностранічны прыемлер, які запрашвае даны з бэкенду Express пасля загрузкі сторанцы.
- Ваша практыка развяртання вже нацэлена на Vercel або падобныя платформы або вы гатовы вжываць такую інфраструктуру — функцыі App Router, якія работають на краю сеті і без сервера, выкарыстоўваюць саме такую среду развяртання.
- Ваш прыемлер Express пераважна аддае сторанцы і обрабоўвае стандартныя операцыі CRUD, а не керуе стойкімі з’єднаннямі, фонавымі задачамі або процесамі з вялікай вычысловой навантажэнням.
Міграцыя не ўзначальна, калі:
- Ваш бэкенд выкаанае значныя задачы, якія выходзяць за межы адрасавання сторонніх сторунак — очэкванні на паведамлення, запланаваныя задачы, гэндпоінты gRPC або стойкія з’ўязкі сокетаў. Кантролеры маршрутаў у Next.js не можу заменіць спецыяльную бэкенд-сервіс; верагодна, вы разместіце Next.js як фронтэнд-шар, залишаючы пазаду окремы сервіс на базе Express або Node.
- Вы сильна залежыце ад створанай экасистемы Express middleware — спецыялізаваных прадаўчыкаў аутэнтыфікацыі, бібліятэк для лімітавання частоты запытаў або інтеграцый для стварэння можлівасцяў адзору — якія заставілі бы вас зноў рэалізаваць усё з нуля за мінімальную практычную корысть.
- Ваша прыкладна програма складаецца галоўнэ чынам з аутэнтыфікованых, інтерактыўных інтерфейсаў панелі керування з мяркавымі выкарыстаннем сэрч-энджынаў — галоўныя прыямлівасці 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› з „next/server“;
import ‹1› з „@/lib/db“;
import ‹2› з „@/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 { useState } 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" якомога нижэй у дрэве компанентав, як толькі можаце. Залічвайте вашы сторункі та лейауты як серверныя компаненты; прыменяйце гэтыя нарадзіны толькі да листоўых компанентаў, якім практычна трэба інтэрактыўнасць. Якщо вы з прыzwычкі дадзеце "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 functionfunction 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 [0] = await jwtVerify(token, secret); // перадаць паверыянны ідэнтыфікатор пользователя да наступных слоёў чераз заголовак запыту
const headers = new Headers(req.headers);
headers.set("x-user-id", String(payload.sub));
return NextResponse.next({ request: [1] });
} 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 для змян, якія запускаюцца за дапамою вашых сабе форм і UI.
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 { createOrder } from "./actions";
export function NewOrderForm() {
return (
<form action={createOrder}>
<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. Яны не прызначаны для зовнішніх корыстувачаў або гарантый адпаведнасці да пакульнай версіі; спраўляйцеся з ямі як з внутранімі далёкімі вызывамі процедураў.
Практычныя тэсты: якщо вы хочаце задокументаваць канец-тупын для каго-небудзь за межамі вашай команды, зробіце яго Route Handler. Якщо ён існуе толькі для падтрымкі кнопкі або формы ў вашам UI, тады Server Action будзе простейшым рашэнням.
Разлікі ў развертванні (Vercel проты ECS/EB)
Команды, якія звыклі развертваць Express на ECS або Elastic Beanstalk, сталкнуцца з фундаментальнымі архітектурнымі змянамі пад час викорыстання Next.js на Vercel. Аплікацыя Express працуе як процес Node з довгачэйшым часам выкарыстоўвання: адзін процес обрабоўвае многа запытанняў, падтрымвае актыўныя з’язкі з базай дадзеных і апрацоўвае запытання з прыемлемымі характерыстыкамі памяці та запуску.
Next.js на Vercel разгружае сторанкі і працоўнікі маршрутаў як адзінаковыя безсерверныя чы роут-функціі. Кожная функцыя запускаецца незалежна, працуе пад савоймі лімітамі выконання і не аднаходзіцца ў спільнай базе падключэнняў да базы дадзеных, на вядомае ад процесу Express. Якщо вы створыце кліента Prisma так сама, як і ў Express, вы перапыніце ліміт падключэнняў да базы дадзеных пад навантажэнням, таму што кожны вызов функцыі можа стварыць савое падключэнне.
// lib/db.ts — неабяжлівы шаблон для serverless 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 runtime.next build && next start) — next dev маскіруе певныя бяды, якія прыходзяць да віда толькі ў режыме продакшну, напрыклад, наявнасць адхыленняў у межах сервера/кліента.Пашырэныя памылкі, якія з’яўляюцца падчас міграцыі:
- Імпорт коду, прызначанага толькі для сервера, у компонент кліента. Калі компонент з атрыбутам
"use client"імпортуе ўсё, што мае доступ даfs, кліента базы дадзеных або секрэтных даных, процес будавання альбо не выйдзе, альбо — што ўжо больш небяпечна — секрэты будуць уключаны ў JavaScript-код кліента. Установіце пакетserver-only, южы выклікаць бяду падчас будавання, а не таямна выліваць крантыльярныя даны.
revalidatePath або revalidateTag пасля дзеяння сервера. Без явной перапрацавкі інтэрфейс паказвае застарелыя даны пасля змены, таму што компаненты сервера можаць быць зберагнутыя ў кэшы."use client" для кожнага компанента. Звычкі, перанесеныя з аплікацый з адной сторонкай, ёсць галоўной прычынай таго, чаму аплікацыя Next.js не можа праўершыць первасную.Заключэнне
Якщо вы все ўсё ж не впевнены, пачніце з простага эксперымента: выберыце адну сторанку, якая активна вікорыстоўваецца і мае важлівую ролю для SEO, у вашай чынной аплякацыі, перабудуйце яе як Server Component, який запытае вашу базу дадзеных або безпасэчна вызвае існуючы Express API, і размістіце яго на маршруце, які не обрабоўваецца вашым Express серверам. Параўняйце час до першага байта і размер пакета прычыну перанаслення іншых сторанак. Пасля паўтарной перапрацоўкі гэтага падходу перанесіце процедуры аутэнтыкацыі ў middleware.ts, а пасля таго поступова, адна за адной, пераканвертуйце самыя популярныя процесы змены дадзеных у Server Actions — поступовая міграцыя є безпечней і практычней, чым цэлыснае перапісванне. Команды, якія сталкаюцца з проблемамі, — гэта тые, якія пераносяць усю базу кода раней, чым з’ясаваюць, дзе Server Components аднаўлівають працу і дзе яны проста дагадваюць новы концэптуальны шар на вялікай, вучоўцы працюючай системе.
Спадневаная літэратура
- Стварэнне інтерфейса пошуку ў React з чыстым аднадзельным кераванням станам — Раздзеляйце запуск падзей, вырахаваныя значэння, трымкі запитоў і адображэнне рэзультатаў, каб правильны адказ падаўся на правы запит.
- Дыбаггаванне RAG пашукам першай стадіі, якая збілася — Раздзеляйце прычыны збоев у вучэнні дадзенняў, ўзяцце іх, гэнераванні рэзультатаў і ўпрацоўкі, каб кожны эксперымент спрямаваны быў на рашэння чытка адзначанага проблемы.
- Ролі «моста» TypeScript 6 у парадку пераходу да натыўнага кампайляра TypeScript 7 — Дазнаецеся, як TypeScript 6 адчынівае змяненняя значэнняў стандартных налашчэнняў, способу разв’язвання модуляў і синтаксіса імпорта, каб падготавіць кодавы базы для быстрэйшага кампайляра TypeScript 7, створанага на мове Go.