Pendahuluan
Pada tutorial ini, saya akan membangun bot Telegram sederhana yang mendukung operasi CRUD (Create, Read, Update, Delete). Studi kasusnya adalah manajemen data item lewat chat Telegram, sehingga kita bisa menambah, melihat, mengubah, dan menghapus data langsung dari bot.
Pondasi proyek dan tujuan CRUD bot Telegram
Saya menggunakan Node.js dan library Telegraf karena setup-nya ringan, dokumentasinya matang, dan cocok untuk membuat bot dengan command berbasis teks. Agar fokus ke konsep CRUD, data disimpan dulu dalam file JSON lokal. Pendekatan ini cocok untuk belajar alur dasar sebelum pindah ke database seperti PostgreSQL atau MongoDB.
- Bot bisa menerima command create data
- Bot bisa menampilkan semua data dan data per ID
- Bot bisa update data berdasarkan ID
- Bot bisa delete data berdasarkan ID
- Semua perubahan data tersimpan secara persisten di file lokal
- Memiliki domain atau sub-domain
- Memiliki akses ke VPS dengan akses sudo
- Sudah install Node.js versi LTS, npm, telegraf
- Sudah memiliki bot token dari BotFather
- Sudah menyiapkan project bot Telegram CRUD di komputer lokal
Instalasi dependensi
1. Lakukan update:
Ubuntu:
sudo apt update && sudo apt upgrade -y
Rocky Linux:
sudo dnf update -y
2. Install nodejs:
Ubuntu:
sudo apt install nodejs -y
Rocky Linux:
sudo dnf install nodejs -y
3. Update npm:
Ubuntu:
sudo npm install -g npm@latest
Rocky Linux:
sudo npm install -g npm@latest
4. Install telegraf:
Ubuntu:
npm install telegraf
Rocky Linux:
npm install telegraf
Struktur folder
bot
├── .env
├── index.js
└── data.json
.env:
BOT_TOKEN=ISI_BOT_TOKEN_ANDA_DISINI
data.json:
[]
index.js:
require("dotenv").config();
const fs = require("node:fs");
const path = require("node:path");
const { Telegraf, Markup } = require("telegraf");
const DATA_FILE = path.join(__dirname, "data.json");
const token = process.env.BOT_TOKEN;
if (!token) {
throw new Error("BOT_TOKEN belum diatur. Silakan isi file .env terlebih dahulu.");
}
function ensureDataFile() {
if (!fs.existsSync(DATA_FILE)) {
fs.writeFileSync(DATA_FILE, '{"users":{}}', "utf8");
}
}
function normalizeStore(parsed) {
let store = { users: {} };
if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && parsed.users) {
store = parsed;
} else if (Array.isArray(parsed)) {
const lastId =
parsed.length === 0 ? 0 : Math.max(...parsed.map((item) => Number(item.id) || 0));
store = {
users: {
legacy: {
lastId,
items: parsed
}
}
};
}
// Migrasi otomatis: Ubah kunci ID numerik (lama) ke format u_ID (baru)
for (const key in store.users) {
if (key !== "legacy" && !key.startsWith("u_") && !isNaN(key)) {
store.users[`u_${key}`] = store.users[key];
delete store.users[key];
}
}
return store;
}
function readStore() {
ensureDataFile();
const raw = fs.readFileSync(DATA_FILE, "utf8");
try {
const parsed = JSON.parse(raw);
return normalizeStore(parsed);
} catch {
return { users: {} };
}
}
function writeStore(store) {
fs.writeFileSync(DATA_FILE, JSON.stringify(store, null, 2), "utf8");
}
function getUserBucket(store, userId) {
if (!userId) return { lastId: 0, items: [] };
// Gunakan prefix 'u_' untuk membedakan ID user murni dengan kunci internal seperti 'legacy'
const key = `u_${userId}`;
if (!store.users[key]) {
store.users[key] = {
lastId: 0,
items: []
};
}
return store.users[key];
}
function createItem(userId, name, description) {
const store = readStore();
const bucket = getUserBucket(store, userId);
const nextId = Number(bucket.lastId || 0) + 1;
const newItem = {
id: nextId,
name,
description,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
bucket.lastId = nextId;
bucket.items.push(newItem);
writeStore(store);
return newItem;
}
function getAllItems(userId) {
const store = readStore();
const bucket = getUserBucket(store, userId);
return bucket.items;
}
function getItemById(userId, id) {
const store = readStore();
const bucket = getUserBucket(store, userId);
return bucket.items.find((item) => Number(item.id) === Number(id)) || null;
}
function updateItem(userId, id, name, description) {
const store = readStore();
const bucket = getUserBucket(store, userId);
const index = bucket.items.findIndex((item) => Number(item.id) === Number(id));
if (index === -1) {
return null;
}
bucket.items[index] = {
...bucket.items[index],
name,
description,
updatedAt: new Date().toISOString()
};
writeStore(store);
return bucket.items[index];
}
function deleteItem(userId, id) {
const store = readStore();
const bucket = getUserBucket(store, userId);
const index = bucket.items.findIndex((item) => Number(item.id) === Number(id));
if (index === -1) {
return false;
}
bucket.items.splice(index, 1);
writeStore(store);
return true;
}
const bot = new Telegraf(token);
const userStates = new Map();
const PAGE_SIZE = 5; // Ukuran halaman lebih kecil agar nyaman di layar HP
function mainMenuKeyboard() {
return Markup.inlineKeyboard([
[Markup.button.callback("Create", "action:create")],
[Markup.button.callback("Read Semua", "action:read_all")],
[Markup.button.callback("Cari ID", "action:read_id")],
[Markup.button.callback("Ubah", "action:update"), Markup.button.callback("Hapus", "action:delete")]
]);
}
function paginationKeyboard(page, totalPages) {
const buttons = [];
if (page > 1) buttons.push(Markup.button.callback("⬅️ Prev", `page:${page - 1}`));
if (page < totalPages) buttons.push(Markup.button.callback("Next ➡️", `page:${page + 1}`));
return Markup.inlineKeyboard([
buttons,
[Markup.button.callback("🏠 Menu Utama", "action:menu")]
]);
}
function buildReadAllView(userId, page = 1) {
const items = getAllItems(userId);
if (items.length === 0) {
return { text: "Data masih kosong.", keyboard: mainMenuKeyboard() };
}
const totalPages = Math.ceil(items.length / PAGE_SIZE);
const safePage = Math.min(Math.max(page, 1), totalPages);
const start = (safePage - 1) * PAGE_SIZE;
const currentItems = items.slice(start, start + PAGE_SIZE);
const list = currentItems
.map((item) => `🔹 *ID: ${item.id}* - ${item.name}`)
.join("\n");
return {
text: `📂 *Daftar Data (Hal: ${safePage}/${totalPages})*\n\n${list}\n\n_Pilih ID untuk detail menggunakan menu Cari ID._`,
keyboard: paginationKeyboard(safePage, totalPages)
};
}
function confirmDeleteKeyboard() {
return Markup.inlineKeyboard([
[
Markup.button.callback("Ya, Hapus", "action:confirm_delete"),
Markup.button.callback("Batal", "action:cancel")
]
]);
}
function formatItem(item) {
return `ID: ${item.id}\nNama: ${item.name}\nDeskripsi: ${item.description}`;
}
function getUserId(ctx) {
return ctx.from?.id;
}
async function showMainMenu(ctx, message = "Silakan pilih aksi CRUD:") {
const keyboard = mainMenuKeyboard();
if (ctx.callbackQuery) {
return ctx.editMessageText(message, {
reply_markup: keyboard.reply_markup,
parse_mode: "Markdown"
});
}
await ctx.reply(message, { reply_markup: keyboard.reply_markup, parse_mode: "Markdown" });
}
bot.start((ctx) => {
const userId = getUserId(ctx);
if (userId) {
userStates.delete(userId);
}
return showMainMenu(ctx, "Halo! Saya bot CRUD. Gunakan tombol di bawah ini.");
});
bot.action("action:menu", async (ctx) => {
const userId = getUserId(ctx);
if (userId) userStates.delete(userId);
await ctx.answerCbQuery();
return showMainMenu(ctx);
});
bot.action("action:create", async (ctx) => {
const userId = getUserId(ctx);
if (userId) {
userStates.set(userId, { action: "create", step: "name" });
}
await ctx.answerCbQuery();
await ctx.reply("📝 *Buat Data*\nMasukkan nama item:", { parse_mode: "Markdown" });
});
bot.action("action:read_all", async (ctx) => {
const userId = getUserId(ctx);
if (!userId) return;
await ctx.answerCbQuery();
const view = buildReadAllView(userId, 1);
await ctx.editMessageText(view.text, {
reply_markup: view.keyboard.reply_markup,
parse_mode: "Markdown"
});
});
bot.action(/page:(\d+)/, async (ctx) => {
const userId = getUserId(ctx);
const page = parseInt(ctx.match[1]);
await ctx.answerCbQuery();
const view = buildReadAllView(userId, page);
await ctx.editMessageText(view.text, {
reply_markup: view.keyboard.reply_markup,
parse_mode: "Markdown"
});
});
bot.action("action:read_id", async (ctx) => {
const userId = getUserId(ctx);
if (userId) {
userStates.set(userId, { action: "read_id", step: "id" });
}
await ctx.answerCbQuery();
await ctx.reply("Masukkan ID item yang ingin dilihat:");
});
bot.action("action:update", async (ctx) => {
const userId = getUserId(ctx);
if (userId) {
userStates.set(userId, { action: "update", step: "id" });
}
await ctx.answerCbQuery();
await ctx.reply("Masukkan ID item yang ingin diupdate:");
});
bot.action("action:delete", async (ctx) => {
const userId = getUserId(ctx);
if (userId) {
userStates.set(userId, { action: "delete", step: "id" });
}
await ctx.answerCbQuery();
await ctx.reply("Masukkan ID item yang ingin dihapus:");
});
bot.action("action:confirm_delete", async (ctx) => {
const userId = getUserId(ctx);
const state = userStates.get(userId);
if (!state || state.action !== "delete" || !state.id) {
await ctx.answerCbQuery("Sesi kadaluarsa.");
return showMainMenu(ctx);
}
const success = deleteItem(userId, state.id);
userStates.delete(userId);
await ctx.answerCbQuery("Data dihapus.");
await ctx.reply(`Data dengan ID ${state.id} berhasil dihapus.`);
return showMainMenu(ctx);
});
bot.action("action:cancel", async (ctx) => {
const userId = getUserId(ctx);
if (userId) userStates.delete(userId);
await ctx.answerCbQuery("Dibatalkan.");
await ctx.reply("Aksi dibatalkan.");
return showMainMenu(ctx);
});
bot.on("text", async (ctx) => {
const userId = getUserId(ctx);
if (!userId) {
return;
}
const state = userStates.get(userId);
const text = (ctx.message?.text || "").trim();
if (!state) {
if (text.startsWith("/") && text !== "/start") {
return ctx.reply("Gunakan /start lalu pilih tombol menu.");
}
return;
}
if (state.action === "create") {
if (state.step === "name") {
if (!text) {
return ctx.reply("Nama item tidak boleh kosong. Masukkan nama item:");
}
userStates.set(userId, {
action: "create",
step: "description",
name: text
});
return ctx.reply("Masukkan deskripsi item:");
}
if (state.step === "description") {
if (!text) {
return ctx.reply("Deskripsi item tidak boleh kosong. Masukkan deskripsi item:");
}
const item = createItem(userId, state.name, text);
userStates.delete(userId);
await ctx.reply(`Data telah disimpan.\n\nID: ${item.id}\nNama: ${item.name}\nDeskripsi: ${item.description}`);
return showMainMenu(ctx);
}
}
if (state.action === "read_id" && state.step === "id") {
const targetId = Number(text);
const item = isNaN(targetId) ? null : getItemById(userId, targetId);
userStates.delete(userId);
if (!item) {
await ctx.reply("Data tidak ditemukan.");
return showMainMenu(ctx);
}
await ctx.reply(formatItem(item));
return showMainMenu(ctx);
}
if (state.action === "update") {
if (state.step === "id") {
const targetId = Number(text);
const item = isNaN(targetId) ? null : getItemById(userId, targetId);
if (!item) {
return ctx.reply("ID tidak ditemukan. Masukkan ID item yang valid:");
}
userStates.set(userId, {
action: "update",
step: "name",
id: Number(text)
});
return ctx.reply("Masukkan nama baru:");
}
if (state.step === "name") {
if (!text) {
return ctx.reply("Nama baru tidak boleh kosong. Masukkan nama baru:");
}
userStates.set(userId, {
action: "update",
step: "description",
id: state.id,
name: text
});
return ctx.reply("Masukkan deskripsi baru:");
}
if (state.step === "description") {
if (!text) {
return ctx.reply("Deskripsi baru tidak boleh kosong. Masukkan deskripsi baru:");
}
const updated = updateItem(userId, state.id, state.name, text);
userStates.delete(userId);
if (!updated) {
await ctx.reply("Data tidak ditemukan.");
return showMainMenu(ctx);
}
await ctx.reply(
`Data berhasil diperbarui.\n\nID: ${updated.id}\nNama: ${updated.name}\nDeskripsi: ${updated.description}`
);
return showMainMenu(ctx);
}
}
if (state.action === "delete" && state.step === "id") {
const targetId = Number(text);
const item = isNaN(targetId) ? null : getItemById(userId, targetId);
if (!item) {
await ctx.reply("Data tidak ditemukan. Masukkan ID yang valid:");
return;
}
userStates.set(userId, {
action: "delete",
step: "confirm",
id: targetId
});
await ctx.reply(
`Konfirmasi Hapus?\n\n${formatItem(item)}`,
confirmDeleteKeyboard()
);
return;
}
userStates.delete(userId);
await ctx.reply("Sesi input direset. Silakan mulai lagi dari menu.");
return showMainMenu(ctx);
});
const useWebhook = Boolean(process.env.WEBHOOK_DOMAIN);
const port = Number(process.env.PORT) || 3000;
const launchOptions = useWebhook
? {
webhook: {
domain: process.env.WEBHOOK_DOMAIN,
port: port,
},
}
: {};
bot.launch(launchOptions)
.then(() => {
if (useWebhook) {
console.log("🚀 Bot berjalan via WEBHOOK");
console.log(`🔗 Domain: ${process.env.WEBHOOK_DOMAIN}`);
console.log(`🔌 Port: ${port}`);
} else {
console.log("⚡ Bot berjalan via POLLING (Lokal)");
}
})
.catch((err) => {
console.error("❌ Gagal menjalankan bot:", err);
});
process.once("SIGINT", () => bot.stop("SIGINT"));
process.once("SIGTERM", () => bot.stop("SIGTERM"));
Kesimpulan
Pada tutorial ini, kita berhasil membangun bot Telegram CRUD yang sederhana namun siap pakai untuk pemula. Dengan pendekatan satu file utama dan penyimpanan lokal di data.json, proses belajar jadi lebih fokus ke konsep inti: bagaimana data dibuat, dibaca, diperbarui, dan dihapus melalui interaksi chat.
Perubahan dari command ke tombol juga membuat pengalaman pengguna lebih profesional. Pengguna cukup memulai dengan /start, lalu seluruh alur CRUD bisa dijalankan lewat menu tombol dengan panduan input bertahap. Pendekatan ini mengurangi kesalahan input, memperjelas alur, dan membuat bot lebih nyaman digunakan.
Meski masih menggunakan file JSON, fondasi aplikasinya sudah kuat untuk dikembangkan lebih lanjut. Tahap berikut yang paling disarankan adalah menambahkan database (misalnya SQLite/MySQL/PostgreSQL), validasi data yang lebih ketat, serta fitur keamanan dan logging agar bot siap dipakai di skenario yang lebih besar.
Baca Juga
29/06/2026
Mengenal Perintah-Perintah Pada Debian atau Ubuntu
Panduan praktis mengenai perintah dasar berbasis CLI (Command Line Interface) untuk mengelola VPS…
0 komentar6 menit baca
29/06/2026
Memasang WordPress dan Caddy Web Server di Debian 13
Memasang WordPress menggunakan Caddy Web Server di Debian 13. Cepat, ringan, dan cocok untuk pemula…
0 komentar9 menit baca
12/06/2026
Antara Python atau NodeJS Untuk Membuat Bot Telegram
Bingung memilih antara Python atau Node.js untuk membuat bot Telegram? Simak perbandingan…
0 komentar3 menit baca
08/01/2026
Panduan Lengkap Menjalankan Bot Telegram NodeJS di Rocky Linux 10 Menggunakan Metode Webhook
Panduan lengkap menjalankan Bot Telegram di Rocky Linux 10 menggunakan metode webhook. Pembahasan…
0 komentar11 menit baca
05/01/2026
Mengenal Root dan Non Root di Server Linux
Memahami fungsi user root dan non-root pada Linux, perbedaan hak akses, risiko keamanan, serta…
0 komentar4 menit baca
04/01/2026
Menjalankan Bot Telegram Python Menggunakan Metode Webhook di Rocky Linux 10
Panduan menjalankan bot Telegram di Rocky Linux 10 agar tetap aktif 24/7. Pembahasan kami…
0 komentar5 menit baca
03/01/2026
Panduan Lengkap Menginstal Ghost di Ubuntu
Cara instal Ghost CMS di server Ubuntu 22.04 atau Ubuntu 24.04 lengkap dengan Nginx, SSL, MySQL dan…
0 komentar4 menit baca
30/12/2025
Memasang WordPress di Rocky Linux 10
Panduan lengkap pemasangan WordPress di Rocky Linux. Membahas instalasi Apache, MariaDB, PHP,…
0 komentar6 menit baca
25/12/2025
Menjalankan Bot Telegram Metode Polling di Ubuntu 24
Panduan lengkap menjalankan bot Telegram non-stop di server Ubuntu 24.04 LTS, mulai dari konsep,…
0 komentar7 menit baca
14/12/2025
Memasang WordPress di Ubuntu 24
Memasang Wordpress di Ubuntu 24 dapat Anda lakukan dengan mudah menggunakan kombinasi Nginx, PHP,…
0 komentar4 menit baca
