редакции
49 лет, ноль опыта, и свой продукт: как я сделал сервис свадебных сайтов за 2 года
Я считал, что вход в айти возможен только в молодости. ZX Spectrum с бейсиком остался в школьном прошлом, а технологии ушли так далеко, что казалось — поезд ушёл, и к программированию я 30 лет не возвращался.
Но мысль о коде не отпускала и 46 пошел на курсы веб-разработки, где освоил основы HTML, CSS, JS и чуть-чуть React. Курсы не закончил, но получил самое важное — уверенность, что могу учиться дальше и сделать что-то своё.
Через два года, результатом стал мой первый продукт — сервис создания сайтов свадебных приглашений finemeet.ru. Fullstack приложение с обработкой медиа, конструктором и авторизацией.
Это обзорная статья серии. Здесь — общая архитектура и ключевые решения. Детальные разборы (с примером кода) — в следующих статьях.
Почему именно свадебные приглашения
Нужна была задача, где есть:
✅ UI/UX (не просто CRUD)
✅ Backend с авторизацией
✅ Обработка медиафайлов
✅ Интеграция с внешними API
✅ Деплой и инфраструктура
Проект должен быть «живым», а не учебным.
Идея: конструктор для свадебных приглашений, Пользователь за 10 минут собирает страницу с фото, видео, картой места и программой торжества. Отправляет ссылку гостям.
Изначальный план (наивный):
- Главная страница
- Редактор
- Личный кабинет
- Страница приглашения
Реальность через 2 года:
- 8 страниц с отдельными бандлами
- Serverless обработка медиа
- Event-driven архитектура
- JWT авторизация
- Docker деплой
- CI/CD
Первый этап: дизайн в Figma и реальность фронтенда
Дизайн получился быстро, Figma — это удобно.
Что я сделал на фронте:
- выбрал ванильный JavaScript — React ещё не понимал достаточно хорошо;
- настроил отдельный webpack для каждой страницы (единый бандл собрать просто не умел);
- использовал GSAP для анимаций на главной — хотелось попробовать эту библиотеку;
Вывод: писать на ванильном JS — нормально. Поддерживать — значительно сложнее. Следующий продукт точно буду делать на фреймворке, душа лежит больше к Vue.
Бэкенд на NestJS: решение, которым я доволен
NestJS выбрал, потому что он показался не очень сложным в изучении с понятным принципом. Учился по YouTube и документации.
Что оказалось удобным:
- чёткая организация кода,
- логичное разделение логики,
- быстрое вхождение.
Сложности:
- JWT авторизация (Guards, Strategies),
- тонкие моменты с загрузкой файлов,
- интеграция с S3.
Пример модуля авторизации:
// auth/auth.controller.ts
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('register')
async register(@Body() registerDto: RegisterDto) {
return this.authService.register(registerDto);
}
@Post('login')
async login(@Body() loginDto: LoginDto) {
return this.authService.login(loginDto);
}
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@CurrentUser() user: User) {
return user;
}
}
Вывод: для новичка NestJS — отличная точка входа в бэкенд.
MongoDB и работа с данными
Выбрал MongoDB из-за простоты, Mongoose упрощает валидацию. В целом база отлично подошла под динамичные пользовательские данные.
Главная боль — медиафайлы
Сжимал на фронте с помощью Compressor.js — быстро и довольно качественно.
import Compressor from 'compressorjs';
const compressedFile = await compress(file, 0.7, 1920, 1080, 700);
async function compress(
file,
quality,
maxHeight,
maxWidth,
convertSize
) {
const removeExtension = (fileName) =>
// fileName.substring(0, fileName.lastIndexOf('.')) || fileName;
fileName
return await new Promise((resolve, reject) => {
new Compressor(file, {
quality,
maxHeight,
maxWidth,
convertSize,
success: (compressedFile) => {
if (compressedFile instanceof File) return resolve(compressedFile);
else {
const compressedFileFromBlob = new File([compressedFile], removeExtension(file.name), {
type: compressedFile.type,
});
return resolve(compressedFileFromBlob);
}
},
error: (err) => {
reject(err);
},
});
});
};
Метрики:
- 3 МБ → 150 КБ за ~1 секунду
- Работает на клиенте
- Не нагружает сервер
Видео
Вот тут сложнее.
Первая версия — попытки пережимать видео через ffmpeg прямо в браузере. Да, оно работало. Нет, это нельзя было использовать.
Видео загружалось настолько медленно, что пользователь мог передумать жениться.
Решение — Serverless-функция о внедрении которой я более подробно напишу в отдельной статье.
Результат.
Видео продолжительностью 28сек. и размером в 51мб.сжималось:
- на фронте более 3-5 минут,
- через Serverless-функцию за 15-20 секунд.
Карты, регистрация, капча — детали, которые вырастают в задачи
Карты
Пробовал Yandex Maps API, но ограничения бесплатного тарифа создавали барьеры. Поменял подход: вставляю iframe, координаты пользователь задаёт самостоятельно. Просто — и работает
Регистрация
Сначала сделал вход через email. Потом почитал про нюансы работы с персональными данными и отказался.
Сделал авторизацию по логину/паролю + секретный вопрос. Минимум бюрократии, всё понятно и прозрачно.
Капча
Сначала написал свою (решение логической задачи), но потом переделал на яндекс-капчу, чтобы пользователь вообще не замечал капчу. Как её легко внедрить вынесу в отдельный материал.
Деплой: Docker, Nginx, Linux и тот момент, когда всё наконец работает
Финальный босс — развернуть проект.
Docker оказался одновременно и спасением, и новым видом боли.
Пришлось разобраться в:
- контейнерах,
- томах,
- nginx-конфигурации,
- HTTPS-сертификатах,
- работе Linux на сервере,
- CI/CD (пусть и простом).
Например образ бекенда сперва собирал вот так:
# Используем официальный образ Node.js
FROM node:18-alpine
# Устанавливаем рабочую директорию внутри контейнера
WORKDIR /app
# Копируем package.json и package-lock.json (или yarn.lock)
COPY package*.json ./
# Устанавливаем зависимости
RUN npm install
# Копируем все файлы проекта
COPY . .
# Собираем проект (если требуется)
RUN npm run build
# Указываем порт, который будет использовать приложение
EXPOSE 6000
# Команда для запуска приложения
CMD ["npm", "run", "start"]
На выходе образ получался чуть больше одного гигабайта.
Потом переделал на такой Dockerfile:
# Этап 1: Установка всех зависимостей и сборка
FROM node:18-alpine AS builder
WORKDIR /app
# Копируем package.json и package-lock.json
COPY package*.json ./
# Устанавливаем ВСЕ зависимости, включая devDependencies
RUN npm ci && npm cache clean —force
# Копируем исходный код
COPY . .
# Отладка: Проверим наличие src/main.ts
RUN ls -la src && test -f src/main.ts || (echo "Error: src/main.ts not found" && exit 1)
# Компилируем проект
RUN npm run build
# Отладка: Проверим содержимое dist
RUN ls -la dist
# Проверяем наличие dist/main.js
RUN test -f dist/main.js || (echo "Error: dist/main.js not found" && exit 1)
# Этап 2: Установка только продакшен-зависимостей
FROM node:18-alpine AS prod
WORKDIR /app
# Устанавливаем tzdata для поддержки часовых поясов
RUN apk add —no-cache tzdata
# Копируем package.json и package-lock.json
COPY package*.json ./
# Устанавливаем только продакшен-зависимости
RUN npm ci —omit=dev && npm cache clean —force
# Настраиваем часовой пояс
RUN ln -snf /usr/share/zoneinfo/Europe/Moscow /etc/localtime && echo "Europe/Moscow" > /etc/timezone
# Этап 3: Финальный образ
FROM node:18-alpine
WORKDIR /app
# Устанавливаем tzdata в финальном образе
RUN apk add —no-cache tzdata
# Настраиваем часовой пояс
RUN ln -snf /usr/share/zoneinfo/Europe/Moscow /etc/localtime && echo "Europe/Moscow" > /etc/timezone
# Копируем скомпилированный код из этапа builder
COPY —from=builder /app/dist ./dist
# Копируем продакшен-зависимости из этапа prod
COPY —from=prod /app/node_modules ./node_modules
COPY package.json .
# Указываем порт
EXPOSE 6000
# Запускаем приложение
CMD ["node", "dist/main.js"]
В результате размер образа сократился до 229MB.
Когда сайт впервые открылся по реальному домену — это был самый приятный момент за весь путь.
Инфраструктура
VPS (1 CPU, 2 GB RAM) — 500p/мес
S3 — 0р. (укладываюсь в бесплатный лимит)
Serverless-функция — 0р. (укладываюсь в бесплатный лимит)
Чему я научился
С нуля пришлось освоить:
- NestJS
- MongoDB
- обработка фото и видео
- Яндекс.Карты
- JWT
- работа с email
- S3
- serverless-функции
- Docker
- Linux
И, конечно, разрастание задач, о которых я даже не подозревал.
Несколько советов тем, кто делает свой продукт
- Работа с файлами — это отдельный мир, Не пытайтесь обрабатывать на основном сервере или на стороне пользователя.
- Структура важнее скорости. Плохо организованный код мстит при масштабировании.
- Не тратьте часы на фичи, которые не дают ценности пользователю.
- Деплой — часть разработки. Она не менее важна, чем написание кода.
- Возраст — не проблема. Желание учиться важнее даты рождения, Не сравнивайте себя с 20-летними.
- Используйте ИИ как ментора, объясняют терпеливо и без осуждения.
Итог
После двух лет самостоятельной работы проект finemeet.ru вышел в онлайн.
Сервис живёт, развивается, приносит пользу и стал для меня доказательством, что стартовать в разработке можно в любом возрасте.