Почему стоит отказаться от фреймворков на этапе MVP
Для быстрого прототипа важнее скорость разработки, чем изящество кода. Современные браузеры уже предоставляют всё необходимое: fetch для запросов, шаблонные строки для рендеринга, события для обработки ввода и History API для базовой навигации. В результате можно обойтись без сборщиков, транспайлеров и тяжёлых зависимостей.
Сравнительные цифры из реальных проектов показывают масштаб различий:
| Параметр | Vanilla JS | React (CRA) | Next.js |
|---|---|---|---|
Размер node_modules | 0 KB | ≈ 300 MB | ≈ 400 MB |
| Начальный bundle (gzip) | 0–5 KB | ≈ 45 KB | ≈ 70 KB |
| Необходимость сборки | Нет | Webpack/Babel | Webpack/SWC |
| Время до первого работающего кода | 30–60 минут | 30–60 минут | 30–60 минут |
| Количество зависимостей для аудита | 0 | ~1 400 | ~1 800 |
Для MVP, где требуется лишь загрузка данных, их отображение, фильтрация и простая навигация, такой набор возможностей браузера полностью покрывает потребности. Это экономит время, упрощает деплой и избавляет от проблем совместимости.
Планируемый продукт: каталог существ
В качестве примера будем реализовывать каталог «существ» – небольшое приложение с поиском и фильтрацией. Пользователь видит список карточек, может задать текстовый запрос, выбрать тип существа и перейти к детальному описанию. Всё взаимодействие происходит через REST‑эндпоинты, а UI построен на чистом JavaScript.
Определяем контракт API до начала фронтенда
Самый эффективный способ избежать «переписываний» – заранее зафиксировать интерфейс между клиентом и сервером. Контракт описывается в простом markdown‑файле или даже на бумаге:
GET /api/creatures– возвращает массив всех существ; поддерживает параметры фильтрации (?type=dragon&search=fire).GET /api/creatures/:id– детали конкретного существа.POST /api/creatures– создание нового (для админ‑панели, опционально).PUT /api/creatures/:id– обновление существующего.DELETE /api/creatures/:id– удаление.
Каждый эндпоинт возвращает JSON‑структуру с полями id, name, type, description, imageUrl. Такая схема позволяет сразу написать тесты на curl или httpie, проверив корректность ответов без написания клиентского кода.
Архитектурные границы между клиентом и сервером
Разделение обязанностей выглядит следующим образом:
- Сервер – отвечает только за бизнес‑логику и хранение данных. Он не знает о том, как клиент будет их отображать. Важно, чтобы ответы были предсказуемыми (однообразные статус‑коды, стабильные поля).
- Клиент – исключительно запросы и рендеринг. Все бизнес‑правила (например, валидация формы) реализуются в JavaScript, а не в шаблонах сервера.
- Контракт – живой документ, который обе стороны используют как единственный источник правды.
Такой подход упрощает масштабирование: при необходимости заменить бэкенд (Spring Boot → FastAPI) клиент останется неизменным, если API не меняется.
Реализация фронтенда без сборки
Структура проекта
/public
index.html
styles.css
/src
api.js // функции-обёртки над fetch
ui.js // генерация DOM‑элементов
app.js // основной цикл и обработчики
API‑модуль (api.js)
export async function getCreatures(params = {}) {
const qs = new URLSearchParams(params).toString();
const resp = await fetch(`/api/creatures${qs ? '?' + qs : ''}`);
if (!resp.ok) throw new Error('Network error');
return resp.json();
}
export async function getCreature(id) {
const resp = await fetch(`/api/creatures/${id}`);
if (!resp.ok) throw new Error('Network error');
return resp.json();
}
Модуль использует async/await и fetch, что делает код читаемым и избавляет от внешних библиотек.
UI‑модуль (ui.js)
export function renderList(creatures) {
const container = document.getElementById('list');
container.innerHTML = '';
creatures.forEach(c => {
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = `
<img src="${c.imageUrl}" alt="${c.name}">
<h3>${c.name}</h3>
<p>Тип: ${c.type}</p>
`;
card.addEventListener('click', () => navigateToDetail(c.id));
container.appendChild(card);
});
}
Шаблонные строки позволяют быстро собрать разметку без JSX и без шаблонизаторов.
Основной скрипт (app.js)
import { getCreatures } from './api.js';
import { renderList } from './ui.js';
let currentFilters = {};
async function loadAndRender() {
try {
const data = await getCreatures(currentFilters);
renderList(data);
} catch (e) {
console.error(e);
alert('Ошибка загрузки данных');
}
}
// Обработчики формы фильтра
document.getElementById('filterForm').addEventListener('submit', e => {
e.preventDefault();
const type = e.target.elements.type.value;
const search = e.target.elements.search.value.trim();
currentFilters = { type, search };
loadAndRender();
});
// Инициализация
loadAndRender();
Код полностью независим от сборщиков, а type="module" в <script> гарантирует поддержку импортов в современных браузерах.
Тестирование и отладка
- curl:
curl http://localhost:3000/api/creatures?type=dragon&search=fireпроверяет корректность фильтрации. - DevTools: вкладка Network позволяет убедиться, что запросы отправляются правильно, а ответы соответствуют схеме.
- Unit‑тесты: можно добавить небольшие тесты для
api.jsс помощьюJest(установить только dev‑зависимость) без влияния на основной процесс сборки.
Преимущества и ограничения подхода
Плюсы
- Минимальное время до первого работающего прототипа.
- Отсутствие зависимости от npm‑пакетов, что упрощает деплой (достаточно скопировать файлы на сервер).
- Чистый раздел клиент‑сервер, легко менять бекенд без правок UI.
- Прямой контроль над размером бандла и загрузкой ресурсов.
Ограничения
- При росте проекта может потребоваться более сложный роутинг и состояние, что заставит добавить библиотеки (например,
page.jsилиredux-lite). - Отсутствие типизации (TypeScript) приводит к более частым ошибкам в больших кодовых базах.
- Нет автоматической оптимизации изображений и CSS, которую предоставляют сборщики.
Тем не менее, для MVP такой набор возможностей полностью покрывает задачи. После подтверждения гипотезы и получения обратной связи можно постепенно вводить более тяжёлые инструменты, но начальная фаза разработки будет быстрой, дешёвой и предсказуемой.