Главное Авторские колонки Вакансии Вопросы
😼
Выбор
редакции
200 0 В избр. Сохранено
Авторизуйтесь
Вход с паролем

49 лет, ноль опыта, и свой продукт: как я сделал сервис свадебных сайтов за 2 года

**TL;DR:** От курсов HTML/CSS до fullstack приложения с serverless обработкой видео, JWT авторизацией. Стек: Vanilla JS, NestJS, MongoDB, S3. Стоимость содержания: 500руб/месяц.
Мнение автора может не совпадать с мнением редакции

Я считал, что вход в айти возможен только в молодости. 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

И, конечно, разрастание задач, о которых я даже не подозревал.

Несколько советов тем, кто делает свой продукт

  1. Работа с файлами — это отдельный мир, Не пытайтесь обрабатывать на основном сервере или на стороне пользователя.
  2. Структура важнее скорости. Плохо организованный код мстит при масштабировании.
  3. Не тратьте часы на фичи, которые не дают ценности пользователю.
  4. Деплой — часть разработки. Она не менее важна, чем написание кода.
  5. Возраст — не проблема. Желание учиться важнее даты рождения, Не сравнивайте себя с 20-летними.
  6. Используйте ИИ как ментора, объясняют терпеливо и без осуждения.

Итог

После двух лет самостоятельной работы проект finemeet.ru вышел в онлайн.

Сервис живёт, развивается, приносит пользу и стал для меня доказательством, что стартовать в разработке можно в любом возрасте.

0
В избр. Сохранено
Авторизуйтесь
Вход с паролем