Building a Trello - Backend from Scratch

I'm Shubham (@shubhamsinghbundela), I'm a Software Engineer, a Full-stack developer, a tech enthusiast, and a technical writer here on @Hashnode. I have a strong zeal to share my acquired knowledge and I am also willing to learn from others.
Core Features
Authentication System (Sign Up / Sign In)
Every user starts here:Users can sign up with credentials
Users can log in securely
Authentication is handled via tokens (JWT)
This ensures every action is tied to an authenticated user.
First-Time Onboarding (Organization Creation)
After logging in for the first time:
User can create an organization
Organization includes:
Name of organisation
Description
This makes the user the admin of that organization
Multi-User Organization System
Admins can:
add users to the organization based on username
Manage members
Users can:
Be part of multiple organizations
Dashboard View
Once logged in:The user first sees a list of all organizations they are part of.
Inside the selected organization, the user can see all boards related to that specific organization
If a user is part of 2 organizations → they will see select organisation
Board-Level Task Management
When user click any board inside a board, users can:Create tasks
Move tasks across stages:
Todo
In Progress
Done
Task Lifecycle
Each task follows a simple lifecycle:
Created in Todo
Moved to In Progress
Completed in Done
Option to delete/remove task
Step 1: Setup Express Server
First, I created a basic Express server:
const express = require("express");
const app = express();
app.use(express.json());
app.listen(3000, () => {
console.log("Server is running on port 3000");
});
Step 2: Setup MongoDB
Create account & login
Create a Cluster
Go to Database Access
- Create username & password
Go to Network Access
- Add IP:
0.0.0.0/0(for development)
- Add IP:
Click Connect
Copy connection string
Example:
mongodb+srv://username:password@cluster.mongodb.net/dbname
MongoDB Connection Code
Before connecting MongoDB, first we need to install mongoose.
Step 1: Install Mongoose
Run this command in your project:
npm install mongoose
Step 2: Connect to MongoDB
Now write the connection code:
const mongoose = require("mongoose");
async function connectDB() {
try {
await mongoose.connect("mongodb+srv://<username>:<password>@todo.dtrpx.mongodb.net/todo");
console.log("MongoDB connected");
} catch (err) {
console.error("Connection error:", err);
}
}
connectDB();
Initially, I tried connecting to MongoDB (cloud) but faced a DNS issue
Problem:
MongoDB connection was failing due to DNS resolution.
Fix:
After watching a YouTube video: , I added this:
const dns = require("dns");
dns.setServers(["1.1.1.1", "8.8.8.8"]);
This fixed the issue and I was able to connect to cloud MongoDB.
Step 3: Create Schema using Mongoose
Before writing any backend or frontend logic, I first designed how my data should look.
This step is very important.
Learning:
Always define your schema first before building APIs or UI.
Because:
It gives clarity on what data you need
Helps structure your database properly
Makes backend logic easier to write
const userSchema = new mongoose.Schema({
username: 'String',
password: "String"
})
const orgSchema = new mongoose.Schema({
orgName: "String",
description: "String",
admin: mongoose.Types.ObjectId,
member: [mongoose.Types.ObjectId]
})
const boardSchema = new mongoose.Schema({
boardName: "String",
organisationId: mongoose.Types.ObjectId
})
const taskSchema = new mongoose.Schema({
description: String,
status: String, // Todo | In Progress | Done
userId: mongoose.Types.ObjectId,
boardId: mongoose.Types.ObjectId
})
const userModel = mongoose.model("users", userSchema);
const orgModel = mongoose.model("organisations", orgSchema);
const boardModel = mongoose.model("boards", boardSchema);
const taskModel = mongoose.model("tasks", taskSchema);
module.exports = {
userModel,
orgModel,
boardModel
}
Step 4: Signup Route
Before writing code, let’s understand the goal.
What we want to achieve:
Allow a new user to register
Store user details in database
Prevent duplicate users
app.post('/signup', async (req,res)=>{
const username = req.body.username;
const password = req.body.password;
const userExists = await userModel.findOne({
username: username
})
if(userExists){
return res.status(403).json({
message: "User already exists"
})
}
const newUser = await userModel.create({
username: username,
password: password
})
res.status(200).json({
id: newUser._id,
message: "user get created"
})
})
Step 5: Signin + JWT Token
Before writing code, let’s understand the goal.
What we want to achieve:
Verify user credentials
If valid → allow login
Generate a JWT token for authentication
app.post("/signin", async (req, res) => {
const { userName, password } = req.body;
const userExist = await userModel.findOne({ userName });
if (!userExist) {
return res.status(404).json({
message: "User not found",
});
}
const token = jwt.sign(
{ userId: userExist.id },
"shubham123"
);
res.json({ token });
});
Step 6: Auth Middleware
This ensures only logged-in users can access routes.
function authMiddleware(req, res, next) {
const token = req.headers.token;
if (!token) {
return res.status(401).json({
message: "Token not provided"
});
}
try {
const decode = jwt.verify(token, "shubham123");
req.userId = decode.userId;
next();
} catch (err) {
return res.status(401).json({
message: "Invalid or expired token"
});
}
}
Step 7: Create Organisation
Requirement
Now that authentication is done, we need to allow users to:
Create an organization after login
Automatically assign the creator as admin
Ensure organization names are unique
Only authenticated users should be able to create organizations.
Implementation
app.post("/create-organisation", authMiddleware, async (req,res)=>{
const orgName = req.body.orgName;
const description = req.body.description;
const orgExist = await orgModel.findOne({
orgName: orgName
})
if(orgExist){
return res.status(403).json({
message: "Organisation already exists"
})
}
const newOrg = await orgModel.create({
orgName: orgName,
description: description,
admin: req.userId,
member: []
})
res.status(200).json({
orgId: newOrg.id
})
})
Step 8: Add Member to Organization
Requirement
Now that organizations are created, the next step is:
Allow admin to add users to their organization
Users are added using their username
Only existing users can be added
Prevent adding the same user twice
Implementation
app.post('/add-member-to-organisation', authMiddleware, async (req,res)=>{
const newMember = req.body.member;
const newMemberUser = await userModel.findOne({
username: newMember
})
if(!newMemberUser){
return res.status(404).json({
message: "User does not exist"
})
}
const orgDetails = await orgModel.findOne({
admin: req.userId
})
if(!orgDetails){
return res.status(404).json({
message: "Organization not found"
})
}
const memberExists = orgDetails.member.includes(newMemberUser._id);
if(memberExists){
return res.status(400).json({
message: "Member already exists in organization"
})
}
orgDetails.member.push(newMemberUser._id);
await orgDetails.save()
res.status(200).json({
message: "Member added successfully"
})
})
Step 9: Delete Member from Organization
Requirement
Admins should also be able to:
- Remove users from the organization
Implementation
app.delete('/delete-member-from-organisation', authMiddleware, async (req, res)=>{
const deleteUser = req.body.username;
const deleteUserData = await userModel.findOne({
username: deleteUser
})
if(!deleteUserData){
return res.status(404).json({
message: "User does not exist"
})
}
const orgDetails = await orgModel.findOne({
admin: req.userId
})
if(!orgDetails){
return res.status(404).json({
message: "Organization not found"
})
}
orgDetails.member = orgDetails.member.filter(
e => e.toString() !== deleteUserData._id.toString()
);
await orgDetails.save()
res.status(200).json({
message: "Member removed successfully"
})
})
Step 10: Create Board
Requirement
Now comes the core feature of Trello:
Each organization should have boards
Only authorized users (admin) can create boards
Each board belongs to a specific organization
Implementation
app.post("/create-board", authMiddleware, async (req,res)=>{
const boardName = req.body.boardName;
const orgDetails = await orgModel.findOne({
admin: req.userId
});
if(!orgDetails){
return res.status(404).json({
message: "Organization not found"
})
}
const newBoard = await boardModel.create({
boardName,
organisationId: orgDetails._id
})
res.status(200).json({
boardId: newBoard._id,
message: "Board created successfully"
})
})
Step 11: Task Management (Create, Update, Delete)
Now comes the most important part of any Trello-like application — Tasks.
This is where users actually track their work.
Requirement:
We want to allow users to:
Create tasks inside a board
Update task status (Todo → In Progress → Done)
Delete tasks when completed or no longer needed
Ensure tasks are tied to the logged-in user
This forms the core task lifecycle system
Create Task
Logic :
When a user creates a task:
Take
description,status, andboardIdValidate:
Board exists
Status is valid
Check duplicate inside same board
Create task linked to board
Implementation
app.post("/create-task", authMiddleware, async (req, res) => {
const { description, status, boardId } = req.body;
if (!description || !status || !boardId) {
return res.status(400).json({
message: "All fields are required"
});
}
const allowedStatus = ["Todo", "In Progress", "Done"];
if (!allowedStatus.includes(status)) {
return res.status(400).json({
message: "Invalid status"
});
}
const board = await boardModel.findById(boardId);
if (!board) {
return res.status(404).json({
message: "Board not found"
});
}
const taskExists = await taskModel.findOne({
userId: req.userId,
description,
boardId
});
if (taskExists) {
return res.status(403).json({
message: "Task already exists in this board"
});
}
const newTask = await taskModel.create({
userId: req.userId,
description,
status,
boardId
});
res.status(200).json({
message: "Task created successfully",
taskId: newTask._id
});
});
Update Task
Logic :
When updating a task:
- Identify task using:
- taskId
If task exists:
Update status
Save changes
This enables moving tasks across stages
Implementation
app.put("/update-task", authMiddleware, async (req, res) => {
const { taskId, status } = req.body;
const allowedStatus = ["Todo", "In Progress", "Done"];
if (!allowedStatus.includes(status)) {
return res.status(400).json({
message: "Invalid status"
});
}
const task = await taskModel.findOne({
_id: taskId,
userId: req.userId
});
if (!task) {
return res.status(404).json({
message: "Task not found"
});
}
task.status = status;
await task.save();
res.status(200).json({
message: "Task updated successfully"
});
});
Delete Task
Logic :
When deleting a task:
Find task using:
- taskId
If exists → delete it from database
Implementation
app.delete("/delete-task", authMiddleware, async (req, res) => {
const { taskId } = req.body;
const task = await taskModel.findOne({
_id: taskId,
userId: req.userId
});
if (!task) {
return res.status(404).json({
message: "Task not found"
});
}
await taskModel.deleteOne({ _id: taskId });
res.status(200).json({
message: "Task deleted successfully"
});
});
Final Thoughts
If you're learning backend development, I highly recommend building something like this — it teaches you far more than just theory.
Complete Source Code
I’ve uploaded the complete project on GitHub. You can check it here:
GitHub Repo:
https://github.com/shubhamsinghbundela/Trello




