Introduction
Building a Telegram bot that can read and process messages from channels—especially private channels—isn't as straightforward as it seems. The standard Telegram Bot API has limitations: it can't access private channels, has rate limiting issues, and relies on webhooks that can be unreliable.
After struggling with these limitations, I discovered TDLib—Telegram's official library that gives you full access to Telegram's features. In this article, I'll show you how to build a production-ready Telegram bot using Node.js and TDLib.
Step 1: Set Up TDLib Client
Install dependencies:
npm install tdl prebuilt-tdlib dotenv
Create your connection file:
// utils/connect.js
const tdl = require("tdl");
const { getTdjson } = require("prebuilt-tdlib");
tdl.configure({ tdjson: getTdjson() });
async function connectClient(apiId, apiHash) {
const client = tdl.createClient({
apiId, apiHash,
databaseDirectory: "_td_database",
filesDirectory: "_td_files",
useMessageDatabase: true
});
// Register event handler BEFORE login
client.on("update", async (update) => {
await processTelegramEvent(update);
});
await client.login();
return client;
}
module.exports = { connectClient };
Important: Get your apiId and apiHash from Telegram's api.
Step 2: Process Incoming Messages
// services/telegramEventService.js
function extractChatId(update) {
if (!update.message?.chat_id) return null;
return String(update.message.chat_id);
}
function extractMessageText(update) {
if (!update.message?.content) return null;
const content = update.message.content;
if (content._ === "messageText") {
return content.text.text;
}
if (content._ === "messagePhoto" && content.caption) {
return content.caption.text;
}
return null;
}
async function processTelegramEvent(update) {
if (update._ !== "updateNewMessage" || !update.message) return;
const chatId = extractChatId(update);
const message = extractMessageText(update);
if (!chatId || !message) return;
console.log(`Message from ${chatId}: ${message.substring(0, 50)}...`);
await processMessage(message, chatId);
}
async function processMessage(message, chatId) {
// Your custom logic here
}
module.exports = { processTelegramEvent };
Step 3: Main Application
// index.js
const { connectClient } = require("./utils/connect");
require("dotenv").config();
const apiId = process.env.API_ID ? Number(process.env.API_ID) : undefined;
const apiHash = process.env.API_HASH;
if (!apiId || !apiHash) {
console.error("Missing API credentials");
process.exit(1);
}
(async () => {
const client = await connectClient(apiId, apiHash);
const me = await client.invoke({ _: "getMe" });
console.log("Logged in as:", me.first_name);
console.log("Bot is listening...");
})();
Step 4: Environment Variables
Create .env:
API_ID=your_api_id_here
API_HASH=your_api_hash_here
Step 5: Docker Deployment
Dockerfile:
FROM node:24-slim
RUN apt-get update && apt-get install -y libglib2.0-0 zlib1g && rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app
COPY package.json package-lock.json* ./
RUN npm ci --only=production
COPY . .
RUN mkdir -p /usr/src/app/logs /usr/src/app/data /usr/src/app/_td_database
ENV NODE_ENV=production
CMD ["node", "index.js"]
docker-compose.yml:
services:
bot:
build: .
restart: unless-stopped
env_file: .env
volumes:
- ./logs:/usr/src/app/logs
- ./data:/usr/src/app/data
- ./_td_database:/usr/src/app/_td_database
Run with:
docker-compose up -d --build
Key Features
Channel Authorization:
const allowedChannels = ["-1001693996492", "-1001640332422"];
if (allowedChannels.includes(chatId)) {
await processMessage(message, chatId);
}
Message History:
const messages = await client.invoke({
_: "getChatHistory",
chat_id: chatId,
limit: 100
});
Lessons Learned
TDLib is Worth the Complexity - Setting up TDLib is harder than the Bot API, but it gives you access to features that simply aren't possible otherwise—like reading from private channels.
Register Handlers Before Login - If you register handlers after client.login(), you'll miss messages. Always register handlers first.
Handle Message Types - TDLib has many message types (text, photo, video). Always check the content._ field.
Use Docker for Production - TDLib has native dependencies. Docker eliminates these issues.
Conclusion
Building a Telegram bot with TDLib and Node.js gives you powerful capabilities that the standard Bot API simply can't match—especially access to private channels and complete message history. While TDLib has a steeper learning curve, the extra control and features are worth it for production applications.
The combination of Node.js's async capabilities and TDLib's full feature set creates a robust foundation for any Telegram automation project. Whether you're monitoring channels, archiving messages, or building complex automation, this setup will handle it reliably at scale.
Related Articles:
- Killing Zombie Processes in Docker - Docker best practices
- Claude Code Best Practices - How I use AI to write production code