<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: AbhiJeet Sachan</title>
    <description>The latest articles on DEV Community by AbhiJeet Sachan (@abhijeet_sachan_34f5d10dc).</description>
    <link>https://dev.to/abhijeet_sachan_34f5d10dc</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1663505%2F4eca066c-ac35-41d0-a06a-9a67f48719a8.jpg</url>
      <title>DEV Community: AbhiJeet Sachan</title>
      <link>https://dev.to/abhijeet_sachan_34f5d10dc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abhijeet_sachan_34f5d10dc"/>
    <language>en</language>
    <item>
      <title>Building Scalable Support Ticket System with Node.js, Express &amp; MongoDB – Part 3</title>
      <dc:creator>AbhiJeet Sachan</dc:creator>
      <pubDate>Wed, 30 Jul 2025 21:17:50 +0000</pubDate>
      <link>https://dev.to/abhijeet_sachan_34f5d10dc/building-scalable-support-ticket-system-with-nodejs-express-mongodb-part-3-1gak</link>
      <guid>https://dev.to/abhijeet_sachan_34f5d10dc/building-scalable-support-ticket-system-with-nodejs-express-mongodb-part-3-1gak</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;HelpMe – Part 3: Expanding Backend Features with Admin &amp;amp; Agent Routes&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the previous part of this series, we built the foundation of ticket management: creating tickets, listing user tickets, and retrieving single tickets.&lt;br&gt;
Now, we’ll extend the backend to handle real-world workflows by introducing admin and agent capabilities, closing tickets, and improving our schema.&lt;/p&gt;

&lt;p&gt;This post is a continuation of Part 2 – if you haven’t read it, I recommend checking that out first.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Goals of Part 3&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;By the end of this post, we will:&lt;/p&gt;

&lt;p&gt;Update the Ticket schema to include timestamps and new fields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add routes for:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Admin: assign tickets, view tickets by user&lt;/li&gt;
&lt;li&gt;Agent: view assigned tickets, close tickets&lt;/li&gt;
&lt;li&gt;Strengthen role-based access control using middleware.&lt;/li&gt;
&lt;li&gt;Add threaded replies so tickets have conversations.&lt;/li&gt;
&lt;li&gt;Allow agents and users to reply to tickets.&lt;/li&gt;
&lt;li&gt;Build admin dashboard analytics routes.&lt;/li&gt;
&lt;li&gt;Add user profile APIs (view/update profile).&lt;/li&gt;
&lt;li&gt;Discuss minor updates to our previous code where necessary.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These steps bring us closer to a multi-role support system, where tickets can flow seamlessly between users, agents, and admins.&lt;/p&gt;


&lt;h2&gt;
  
  
  &lt;strong&gt;1. Updating the Ticket Schema&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Why?&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;To track ticket history with createdAt/updatedAt.&lt;/li&gt;
&lt;li&gt;To record which agent is assigned to a ticket.&lt;/li&gt;
&lt;li&gt;To manage ticket lifecycle statuses (open, in-progress, closed).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;File:models/Ticket.js&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import mongoose from 'mongoose';

const ticketSchema = new mongoose.Schema({
  title: "{ type: String, required: true },"
  description: "String,"
  priority: { type: String, enum: ['low', 'medium', 'high'], default: 'low' },
  status: { type: String, enum: ['open', 'in-progress', 'closed'], default: 'open' },
  createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  assignedTo: { type: mongoose.Schema.Types.ObjectId, ref: 'User', default: null }
}, {
  timestamps: true
});

export default mongoose.model('Ticket', ticketSchema);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: timestamps: true automatically adds createdAt and updatedAt fields.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mongoose Methods You’ll See Often&lt;/strong&gt;&lt;br&gt;
Before we jump into the routes, let’s clarify common Mongoose methods:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. find(query):&lt;/strong&gt;&lt;br&gt;
Fetches all documents matching the query.&lt;br&gt;
Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ticket.find({ createdBy: userId })

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns an array of tickets.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;findById(id):&lt;br&gt;
Finds a document by its unique MongoDB _id.&lt;br&gt;
Returns a single document or null.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;findByIdAndUpdate(id, update, options):&lt;br&gt;
Finds a document by _id, updates it, and (if { new: true } is passed) returns the updated document.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;populate(field, select):&lt;br&gt;
Replaces an ID stored in a field with the actual referenced document.&lt;br&gt;
Example:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.populate('assignedTo', 'username')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will include the assigned agent's username in the result.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sort({ field: -1 }):&lt;/li&gt;
&lt;li&gt;Sorts the query results.
-1 means descending order, 1 means ascending.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;These methods are crucial to querying MongoDB effectively.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;2. Building Admin and Agent Features&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;We’ll add new controller functions in controllers/ticketController.js.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Admin: Assign Tickets&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Why?&lt;/em&gt;&lt;br&gt;
Admins should be able to assign a ticket to an agent, moving its status to in-progress.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Route: POST /api/tickets/assign/:ticketId&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Controller Code:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const assignTicket = async (req, res) =&amp;gt; {
  try {
    const { agentId } = req.body;
    const ticket = await Ticket.findByIdAndUpdate(
      req.params.ticketId,
      { assignedTo: agentId, status: 'in-progress' },
      { new: true }
    ).populate('assignedTo', 'username');

    if (!ticket) return res.status(404).json({ message: 'Ticket not found' });

    res.json(ticket);
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: 'Error assigning ticket' });
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We use findByIdAndUpdate() to update the assignedTo field and set the status to in-progress.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;{ new: true } ensures we get the updated document back.&lt;/li&gt;
&lt;li&gt;.populate() fetches agent details so you don’t need another query.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Admin: View Tickets by User&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Why?&lt;/em&gt;&lt;br&gt;
Admins often need to see all tickets created by a specific user for auditing or follow-up.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Route: GET /api/tickets/user/:userId&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Controller Code:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const getTicketsByUser = async (req, res) =&amp;gt; {
  try {
    const tickets = await Ticket.find({ createdBy: req.params.userId })
      .sort({ createdAt: -1 })
      .populate('assignedTo', 'username');

    res.json(tickets);
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: 'Error fetching user tickets' });
  }
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Agent: View Assigned Tickets&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Why?&lt;/em&gt;&lt;br&gt;
Agents need a dedicated list of tickets assigned to them.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Route: GET /api/tickets/assigned&lt;br&gt;
Controller Code:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const getAssignedTickets = async (req, res) =&amp;gt; {
  try {
    const tickets = await Ticket.find({ assignedTo: req.user.id })
      .sort({ updatedAt: -1 });
    res.json(tickets);
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: 'Error fetching assigned tickets' });
  }
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Close a Ticket&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Why?&lt;/em&gt;&lt;br&gt;
Agents (for their own tickets) and admins should be able to close tickets, changing their status to “closed.”&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Route: PUT /api/tickets/:ticketId/close&lt;br&gt;
Controller Code:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const closeTicket = async (req, res) =&amp;gt; {
  try {
    const ticket = await Ticket.findById(req.params.ticketId);
    if (!ticket) return res.status(404).json({ message: 'Ticket not found' });

    const { role, id } = req.user;

    // Only assigned agent or any admin can close
    if (role === 'agent' &amp;amp;&amp;amp; ticket.assignedTo?.toString() !== id) {
      return res.status(403).json({ message: 'Agents can only close their own tickets' });
    }

    ticket.status = 'closed';
    await ticket.save();
    res.json(ticket);
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: 'Error closing ticket' });
  }
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;3. Securing Routes with Middleware&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;We use roleMiddleware to protect these routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { roleMiddleware } from '../middleware/roleMiddleware.js';

router.post('/assign/:ticketId', roleMiddleware(['admin']), assignTicket);
router.get('/user/:userId', roleMiddleware(['admin']), getTicketsByUser);
router.put('/:ticketId/close', roleMiddleware(['admin', 'agent']), closeTicket);
router.get('/assigned', roleMiddleware(['agent']), getAssignedTickets);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This ensures:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Only admins can assign or view all tickets.&lt;/li&gt;
&lt;li&gt;Agents can only view their assigned tickets.&lt;/li&gt;
&lt;li&gt;Agents can close only their own tickets.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  ** Testing**
&lt;/h2&gt;

&lt;p&gt;Use Postman to test these routes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Assign a ticket:
POST /api/tickets/assign/:ticketId with agentId in JSON body.&lt;/li&gt;
&lt;li&gt;View tickets by user:
GET /api/tickets/user/:userId&lt;/li&gt;
&lt;li&gt;Close a ticket:
PUT /api/tickets/:ticketId/close&lt;/li&gt;
&lt;li&gt;View assigned tickets (as agent):
GET /api/tickets/assigned&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;4.1. Why Replies Are Important&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;So far, a ticket was just a title and description.&lt;br&gt;
But in a real support system, tickets involve conversation threads between the user and the agent.&lt;br&gt;
We need a way to store these replies.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;4.2. Adding a Message Schema&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Why a separate schema?&lt;/em&gt;&lt;br&gt;
Keeping messages in their own collection makes it easier to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scale conversations&lt;/li&gt;
&lt;li&gt;Query by ticket&lt;/li&gt;
&lt;li&gt;Manage who sent each message&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Code: models/Message.js&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import mongoose from 'mongoose';

const messageSchema = new mongoose.Schema({
  ticket: { type: mongoose.Schema.Types.ObjectId, ref: 'Ticket', required: true },
  sender: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  content: { type: String, required: true }
}, { timestamps: true });

export default mongoose.model('Message', messageSchema);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key points:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ticket links the message to a ticket.&lt;/li&gt;
&lt;li&gt;sender is the user or agent who sent the reply.&lt;/li&gt;
&lt;li&gt;timestamps lets us track the conversation history in orde
r.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;5. Reply Routes&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Update Needed in Old Code&lt;/em&gt;&lt;br&gt;
In routes/ticketRoutes.js, we need to add verifyToken middleware (if not already added) so only logged-in users/agents can reply.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;POST /api/tickets/:ticketId/replies&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
Purpose: Add a reply to a ticket.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Controller (controllers/messageController.js):&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import Message from '../models/Message.js';
import Ticket from '../models/Ticket.js';

export const addReply = async (req, res) =&amp;gt; {
  try {
    const { content } = req.body;
    const ticket = await Ticket.findById(req.params.ticketId);

    if (!ticket) {
      return res.status(404).json({ message: 'Ticket not found' });
    }

    // Only admin, agent, or ticket creator can reply
    if (
      req.user.role === 'agent' &amp;amp;&amp;amp;
      ticket.assignedTo?.toString() !== req.user.id
    ) {
      return res.status(403).json({ message: 'Agents can only reply to their assigned tickets' });
    }

    if (
      req.user.role === 'user' &amp;amp;&amp;amp;
      ticket.createdBy.toString() !== req.user.id
    ) {
      return res.status(403).json({ message: 'Users can only reply to their own tickets' });
    }

    const message = await Message.create({
      ticket: ticket._id,
      sender: req.user.id,
      content
    });

    res.status(201).json(message);
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: 'Error adding reply' });
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;em&gt;GET /api/tickets/:ticketId/replies&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
Purpose: Fetch all replies for a given ticket.&lt;/p&gt;

&lt;p&gt;Create a controller getReply, try the logic yourselves.&lt;br&gt;
_Hint: It will use this query- Message.find({ ticket: req.params.ticketId }) _&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding Routes&lt;/strong&gt;&lt;br&gt;
In routes/messageRoutes.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import express from 'express';
import { verifyToken } from '../middleware/authMiddleware.js';
import { addReply, getReplies } from '../controllers/messageController.js';

const router = express.Router();

router.post('/:ticketId/replies', verifyToken, addReply);
router.get('/:ticketId/replies', verifyToken, getReplies);

export default router;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;6. Admin Dashboard Analytics&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Admins need insights like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many tickets are open/closed&lt;/li&gt;
&lt;li&gt;Which agents have the most assigned tickets&lt;/li&gt;
&lt;li&gt;Ticket resolution rates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Controller (controllers/adminController.js&lt;/em&gt;&lt;br&gt;
We’ll use MongoDB aggregation to compute statistics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import Ticket from '../models/Ticket.js';

export const getDashboardStats = async (req, res) =&amp;gt; {
  try {
    const totalTickets = await Ticket.countDocuments();
    const openCount = await Ticket.countDocuments({ status: 'open' });
    const closedCount = await Ticket.countDocuments({ status: 'closed' });
    const inProgressCount = await Ticket.countDocuments({ status: 'in-progress' });

    // Count tickets grouped by agent
    const agentWorkload = await Ticket.aggregate([
      { $match: { assignedTo: { $ne: null } } },
      { $group: { _id: '$assignedTo', count: { $sum: 1 } } },
      { $sort: { count: -1 } }
    ]);

    res.json({
      totalTickets,
      openCount,
      inProgressCount,
      closedCount,
      agentWorkload
    });
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: 'Error fetching dashboard stats' });
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, Register these routes in your routes/adminRoutes.js.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;7. User Profile Routes&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Users should be able to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;View their profile&lt;/li&gt;
&lt;li&gt;Update their username or email (but not their role)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Controller (controllers/userController.js)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import User from '../models/User.js';

export const getProfile = async (req, res) =&amp;gt; {
  try {
    const user = await User.findById(req.user.id).select('-password');
    res.json(user);
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: 'Error fetching profile' });
  }
};

export const updateProfile = async (req, res) =&amp;gt; {
  try {
    const { username, email } = req.body;
    const user = await User.findById(req.user.id);

    if (username) user.username = username;
    if (email) user.email = email;

    await user.save();
    res.json({ message: 'Profile updated', user });
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: 'Error updating profile' });
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What We Achieved in Part 3&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We extended our backend to handle multi-role workflows:&lt;/li&gt;
&lt;li&gt;Admins can assign tickets and audit users.&lt;/li&gt;
&lt;li&gt;Agents can view and close tickets assigned to them.&lt;/li&gt;
&lt;li&gt;We explained important Mongoose methods for beginners.&lt;/li&gt;
&lt;li&gt;We enforced strict role-based access control.&lt;/li&gt;
&lt;li&gt;Threaded replies: A new Message schema and routes for conversations.&lt;/li&gt;
&lt;li&gt;Reply permissions: Users and assigned agents can reply; no one else.&lt;/li&gt;
&lt;li&gt;Analytics: Admin dashboard stats with MongoDB aggregation.&lt;/li&gt;
&lt;li&gt;User profile APIs: Users can view and update their details securely.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;GitHub Repo: &lt;a href="https://github.com/Abhijeet002/HelpMe-AI-Powered-Support-Ticket-System" rel="noopener noreferrer"&gt;github&lt;/a&gt;&lt;br&gt;
If you did not read the last part-&lt;a href="https://dev.to/abhijeet_sachan_34f5d10dc/building-helpme-webapp-part-2-core-ticket-routes-with-express-mongoose-3fdm"&gt;part-2&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This concludes Part 3 of the series. If you are new to backend development, take time to understand the query methods and role-based design—they are essential for building secure, scalable systems.&lt;br&gt;
We now have a full conversation layer and analytics capabilities built into our backend!&lt;/p&gt;




&lt;p&gt;What’s Next&lt;br&gt;
In the next post, we will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Integrate Cloudinary for attachments.&lt;/li&gt;
&lt;li&gt;Add AI-powered sentiment analysis &amp;amp; auto-tagging.&lt;/li&gt;
&lt;li&gt;Begin frontend work with React/Next.js to bring everything together visually.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>helpme</category>
      <category>ai</category>
      <category>mern</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building Scalable Support Ticket System with Node.js, Express &amp; MongoDB – Part 2</title>
      <dc:creator>AbhiJeet Sachan</dc:creator>
      <pubDate>Sat, 12 Jul 2025 22:23:44 +0000</pubDate>
      <link>https://dev.to/abhijeet_sachan_34f5d10dc/building-helpme-webapp-part-2-core-ticket-routes-with-express-mongoose-3fdm</link>
      <guid>https://dev.to/abhijeet_sachan_34f5d10dc/building-helpme-webapp-part-2-core-ticket-routes-with-express-mongoose-3fdm</guid>
      <description>&lt;p&gt;&lt;em&gt;In this follow‑up, we’ll implement the ticket creation, user ticket listing, and single ticket view endpoints . You’ll learn why we choose specific methods, how we structure controllers, and what’s coming next.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Quick Recap (Part 1)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Project setup with Express, Mongoose, environment variables, and cookie‑based JWT auth&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schemas&lt;/strong&gt;: User and Ticket models with proper validations and references&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication&lt;/strong&gt;: register/login/logout routes with bcrypt hashing and HTTP‑only cookies&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Middleware&lt;/strong&gt;: verifyToken to validate JWTs, and roleMiddleware() for access control&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Objectives of This Post&lt;/strong&gt;
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create a ticket (POST /api/tickets)&lt;/li&gt;
&lt;li&gt;List my tickets (GET /api/tickets/my-tickets)&lt;/li&gt;
&lt;li&gt;View a single ticket (GET /api/tickets/:ticketId)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;You’ll see how we:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Organize controllers to separate business logic from routes&lt;/li&gt;
&lt;li&gt;Use Mongoose methods for efficient querying and population&lt;/li&gt;
&lt;li&gt;Return consistent responses and handle errors gracefully&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Controller Setup&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A controller is a module or function responsible for handling the  logic associated with a specific route or set of routes. It acts as an intermediary between the incoming HTTP request (handled by the route) and the application's data and services.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Things to Remember When Defining a Controller in Express&lt;/strong&gt;-&lt;br&gt;
Each controller should do one thing well: handle the logic for a single route or resource.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// example
const getUserById = (req, res) =&amp;gt; {
  const userId = req.params.id;
  // fetch user logic
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;1. Use &lt;em&gt;req&lt;/em&gt;, &lt;em&gt;res&lt;/em&gt;, and _next _Properly&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use req to access request data&lt;/li&gt;
&lt;li&gt;Use res to send response&lt;/li&gt;
&lt;li&gt;Use next() to pass errors or call next middleware
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const getUser = (req, res, next) =&amp;gt; {
  try {
    // Logic here
    res.status(200).json({ message: 'User found' });
  } catch (err) {
    next(err); // Pass error to error-handling middleware
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Return Consistent Response Structure&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;res.status(200).json({
  success: true,
  data: user,
  message: "User fetched successfully"
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Maintain&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;success: true/false&lt;/li&gt;
&lt;li&gt;data: ...&lt;/li&gt;
&lt;li&gt;message: ...&lt;/li&gt;
&lt;li&gt;Use status codes properly, for more check this npm package &lt;a href="https://www.npmjs.com/package/http-status-codes" rel="noopener noreferrer"&gt;http-status-codes
&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This helps to handle responses easily.&lt;/p&gt;

&lt;p&gt;First, let’s create a controllers/ticketController.js to house our ticket logic:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;controllers/ticketController.js&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import Ticket from '../models/Ticket.js';

// @desc    Create a new ticket
// @route   POST /api/tickets
// @access  Private (user)
export const createTicket = async (req, res) =&amp;gt; {
  try {
    const { title, description, priority } = req.body;
    const ticket = new Ticket({
      title,
      description,
      priority,
      createdBy: req.user.id
    });
    const saved = await ticket.save();
    res.status(201).json(saved);
  } catch (err) {
    console.error(err);
    res.status(500).json({ message: 'Server error creating ticket' });
  }
};

// @desc    Get tickets for logged‑in user
// @route   GET /api/tickets/my-tickets
// @access  Private (user)
export const getMyTickets = async (req, res) =&amp;gt; {
  try {
    const tickets = await Ticket.find({ createdBy: req.user.id })
      .sort({ createdAt: -1 });
    res.json(tickets);
  } catch (err) {
    console.error(err);
    res.status(500).json({ message: 'Server error fetching tickets' });
  }
};

// @desc    Get single ticket by ID
// @route   GET /api/tickets/:ticketId
// @access  Private (user, agent, admin)
export const getTicketById = async (req, res) =&amp;gt; {
  try {
    const ticket = await Ticket.findById(req.params.ticketId)
      .populate('createdBy', 'username email');
    if (!ticket) {
      return res.status(404).json({ message: 'Ticket not found' });
    }
    res.json(ticket);
  } catch (err) {
    console.error(err);
    res.status(500).json({ message: 'Server error fetching ticket' });
  }
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Later we'll be updating these logics if needed to scale and optimize our app.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why This Approach?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dedicated Controller: Keeps route definitions clean and focuses on business logic here.&lt;/li&gt;
&lt;li&gt;new Ticket() + .save(): Straightforward Mongoose pattern to create and persist documents.&lt;/li&gt;
&lt;li&gt;Ticket.find(): Efficiently retrieves user‑specific tickets. We sort by creation date for UX.&lt;/li&gt;
&lt;li&gt;.sort() ensures newest tickets appear first&lt;/li&gt;
&lt;li&gt;populate(): Automatically replaces ObjectId fields with user details, avoiding extra client requests.&lt;/li&gt;
&lt;li&gt;Error Handling: Catches unexpected failures and returns a 500 status with a clear message.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Defining the Routes&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next, wire up these controllers in routes/ticketRoutes.js&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;// routes/ticketRoutes.js&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import express from 'express';
import {
  createTicket,
  getMyTickets,
  getTicketById
} from '../controllers/ticketController.js';
import { verifyToken } from '../middleware/verifyToken.js';

const router = express.Router();

// All routes under /api/tickets are private
router.use(verifyToken);

router.post(
  "/create",
  verifyToken,
  roleMiddleware(["user"]),
  createTicket
);
router.get("/my-tickets", verifyToken, getMyTickets);
router.get("/:ticketId", verifyToken, getTicketById);

export default router;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we have used the &lt;em&gt;verifyToken&lt;/em&gt;, which is a middleware and it makes sure the user is authenticated or not. if not authenticated, the user will not be able to see tickets.&lt;/p&gt;

&lt;p&gt;Middleware: Functions that have access to the request object (req), the response object (res), and the next middleware function in the application's request-response cycle&lt;/p&gt;

&lt;p&gt;How to create a middleware:  a middleware is a normal function that requires three argument: req, res, next. it has access to req and res, and end the request-response cycle, or pass control to the next middleware in the stack.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const middlewareFunction = (req, res, next) =&amp;gt; {
  // Your logic here
  console.log('Middleware executed');
  next(); // Pass control to the next middleware or route handler
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for more details on middleware check &lt;a href="https://expressjs.com/en/guide/writing-middleware.html" rel="noopener noreferrer"&gt;express official website&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then import this in server.js/index.js:&lt;br&gt;
&lt;code&gt;import ticketRoutes from './routes/ticketRoutes.js';&lt;br&gt;
app.use('/api/tickets', ticketRoutes);&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
Now We are done with Ticket creation endpoints and User‑specific ticket listing. &lt;br&gt;
The next step is to create admin routes to -&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;View all tickets by user&lt;/li&gt;
&lt;li&gt;Assigning tickets to agents&lt;/li&gt;
&lt;li&gt;Agent endpoints – view assigned tickets&lt;/li&gt;
&lt;li&gt;Threaded replies using a conversation schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: In previous post we have created a new middleware &lt;strong&gt;&lt;em&gt;roleMiddleware&lt;/em&gt;&lt;/strong&gt; that authenticates the user by their roles(user, agent or admin).&lt;br&gt;
Further in this app, wherever we need to authenticate or to check the user by thier roles, we have to use this middleware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Challenge yourself:&lt;/em&gt;&lt;/strong&gt; Create a &lt;em&gt;deleteTicket&lt;/em&gt; controller to delete tickets(only user can do) by yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tips:&lt;/strong&gt; &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get tickets from params: 
&lt;code&gt;const { ticketId } = req.params;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Find ticket by id, if no ticket found return- No tickets found. Otherwise delete the ticket by &lt;em&gt;findByIdAndDelete&lt;/em&gt; method.&lt;/li&gt;
&lt;li&gt;Use try, catch block properly.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Next Steps:&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;We are going to create admin routes and agent routes to&lt;br&gt;
-Assign tickets(for admin)&lt;br&gt;
-Get tickets by user Id(admin)&lt;br&gt;
-Close a ticket(admin, agent)&lt;br&gt;
-See assigned tickets(agent)&lt;/p&gt;

&lt;p&gt;Before defining these routes, We need some changes in our Ticket schema-&lt;br&gt;
To check/assign the tickets we have to add a key to &lt;strong&gt;Ticket schema&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assignedTo:  { type: mongoose.Schema.Types.ObjectId, ref: 'User', default: null }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Creating controllers&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;assignTicket controller-&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Assign a ticket to an agent&lt;/li&gt;
&lt;li&gt;POST /api/tickets/assign/:ticketId&lt;/li&gt;
&lt;li&gt;Access: Admin
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import Ticket from '../models/Ticket.js';
export const assignTicket = async (req, res) =&amp;gt; {
  try {
    const { agentId } = req.body;
    const ticket = await Ticket.findByIdAndUpdate(
      req.params.ticketId,
      { assignedTo: agentId, status: 'in-progress' },
      { new: true }
    );
    if (!ticket) return res.status(404).json({ message: 'Ticket not found' });
    return res.json(ticket);
  } catch (err) {
    console.error(err);
    return res.status(500).json({ message: 'Error assigning ticket' });
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Uses findByIdAndUpdate to set assignedTo and move status to “in‑progress.”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Returns the updated document via { new: true }&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;getTicketsByUser Controller:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Get all tickets raised by a specific user&lt;/li&gt;
&lt;li&gt;GET /api/tickets/user/:userId&lt;/li&gt;
&lt;li&gt;Access: Admin
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const getTicketsByUser = async (req, res) =&amp;gt; {
  try {
    const tickets = await Ticket.find({ createdBy: req.params.userId })
      .sort({ createdAt: -1 })
      .populate('assignedTo', 'username');
    return res.json(tickets);
  } catch (err) {
    console.error(err);
    return res.status(500).json({ message: 'Error fetching user tickets' });
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Filters by createdBy to fetch all tickets a given user has opened.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Populates assignedTo.username so the admin sees who’s handling each ticket.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;closeTicket controller:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Close a ticket (either admin or agent)&lt;br&gt;
PUT /api/tickets/:ticketId/close&lt;br&gt;
Access: Admin, Agent&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const closeTicket = async (req, res) =&amp;gt; {
  try {
    const ticket = await Ticket.findById(req.params.ticketId);
    if (!ticket) return res.status(404).json({ message: 'Ticket not found' });

    // Only assigned agent or any admin may close
    const { role, id } = req.user;
    if (role === 'agent' &amp;amp;&amp;amp; ticket.assignedTo?.toString() !== id) {
      return res.status(403).json({ message: 'Agents can only close their own tickets' });
    }

    ticket.status = 'closed';
    await ticket.save();
    return res.json(ticket);
  } catch (err) {
    console.error(err);
    return res.status(500).json({ message: 'Error closing ticket' });
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Fetches the ticket, checks permissions (agent may only close their own).&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Sets status = 'closed' and saves—triggering an updatedAt timestamp update.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;getAssignedTickets controller&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Get tickets assigned to the logged-in agent&lt;/li&gt;
&lt;li&gt;GET /api/tickets/assigned&lt;/li&gt;
&lt;li&gt;Access: Agent
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const getAssignedTickets = async (req, res) =&amp;gt; {
  try {
    const tickets = await Ticket.find({ assignedTo: req.user.id })
      .sort({ createdAt: -1 }) // newest first
      .populate("createdBy", "username email role") // optional: enrich with user info
      .populate("assignedTo", "username email");

    if (!tickets.length) {
      return res.status(404).json({ message: "No assigned tickets found" });
    }
    return res.status(200).json({ tickets });
  } catch (error) {
    console.error("Error fetching assigned tickets:", error);
    return res
      .status(500)
      .json({ message: "Failed to fetch assigned tickets" });
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Agents retrieve only tickets where assignedTo === req.user.id.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Sorting by updatedAt surfaces the most recently active tickets first.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Route Definitions&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Wire these into routes/ticketRoutes.js and protect them with roleMiddleware.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import express from 'express';
import {
  createTicket,
  getMyTickets,
  getTicketById,
  assignTicket,
  getTicketsByUser,
  closeTicket,
  getAssignedTickets
} from '../controllers/ticketController.js';
import { verifyToken } from '../middleware/verifyToken.js';
import { roleMiddleware } from '../middleware/roleMiddleware.js';

const router = express.Router();
router.use(verifyToken);

// Admin only
router.post(
  '/assign/:ticketId',
  verifyToken,
  roleMiddleware(['admin']),
  assignTicket
);
router.get(
  '/user/:userId',
  verifyToken,
  roleMiddleware(['admin']),
  getTicketsByUser
);

// Admin &amp;amp; Agent can close
router.put(
  '/:ticketId/close',
  verifyToken,
  roleMiddleware(['admin','agent']),
  closeTicket
);

// Agent only
router.get(
  "/assigned",
  verifyToken,
  roleMiddleware(["agent"]),
  getAssignedTickets
);

export default router;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;br&gt;
To ensure agents can only interact with their own tickets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Enforce roleMiddleware(['agent']) on any agent‑specific endpoints (/assigned, later “reply” routes).&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;In closeTicket, verify req.user.id === ticket.assignedTo.toString() before allowing closure.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;In future reply/message controllers, always check that the actor is either the ticket creator or the assigned agent.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Summary of Changes&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Schema: added timestamps: true.&lt;/li&gt;
&lt;li&gt;Controllers: four new methods for assignment, user‑scoped listing, closing, and agent‑scoped listing.&lt;/li&gt;
&lt;li&gt;Routes: protected with verifyToken + roleMiddleware for precise access control.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Final Notes&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;What We’ve Covered in This Post (Part 2):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Implemented ticket creation (POST /api/tickets) with input validation and error handling&lt;/li&gt;
&lt;li&gt;Built user ticket listing (GET /api/tickets/my-tickets) with filtering, sorting, and field projection&lt;/li&gt;
&lt;li&gt;Created single ticket retrieval (GET /api/tickets/:ticketId) leveraging .populate() for rich user data&lt;/li&gt;
&lt;li&gt;Added admin routes: ticket assignment and fetching by user ID&lt;/li&gt;
&lt;li&gt;Added agent routes: viewing assigned tickets and closing tickets securely&lt;/li&gt;
&lt;li&gt;Demonstrated how to protect endpoints with verifyToken and roleMiddleware&lt;/li&gt;
&lt;li&gt;Updated the Ticket schema to include timestamps for audit trails&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What’s in Next Parts&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Admin endpoints: list all tickets, fetch by user ID&lt;/li&gt;
&lt;li&gt;Agent actions: view assigned tickets, add replies&lt;/li&gt;
&lt;li&gt;Adding cloudinary database to store files&lt;/li&gt;
&lt;li&gt;Conversation threads: design a Message schema and link to tickets&lt;/li&gt;
&lt;li&gt;AI integration: outline methods for auto‑tagging and sentiment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Previous Post: To better understand this project read the previous post &lt;a href="https://dev.to/abhijeet_sachan_34f5d10dc/building-a-scalable-support-ticket-system-with-nodejs-express-mongodb-2kj6"&gt;Part-1&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;🔗 Explore &amp;amp; Contribute&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;All code is on GitHub—feel free to fork, star, and suggest improvements:&lt;br&gt;
&lt;a href="https://github.com/Abhijeet002/HelpMe-AI-Powered-Support-Ticket-System" rel="noopener noreferrer"&gt;Helpme github-repo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow me on &lt;a href="https://dev.to/abhijeet_sachan_34f5d10dc"&gt;Dev.to&lt;/a&gt; or &lt;a href="https://www.linkedin.com/in/abhijeet-sachan/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; for the next parts of this series.&lt;br&gt;
Got questions or feedback? Let me know in the comments — happy to help!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>ai</category>
      <category>helpme</category>
    </item>
    <item>
      <title>Building a Scalable Support Ticket System with Node.js, Express &amp; MongoDB</title>
      <dc:creator>AbhiJeet Sachan</dc:creator>
      <pubDate>Tue, 01 Jul 2025 13:53:32 +0000</pubDate>
      <link>https://dev.to/abhijeet_sachan_34f5d10dc/building-a-scalable-support-ticket-system-with-nodejs-express-mongodb-2kj6</link>
      <guid>https://dev.to/abhijeet_sachan_34f5d10dc/building-a-scalable-support-ticket-system-with-nodejs-express-mongodb-2kj6</guid>
      <description>&lt;p&gt;**&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: Backend Foundation creating a Ticket raising platform
&lt;/h2&gt;

&lt;p&gt;**&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A practical guide to creating real-world backend systems using Mongoose, Express middleware, and secure authentication.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Customer support systems are an essential part of any tech-driven business, and building one from scratch is a great way to learn full-stack development with real-world requirements.&lt;/p&gt;

&lt;p&gt;In this series, I'm documenting the development of HelpMe, an AI-powered support ticket system built using the MERN stack (MongoDB, Express, React, Node.js). The goal is to simulate a production-grade ticketing platform with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Role-based access (user, agent, admin)&lt;/li&gt;
&lt;li&gt;JWT-based authentication&lt;/li&gt;
&lt;li&gt;Ticket lifecycle management&lt;/li&gt;
&lt;li&gt;Modular, scalable backend structure&lt;/li&gt;
&lt;li&gt;AI features (coming soon)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post will cover the backend foundation — ideal for anyone looking to build a professional-grade project using Express and MongoDB.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Abhijeet002/HelpMe-AI-Powered-Support-Ticket-System" rel="noopener noreferrer"&gt;Github repo&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;

&lt;p&gt;Organizing files in a clean, modular way is key to scaling your backend efficiently. Here's how the backend is structured:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9er0wkmdrmmxi31ywvyj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9er0wkmdrmmxi31ywvyj.png" alt="Project structure" width="277" height="804"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment Setup
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Initialize the project
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm init -y
npm install express mongoose dotenv cookie-parser bcryptjs jsonwebtoken

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Create the entry file&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;// server.js&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import express from 'express';
import mongoose from 'mongoose';
import dotenv from 'dotenv';
import cookieParser from 'cookie-parser';

dotenv.config();
const app = express();

app.use(express.json());
app.use(cookieParser());

// Routes (example)
import authRoutes from './routes/auth.js';
app.use('/api/auth', authRoutes);

// Connect to MongoDB
mongoose.connect(process.env.MONGO_URI)
  .then(() =&amp;gt; app.listen(5000, () =&amp;gt; console.log('Server running')))
  .catch(err =&amp;gt; console.error('MongoDB connection failed:', err));

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Defining the Schemas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;User Schema
// models/User.js
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import mongoose from 'mongoose';

const userSchema = new mongoose.Schema({
  username: { type: String, required: true, trim: true, unique: true },
  email:    { type: String, required: true, trim: true, lowercase: true, unique: true },
  password: { type: String, required: true, minlength: 6 },
  role:     { type: String, enum: ['user', 'admin', 'agent'], default: 'user' }
});

export default mongoose.model('User', userSchema);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Ticket Schema
// models/Ticket.js
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import mongoose from 'mongoose';

const ticketSchema = new mongoose.Schema({
  title:       { type: String, required: true },
  description: { type: String },
  priority:    { type: String, enum: ['low', 'medium', 'high'], default: 'low' },
  status:      { type: String, enum: ['open', 'in-progress', 'closed'], default: 'open' },
createdBy: {type: mongoose.Schema.Types.ObjectId, ref: "User",    required: true,
    },
});

export default mongoose.model('Ticket', ticketSchema);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why Schema Design Matters
&lt;/h2&gt;

&lt;p&gt;Relationships like createdBy and assignedTo are handled using MongoDB references (ObjectId), enabling efficient population and querying.&lt;/p&gt;

&lt;p&gt;Enumerations ensure valid values for priority, status, and role, enforcing business rules at the DB level.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Authentication System&lt;/strong&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Register, Login &amp;amp; Logout
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Auth routes include:
POST /register – Create a user account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;POST /login – Authenticate and store JWT in an HTTP-only cookie&lt;/p&gt;

&lt;p&gt;POST /logout – Clear the authentication cookie&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// routes/auth.js
import express from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import User from '../models/User.js';

const router = express.Router();

router.post('/register', async (req, res) =&amp;gt; {
  const { username, email, password } = req.body;
  const hashed = await bcrypt.hash(password, 10);
  const user = new User({ username, email, password: hashed });
  await user.save();
  res.status(201).json({ message: 'Registration successful' });
});

router.post('/login', async (req, res) =&amp;gt; {
  const { email, password } = req.body;
  const user = await User.findOne({ email });
  if (!user || !(await bcrypt.compare(password, user.password))) {
    return res.status(401).json({ message: 'Invalid credentials' });
  }

  const token = jwt.sign({ id: user._id, role: user.role }, process.env.JWT_SECRET);
  res.cookie('token', token, { httpOnly: true });
  res.status(200).json({ message: 'Login successful' });
});

router.post('/logout', (req, res) =&amp;gt; {
  res.clearCookie('token');
  res.status(200).json({ message: 'Logged out' });
});

export default router;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Middleware: Secure Access&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;To protect routes and limit access based on roles, we add two middlewares:&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Verify JWT
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// middleware/verifyToken.js
import jwt from 'jsonwebtoken';

export const verifyToken = (req, res, next) =&amp;gt; {
  const token = req.cookies.token;
  if (!token) return res.status(401).json({ message: 'Unauthorized' });

  jwt.verify(token, process.env.JWT_SECRET, (err, decoded) =&amp;gt; {
    if (err) return res.status(403).json({ message: 'Invalid token' });
    req.user = decoded;
    next();
  });
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Role-Based Access
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// middleware/roleMiddleware.js
export const roleMiddleware = (roles) =&amp;gt; (req, res, next) =&amp;gt; {
  if (!roles.includes(req.user.role)) {
    return res.status(403).json({ message: 'Access denied' });
  }
  next();
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This system allows you to protect endpoints like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.get('/tickets/all', verifyToken, roleMiddleware(['admin']), controller);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;What’s Covered So Far&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Project structure &amp;amp; setup                      ✅ Done &lt;br&gt;
User &amp;amp; Ticket schemas                          ✅ Done &lt;br&gt;
Auth routes (&lt;code&gt;/register&lt;/code&gt;, &lt;code&gt;/login&lt;/code&gt;, &lt;code&gt;/logout&lt;/code&gt;) ✅ Done &lt;br&gt;
JWT &amp;amp; role-based middleware                    ✅ Done &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Coming in Part 2&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Next, I’ll cover:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ticket creation and viewing routes&lt;/li&gt;
&lt;li&gt;Admin: Assign ticket to agent&lt;/li&gt;
&lt;li&gt;Agent dashboard: View assigned tickets&lt;/li&gt;
&lt;li&gt;Threaded replies using a conversation schema&lt;/li&gt;
&lt;li&gt;Integrating AI models for tagging &amp;amp; sentiment&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Final Notes
&lt;/h2&gt;

&lt;p&gt;If you're looking to build a full-featured backend using modern Node.js practices — especially for multi-role apps — this architecture is production-ready and extendable.&lt;/p&gt;

&lt;p&gt;Feel free to explore, clone, or contribute to the project below.&lt;/p&gt;

&lt;p&gt;🔗 GitHub Repo: &lt;a href="https://github.com/Abhijeet002/HelpMe-AI-Powered-Support-Ticket-System" rel="noopener noreferrer"&gt;https://github.com/Abhijeet002/HelpMe-AI-Powered-Support-Ticket-System&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Follow me on &lt;a href="https://dev.to/abhijeet_sachan_34f5d10dc"&gt;Dev.to&lt;/a&gt; or &lt;a href="https://www.linkedin.com/in/abhijeet-sachan/" rel="noopener noreferrer"&gt;LinkedIn &lt;/a&gt;for the next parts of this series.&lt;br&gt;
Got questions or feedback? Let me know in the comments — happy to help!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>mern</category>
      <category>helpme</category>
    </item>
  </channel>
</rss>
