# Refactoring a Trello Backend into a Scalable REST API Architecture

Till now, the backend was working, but everything was in a **single file / unstructured format**.

Now the goal is to:

*   Follow **clean folder structure**
    
*   Make code **scalable & maintainable**
    

* * *

Before diving into this structured version, I recommend reading my initial implementation where everything was built in a single file: **Building a Trello - Backend from Scratch** → [https://shubhamsinghbundela.hashnode.dev/building-a-trello-backend-from-scratch](https://shubhamsinghbundela.hashnode.dev/building-a-trello-backend-from-scratch)

* * *

### Step 1: Initialize Project

```javascript
npm init -y
```

It Creates `package.json`

* * *

### Step 2: Install Dependencies

```plaintext
npm install express
npm install --save-dev nodemon
npm install dotenv // This package is used to load environment variables from the .env file.
```

* * *

### Step 3: Setup App Entry (src/app.js)

Create `src/app.js`

```javascript
import express from "express";

const app = express();

app.use(express.json());

export default app;
```

This file is responsible for:

*   Initializing express app
    
*   Adding global middlewares
    

* * *

### Step 4: Environment Variables

Create `.env` file:

```javascript
PORT=3000
NODE_ENV=development
MONGODB_URI=your_mongodb_connection_string
```

* * *

### Step 5: Server Entry Point (server.js)

create `server.js`

```javascript
import "dotenv/config"; //This automatically loads .env variables into process.env
import app from "./src/app.js";

const PORT = process.env.PORT || 3000;

const start = async () => {
    app.listen(PORT, () => {
        console.log(`Server is running at ${PORT} in ${process.env.NODE_ENV} mode`);
    });
};

start().catch((err) => {
    console.error("Failed to start server", err);
    process.exit(1);
});
```

This file is responsible for:

*   Starting the server
    
*   Handling startup errors
    

* * *

### Step 6: Scripts Setup

Update `package.json`:

```javascript
"scripts": {
  "start": "node server.js",
  "dev": "nodemon server.js"
}
```

Use:

```plaintext
npm run dev
```

* * *

### Step 7: Setup Database Connection

Create:

```plaintext
src/common/config/db.js
```

```js
import mongoose from "mongoose";
import dns from "dns";

dns.setServers(["1.1.1.1", "8.8.8.8"]);

const connectDB = async () => {
    const conn = await mongoose.connect(process.env.MONGODB_URI);

    console.log(`MongoDB connected: ${conn.connection.host}`);
};

export default 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:

```javascript
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 8: Standard API Response Structure

**Create:**

```plaintext
src/common/utils/api-response.js
```

```javascript
class ApiResponse {
    static ok(res, message, data = null) {
        return res.status(200).json({
            success: true,
            message,
            data
        });
    }
}

export default ApiResponse;
```

**Why This is Important**

Instead of writing:

```javascript
res.status(200).json({ message: "Success" });
```

To avoid repeating response logic across multiple APIs, I implemented a reusable utility using the DRY (Don't Repeat Yourself) principle:

```javascript
ApiResponse.ok(res, "Success", data);
```

* * *

### Step 9: Standard API Error Structure

While building APIs, handling errors properly is **as important as handling success responses**.

1**. Create Custom Error Class**

Create:

```plaintext
src/common/utils/api-error.js
```

```javascript
class ApiError extends Error {
    // Inherit from built-in JavaScript Error
    constructor(statusCode, message) {
        super(message);

        this.statusCode = statusCode;

        // Capture clean stack trace
        Error.captureStackTrace(this, this.constructor);
    }

    static badRequest(message = "Bad Request") {
        return new ApiError(400, message);
    }

    static forbidden(message = "Forbidden") {
        return new ApiError(403, message);
    }

    static notFound(message = "Not Found") {
        return new ApiError(404, message);
    }
}

export default ApiError;
```

**Why Extend** `Error`**?**

JavaScript provides built-in errors like:

*   `Error`
    
*   `TypeError`
    
*   `ReferenceError`
    

But they **don’t include HTTP status codes**, which are required in APIs.

**2\. Global Error Middleware**

Create:

```plaintext
src/common/middleware/error.middleware.js
```

```javascript
import ApiError from "../utils/api-error.js";

const errorHandler = (err, req, res, next) => {

    // Handle known (custom) errors
    if (err instanceof ApiError) {
        return res.status(err.statusCode).json({
            success: false,
            message: err.message
        });
    }

    // Handle unknown errors
    return res.status(500).json({
        success: false,
        message: "Internal Server Error"
    });
};

export default errorHandler;
```

**3\. Register Middleware in App**

Update `src/app.js`:

```javascript
import express from "express";
import authRoute from "./modules/auth/auth.routes.js";
import errorHandler from "./common/middleware/error.middleware.js";

const app = express();
app.use(express.json());

app.use(errorHandler);

export default app;
```

This must be the **last middleware** in the app.  
  
**Final Error Flow**

```plaintext
Request → Route → Controller → Service → throws ApiError → Passes to errorHandler middleware → JSON response 
```

* * *

### Step 10: Authentication Module (Modular Structure)

Now that we have:

*   Clean folder structure
    
*   Database setup
    
*   Standard response & error handling
    

Next step is to build a **modular authentication system**

* * *

**Module Structure**

Inside `src/modules/auth/`, I created:

```plaintext
auth/
├── auth.controller.js
├── auth.middleware.js
├── auth.model.js
├── auth.routes.js
├── auth.service.js
```

* * *

**Why This Structure?**

Each layer has a clear responsibility:

*   **Model** → Database schema
    
*   **Routes** → Define API endpoints
    
*   **Controller** → Handle request/response
    
*   **Service** → Business logic
    
*   **Middleware** → Authentication / validation
    

This makes code **clean, maintainable, and scalable**

* * *

**Create User Schema (**`auth.model.js`**)**

```javascript
import mongoose from "mongoose";

const userSchema = new mongoose.Schema({
    username: String,
    password: String
}, { timestamps: true });

const userModel = mongoose.model("users", userSchema);

export default userModel;
```

`timestamps` automatically adds:

*   `createdAt`
    
*   `updatedAt`
    

* * *

**Register Auth Routes in App.js**

Update `src/app.js`:

```javascript
import express from "express";
import authRoute from "./modules/auth/auth.routes.js";
import errorMiddleware from "./common/middleware/error.middleware.js";

const app = express();

app.use(express.json());

// Register routes
app.use("/api/auth", authRoute);

// Global error handler
app.use(errorMiddleware);

export default app;
```

All auth APIs will now be available under:

```plaintext
/api/auth/*
```

* * *

**Define Routes (**`auth.routes.js`**)**

```javascript
import { Router } from "express";
import * as controller from "./auth.controller.js";

const router = Router();

router.post("/signup", controller.signup);

export default router;
```

* * *

**Controller Layer (**`auth.controller.js`**)**

```javascript
import ApiResponse from "../../common/utils/api-response.js";
import * as authService from "./auth.service.js";

const signup = async (req, res, next) => {
  try {
    const user = await authService.signup(req.body);

    ApiResponse.ok(res, "User get Created", user);
  } catch (error) {
    next(error); // pass error to global error middleware
  }
};

export { signup };

```

**Responsibility of Controller**

*   Calls service layer
    
*   Sends response using `ApiResponse`
    
*   Keeps logic minimal
    

* * *

**Business Logic (**`auth.service.js`**)**

```javascript
import userModel from "./auth.model.js";
import ApiError from "../../common/utils/api-error.js";

const signup = async ({ username, password }) => {

    // Step 1: Check if user already exists
    const userExists = await userModel.findOne({ username });

    if (userExists) {
        throw ApiError.forbidden("User Already Exists");
    }

    // Step 2: Create new user
    const newUser = await userModel.create({
        username,
        password
    });

    return newUser;
};

export {
    signup
};
```

* * *

**Final Result: Testing /api/auth/signup API Using Postman**

![](https://cdn.hashnode.com/uploads/covers/624226a5db84f8c50fa5b247/918612d0-fcee-4c57-8864-3a4cf67f5726.png align="center")

* * *

**Request Flow**

```plaintext
POST /api/auth/signup
  ↓
auth.routes.js
  ↓
auth.controller.js
  ↓
auth.service.js
  ↓
MongoDB
```

* * *

### Step 11: Implement Signin

Now that the signup API is ready, the next step is to allow users to **login (signin)** and generate a token for authentication.

* * *

**Define Routes**

Inside `auth.route.js`, we define the signin route:

```javascript
import { Router } from "express";
import * as controller from "./auth.controller.js";

const router = Router();

router.post("/signup", controller.signup);
router.post("/signin", controller.signin);

export default router;
```

This creates an endpoint:

```plaintext
POST /api/auth/signin
```

* * *

**Controller Layer**

Now we handle the request in the controller.

```javascript
const signin = async (req, res, next) => {
  try {
    const token = await authService.signin(req.body);

    ApiResponse.ok(res, "Signin successfully", token);
  } catch (error) {
    next(error);
  }
};
```

#### Responsibility of Controller:

*   Receive request data (`req.body`)
    
*   Call service layer
    
*   Send response using `ApiResponse`
    
*   Pass errors to global error handler
    

* * *

**Service Layer (Business Logic)**

Now the actual authentication logic lives in `auth.service.js`:

```javascript
const signin = async ({ username, password }) => {
  const userExists = await userModel.findOne({
    username: username,
    password: password,
  });

  if (!userExists) {
    throw ApiError.notFound("User not found");
  }

  const token = jwt.sign(
    {
      userId: userExists.id,
    },
    "shubham123"
  );

  return token;
};
```

* * *

**Final Result: Testing /api/auth/signin API Using Postman**

![](https://cdn.hashnode.com/uploads/covers/624226a5db84f8c50fa5b247/ee753d49-ad84-4e26-ab05-23448d2e26b3.png align="center")

* * *

### Step 12: Create organisation inside Organisation Module

Now that authentication is complete, the next step is to allow users to create organisations — similar to how Trello works.

* * *

**Requirements**

After login, a user should be able to:

*   Create an organisation
    
*   Automatically become the **admin**
    
*   Ensure organisation names are **unique**
    
*   Only **authenticated users** can create organisations
    

* * *

**Module Structure**

Inside `src/modules/`, create a new folder:

```plaintext
src/modules/org/
├── org.controller.js
├── org.middleware.js
├── org.model.js
├── org.routes.js
├── org.service.js
```

* * *

**Create Organisation Schema**

Create:

```plaintext
src/modules/org/org.model.js
```

```javascript
import mongoose from "mongoose";

const orgSchema = new mongoose.Schema({
    orgName: "String",
    description: "String",
    admin: mongoose.Types.ObjectId,
    member: [mongoose.Types.ObjectId]
}, { timestamps: true });

const orgModel = mongoose.model("organisations", orgSchema);

export default orgModel;
```

* * *

**Authentication Middleware**

To ensure only logged-in users can create organisations:

Inside:

```plaintext
src/modules/auth/auth.middleware.js
```

```javascript
import jwt from "jsonwebtoken";

const authMiddleware = (req, res, next) => {
    const token = req.headers.token;

    const decode = jwt.verify(token, "shubham123");

    req.userId = decode.userId;

    next();
};

export default authMiddleware;
```

* * *

**Register Organisation Routes**

Update `src/app.js`:

```javascript
import orgRoute from "./modules/org/org.routes.js";

app.use("/api/org", orgRoute);
```

Now all organisation APIs will be available under:

```plaintext
/api/org/* 
```

* * *

**Define Routes**

**Create:**

```plaintext
src/modules/org/org.routes.js
```

```javascript
import { Router } from "express";
import * as controller from "./org.controller.js";
import authMiddleware from "../auth/auth.middleware.js";

const router = Router();

router.post(
  "/create-organisation",
  authMiddleware,
  controller.createOrganisation
);

export default router;
```

* * *

**Controller Layer**

**Create:**

```plaintext
src/modules/org/org.controller.js
```

```javascript
import ApiResponse from "../../common/utils/api-response.js";
import * as orgService from "./org.service.js";

const createOrganisation = async (req, res, next) => {
  try {
    const org = await orgService.createOrganisation(req);

    ApiResponse.ok(
      res,
      "Org get created",
      org
    );

  } catch (error) {
    next(error);
  }
};

export { createOrganisation };
```

**Responsibility of Controller**

*   Calls service layer
    
*   Sends success response
    
*   Passes errors to global middleware
    

* * *

**Business Logic (Service Layer)**

Create:

```plaintext
src/modules/org/org.service.js
```

```javascript
import ApiError from "../../common/utils/api-error.js";
import orgModel from "./org.model.js";

const createOrganisation = async (req) => {

    // Step 1: Check if organisation already exists
    const orgExist = await orgModel.findOne({
        orgName: req.body.orgName
    });

    if (orgExist) {
        throw ApiError.forbidden("Organisation already exists");
    }

    // Step 2: Create organisation
    const newOrg = await orgModel.create({
        orgName: req.body.orgName,
        description: req.body.description,
        admin: req.userId,
        member: []
    });

    return newOrg;
};

export { createOrganisation };
```

* * *

**Request Flow**

```plaintext
POST /api/org/create-organisation
   ↓
auth.middleware.js (verify token)
   ↓
org.controller.js
   ↓
org.service.js
   ↓
MongoDB
   ↓
Response
```

* * *

**Final Result: Testing api/org/create-organisation API Using Postman**

![](https://cdn.hashnode.com/uploads/covers/624226a5db84f8c50fa5b247/71a65463-46d8-4f7d-b3b2-99ed54975c40.png align="center")

* * *

### Step 13: Add Members to Organisation

Now that organisations are created, the next step is to allow admins to add members — similar to how teams work in Trello.

* * *

**Requirement**

*   Admin can add users to their organisation
    
*   Users are added using their **username**
    
*   Only **existing users** can be added
    
*   Prevent adding the **same user twice**
    

* * *

**Update Routes**

Update:

```plaintext
src/modules/org/org.routes.js
```

```javascript
import { Router } from "express";
import * as controller from "./org.controller.js";
import authMiddleware from "../auth/auth.middleware.js";

const router = Router();

router.post("/create-organisation", authMiddleware, controller.createOrganisation);

router.post("/add-member", authMiddleware, controller.addMember);

export default router;
```

* * *

**Controller Layer**

Update:

```plaintext
src/modules/org/org.controller.js
```

```javascript
const addMember = async (req, res, next) => {
  try {
    const data = await orgService.addMember(req);

    ApiResponse.ok(
      res,
      "Member get added",
      data
    );

  } catch (error) {
    next(error);
  }
};

export {
  addMember
};
```

* * *

**Responsibility**

*   Calls service layer
    
*   Sends response
    
*   Passes errors to middleware
    

* * *

**Business Logic (Service Layer)**

Update:

```plaintext
src/modules/org/org.service.js
```

```javascript
import ApiError from "../../common/utils/api-error.js";
import orgModel from "./org.model.js";
import userModel from "../auth/auth.model.js";

const addMember = async (req) => {

    const newMember = req.body.member;

    // Step 1: Check user exists
    const newMemberUser = await userModel.findOne({
        username: newMember
    });

    if (!newMemberUser) {
        throw ApiError.notFound("User not exist");
    }

    // Step 2: Find organisation (admin only)
    const orgDetails = await orgModel.findOne({
        admin: req.userId
    });

    if (!orgDetails) {
        throw ApiError.notFound("Organisation not exists");
    }

    // Step 3: Prevent duplicate members
    const memberExists = orgDetails.member.includes(newMemberUser._id);

    if (memberExists) {
        throw ApiError.badRequest("Member already exists in organisation");
    }

    // Step 4: Add member
    orgDetails.member.push(newMemberUser._id);

    await orgDetails.save();

    return orgDetails;
};

export {
    addMember
};
```

* * *

**Request Flow**

```plaintext
POST /api/org/add-member
  ↓
auth.middleware.js (verify token)
  ↓
org.controller.js
  ↓
org.service.js
  ↓
MongoDB
  ↓
Response
```

* * *

**Final Result: Testing api/org/add-member API Using Postman**

![](https://cdn.hashnode.com/uploads/covers/624226a5db84f8c50fa5b247/392ff088-cc47-46aa-95c6-7867390c4bb4.png align="center")

* * *

### Step 14: Remove Member from Organisation

After adding members, the next important feature is allowing admins to remove members — similar to how team management works in Trello.

* * *

**Requirement**

Admins should be able to:

*   Remove users from the organisation
    
*   Only remove **existing members**
    
*   Only **admin** can perform this action
    

* * *

**Update Routes**

Update:

```plaintext
src/modules/org/org.routes.js
```

```javascript
import { Router } from "express";
import * as controller from "./org.controller.js";
import authMiddleware from "../auth/auth.middleware.js";

const router = Router();

router.post("/create-organisation", authMiddleware, controller.createOrganisation);

router.post("/add-member", authMiddleware, controller.addMember);

router.delete("/delete-member", authMiddleware, controller.deleteMember);

export default router;
```

* * *

**Controller Layer**

Update:

```plaintext
src/modules/org/org.controller.js
```

```javascript
const deleteMember = async (req, res, next) => {
  try {
    const data = await orgService.deleteMember(req);

    ApiResponse.ok(
      res,
      "Member get deleted",
      data
    );

  } catch (error) {
    next(error);
  }
};

export {
  deleteMember
};
```

* * *

**Business Logic (Service Layer)**

Update:

```plaintext
src/modules/org/org.service.js
```

```javascript
import ApiError from "../../common/utils/api-error.js";
import orgModel from "./org.model.js";
import userModel from "../auth/auth.model.js";

const deleteMember = async (req) => {

    const deleteUser = req.body.username;

    // Step 1: Check user exists
    const deleteUserData = await userModel.findOne({
        username: deleteUser
    });

    if (!deleteUserData) {
        throw ApiError.notFound("User not exist");
    }

    // Step 2: Find organisation (admin only)
    const orgDetails = await orgModel.findOne({
        admin: req.userId
    });

    if (!orgDetails) {
        throw ApiError.notFound("Organisation not exists");
    }

    // Step 3: Check if user is actually a member
    const isMember = orgDetails.member.some(
        id => id.toString() === deleteUserData._id.toString()
    );

    if (!isMember) {
        throw ApiError.badRequest("User is not a member of this organisation");
    }

    // Step 4: Remove member
    orgDetails.member = orgDetails.member.filter(
        id => id.toString() !== deleteUserData._id.toString()
    );

    await orgDetails.save();

    return orgDetails;
};

export {
    deleteMember
};
```

* * *

**Request Flow**

```plaintext
DELETE /api/org/delete-member
   ↓
auth.middleware.js (verify token)
  ↓
org.controller.js
  ↓
org.service.js
  ↓
MongoDB
  ↓
Response
```

* * *

**Final Result: Testing api/org/delete-member API Using Postman**

![](https://cdn.hashnode.com/uploads/covers/624226a5db84f8c50fa5b247/97de3e07-5f7d-4b7a-883a-d4652c02cf40.png align="center")

* * *

### Step 15: Create Board

Now comes the most important feature — boards — just like in Trello.

* * *

**Requirement**

*   Each organisation can have multiple boards
    
*   Only **authorized users (admin)** can create boards
    
*   Each board belongs to a **specific organisation**
    

* * *

**Module Structure**

Inside `src/modules/`, create:

```plaintext
board/
├── board.controller.js
├── board.middleware.js
├── board.model.js
├── board.routes.js
├── board.service.js
```

* * *

**Create Board Schema**

Create:

```plaintext
src/modules/board/board.model.js
```

```javascript
import mongoose from "mongoose";

const boardSchema = mongoose.Schema({
    boardName: "String",
    organisationId: mongoose.Types.ObjectId
});

const boardModel = mongoose.model("boards", boardSchema);

export default boardModel;
```

* * *

**Define Routes**

Create:

```plaintext
src/modules/board/board.routes.js
```

```javascript
import { Router } from "express";
import authMiddleware from "../auth/auth.middleware.js";
import * as controller from "./board.controller.js";

const router = Router();

router.post("/create-board", authMiddleware, controller.createBoard);

export default router;
```

* * *

**Register Route in App**

Update `src/app.js`:

```javascript
import boardRoute from "./modules/board/board.routes.js";

app.use("/api/board", boardRoute);
```

* * *

**Controller Layer**

Create:

```plaintext
src/modules/board/board.controller.js
```

```javascript
import ApiResponse from "../../common/utils/api-response.js";
import * as boardService from "./board.service.js";

const createBoard = async (req, res, next) => {
  try {
    const data = await boardService.createBoard(req);

    ApiResponse.ok(
      res,
      "Board get created",
      data
    );

  } catch (error) {
    next(error);
  }
};

export {
  createBoard
};
```

* * *

**Business Logic (Service Layer)**

Create:

```plaintext
src/modules/board/board.service.js
```

```javascript
import ApiError from "../../common/utils/api-error.js";
import orgModel from "../org/org.model.js";
import boardModel from "./board.model.js";

const createBoard = async (req) => {

    const boardName = req.body.boardName;

    // Step 1: Check organisation (admin only)
    const orgDetails = await orgModel.findOne({
        admin: req.userId
    });

    if (!orgDetails) {
        throw ApiError.notFound("Organisation not exists");
    }

    // Step 2: Create board
    const newBoard = await boardModel.create({
        boardName,
        organisationId: orgDetails._id
    });

    return newBoard;
};

export {
    createBoard
};
```

* * *

**Request Flow**

```plaintext
POST /api/board/create-board
  ↓
auth.middleware.js (verify token)
  ↓
board.controller.js
  ↓
board.service.js
  ↓
MongoDB
  ↓
Response
```

* * *

**Final Result: Testing api/board/create-board API Using Postman**  

![](https://cdn.hashnode.com/uploads/covers/624226a5db84f8c50fa5b247/dc4e749d-d296-4988-9346-48322b62bb01.png align="center")

* * *

### Step 16: Task Management (Create Task)

Now comes the most important part of any Trello-like application — **Tasks**.

This is where users actually track their work.

* * *

**Requirement**

We want users to:

*   Create tasks inside a board
    
*   Update task status (**Todo → In Progress → Done**) *(next step)*
    
*   Delete tasks *(next step)*
    
*   Ensure tasks are tied to the logged-in user
    

This forms the **core task lifecycle system**

* * *

**Module Structure**

Inside `src/modules/`, create:

```plaintext
task/
├── task.controller.js
├── task.middleware.js
├── task.model.js
├── task.routes.js
├── task.service.js
```

* * *

**Task Schema**

Create:

```plaintext
src/modules/task/task.model.js
```

```javascript
import mongoose from "mongoose";

const taskSchema = mongoose.Schema({
    description: "String",
    status: "String",
    boardId: mongoose.Types.ObjectId,
    userId: mongoose.Types.ObjectId,
});

const taskModel = mongoose.model("tasks", taskSchema);

export default taskModel;
```

* * *

**Define Routes**

Create:

```plaintext
src/modules/task/task.routes.js
```

```javascript
import { Router } from "express";
import authMiddleware from "../auth/auth.middleware.js";
import * as controller from "./task.controller.js";

const router = Router();

router.post("/create-task", authMiddleware, controller.createTask);

export default router;
```

* * *

**Register Route in App**

Update `src/app.js`:

```javascript
import taskRouter from "./modules/task/task.routes.js";

app.use("/api/task", taskRouter);
```

* * *

**Controller Layer**

Create:

```plaintext
src/modules/task/task.controller.js
```

```javascript
import ApiResponse from "../../common/utils/api-response.js";
import * as taskService from "./task.service.js";

const createTask = async (req, res, next) => {
  try {
    const data = await taskService.createTask(req);

    ApiResponse.ok(
      res,
      "Task created successfully",
      data
    );

  } catch (error) {
    next(error);
  }
};

export {
  createTask
};
```

* * *

**Business Logic (Service Layer)**

Create:

```plaintext
src/modules/task/task.service.js
```

```javascript
import ApiError from "../../common/utils/api-error.js";
import boardModel from "../board/board.model.js";
import taskModel from "./task.model.js";

const createTask = async (req) => {

    const { description, status, boardId } = req.body;

    // Step 1: Validate input
    if (!description || !status || !boardId) {
        throw ApiError.badRequest("All fields are required");
    }

    // Step 2: Validate status
    const allowedStatus = ["Todo", "In Progress", "Done"];

    if (!allowedStatus.includes(status)) {
        throw ApiError.badRequest("Invalid status");
    }

    // Step 3: Check board exists
    const board = await boardModel.findById(boardId);

    if (!board) {
        throw ApiError.notFound("Board not found");
    }

    // Step 4: Prevent duplicate task in same board
    const taskExists = await taskModel.findOne({
        userId: req.userId,
        description,
        boardId
    });

    if (taskExists) {
        throw ApiError.forbidden("Task already exists in this board");
    }

    // Step 5: Create task
    const newTask = await taskModel.create({
        userId: req.userId,
        description,
        status,
        boardId
    });

    return newTask;
};

export {
    createTask
};
```

* * *

**Request Flow**

```plaintext
POST /api/task/create-task
        ↓
auth.middleware.js (verify token)
        ↓
task.controller.js
        ↓
task.service.js
        ↓
MongoDB
        ↓
Response
```

* * *

**Final Result: Testing api/task/create-task API Using Postman**

![](https://cdn.hashnode.com/uploads/covers/624226a5db84f8c50fa5b247/d22beb16-0eac-4a8f-9f84-63eb52bacb1f.png align="center")

* * *

### Step 17: Update Task Status

Now that tasks can be created, the next step is to update their status — this is what enables the classic workflow of moving tasks across stages (Todo → In Progress → Done), just like in Trello.

* * *

**Requirement**

When updating a task:

*   Identify the task using `taskId`
    
*   Validate the new `status`
    
*   Ensure the task belongs to the logged-in user
    
*   Update the status
    

* * *

**Define Route**

Update:

```plaintext
src/modules/task/task.routes.js
```

```javascript
import { Router } from "express";
import authMiddleware from "../auth/auth.middleware.js";
import * as controller from "./task.controller.js";

const router = Router();

router.post("/create-task", authMiddleware, controller.createTask);

router.put("/update-task", authMiddleware, controller.updateTask);

export default router;
```

* * *

**Controller Layer**

Update:

```plaintext
src/modules/task/task.controller.js
```

```javascript
const updateTask = async (req, res, next) => {
  try {
    const data = await taskService.updateTask(req);

    ApiResponse.ok(
      res,
      "Task updated successfully",
      data
    );

  } catch (error) {
    next(error);
  }
};

export {
  updateTask
};
```

* * *

**Business Logic (Service Layer)**

Update:

```plaintext
src/modules/task/task.service.js
```

```javascript
import ApiError from "../../common/utils/api-error.js";
import taskModel from "./task.model.js";

const updateTask = async (req) => {

    const { taskId, status } = req.body;

    // Step 1: Validate status
    const allowedStatus = ["Todo", "In Progress", "Done"];

    if (!allowedStatus.includes(status)) {
        throw ApiError.badRequest("Invalid status");
    }

    // Step 2: Find task (only owner can update)
    const task = await taskModel.findOne({
        _id: taskId,
        userId: req.userId
    });

    if (!task) {
        throw ApiError.notFound("Task not found");
    }

    // Step 3: Update status
    task.status = status;

    await task.save();

    return task;
};

export {
    updateTask
};
```

* * *

**Request Flow**

```plaintext
PUT /api/task/update-task
        ↓
auth.middleware.js (verify token)
        ↓
task.controller.js
        ↓
task.service.js
        ↓
MongoDB
        ↓
Response
```

* * *

**Final Result: Testing api/task/update-task API Using Postman**

![](https://cdn.hashnode.com/uploads/covers/624226a5db84f8c50fa5b247/dab48cd0-ef37-42cf-833d-2736e4d74881.png align="center")

* * *

### Step 18: Delete Task

Now that tasks can be created and updated, the final step in the task lifecycle is deleting tasks — just like removing completed work in Trello.

* * *

**Requirement**

When deleting a task:

*   Identify the task using `taskId`
    
*   Ensure the task belongs to the logged-in user
    
*   If it exists → delete it from the database
    

* * *

**Define Route**

Update:

```plaintext
src/modules/task/task.routes.js
```

```javascript
import { Router } from "express";
import authMiddleware from "../auth/auth.middleware.js";
import * as controller from "./task.controller.js";

const router = Router();

router.post("/create-task", authMiddleware, controller.createTask);
router.put("/update-task", authMiddleware, controller.updateTask);
router.delete("/delete-task", authMiddleware, controller.deleteTask);

export default router;
```

* * *

**Controller Layer**

Update:

```plaintext
src/modules/task/task.controller.js
```

```javascript
const deleteTask = async (req, res, next) => {
  try {
    const data = await taskService.deleteTask(req);

    ApiResponse.ok(
      res,
      "Task deleted successfully",
      data
    );

  } catch (error) {
    next(error);
  }
};

export {
  deleteTask
};
```

* * *

**Business Logic (Service Layer)**

Update:

```plaintext
src/modules/task/task.service.js
```

```javascript
import ApiError from "../../common/utils/api-error.js";
import taskModel from "./task.model.js";

const deleteTask = async (req) => {

    const { taskId } = req.body;

    // Step 1: Find task (only owner can delete)
    const task = await taskModel.findOne({
        _id: taskId,
        userId: req.userId
    });

    if (!task) {
        throw ApiError.notFound("Task not found");
    }

    // Step 2: Delete task
    await taskModel.deleteOne({ _id: taskId });

    return task;
};

export {
    deleteTask
};
```

* * *

**Request Flow**

```plaintext
DELETE /api/task/delete-task
        ↓
auth.middleware.js (verify token)
        ↓
task.controller.js
        ↓
task.service.js
        ↓
MongoDB
        ↓
Response
```

* * *

**What Changed from Previous Approach?**

Before:

*   All logic in one file
    
*   Hard to scale
    

Now:

*   Clean modular structure
    
*   Separation of concerns
    
*   Reusable logic
    
*   Easy to maintain
    

* * *

### **Complete Source Code**

I’ve uploaded the complete project on GitHub. You can check it here:

**GitHub Repo:**  
[https://github.com/shubhamsinghbundela/Rest-API-Trello](https://github.com/shubhamsinghbundela/Rest-API-Trello/)
