Skip to main content

Command Palette

Search for a command to run...

Building a Trello - Backend from Scratch

Updated
9 min read
Building a Trello - Backend from Scratch
S

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

  1. 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.

  2. 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

  3. Multi-User Organization System

    Admins can:

    • add users to the organization based on username

    • Manage members

    • Users can:
      Be part of multiple organizations

  4. 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

  5. Board-Level Task Management
    When user click any board inside a board, users can:

    • Create tasks

    • Move tasks across stages:

      • Todo

      • In Progress

      • Done

  6. Task Lifecycle

    Each task follows a simple lifecycle:

    1. Created in Todo

    2. Moved to In Progress

    3. Completed in Done

    4. 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

  1. Go to https://cloud.mongodb.com/

  2. Create account & login

  3. Create a Cluster

  4. Go to Database Access

    • Create username & password
  5. Go to Network Access

    • Add IP: 0.0.0.0/0 (for development)
  6. Click Connect

  7. 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:

  1. Take description, status, and boardId

  2. Validate:

    • Board exists

    • Status is valid

  3. Check duplicate inside same board

  4. 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:

  1. Identify task using:
  • taskId
  1. 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:

  1. Find task using:

    • taskId
  2. 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

More from this blog

Shubham Tech. Blog's

55 posts

Problem Solver | Currently Working As a Full Stack Developer, Community Leader At @Dev_Matrix | Previously Contributor at @RealDevSquad, @TeamShiksha