EVER AI · Блог · Строю с ИИ · практика

Собрал форму записи для клиники через Claude Code: от пустого файла до продакшена

Практика8 минКухня EVER AI
Форма записи для клиники, собранная через Claude Code

Задача простая на словах: форма записи для клиники — имя, телефон, услуга. Заполнил — заявка падает в Telegram администратору, форма живёт на своём домене и работает ночью. Я не программист, я бывший руководитель продаж. Собрал это через Claude Code, без классического кодинга. Показываю весь путь — вместе с местами, где я споткнулся.

0шагов до продакшена
3поля: имя · телефон · услуга
24/7заявка сразу в Telegram
01

Задачу надо ставить как ТЗ, а не как желание

Главная ошибка новичка — написать Claude Code «сделай форму записи» и ждать магии. Он что-нибудь сделает, но не то, что у тебя в голове. Claude Code силён ровно настолько, насколько точно ты назвал результат. Формулируй как инженер, который принимает работу: какие поля, куда уходит заявка, что должно произойти после отправки.

размытая задача

✕ «сделай форму записи для клиники»

✕ ИИ угадывает поля, дизайн, логику

✕ переделываешь три раза

задача как ТЗ

✓ «форма: имя, телефон, услуга (выпадающий список)»

✓ «POST на /api/booking, тело — JSON»

✓ «после отправки — заявка в Telegram + запись в базу»

Разница не в вежливости, а в количестве переделок. На размытой формулировке ты потратишь вечер на споры с ИИ. На точной — он соберёт скелет с первого раза, а ты будешь только докручивать. Я держу в голове три вопроса перед каждой задачей: что на входе, куда уходит, что происходит после. Ответил на все три — можешь формулировать.

02

Фронт формы: три поля, валидация телефона и ловушка для ботов

Начинаем с того, что видит пациент. Обычная HTML-форма: имя, телефон, услуга. Две вещи, которые прошу у Claude Code сразу, — проверку телефона (чтобы не приходили заявки с пустым номером) и honeypot: скрытое поле, которое человек не видит и не заполнит, а бот заполнит. Заполнено — значит бот, заявку молча выкидываем.

<form id="booking">
  <input name="name" placeholder="Имя" required>
  <input name="phone" placeholder="+7 ___ ___ __ __"
         pattern="\+?[0-9\s\-]{10,}" required>
  <select name="service" required>
    <option value="">Услуга</option>
    <option>Консультация</option>
    <option>УЗИ</option>
  </select>
  <!-- ловушка для ботов: человек её не видит -->
  <input name="website" tabindex="-1" autocomplete="off"
         style="position:absolute;left:-9999px">
  <button>Записаться</button>
</form>

<script>
document.getElementById('booking').onsubmit = async (e) => {
  e.preventDefault();
  const data = Object.fromEntries(new FormData(e.target));
  const res = await fetch('/api/booking', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data)
  });
  alert(res.ok ? 'Заявка принята' : 'Ошибка, позвоните нам');
};
</script>
// фронт формы: атрибут pattern проверяет телефон, honeypot ловит ботов

Валидацию через pattern браузер делает сам, без единой строки логики. Это не защита от всего, но пустые и мусорные номера отсекает на входе. Дальше — fetch отправляет собранные поля на бэкенд-эндпоинт. Всё, что видит пациент, уместилось в один экран.

03

Бэкенд-эндпоинт: куда падает заявка

Фронт красивый, но сам по себе он никуда ничего не отправит. Нужен серверный роут — адрес, куда прилетает POST, и который делает две вещи: шлёт заявку в Telegram администратору и пишет её в базу, чтобы ничего не потерялось. Токены и ключи — только на сервере, в переменных окружения. На фронт их выносить нельзя: любой откроет код страницы и заберёт.

// /api/booking — серверный роут (Next.js-стиль)
export async function POST(req) {
  const { name, phone, service, website } = await req.json();

  // honeypot заполнен → это бот, тихо выходим
  if (website) return Response.json({ ok: true });

  // валидация на сервере, не только в браузере
  if (!name || !/[0-9]{10,}/.test((phone||'').replace(/\D/g,'')))
    return Response.json({ ok: false }, { status: 400 });

  // 1. сигнал в Telegram администратору
  await fetch(`https://api.telegram.org/bot${process.env.TG_TOKEN}/sendMessage`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      chat_id: process.env.TG_CHAT,
      text: `Новая запись\nИмя: ${name}\nТелефон: ${phone}\nУслуга: ${service}`
    })
  });

  // 2. пишем в базу сервисной ролью (см. грабли ниже)
  await supabaseAdmin.from('bookings').insert({ name, phone, service });

  return Response.json({ ok: true });
}
// TG_TOKEN, TG_CHAT — плейсхолдеры, реальные ключи живут в переменных окружения сервера

Три защиты, которые прошу заложить сразу: honeypot проверяем и на сервере (не только в браузере — бот шлёт запрос напрямую, минуя форму); валидацию дублируем на сервере; и хорошо бы поставить простой лимит — не больше N заявок с одного адреса в минуту, чтобы форму не завалили тысячей отправок. Последнее можно добавить позже, но заложить в голову — сейчас.

04

Git: фиксируем работу, чтобы не потерять

Пока всё лежит только на твоём ноутбуке — этого не существует. Git — это точки сохранения. Работаю в отдельной ветке, чтобы не трогать рабочую версию сайта, фиксирую изменения коммитом и отправляю на сервер. Три команды, которые прошу Claude Code выполнить и которые ты быстро запомнишь сам:

git checkout -b booking-form      # новая ветка под форму
git add .                         # собрать изменения
git commit -m "форма записи + эндпоинт /api/booking"
git push origin booking-form      # отправить на GitHub
// ветка изолирует работу; коммит — точка сохранения; push уводит код в облако

Отдельная ветка — это страховка. Если что-то сломается, рабочий сайт стоит нетронутым, а ты спокойно доводишь форму в своей ветке. Claude Code сам подскажет команды, но понимать, что делает каждая, стоит — это твои точки отката.

05

Vercel: push в Git → форма в проде

Дальше самое приятное. Vercel привязан к репозиторию: как только код уходит в Git, он сам собирает и публикует сайт. Никаких серверов руками. После деплоя первым делом проверяю, что эндпоинт живой — бью в него из терминала curl, не открывая форму:

# проверяем, что эндпоинт принимает заявку
curl -X POST https://klinika.kz/api/booking \
  -H "Content-Type: application/json" \
  -d '{"name":"Тест","phone":"+77001234567","service":"УЗИ"}'

# ждём в ответ: {"ok":true} и сообщение в Telegram
// curl шлёт тестовую заявку прямо в прод — если пришло в Telegram, эндпоинт жив

Пришёл {"ok":true} и сообщение в Telegram — бэкенд работает. Осталось подключить домен. В Vercel добавляешь свой адрес, он выдаёт, что прописать у регистратора: либо A-запись на IP Vercel, либо сменить nameservers на серверы Vercel. Второе проще — Vercel сам рулит всеми записями. DNS обновляется от нескольких минут до пары часов, тут остаётся только подождать.

01
Push в Git
Отправил ветку — Vercel увидел изменения и сам запустил сборку. Ничего не нажимаешь.
02
Автодеплой
Через минуту-две форма и эндпоинт уже в вебе по временному адресу Vercel.
03
Проверка curl
Тестовая заявка прямо в прод. Пришла в Telegram — значит цепочка форма → сервер → сигнал жива.
04
Домен
A-запись или nameservers у регистратора. Подождал, пока DNS обновится — форма на своём адресе.
06

Грабли: где я реально споткнулся

Теперь честная часть, ради которой стоит читать чужой опыт. Каждый из этих трёх пунктов стоил мне времени, и на каждом я сначала грешил на ИИ, а виноват был не он.

База молча не писала заявки. Форма отправлялась, Telegram приходил, а в базе пусто. Причина — защита доступа на уровне строк (RLS): по умолчанию она запрещает запись анонимному клиенту. Это правильно, что она включена, но эндпоинт должен писать сервисной ролью — отдельным серверным ключом, который эти ограничения обходит легально. Как только эндпоинт пошёл в базу сервисным ключом (а не публичным), заявки легли на место. Публичный ключ на фронт, сервисный — только на сервер.

CORS резал запрос. Пока форма и эндпоинт на одном домене — всё гладко. Стоит вынести форму на отдельный адрес — браузер начинает блокировать запрос с чужого источника. Лечится заголовками на эндпоинте, разрешающими твой домен. Знать про это заранее — сэкономить полдня гадания, почему «в одном месте работает, в другом нет».

Сессии и переменные окружения. Локально всё работало, в проде эндпоинт падал. Оказалось — забыл добавить TG_TOKEN и ключи базы в переменные окружения на самом Vercel. На ноутбуке они лежали в локальном файле, а в облако не уехали (и правильно — секреты в Git не коммитят). Правило: каждый ключ, который есть локально, продублируй в настройках Vercel.

Задачу ставь как ТЗ. Секреты — только на сервере. В базу пиши сервисной ролью. Проверяй эндпоинт через curl до того, как открыл форму. И тогда «собрать форму записи» — это вечер, а не проект на месяц.

Это ровно тот же принцип, на котором держатся мои большие системы: маленькие проверяемые кирпичики, каждый из которых можно потрогать отдельно. Если хочешь увидеть, как из таких кирпичиков собирается связка ИИ с живой базой бизнеса — я разбирал это в отдельной статье про подключение Claude к CRM за вечер.

Два пути дальше

У тебя клиника или бизнес и нужна такая же форма записи, которая шлёт заявку сразу в Telegram и не теряет ни одной, — напиши слово РАЗБОР в директ @la_konstant: разберём твою воронку записи за 60 минут. Хочешь сам научиться собирать такие вещи через Claude Code, без классического программирования, — напиши ОБУЧЕНИЕ в директ, беру 2–3 человека на реальных проектах.

Написать в директ → @la_konstant