
Меня зовут Евгений Новиков, я создаю приложения на Flutter и сделал собственный фреймворк для fullstack-разработки на Dart.
Многие могли видеть рекламу моего Telegram-канала или даже меня самого — год назад я выступал на CrossConf в Москве.
В этой статье хочу рассказать про фреймворк и путь его создания.
С чего всё началось
Fullstack Dart появился в моей жизни 5 лет назад — я ушёл с работы, чтобы развивать собственную студию. Я предполагал, что лучше выбрать один стек как специализацию и развиваться в нём. (Сейчас я так не думаю.)
Я уже владел Flutter немного и очень верил в него. (И сейчас верю.) Поэтому меня особенно привлёк зарождавшийся тогда Serverpod — помню, что первые проекты были на версии 1.1 или 1.2. Их лозунг — missing server for Flutter, а главная фишка — кодогенерация контракта между сервером и приложением: это касается и моделей, и методов API.
Я подумал, что этот инновационный подход будет моим конкурентным преимуществом, позволит мне быстрее и лучше делать приложения. Все мои предположения оказались неверными: моностек скорее ограничивал меня, а делать первые шаги в бизнесе и одновременно изобретать велосипед оказалось катастрофически тяжёлой историей. Не делайте так))
Тем не менее я был авантюристом и выбрал свой путь. Сначала я просто делал проекты на Serverpod, но очень скоро начала проявляться моя склонность к оптимизации. Мне жутко не нравилось, что под каждый запрос от приложения нужно делать отдельный эндпоинт, обрабатывать в нём ошибки и формировать ответ.
Мне также не нравилось, что в самом приложении для каждой модели появлялся отдельный репозиторий с кучей логики. Тогда я ещё не очень хорошо разбирался в архитектуре приложений, и всё это превращалось в ужасную помойку.
Но больше всего меня ужасало, что каждый разработчик на каждом проекте делал всё немного по-своему, а одни и те же проблемы повторялись на разных проектах…
Тогда и зародилась идея DartWay — разработать единую универсальную методологию для разработки приложений.
Но от рождения идеи до её внятной реализации прошло немало времени. Вспоминаю свои первые наброски — это была жуткая боль.
Например, я пытался сделать механизм, где все модели лежали в едином пуле и доставались через какие-то жуткие extension на енуме — в каждом виджете, где понадобятся.
Выглядело это примерно так — кусок страницы из проекта тех времён:
// Модель достаётся из общего пула и ищется прямо в виджете final CourseModel courseModel = context .watch<EntityListState<CourseModel>>() .list! .firstWhere((e) => CourseFields.id.of(e) == context.read<Map<NavigationParams<dynamic>, dynamic>>()[ NavigationParams.id]); // Поля — не course.title, а через енум return AppScaffold( title: CourseFields.title.of(courseModel) ?? '-', // ... );
Потом целыми неделями сидел над тем, чтобы склеить динамические типы, приходящие от сервера, и дженерик-методы в репозитории. Отдельным приколом стала минификация типов в продакшн-сборках для веба, об которую всё ломалось))
Но так или иначе каждый месяц мучений приносил новый кирпичик в фундамент фреймворка, новое архитектурное решение и продвигал меня к тому, что есть сейчас.
Идея и философия DartWay
Самое главное, что нужно понимать про DartWay: это ужасно opinionated фреймворк. Он предписывает буквально всё — от логики взаимодействия с бэкендом до именования переменных и структуры папок.
DartWay родился из одной простой гипотезы: для каждого вопроса можно подобрать настолько универсальное и гибкое решение, что 99% проектов смогут им пользоваться и не париться.
Опытным разработчикам, привыкшим к своей методологии или несогласным с моими подходами, это может очень не понравиться. Но я делаю фреймворк не для того, чтобы всем нравиться. Цель простая — максимально ускорить и упростить запуск надёжных мобильных приложений.
А главный вызов — как обеспечить гибкость функционала и реализацию любых запросов клиента при строгой структуре и на единых механизмах.
Что для этого главное в DartWay.
1. Строгий контракт с бэкендом
Клиент может только отправлять на сервер и получать с него модели — точнее, DTO. Все запросы проходят через один и тот же конвейер.
На бэкенде это проверка полномочий, валидация, работа с базой в транзакции, формирование ответа и дополнительных обновлений, рассылка обновлений по сокетам всем, кто на них подписан.
На фронте все ответы сервера тоже обрабатывает единый репозиторий и раздаёт подписчикам.
Благодаря этому отдельной фиче не нужно думать, на кого ещё влияют её обновления, — это решают центральный репозиторий и подписки.
Как это выглядит. Фича — несколько классов в общем пакете, который компилируют и сервер, и приложение:
final class ListNews extends DwListRequest<NewsPost> with _$ListNews { const ListNews(); @override List<DwLiveChannel> get channels => const [ DwLiveChannel(DartwayExampleChannel.news), ]; } final class PublishNews extends DwActionCommand<NewsPost> with _$PublishNews { const PublishNews({required this.title, required this.text}); final String title; final String text; }
На сервере — один обработчик на вызов: кто может его вызвать, что он делает и кто об этом узнает:
DwCallHandler.command<PublishNews, NewsPost>( access: AppAccess.staff, handle: (ctx, command) async { final me = await ctx.profile; final row = await ctx.db.newsPosts.insert( NewsPostRow( authorProfileId: me.id!, title: command.title.trim(), text: command.text.trim(), createdAt: DateTime.now(), ), ); final post = (await ContentObjects.news(ctx.db, [row], author: me)).single; ctx.publish(AppChannels.news, post); return post; }, ),
В приложении — живой список и кнопка, которая не опубликует дважды:
ref.watch(dw.request(const ListNews())) // AsyncValue<List<NewsPost>> onTap: dw.action((_) => dw.command(PublishNews(title: title, text: text)))
Сотрудник публикует новость — она появляется у всех, у кого список открыт, на своём месте, без перезагрузки. Отказ приходит кодом и показывается на языке пользователя. Повторная команда выполняется один раз. Ни эндпоинта, ни маршрута, ни клиентского метода, ни кэша для этого никто не писал. Под капотом — POST /dw/<Type> на каждый тип и один сокет /dw/live для живых каналов.
2. Строгие правила структуры приложения
Строгие инструкции по структуре Flutter-приложения, архитектуре фич и границам ответственности. Отделение логики от UI — не рекомендация, а набор правил, которые проверяют линтер и скрипты. Циклические зависимости и бесконтрольное разрастание файлов исключены.
Так выглядит фича в шаблоне проекта:
lib/app/profile/ profile_page/ profile_page.dart ← единственный публичный файл фичи logic/ profile_change.dart profile_page_commands.dart widgets/ avatar_picker.dart profile_settings_widget.dart identity/ ← вложенная фича, те же правила profile_identity_section.dart logic/identity_change_controller.dart widgets/identity_change_sheet.dart
dartway check следит, чтобы так и оставалось: у фичи один публичный файл, в чужие logic/ и widgets/ ходить нельзя, циклов между фичами нет, файл длиннее 350 строк — повод разделить. Нарушение выглядит так:
lib/app/home/home_page.dart imports the internals of app/profile/profile_page (lib/app/profile/profile_page/widgets/avatar_picker.dart) — only the public file of a feature may be imported from outside it
Именно благодаря этим правилам я не беспокоюсь о коде, который пишут агенты. Худшее, что может случиться, — локальная неоптимальность внутри фичи на 500 строк. Структура проекта и корректность архитектуры гарантированы.
3. Готовый фундамент и модули — и при этом гибкие
Моя самая большая боль всегда — повторение кода между проектами. Сделали аутентификацию в одном проекте. Потом ещё в двух. Потом нашли баг или добавили фишку в одном из них — и начинаем руками перетаскивать доработку во все проекты. Это ужасно.
Другая крайность — готовые пакеты. Сколько я их видел, везде одна и та же проблема: работает ровно один сценарий, поменять нельзя буквально ничего, кроме цвета кнопок. В итоге на первом же проекте приходится форкать и править под себя.
Я убеждён, что главное достоинство DartWay — баланс готового и гибкой настройки. Фреймворк закрывает все основные задачи: аутентификацию, репозиторий, веб-сокеты, работу сервера. Но везде есть колбэки и параметры, чтобы решить любую проектную задачу.
Например, вход. Аккаунты и способы входа — забота фреймворка: он знает, что кто-то вошёл и с каким телефоном или почтой. А кто этот человек для проекта — профиль, роль, что делать с его данными при удалении — решает проект в колбэках:
DwAuthConfig( // как доставить код — SMS, почта, свой шлюз deliverCode: (ctx, kind, identifier, code, accountId) async { ... }, // профиль создаётся вместе с аккаунтом, в той же транзакции onAccountCreated: (ctx, accountId, kind, identifier, origin) async { final profile = await ProfileChanges.create(ctx, accountId, origin); ctx.publish(AppChannels.admin, await ProfileObjects.profile(ctx, profile)); }, // удалить данные человека или оставить «надгробие» — решает проект onAccountDeleting: (ctx, accountId) async { ... }, )
Фреймворк вырос из потребностей студии: всё должно работать так, как захочет клиент, — значит, никаких компромиссов. Любое поведение можно настроить, любую задачу решить.
И отдельный уровень — шаблон. Многие вещи не зашиты в пакеты фреймворка, а сделаны в шаблоне проекта: они готовы к использованию, но никак не ограничивают редактирование и настройку.
Из последнего — сделал симпатичный дашборд в админке. Теперь такой же будет из коробки в каждом новом проекте.

Именно этой гибкости мне не хватало в Serverpod — и это привело к решению от него отказаться.
Почему я отказался от Serverpod
Больше трёх лет мы с командой делали проекты на Serverpod, и первая версия DartWay базировалась на нём — по сути, DartWay был модулем для Serverpod.
На старте это было удобно — взять готовую основу. И Serverpod был очень неплох в этой роли: достойная реализация сервера, ORM и генерации контракта, очень качественная документация и отсутствие багов. Как фреймворк для проекта я могу рекомендовать его и сейчас. Но как фундамент для DartWay он стал меня ограничивать.
Расскажу об основных моментах — как иллюстрацию того, насколько по-разному одни и те же вопросы решаются на уровне фреймворка.
Архитектура модулей
Serverpod даёт готовые модули аутентификации и чатов. Но все модели в них лежат внутри модуля — и проект не может добавить новое поле в класс пользователя или свой тип сообщения в чат.
Поэтому ни один мой проект не мог взять их модули без доработок: мы копировали их к себе и правили. За это время сам фреймворк уходил на десяток версий вперёд, а мы не могли обновиться.
В общем, кошмар. В итоге я сначала отказался от их чатов, потом сделал собственную аутентификацию.
В DartWay модель пользователя целиком принадлежит проекту и свободно меняется под его нужды. Фреймворк управляет только токенами доступа и провайдерами аутентификации, а с моделью проекта связывается через пару типизированных колбэков — как в примере со входом выше.
Результат — 100% гибкость.
Контракт моделей и API
Главная фишка Serverpod — генерация зеркальных моделей для бэка и фронта. Зеркальных, потому что это всё-таки разные классы, хоть и называются одинаково. Не знаю, почему они пошли этим путём, но у подхода два неприятных эффекта.
Нельзя шарить бизнес-логику. Раз это разные классы без общего предка, нельзя просто навесить extension и сделать единую валидацию на фронте и на бэке. Насколько я знаю, в последнем релизе для этого появилось какое-то решение, но детально я уже не смотрел.
Единый список моделей для приложения и сервера. Сначала это казалось огромным плюсом: нет дублирования, одни и те же классы, всё здорово. Но чем глубже я погружался в мобильную разработку и архитектуру, тем яснее понимал, что подход неверен.
Модели ORM, которые сохраняются в базу, не должны протекать в приложение: набор полей в них не совпадает со сценариями использования и полномочиями пользователей.
В Serverpod для этого есть костыли — превратить модель в её публичную версию или закрыть отдельные поля как server only. Но каждый раз это было индивидуальное решение, а я хотел единый, универсальный подход.
Поэтому в новом DartWay модели живут только на сервере. Контракт между приложением и сервером вынесен в shared-пакет и состоит из DTO — отдельных сущностей под каждый сценарий взаимодействия приложения с сервером.
Почему так долго
Отказ от Serverpod назревал давно. Я понимал, что он ограничивает меня и создаёт трудности, но пугала необходимость переписать такие сложные модули, как HTTP-сервер, ORM и генерация контракта. С развитием нейросетей всё поменялось…
Новый DartWay — на агентах и для агентов
С начала 2026 года я стал передавать многие задачи по разработке нейросетям, а к середине года практически перестал касаться кода руками.
Сейчас я твёрдо убеждён, что единственный реальный процесс будущего — разработка в режиме Dark Factory, когда агенты ведут разработку полностью самостоятельно, включая код-ревью и приёмочное тестирование.
Именно под это будущее я теперь строю DartWay. Фреймворк, который создавался для контроля качества работы джунов, стал идеальной конституцией для агентов.
Неделя без Serverpod
Так закончилась и история с Serverpod. 13 сентября я на импульсе попросил Claude подготовить план, около часа мы обсуждали детали — и через день новая версия DartWay была готова: свой сервер прямо на dart:io и своя ORM поверх postgres. Ещё несколько дней ушло на перенос проектов, и 19 сентября Serverpod не осталось нигде.
Самое удивительное — всё заработало сразу. Не помню ни одного бага, связанного именно с переписыванием.
Конституция для агентов
Доверять нейросетям так много я могу благодаря DartWay. Пару лет я делал его как методологию: много не самых очевидных решений, которые вместе снижают сложность, убирают риски и делают разработку простой и контролируемой.
Главное — архитектуру проверяет машина, а не человек. Код сверяется с исходниками, миграции — со схемой, структура — с правилами. Агент не развалит проект к третьей фиче.
И безопасность по умолчанию: без правила доступа обработчик не запустится. Каждая подписка требует вошедшего пользователя и правила для своего канала; канал, для которого правило не написали, отказывает всем.
Поэтому у меня нет плавающих багов. Данные, сервер, авторизация — всё с одного запроса. Даже вход через Google и Apple заработал с первого раза. Это заслуга не нейросети, а методологии, внутри которой она работает.
Из 658 коммитов во фреймворке 458 написаны в соавторстве с Claude — и код в проектах на нём тоже пишут агенты. Для них в DartWay есть три вещи:
Тулкит — 22 навыка для агента: как добавить фичу, написать миграцию, протестировать. Недавно ужали его втрое: агенту тоже вредно читать лишнее.
Закон синхронности — API, пример, шаблон, документация и заметка о миграции меняются в одном PR. По этим заметкам агент сам обновляет проекты.
Журнал решений — каждое архитектурное решение записано с причиной. Агент читает, почему сделано так, а не угадывает.
Заключение
Что дальше
Фреймворк пока 0.x, и я этого не скрываю. Релизы выходят в ветке stable, а проект переходит на новую версию одной командой dartway update — по тем самым заметкам о миграции. На нём работают наши живые продукты.
Ближайший фокус — автотесты: их количество и точность. И сам процесс разработки: меньше циклов код-ревью и меньше токенов на задачу.
Меня волнует не только код пакетов, но и весь процесс создания приложения. Поэтому вокруг фреймворка я строю DartWay Studio — готовую фабрику разработки на AI-агентах. Так же, как с фреймворком, я прорабатываю все процессы продуктовой разработки, чтобы сделать создание приложения простым и прозрачным. Сейчас Studio в закрытой бете.
Попробовать фреймворк
Откройте пустую папку в любом AI-ассистенте — Claude Code, Cursor, Codex — и попросите выполнить две команды:
dart pub global activate dartway_cli dartway quickstart
Дальше скажите, что хотите: «создай проект DartWay my_app и подними его». Получится работающее приложение — вход по одноразовому коду, профили, роли, админка, навигация, свой UI-кит, тесты с обеих сторон — и ни одной доменной модели: домен ваш.
Если попробуете и что-то сломается — issue на GitHub или сообщение мне помогут больше, чем звёздочка. Репозиторий: github.com/dartway/dartway, документация — dartway.dev.
А чтобы следить, как развиваются DartWay и Studio, — подписывайтесь на мой Telegram-канал @ai_native_by_novikov.
