WhatsApp Bot Node.js Twilio Case Study: Building TwoDo
How I built TwoDo, a WhatsApp-based task tracker, using Node.js, Twilio, and MongoDB - a real case study with code.

A WhatsApp bot Node.js Twilio project is a good way to learn webhooks because the feedback loop is real: a person sends a message, Twilio calls your endpoint, your backend decides what to do, and the user receives a reply inside WhatsApp. TwoDo is a WhatsApp-based task tracker built around that idea.
The product idea is simple: instead of opening a separate app to capture tasks, users can send task commands to WhatsApp and let the bot store, list, and update them.
WhatsApp Bot Node.js Twilio: Why I Built TwoDo
TwoDo came from noticing how often tasks appear inside chat conversations. Opening a separate task app can feel like friction when the thought is already in WhatsApp. A bot makes capture feel closer to the user’s existing habit.
The first thing I wanted to prove was small: can a user send a simple command, have the backend understand it, store it safely, and reply in a way that feels useful rather than noisy? That question makes the technical architecture clearer because every part of the system has to support a quick chat-based workflow.
Architecture Overview
The basic architecture:
WhatsApp message
-> Twilio WhatsApp webhook
-> Node.js Express endpoint
-> Command parser
-> MongoDB tasks collection
-> TwiML reply
-> WhatsApp user
Twilio receives the WhatsApp message and sends an HTTP POST request to your webhook URL. Your server reads the message body, performs the task action, and returns TwiML so Twilio can send a reply.
Setting Up the Webhook Endpoint
Install the basics:
npm install express twilio mongodb dotenv
npm install --save-dev typescript tsx @types/express
Here is a minimal Express webhook:
import "dotenv/config";
import express from "express";
import twilio from "twilio";
const app = express();
app.use(express.urlencoded({ extended: false }));
app.post("/webhooks/whatsapp", async (req, res) => {
const from = String(req.body.From ?? "");
const body = String(req.body.Body ?? "").trim();
const twiml = new twilio.twiml.MessagingResponse();
twiml.message(`Received from ${from}: ${body}`);
res.type("text/xml").send(twiml.toString());
});
app.listen(3000, () => {
console.log("TwoDo webhook listening on http://localhost:3000");
});
In local development, expose the server with a tunnel such as ngrok, then paste the public URL into Twilio’s WhatsApp sandbox webhook settings:
https://your-tunnel-url.ngrok-free.app/webhooks/whatsapp
Parsing Incoming Task Commands
A task bot needs a small command language. Start with a few commands:
add Buy fuellistdone 3help
The parser can be plain TypeScript:
type TaskCommand =
| { type: "add"; title: string }
| { type: "list" }
| { type: "done"; taskNumber: number }
| { type: "help" }
| { type: "unknown"; input: string };
function parseTaskCommand(input: string): TaskCommand {
const text = input.trim();
if (/^help$/i.test(text)) return { type: "help" };
if (/^list$/i.test(text)) return { type: "list" };
const addMatch = text.match(/^add\s+(.+)$/i);
if (addMatch?.[1]) {
return { type: "add", title: addMatch[1].trim() };
}
const doneMatch = text.match(/^done\s+(\d+)$/i);
if (doneMatch?.[1]) {
return { type: "done", taskNumber: Number(doneMatch[1]) };
}
return { type: "unknown", input: text };
}
This is intentionally boring. Bots become easier to improve when the command parser is separate from the webhook handler.
Storing Tasks in MongoDB
Create a database helper:
import { MongoClient } from "mongodb";
const client = new MongoClient(process.env.MONGODB_URI ?? "");
export async function tasksCollection() {
await client.connect();
return client.db("twodo").collection("tasks");
}
Then handle commands:
async function handleCommand(userPhone: string, input: string) {
const command = parseTaskCommand(input);
const tasks = await tasksCollection();
if (command.type === "add") {
await tasks.insertOne({
userPhone,
title: command.title,
completed: false,
createdAt: new Date(),
});
return `Added: ${command.title}`;
}
if (command.type === "list") {
const openTasks = await tasks
.find({ userPhone, completed: false })
.sort({ createdAt: 1 })
.limit(10)
.toArray();
if (openTasks.length === 0) return "No open tasks.";
return openTasks
.map((task, index) => `${index + 1}. ${task.title}`)
.join("\n");
}
if (command.type === "help") {
return "Try: add Buy fuel, list, or done 1";
}
return "I did not understand that. Send help for examples.";
}
Then wire it into the webhook:
app.post("/webhooks/whatsapp", async (req, res) => {
const from = String(req.body.From ?? "");
const body = String(req.body.Body ?? "").trim();
const reply = await handleCommand(from, body);
const twiml = new twilio.twiml.MessagingResponse();
twiml.message(reply);
res.type("text/xml").send(twiml.toString());
});
Challenges to Review Honestly
The exact TwoDo details should be replaced with real notes, but the common challenges in this kind of project include:
- Designing commands that feel natural in chat.
- Handling duplicate messages or repeated webhook delivery.
- Mapping WhatsApp sender IDs to users.
- Keeping replies short enough for chat.
- Avoiding accidental task updates when parsing is ambiguous.
- Deciding what belongs in MongoDB versus temporary state.
For a real case study, add the specific bug that took longest, what you changed, and what you would build differently now.
What’s Next for TwoDo
Useful next steps could include reminders, task due dates, natural-language parsing, voice note transcription, or a small web dashboard for reviewing task history.
The important product question is whether WhatsApp remains the fastest place to capture the task. If yes, the bot should stay lightweight and helpful instead of becoming a full app trapped inside chat.
Related Posts
- Why I Built TwoDo: A WhatsApp-Based Task Tracker
- MongoDB for Beginners: Storing Data for Your First App
If you are exploring a WhatsApp, chatbot, or workflow automation idea, Beyond Just Digital can help turn the webhook sketch into a real service with fewer sharp edges.
