
Привет, Хабр! В предыдущей статье о библиотеке grammY мы подробно разобрали основы создания Telegram-ботов на JavaScript. Кажется, настало время погрузиться в тему глубже и научиться добавлять более сложные фичи.
Мы подготовил пару инструкций по этой теме. В этой части разберем, как подключить базу данных и настроить регистрацию пользователей. А еще заложим фундамент, чтобы в будущем сделать интерактивное меню и подключить платежный модуль. По итогу у нас получится бот с простым, но рабочим онлайн-магазином, оплатой и взаимодействием с MongoDB. Подробности под катом!
Создание бота и получение токена
Первым делом необходимо создать — по сути, зарегистрировать — своего бота в Telegram. Для этого нужно сделать всего несколько действий.
1. Открываем Telegram и в поиске вбиваем BotFather.
2. Отправляем ему команду /newbot, после чего будет предложено выбрать имя и уникальный username — причем он должен обязательно содержать слово bot.
3. После успешного создания бота BotFather пришлет длинное сообщение, в котором будет токен в формате 1234567890:UIEaeSx_YsRXdD-C39M0t1PzcdnZZ4HgsKq — никогда не показывайте и не публикуйте нигде свой токен, так как с помощью него кто угодно сможет управлять вашим ботом.

Регистрация бота в Telegram.
Все готово — теперь можно переходить к развертыванию проекта.

Развертывание проекта
Писать бота мы будем не с нуля. Пропустим создание проекта и стартовой структуры — все нужные файлы находятся в моем репозитории, который вам необходимо просто клонировать с помощью следующей команды:
git clone https://github.com/arseniypom/grammy-tg-bot.git
В проекте есть следующие файлы:
– .gitignore — чтобы не залить на GitHub ненужные зависимости и секретные env переменные (например, ключ от бота);
– index.js — файл с кодом Telegram-бота;
– package.json и package-lock.json — информация о проекте, скрипты и зависимости.
Рассмотрим файл index.js:
import 'dotenv/config';
import { Bot, GrammyError, HttpError } from 'grammy';
const bot = new Bot(process.env.BOT_TOKEN);
// Ответ на команду /start
bot.command('start', (ctx) =>
ctx.reply('Привет! Отправь мне любой текст, и я его повторю.'),
);
// Ответ на любое сообщение
bot.on('message', (ctx) => {
ctx.reply(ctx.message.text);
});
// Обработка ошибок согласно документации
bot.catch((err) => {
const ctx = err.ctx;
console.error(`Error while handling update ${ctx.update.update_id}:`);
const e = err.error;
if (e instanceof GrammyError) {
console.error('Error in request:', e.description);
} else if (e instanceof HttpError) {
console.error('Could not contact Telegram:', e);
} else {
console.error('Unknown error:', e);
}
});
// Функция запуска бота
async function startBot() {
try {
bot.start();
console.log('Bot started');
} catch (error) {
console.error('Error in startBot:', error);
}
}
startBot();
Чтобы запустить бота, необходимо выполнить всего три шага.
1. Установите зависимости:
npm i
2. Создайте файл .env с ключом вашего бота:
BOT_TOKEN=1234567890:UIEaeSx_YsRXdD-C39M0t1PzcdnZZ4HgsKq
3. Выполните команду для запуска в консоли:
npm run start
Теперь можно открыть и протестировать бота в Telegram:

Подключение TypeScript
Перед тем, как добавить TypeScript в проект, занесем файл index.js в папку src. Делается это для того, чтобы все файлы .ts лежали в одной папке — так будет проще конфигурировать проект. Сделали? Тогда идем дальше.
1. Устанавливаем необходимые зависимости такой командой:
npm i -D typescript tsx @types/node
Здесь typescript — сам TypeScript, tsx — исполнитель TypeScript-кода в среде Node.js, который позволяет напрямую запускать его без предварительной компиляции в JavaScript. А @types/node — пакет с TypeScript-определениями типов для встроенных модулей и API Node.js.
2. Чтобы TypeScript корректно работал, создаем конфиг-файл tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}
В этом файле мы устанавливаем целевую версию для компиляции JS (ES2020), включаем строгую проверку типов, добавляем папку для скомпилированных файлов (dist), указываем где лежат исходники (src), говорим компилятору обрабатывать все файлы из папки src и игнорировать node_modules.
3. Сразу добавляем папку dist в .gitignore:
.env
node_modules
.DS_Store
dist
4. Обновляем package.json — в нем нужно заменить main на dist/index.js, так как в случае с продакшн-сборкой именно там будет находиться файл JavaScript. Также необходимо обновить скрипты:
"main": "dist/index.js",
"scripts": {
"dev": "nodemon --exec tsx src/index.ts",
"build": "tsc",
"start:prod": "node dist/index.js"
},
5. Осталось только переименовать index.js в index.ts в папке src. Как только вы это сделаете, сразу увидите две ошибки в файле:

TypeScript ругается, что мы передаем env-переменную в new Bot, хотя на самом деле не знаем, существует она или нет. Пофиксим, добавив проверку с if:
const BOT_API_KEY = process.env.BOT_API_KEY;
if (!BOT_API_KEY) {
throw new Error('BOT_API_KEY is not defined');
}
const bot = new Bot(BOT_API_KEY);
Вторая проблема заключается в следующем: так как в bot.on мы выставили слушатель на событие message в целом, то не знаем, есть ли в нем текст или нет. И это логично, ведь сообщение может состоять, например, просто из картинки. Пофиксим это, уточнив, что хотим реагировать только на те сообщения с текстом. Для этого к message добавим :text.
bot.on('message:text', (ctx) => {
ctx.reply(ctx.message.text);
});
Отлично — TypeScript подключен. Переходим к подключению базы данных и контейнеризации бота.
Подключение MongoDB и настройка Docker
Если вы ни разу не работали с Docker, не переживайте — я максимально подробно распишу все шаги.
Шаг 1. Устанавливаем Docker. Для начала убедитесь, что на вашей машине установлен Docker и Docker Compose. Если еще нет, воспользуйтесь официальной документацией. Для Windows и Mac подойдет Docker Desktop, для Linux — своя сборка в зависимости от дистрибутива. После установки нужно проверить работу с помощью следующих команд:
docker -v
docker-compose -v

Шаг 2. Создаем Dockerfile. В корне проекта (где лежит package.json и папка src) создаем файл Dockerfile со следующим содержимым:
# Базовый образ Node.js (легковесная версия на Alpine Linux)
FROM node:18-alpine
# Создаем рабочую директорию в контейнере
WORKDIR /app
# Копируем package.json и package-lock.json
COPY package*.json ./
# Устанавливаем зависимости
RUN npm install
# Копируем весь проект внутрь контейнера
COPY . .
# Собираем TypeScript
RUN npm run build
# Задаем команду, которая будет выполняться при запуске контейнера
# Для dev-режима может быть и "npm run dev", но сейчас для примера укажем старт продакшн-сборки
CMD ["npm", "run", "start:prod"]
В Dockerfile в качестве базы указали образ node:18-alpine, чтобы контейнер был легким (Alpine — урезанная Linux-система), задали рабочую директорию /app внутри контейнера, скопировали и установили пакеты. Кроме того — перенесли весь исходный код, скомпилировали TypeScript и определили команду для запуска бота в продакшн-режиме.
Шаг 3. Создаем docker-compose.yml для одновременной работы бота и MongoDB. Чтобы одновременно запустить и бота, и базу MongoDB, создадим в корне проекта файл docker-compose.yml. В нем опишем два сервиса: bot (наш Telegram-бот) и mongo (база данных).
services:
bot:
build: .
container_name: telegram-bot
env_file:
- .env
# Если нужно "пробросить" порт для webhook,
# то указываем, например: "3000:3000".
# В нашем случае используется long polling, так что доступ к порту не требуется,
# но вот пример вам на будущее:
ports:
- '3000:3000'
depends_on:
- mongo
networks:
- app-network
mongo:
image: mongo:6
container_name: mongo-db
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: rootpassword
ports:
- '27017:27017'
networks:
- app-network
networks:
app-network:
Расшифровка
- build:. — говорим Docker Compose, что нужно собрать контейнер для бота, используя Dockerfile, лежащий в текущей директории.
- container_name — задаем удобное имя контейнеру, чтобы потом не путаться.
- env_file: — .env — подгружаем переменные окружения из .env (там у нас лежит BOT_TOKEN и любые другие ключи).
- depends_on — показывает, что bot стартует после запуска mongo.
- mongo — здесь мы не билдим ничего, а берем готовый образ mongo:6.
- environment — задаем учетные данные для Mongo.
- ports — пробрасываем порт 27017, чтобы при необходимости подключаться к базе извне.
- networks — определяем виртуальную сеть app-network, в которой эти контейнеры будут дружить между собой. Оставляем ее «пустой» (то есть без указания дополнительных параметров). В этом случае Docker Compose автоматически создаст сеть с настройками по умолчанию.
Шаг 4. Создаем .dockerignore и обновляем .env. Чтобы автоматически не добавлять в контейнер лишние файлы (например, node_modules или логи), создадим в корне проекта .dockerignore (аналог .gitignore, но для Docker) с таким содержимым:
node_modules<br>dist<br>.git<br>.gitignore<br>docker-compose.yml
В .env рядом с токеном создадим переменную для доступа к базе данных:
BOT_TOKEN=7931645971:AAHoi2edFIHnym2DogmfLrzoToMAio0TOlc
MONGODB_URI=mongodb://root:rootpassword@mongo:27017/telegram_bot?authSource=admin
Шаг 5. Запускаем проект. Протестируем сборку. Запускаем установленную программу Docker, а затем возвращаемся в терминал в папке с проектом и выполняем:
docker-compose up -d
Параметр -d запустит все в фоновом режиме. Сперва будет загружен образ mongo, а затем наши контейнеры должны собраться и подняться.

Вывод команды docker-compose up-d.
Проверяем, поднялись ли наши контейнеры:
docker ps

Вы увидите два контейнера: telegram-bot и mongo-db. Если что-то пошло не так, смотрим логи:
docker-compose logs -f
С помощью данной команды можно проверить, нет ли ошибок при установке зависимостей или при запуске нашего бота.
Шаг 6. Подключаемся к базе. Для этого установим зависимости mongoose и его типы:
npm i mongoose && npm i -D @types/mongoose
Теперь настроим подключение к базе данных через mongoose. В файле index.ts импортируем mongoose:
import mongoose from 'mongoose';
Далее обновим функцию запуска бота:
async function startBot() {
const MONGODB_URI = process.env.MONGODB_URI;
if (!MONGODB_URI) {
throw new Error('MONGODB_URI is not defined');
}
try {
await mongoose.connect(MONGODB_URI);
bot.start();
console.log('MongoDB connected & bot started');
} catch (error) {
console.error('Error connecting to MongoDB:', error);
}
}
Чтобы было удобнее локально разрабатывать бота, быстрее всего будет перейти обратно на npm run dev. Но при этом нам важно, чтобы была доступна и база. Что для этого нужно сделать?
Во-первых, нужно заменить строку для подключения в .env:
BOT_TOKEN=7931645971:AAHoi2edFIHnym2DogmfLrzoToMAio0TOlc
# MONGODB_URI=mongodb://root:rootpassword@mongo:27017/telegram_bot?authSource=admin
MONGODB_URI=mongodb://root:rootpassword@localhost:27017
Во-вторых, необходимо остановить бота в Docker:
docker compose stop bot
Теперь запустим бота командой и убедимся, что все работает:

Запуск бота командой npm run dev, вывод.
Разработка регистрации пользователя
Регистрация пользователя, а точнее сохранение его данных в нашу базу данных, будет происходить по команде /start. Чтобы создать модель юзера, давайте узнаем, какие его данные нам доступны. Для этого в команде выведем в консоль данные отправителя сообщения:
bot.command('start', (ctx) => {
console.log(ctx.from);
ctx.reply('Привет! Отправь мне любой текст, и я его повторю.');
});
Отправляем боту команду /start и видим в консоли такой объект:
{
id: 265123456,
is_bot: false,
first_name: 'Arseniy',
username: 'realusername',
language_code: 'en',
is_premium: true
}
Из него нам пригодится id, имя (first_name) и username. Создадим интерфейс и модель Mongo с этими данными в папке src/models. Файл назовем User.ts:
import { Schema, model, Document } from 'mongoose';
export interface IUser extends Document {
telegramId: number;
firstName: string;
username: string;
createdAt: Date;
}
const userSchema = new Schema<IUser>(
{
telegramId: {
type: Number,
required: [true, 'Telegram ID is required'],
unique: true,
},
firstName: { type: String },
username: { type: String },
},
{
timestamps: true,
},
);
export const User = model<IUser>('User', userSchema);
Теперь обновим обработку команды /start в файле src/index.ts, чтобы реализовать проверку и регистрацию пользователя: если пользователь уже зарегистрирован (данные сохранены в БД), будем просто отвечать ему сообщением. Если еще не зарегистрирован, то сперва сохранять его данные, а затем отвечать.
Обновленный код:
// Ответ на команду /start
bot.command('start', async (ctx) => {
if (!ctx.from) {
return ctx.reply('Error: User information not available');
}
const { id, first_name, username } = ctx.from;
try {
// Ищем пользователя в базе данных и сохраняем найденный
// документ в переменную existingUser
const existingUser = await User.findOne({ telegramId: id });
// Если пользователь уже существует, отправляем сообщение
// и выходим из функции
if (existingUser) {
return ctx.reply('Вы уже зарегистрированы');
}
// Создаем нового пользователя
const newUser = new User({
telegramId: id,
firstName: first_name,
username,
});
// Сохраняем нового пользователя в базу данных
await newUser.save();
// Отправляем сообщение о успешной регистрации
ctx.reply('Вы успешно зарегистрированы!');
} catch (error) {
console.error('Ошибка при регистрации пользователя:', error);
ctx.reply('Произошла ошибка, попробуйте позже.');
}
});
Протестируем Telegram-бота:

И первый, и повторный вызовы команды /start отработали верно. Проверим, что данные сохранились в БД: для этого можно использовать программу MongoDB Compass, она позволяет подключиться к любой базе на монго и посмотреть её содержимое.
1. Устанавливаем программу с официального сайта, запускаем и жмем Add new connection:

2. Далее вводим строку из файла .env:

3. Нажимаем Save & Connect и в левой панели видим новый кластер:

4. Открываем раздел test → users:

Видим, что данные сохранились: слева — коллекция users, справа — единственный документ в коллекции, то есть первый зарегистрированный пользователь.
Следующий шаг — добавить интерактивное меню. Но об этом я расскажу в следующей статье. Подписывайтесь и следите за новыми материалами в блоге Selectel.
Автор: Арсений Помазков, фронтенд-разработчик и автор одноименного YouTube-канала
Комментарии (3)
devprodest
21.05.2025 13:45Для продакшена возможно лучшим будут пересобрать образ и избавиться от ненужных файлов в виде исходников, девзависимостей и др.
Malik741
интересно, спасибо