Перейти до вмісту
Налаштування
Тема
Режим
Мова

Історія збірки, частина 1: архітектура і ставки

8 хв читання
  • build-story
  • architecture
AI-стислий виклад

Ця стаття описує перебудову портфоліо розробника на надійну платформу. Вона об'єднує фронтенд на Next.js, адмін-панель на Vue, API на NestJS/Postgres та Astro-вітрину завдяки спільним пакетам. Архітектура ставить у пріоритет зручність підтримки, суворі обмеження та автоматизацію замість звичайного редизайну.

Excerpt: Чотири застосунки, один реліз: як складається ядро, що саме переміг кожен великий технологічний вибір, і обмеження, які сформували все це, — залишені наостанок.

---

Попередня версія цього сайту досі існує — заморожена в гілці old-version. Це було звичайне портфоліо розробника: раз спроєктоване, раз викладене й потроху покинуте, бо будь-яке оновлення — проєкт, назва посади, рядок у CV — означало правку коду, збірку й повторний деплой. Саме такою смертю помирає кожне портфоліо розробника. Тож перебудові не дозволили бути редизайном. Вона мусила стати платформою: контент — це дані, публікація — форма в адмін-панелі, а публічний сайт — лише найпомітніша поверхня.

Ядро цієї платформи — чотири застосунки й кілька спільних пакетів. Цей допис — про те, як вони складаються докупи, і про те, що саме переміг кожен великий вибір, бо код завжди показує лише переможця. Спершу картина.

The core — four apps, one release
джерело
Caddy · 443, TLSweb · Next.js 15 · SSRdashboard · Vue 3 · SPAshowcase · Astro 7 · the starter packapi · NestJS 11 on FastifyPostgres · DrizzleShared packages — one owner eachtokens · CSS variables, 8 palettesui · React primitivesui-vue · Vue primitivesapi-client · typed from OpenAPIcontent · prose model + fallback copy reverse_proxyreverse_proxyfile_serverfile_serverread, SSR + ISRrevalidate on publishwrites everythingreads write-upsfallback when the api is silent
# title: The core — four apps, one release
direction: down

caddy: Caddy · 443, TLS

web: web · Next.js 15 · SSR
dashboard: dashboard · Vue 3 · SPA
showcase: showcase · Astro 7 · the starter pack
api: api · NestJS 11 on Fastify
db: Postgres · Drizzle

shared: Shared packages — one owner each {
  tokens: tokens · CSS variables, 8 palettes
  ui: ui · React primitives
  uivue: ui-vue · Vue primitives
  apiclient: api-client · typed from OpenAPI
  content: content · prose model + fallback copy
}

caddy -> web: reverse_proxy
caddy -> api: reverse_proxy
caddy -> dashboard: file_server
caddy -> showcase: file_server

web -> api: read, SSR + ISR
api -> web: revalidate on publish
dashboard -> api: writes everything
showcase -> api: reads write-ups
api -> db

shared.tokens -> shared.ui
shared.tokens -> shared.uivue
shared.ui -> web
shared.ui -> showcase
shared.uivue -> dashboard
shared.uivue -> showcase
shared.apiclient -> web
shared.apiclient -> dashboard
shared.apiclient -> showcase
shared.content -> api
shared.content -> web: fallback when the api is silent

api.class: key

Чотири застосунки, один реліз. web — сайт, який ви читаєте: Next.js 15.5 на React 19, серверний рендеринг, англійською та українською з першого дня. api — NestJS 11 на Fastify, єдиний процес, що говорить із Postgres. dashboard — односторінковий застосунок на Vue 3.5, приватна адмінка, яка пише все, що платформа публікує. Стрілка від api назад до web — момент публікації: збереження в дашборді змушує api сказати web перевалідувати сторінки, тож вони оновлюються в мить публікації, а не за таймером. showcase — Astro 7, що тримає React- і Vue-острови на тих самих сторінках. Якийсь час він був вирізаний з MVP; тепер він повернувся — живий і навмисно понижений: живе під блогом як відкритий стартовий набір UI-бібліотеки. Під ними — спільні пакети, у кожного один власник: tokens у корені, два UI-набори поверх нього, api-client, типізований з OpenAPI-документа API, і content із моделлю прози та стартовими текстами. Усе, у чому чотири застосунки мусять погоджуватися, живе в пакеті. Так згода стає ребром залежностей, яке перевіряє збірка, а не домовленістю, яку хтось мусить пам'ятати.

Ставки — і що вони витіснили

Next.js для сайту

Сайт — це вітрина. Його мають індексувати, і він має малюватися з першого візиту, на повільному з'єднанні, ще до того, як приїде JavaScript. Це відкидає варіант, до якого React-розробник тягнеться за замовчуванням: клієнтський SPA, що віддає порожню сторінку й обіцянку. З App Router і повним серверним рендерингом кожен маршрут приходить як HTML; React потім робить його інтерактивним, а не змушує його існувати. Для застосунку за логіном компроміс SPA прийнятний. Для сторінки, яку незнайомець бачить першою, — це хибний вибір.

NestJS на Fastify для API

Тут я мало не пішов іншим шляхом — мікрофреймворк: менше церемоній, швидший перший ендпоінт, повна свобода в розкладці коду. Саме ця свобода і є проблемою. Цю платформу підтримує одна людина, яка залишить API на місяці, а потім повернеться, і Nest-модуль відповідає на питання людини, що повертається, — де оголошено маршрут, де живе його логіка, звідки беруться залежності, — ще до того, як прочитано перший рядок. Впровадження залежностей і примусова структура — це податок, коли код пишеш, і субсидія щоразу, коли його перечитуєш, а соло-API перечитують куди частіше, ніж пишуть. Знизу — Fastify як HTTP-адаптер, обраний замість Express заради пропускної здатності; у Nest це питання конфігурації, а не переписування. Валідація — zod на краях, логи — pino.

Postgres і Drizzle

Дані реляційні й ніколи не збиралися вдавати інше, тож база — Postgres. Drizzle виграв місце ORM однією властивістю: схема — це TypeScript у спільному пакеті, тож форма, яку забезпечує база, — та сама форма, з якою компілюється кожен застосунок, що торкається даних. Переможеним лишився ORM із власною мовою схем і кроком кодогенерації, де кожна генерація — вікно, у якому типи й база можуть розійтися. Тут генерації немає. Схема і є типами.

Vue для дашборда

Чесна половина відповіді: я хотів збудувати щось справжнє на Vue. Захищена половина — вибір окуповує себе. Вітрина обіцяє кожен UI-елемент у React, у Vue і в чистому HTML/CSS, а Vue-набір компонентів, яким не користується жоден справжній застосунок, був би демоверсією — коректною у вакуумі й не випробуваною справжнім використанням. Дашборд і є тим застосунком: SPA на Vue 3.5 із Vite 8, pinia та vue-router, що щодня ганяє Vue-примітиви. Саме він тримає Vue-сторону дизайн-системи чесною. Ціна реальна й названа одразу: кожен примітив існує двічі, у @eakz/ui і в @eakz/ui-vue. Вони вже навіть не однакової конструкції — Vue-примітиви обгортають reka-ui, headless-бібліотеку, а React-примітиви лишаються збудованими вручну, — і там, де близнюки різняться, вітрина каже про це прямо, а не ховає. Дешева альтернатива — React-дашборд на вже наявному наборі — коштувала б удвічі менше й не довела б нічого.

Astro для вітрини

Робота вітрини — покласти той самий елемент на одну сторінку тричі: варіант React, варіант Vue і варіант на чистому HTML/CSS. Мій перший опис був хибним — я назвав це майданчиком мікрофронтендів, а це слово не витримує перевірки: мікрофронтенди означають незалежні деплої та незалежне володіння рантаймом, а у вітрини немає ні того, ні того. Насправді цій роботі потрібен хост островів, байдужий до фреймворків, і Astro 7 — саме це: сама сторінка — статичний HTML, а кожен вбудований компонент гідрується сам по собі — острів React 19 поруч з островом Vue 3.5 поруч із розміткою, що не гідрується взагалі. Жоден із двох фреймворків не зміг би нативно тримати інший; Astro тримає обидва однаково байдуже. Це єдиний застосунок, що залежить від @eakz/ui і @eakz/ui-vue водночас, і це робить його перевіркою паритету не менше, ніж галереєю.

Після першої чернетки цього допису вітрина ще й набула чіткішої ідентичності. Це не каталог, а стартовий набір. Вона виставляє елементи, з яких web і dashboard справді збудовані, показує їхній код і показує, як їх забрати собі. Код на експонаті витягується зі справжнього файлу під час збірки — посилання, що перестало розв'язуватися, валить збірку, — тож те, що ви читаєте, і є те, що працює, і розійтися вони не можуть. А текст про кожен елемент — це контент із CMS, як і все інше на платформі: саме публікація тексту виводить елемент у список.

Tailwind v4 — і токени під ним

Стилі — Tailwind v4 скрізь, але утиліти тут лише спосіб доставки: кожен колір розв'язується в CSS-змінну з @eakz/tokens, тож уся платформа перефарбовується заміною одного атрибута. Вісім палітр, кожна в темному й світлому режимах, і кожна проходить автоматичний тест контрасту за WCAG AA всередині пакета токенів. Палітра, що не пройшла, не може вийти — і гейт уже вбив одну: obsidian, запланована палітра за замовчуванням, була відкинута, коли її світлий режим не витримав, і палітрою-ідентичністю стала dracula. Альтернативою був Tailwind у звичному вжитку, з кольорами, зашитими клас за класом у кожному застосунку: безкоштовно в перший день і переписування в день, коли захочеться другої теми.

Один шов, один запасний шлях

Сайт читає API через єдиний шов — apps/web/src/lib/editorial.ts — і ніде більше. Коли API відсутній або відповідає чимось, чого рендерери не можуть намалювати, шов відкочується до текстів у @eakz/content; обидві гілки повертають однакову форму, тож жоден компонент не знає, яку саме отримав. Звична альтернатива — виклики fetch, розкидані по компонентах, кожен зі своїм уявленням про стан помилки, і сайт, що деградує непередбачувано. Тут відмова спроєктована в одному місці.

CV як дані

CV — це файл JSON Resume: одне джерело правди, з якого рендеряться і таймлайн на сайті, і PDF. Відкинута альтернатива — скрейпити LinkedIn заради синхронізації. Це порушує їхні умови користування, а портфоліо не варто починати з порушення ToS.

Обмеження — наостанок

Кожен вибір вище сформували три обмеження, і я навмисно залишив їх на кінець: прочитані першими, вони звучать як виправдання; прочитані останніми — пояснюють форму всього одразу. Це будує одна людина, вечорами, — усе, що потребує команди для підтримки, відпадає; тому згода живе в пакетах, тому схема — це спільні типи, а не племінне знання, і тому в API структура, що переживає мою власну відсутність. Платформа лишається придатною до випуску на кожному кроці: ніщо не потрапляє в основну гілку, доки не пройдуть lint, перевірка типів, тести та збірка, а гейт контрасту AA — те саме правило, застосоване до кольору. І сервер лише запускає контейнери. Збірки на ньому не відбуваються ніколи — вони живуть у CI, тож у машини, що віддає цю сторінку, рівно одна робота.

Оце і є ядро — чотири застосунки та їхні пакети, самодостатній продукт сам по собі. Частина 3 цієї серії — про те, що до нього під'єднується.

917dfce