Раздзелэнне шароў домэна, дадзенняў і UI у кодбазе Next.js App Router
Аналіз прыкладу з Pokédex, який паказывае, як раздзеліць прыемпл Прыемпля Router у Next.js на шары домэна, дадзейнаў і прэzentацыі за дапамою Prisma, Zod, аутантыкацыі через кукі і кэшавання.
Крыніцы: Clean Architecture for Frontend with Next.js (App Router): A Practical Pokédex Case Study
Next.js App Router ператварае тое, што раней было простым одностранічным додаткам на React, у распрацоўвальную среду цэлага стака: серверная рэндарызацыя, обробкі маршрутаў, кэшаванне і прымытны доступ да базы дадзеных — усё гэта знаходзіцца ў аднам і тым жа репазітарыі. Якшто спрыяваць гэтай базе коду як „толькі компонентам“, яна быстра паслабляецца, з’яўляюцься сторанкі, якіе вярбуюцца з базы дадзеных, віджэты, якіе вызываюць API трохіх сторон, і бізнес-правіла, спрывязаныя з умовамі JSX. У гэтым кераванні прыкладам прымаецца Clean Architecture для проекта App Router, як прыклад выступае невялікі додатак Pokédex, показваючы, дзе належыць кожны аспект і якія падступнія Next.js выяўляе гэтая структура.
Прыкладны додатак і яго абмежэння
Pokédex ўмышлена маленькі, але включае ўсе виды залежнасцяў, якія є у рэальным продукце:
- Аддаленыя даны: публічны PokeAPI, для якога не патрэбны ключы API.
/app/api/*.Што выходзіць з-пад контролу без меж
Next.js дазволяе будзь-яму файлу доступаць да будзь-чаго, і самэй так кодавыя базы становяцца заплутанымі. Тыпавыя симптамы:
- Файлы сторанак, які безпасэчна імпортуюць кліента Prisma;
- Компаненты, які самі вызываюць PokeAPI;
- Правілы домэны, выражаныя як спецыяльныя умовні выразы ў інтэрфейсе;
- Логіку, якую нельга працаваць без запуску фрэймворка, таму што всё з’ўязана з яным.
Чыстая архітэктура суперашчотна з гэтым: правілы домэны больш не залежаць ад фрэймворкаў, джерела даных можна заменіць без правак у інтэрфейсе, сценарыі викорыстання можна працаваць без базы дадзеных чы рантайму Next.js, а механізмы зберагчэння чы экраны можна змяніць без перапісву коду.
Тры шары і адна напрамоўка залежнасцяў
Проект разделяецца на тры шары, а залежнасці выраховваныя толькі ў напрамоўку да домэны:
- Домэна (ядро): энтыты, інтерфейсы рэпазітарыў і сценарыі викорыстання. У яй няма імпортаў Next.js, няма Prisma і няма конкрэтных реалізацый.
Корыстны тэст: выдаліце папкі з данымі і прадставленнем, і домэн все рава должен працаваць без памылак.
Зменшэнне размеру папкі App Router
Чыстая ментальная модэль пачынаецца з чыстай структуры дырэктарый. Дырэктарыя /app спрацоўвае як канчатак фрэймворку і мае толькі тое, што Next.js патрэбна для адкрыць:
- маршруты і групы маршрутаў;
layout.tsx;- обрабоўчыкі маршрутаў пад
app/api/**.
Логіка бізнесу не патрабуецца там. У Pokédex-е app/ мае корневую структуру, групу маршрутаў (public) (заходжанне, рэгістрацыя, дакладнай інфармацыя пра Pokémon), групу (app) (спіс і улюбленыя) адносова працэўнікі api/. Усе значныя элементы знаходзяцца пад даменамі libs/domain/**, libs/data/** і libs/presentation/**, а таксама ў папцы prisma/, дзе знаходзяцца схема і міграцыі.
Слой дамена: толькі правіла і контракты
У дамене ёсць тры відэны элементаў:
- Энтытэты, такія як
PokemonіUser. - Інтэрфейсы репазітарыяў, такія як
PokemonRepo,FavoritesRepoіUserRepo.
GetPokemonDetails, ListPokemon, AddFavorite, RemoveFavorite, GetFavorites, RegisterUser і LoginUser.Два правіла дапамагаюць падтрымліваць чыстасць. Першае — нічога ў домэне не можа імпортуваць Next.js, Prisma чыя-небудзь модулі, спецыфічныя для часу выканання. Другае — домэна вяртае ентыціі домэны, ніколі моделі зберагчыка дадзэнняя, таму тип рядка Prisma не можа прасочыцца ў інтэрфейс праз сцэну выкарыстання.
Сцэны выкарыстання залежаць толькі ад інтэрфейсаў репазітарыю, а не ад іх рэалізацый. AddFavorite прымеўае FavoritesRepo через свой канстрактар чы робатню і не можа адразнаць Postgres ад тэстовага двойніка ў памяці. Гэтае перыўстанне являеся сутнёю гэтага патэрану; правіло пераканання аб обмежаных імпортах дапамагае яго рэалізавацыі.
Слой дадзэння: дзе знаходзіцца інфраструктура
Усе, што стосуецца зовнішньага свету, скарыстоўваецца ў шары дадзенаў:
- кліент Prisma і яго схема;
- рэалізацыі рэпазітарыяў, пазначаныя суфіксам
Impl; - удаленыя службы, якія абгружаюць PokeAPI;
- локальныя службы для базы дадзенаў (улюбленыя элементы і кэш);
- DTO-ы і маперы, якія перакладаюць данні з форматаў канектавання ў аб’екты;
- палітіка кэшавання з трымкам TTL у семь дзён.
Маленькі контэйнер для впрыску залежнасцяў у libs/data/di соедынае рэалізацыі з інтарфейсамі, тады рэшта прыкладу запрашае у контэйнера готовы варіант выкарыстоўвання замест таго, каб сам ствараць рэпазітарыяў.
Спалучэнне удаленага і локальнага джерела
Данные з далёкага источніка прыходзяць з PokeAPI через далёкую службу; локальная частка складаецца з двух табліцаў: Favorites, якая прыязначана атрыбутаваным пользователям, і PokemonCache, якая зберагае первасныя даны разам з пазначкамі часу.
PokemonRepoImpl выбірае, які істочнік належыць выкарыстаць, таму ні способы викорыстання, ні інтэрфейс не ведаюць пра наявнасць кэшавання.
Палітыка кэшавання на семь дзён
Даныя пра Pokémon кэшуюцца локальна, ў результате чаго сторанкі з деталямі завантажваюцца быстрей, а не патрабуецца паўтарна ўзаемадзейсцоўваць з безкоштовным публічным API. Алгорытм прыняцтва рашэння прост: шукаецца кэшаваны запис; якщо ён існуе і пазначка часу на яго старэйшая за семь дзён, ён перадаецца і вяртаецца; інакш падымаюцься даны з PokeAPI, пераверачаецца ўхідны пакет, яго дадаюць у PokemonCache з новай пазначкай часу і вяртаецца пераданы об’ект.
Зробіце перагляд тэрміну давальнасці даных маленькай чыстой функцыяй (напрыклад, у libs/data/utils/cache.ts) так, каб яе было лёгка ў тэставанні. Якщо PokeAPI не працюе, калі даныя сталаўшыся недзейнасцю, рассмотріце можлівасць выдачы старой копіі заместо таго, каб працэс завернуўся з адмовай; гэты слой — правы месца для такой політікі.
Обробнікі маршрутаў як стабільныя контракты
Server Components моглі б безпасобна вызываць спецыяльныя функцыі, але ў гэтым проекте доступ да даных усё рава адбываецца через обробнікі пад /app/api/**, які выступае як проксі для фронтэнду. Є тры прычыны.
- Одна інтэрфейсная прамова служыць для серверных відображэнняў, кліентскіх элементаў і будучых зовнішніх корыстувачаў.
- Гэта падтрымлівае цэласнасць системы, адколі весь доступ да даных праходзіць чераз адныя і тыя ж контракты.
- Гэта паслядова падходзіць да принцыпаў чыстай архітэктуры: HTTP API ёсць проста ўзлёгкашчыкам навакола ядра.
Ёсць большае колькасць інфармацыі пра гэты шаблон у стацыі «Ператворэнне працоўнікаў маршрутаў Next.js у спецыяльны шар BFF».
Тут важнае значэнне мае адзін детал, залежны ад версіі. У няўякіх пасляльніх выданнях Next.js (пачынаючы з Next.js 15) атрыбут params у дынамічных сегментах ёсць об’ект типу Promise. Працоўнікі маршрутаў і сторынкі должны await params перш чым чытаць атрыбут name; пераканайцеся, што вядомы вам точны формат атрыбута ў вашай версіі, у документацыі.
Автаналіз на аднойчынных файлах
Автаналіз адбываецца чераз тры канцэнтры:
POST /api/auth/loginпераканаецца ў правамоўнасці даных і стварае httpOnly аднойчынны файл, напрыкладpokedex_token.GET /api/auth/sessionчытае гэты аднойчынны файл, пераканаецца ў токэне і вяртае текущы статус сесіі і інфармацыю пра корыстніка.
POST /api/auth/logout адзьяуляе кукі і адпавядае { ok: true }.Чаму httpOnly кукі?
httpOnly кукі нельга чытаць з JavaScript-кода кліента, таму вбудаваныя скрыпты не можаюць выкарастаць JWT, а код кліента ніколі не зберагае і не прыўязвае токены. Паколькі браузер адправляе кукі автаматычна, яе трэба адмацаваць з атрыбутамі SameSite і Secure, а таксама вжываць захіст проты CSRF для запісу дадзеных.
Прадаўцы сесій для UI
Інтерфейс пакуль должен ведаць, чыя-то ўжо заўвязаны, хто гэта, і калі трэба апавернуць даны пасля заўвязывання або вызвалення. Невялікі SessionProvider на стороне кліента адпавядае на гэтыя запыткі. Ён заваношвае сесію з /api/auth/session, зберагае значэння status, isLoggedIn і user у стане React, а таксама надае метады syncAfterLogin() і logout(). Компаненты выкарыстоўваюць гэты контекст і ніколі не чапаюцься за токены.
Компаненты сервера, Suspense і скелетныя компаненты
Там, дзе гэта можа быць корыстным, відворы прыводзяцца на сервере і абгружаюцца за дапамою Suspense. Гэта дае шырэйшае адразу вывісцца зместу, можлівасць індексавання стороніц і просты плэўс дадзеных. Тыповая стораніца складаецца з двух частак: межы Suspense, якая ў разе проблем выкорыстоўвае скелетны компанент, і серверскага відвору, які заваношвае даны і выводзіць готовы інтерфейс.
Перадача кукі на внутрашняя запытка
У падчынні BFF існуе тонкія пастка. Калі серверны компанент вызывае аднаго з прыемнікаў маршрута самай аплікацыі через абсалютны URL, fetch з боку сервера ўжо є абсалютна новы выходны запит, таму кукі корыстніка не прадаюцца автаматычна. Рашэння — прачытаць заголовак cookie, які надходзіць, за дапамою headers(), і явна перадаць його як заголовак Cookie пад час вызыву /api/favorites. Гэта стварае проблемы найчастая, калі URL формуюецца на адваротнае з host і протаколу запита.
Альтэрнатыўна можна зовсама адмовіцца ад внутрашняга HTTP і заставіць серверскія відобразы вызываць кейсы выкарыстоўвання безпосередна через контейнер. HTTP-шлях зберагае адны контракт для всьго, але за цэну дадатковага перехоўду пакета ў сецыі пры кожным відобразэнні і залежнасці ад таго, чы рэальна даступна серверная частка (чыя таксама не падходзіць для статычных будов). Безпосереднія вызывы кейсаў выкарыстоўвання є болей шырокія і простыя, але значаць, што серверскія відобразы і кліентскія віджэты выкарыстоўваюць разныя точкі входу. У гэтым проекте зберагаюцца стабільныя контракты API і перадаюцься кукі.
Улюбленыя элементы ад пачатку да канца
Для улюбленых элементаў патрэбны аутентыфікованы корыстувач, чытанне дадзеных з базы і змены на боку кліента. Гэта рэалізуецца за дапамою двух частак:
- FavoritesView — серверскі компонент, які запрашае
/api/favoritesз пераданымі кукі і відобразае спіс.
DELETE /api/favorites/:name, а пасця вызывае router.refresh(), ў результаты чаго перзапрашваецца спіс, выроблены серверам.router.refresh() перзапрацоўвае кампаненты сервера данага маршруту, захоўваючы стан кліента.
Параболка кожнага межовага пункту за дапамогою Zod
Zod пераглядае даны ў кожны момент, калі яны пераходзяць межу:
- Адпаведзі з PokeAPI, якія параболкаюцца як удаленныя DTO-ы пры перадачы дадзенаў;
- Тэлы запытав, надсылаемых да тэрмінаў аутантыкацыі;
- Значэння JSON, якія зберагаюцца ў базе дадзенаў, напрыклад, кэшаваныя деталі.
Параболка на межовых пунктах ператварае тыхое зміненне структуры дадзенаў, напрыклад, змену назвы поля ў вышэйшых шарах, у чысткі раннія памылку і задокументавае кожны контракт у кодзе.
Мінімальная, але корыстная база тэстав
Для падчырання архітектуры не трэба вялікаг комплексу тэстоў. Две групы тэстаў вельмі дапамагаюць:
- Адказчык для пераканання ў застарэнні кэша: на аднойчы даны часавой меткі, пераканацца, чы роўна ён старэй семі дзён, уключаючы крайняе разлікі.
- Працэс выкарыстоўвання: заменіць інтэрфейсы репазітарыя на макі і пераканацца, што кожны прыклад выкарыстоўвання правільна іх вызывае. Працэсы, зв’язаныя з Prisma, не трэба включаць у тэсты домэна.
Магчымасць тэставання прыкладаў выкарыстоўвання без запуску Next.js — гэта тое, чым явна прадаецца іерархія слоёў.
Запуск коду локальна
Репазітарыю можна кланаваць через HTTPS:
git clone https://github.com/mahmoudmagdi/pokemon-clean-arch.git
cd pokemon-clean-arch
npm install
альбо через SSH, якщо у вас є канфігураны ключ у GitHub:
git clone git@github.com:mahmoudmagdi/pokemon-clean-arch.git
cd pokemon-clean-arch
npm install
Далей створыце файл сэраўісу на аднойчы шаблоне, які быў даны, і заполніце ў яму URL базы дадзеных та секрэты, якія там перыявлены:
cp .env.example .env
Запускайце контэйнер PostgreSQL 16. Назва базы дадзеных, паведамленнік і пароль тут ўжо толькі для локальнага развіцця і должны падходзіць да тых, якія вы задалі ў .env; названы том зберагае данні пасля перзапуску контэйнера.
docker run -d \
--name pokedex-postgres \
-p 5432:5432 \
-e POSTGRES_DB=pokedex_db \
-e POSTGRES_USER=pokedex_user \
-e POSTGRES_PASSWORD=pokedex_password \
-v pokedex_pg_data:/var/lib/postgresql/data \
postgres:16
Застосавайце міграцыі Prisma, каб створыць тэблі:
npx prisma migrate dev
На заканчыце запускайце сервер развіцця:
npm run dev
Версія, якая была размешчана, доступна як жывая дама.
Заключанне
Чыстая архітектура ў Next.js главная часткаю якраз і ў выборы таго, дзе знаходзіцца кожны вид ведамасцей:
- Залічвайце
/appяк тонкі шар адаптатора і перамешчайце логіку ўlibs. - Дазвольце домэну задаць інтарфейсы і энтытэты, а толькі шар дадзеных нехай ведае пра Prisma, HTTP і кэшаванне.
Для пратотыпа на вякенд гэта можа быць большай штурхановкай, чым трэба; для аплікацыі, якая будзе продаваліцца, межы дапамагаюць заставіць кожныя змены локальнымі.