Skip to main content

How to Build a Telegram Bot with Node.js and TDLib to Read and Process Messages

5 min readBy Hamza

Learn how to build a production-ready Telegram bot using Node.js and TDLib that can read and process messages from any channel, including private channels.

telegramnodejstdlibbotautomation

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: