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.
Quick Recap (Part 1)
Project setup with Express, Mongoose, environment variables, and cookie‑based JWT auth
Schemas: User and Ticket models with proper validations and references
Authentication: register/login/logout routes with bcrypt hashing and HTTP‑only cookies
Middleware: verifyToken to validate JWTs, and roleMiddleware() for access control
Objectives of This Post
- Create a ticket (POST /api/tickets)
- List my tickets (GET /api/tickets/my-tickets)
- View a single ticket (GET /api/tickets/:ticketId)
You’ll see how we:
- Organize controllers to separate business logic from routes
- Use Mongoose methods for efficient querying and population
- Return consistent responses and handle errors gracefully
Controller Setup
- 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.
Things to Remember When Defining a Controller in Express-
Each controller should do one thing well: handle the logic for a single route or resource.
// example
const getUserById = (req, res) => {
const userId = req.params.id;
// fetch user logic
};
1. Use req, res, and _next _Properly
- Use req to access request data
- Use res to send response
- Use next() to pass errors or call next middleware
const getUser = (req, res, next) => {
try {
// Logic here
res.status(200).json({ message: 'User found' });
} catch (err) {
next(err); // Pass error to error-handling middleware
}
};
2. Return Consistent Response Structure
res.status(200).json({
success: true,
data: user,
message: "User fetched successfully"
});
- Maintain:
- success: true/false
- data: ...
- message: ...
- Use status codes properly, for more check this npm package http-status-codes
This helps to handle responses easily.
First, let’s create a controllers/ticketController.js to house our ticket logic:
controllers/ticketController.js
import Ticket from '../models/Ticket.js';
// @desc Create a new ticket
// @route POST /api/tickets
// @access Private (user)
export const createTicket = async (req, res) => {
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) => {
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) => {
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' });
}
};
Later we'll be updating these logics if needed to scale and optimize our app.
Why This Approach?
- Dedicated Controller: Keeps route definitions clean and focuses on business logic here.
- new Ticket() + .save(): Straightforward Mongoose pattern to create and persist documents.
- Ticket.find(): Efficiently retrieves user‑specific tickets. We sort by creation date for UX.
- .sort() ensures newest tickets appear first
- populate(): Automatically replaces ObjectId fields with user details, avoiding extra client requests.
- Error Handling: Catches unexpected failures and returns a 500 status with a clear message.
Defining the Routes
- Next, wire up these controllers in routes/ticketRoutes.js
// routes/ticketRoutes.js
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;
Here, we have used the verifyToken, 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.
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
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.
const middlewareFunction = (req, res, next) => {
// Your logic here
console.log('Middleware executed');
next(); // Pass control to the next middleware or route handler
};
for more details on middleware check express official website
Then import this in server.js/index.js:
import ticketRoutes from './routes/ticketRoutes.js';
app.use('/api/tickets', ticketRoutes);
Now We are done with Ticket creation endpoints and User‑specific ticket listing.
The next step is to create admin routes to -
- View all tickets by user
- Assigning tickets to agents
- Agent endpoints – view assigned tickets
- Threaded replies using a conversation schema
Note: In previous post we have created a new middleware roleMiddleware that authenticates the user by their roles(user, agent or admin).
Further in this app, wherever we need to authenticate or to check the user by thier roles, we have to use this middleware.
Challenge yourself: Create a deleteTicket controller to delete tickets(only user can do) by yourself.
Tips:
- Get tickets from params:
const { ticketId } = req.params;
- Find ticket by id, if no ticket found return- No tickets found. Otherwise delete the ticket by findByIdAndDelete method.
- Use try, catch block properly.
Next Steps:
We are going to create admin routes and agent routes to
-Assign tickets(for admin)
-Get tickets by user Id(admin)
-Close a ticket(admin, agent)
-See assigned tickets(agent)
Before defining these routes, We need some changes in our Ticket schema-
To check/assign the tickets we have to add a key to Ticket schema:
assignedTo: { type: mongoose.Schema.Types.ObjectId, ref: 'User', default: null }
Creating controllers
- assignTicket controller-
- Assign a ticket to an agent
- POST /api/tickets/assign/:ticketId
- Access: Admin
import Ticket from '../models/Ticket.js';
export const assignTicket = async (req, res) => {
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' });
}
};
- Uses findByIdAndUpdate to set assignedTo and move status to “in‑progress.”
- Returns the updated document via { new: true }
- getTicketsByUser Controller:
- Get all tickets raised by a specific user
- GET /api/tickets/user/:userId
- Access: Admin
export const getTicketsByUser = async (req, res) => {
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' });
}
};
- Filters by createdBy to fetch all tickets a given user has opened.
- Populates assignedTo.username so the admin sees who’s handling each ticket.
- closeTicket controller:
Close a ticket (either admin or agent)
PUT /api/tickets/:ticketId/close
Access: Admin, Agent
export const closeTicket = async (req, res) => {
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' && 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' });
}
};
- Fetches the ticket, checks permissions (agent may only close their own).
- Sets status = 'closed' and saves—triggering an updatedAt timestamp update.
- getAssignedTickets controller
- Get tickets assigned to the logged-in agent
- GET /api/tickets/assigned
- Access: Agent
export const getAssignedTickets = async (req, res) => {
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" });
}
};
- Agents retrieve only tickets where assignedTo === req.user.id.
- Sorting by updatedAt surfaces the most recently active tickets first.
Route Definitions
Wire these into routes/ticketRoutes.js and protect them with roleMiddleware.
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 & Agent can close
router.put(
'/:ticketId/close',
verifyToken,
roleMiddleware(['admin','agent']),
closeTicket
);
// Agent only
router.get(
"/assigned",
verifyToken,
roleMiddleware(["agent"]),
getAssignedTickets
);
export default router;
Note:
To ensure agents can only interact with their own tickets:
- Enforce roleMiddleware(['agent']) on any agent‑specific endpoints (/assigned, later “reply” routes).
- In closeTicket, verify req.user.id === ticket.assignedTo.toString() before allowing closure.
- In future reply/message controllers, always check that the actor is either the ticket creator or the assigned agent.
Summary of Changes
- Schema: added timestamps: true.
- Controllers: four new methods for assignment, user‑scoped listing, closing, and agent‑scoped listing.
- Routes: protected with verifyToken + roleMiddleware for precise access control.
Final Notes
What We’ve Covered in This Post (Part 2):
- Implemented ticket creation (POST /api/tickets) with input validation and error handling
- Built user ticket listing (GET /api/tickets/my-tickets) with filtering, sorting, and field projection
- Created single ticket retrieval (GET /api/tickets/:ticketId) leveraging .populate() for rich user data
- Added admin routes: ticket assignment and fetching by user ID
- Added agent routes: viewing assigned tickets and closing tickets securely
- Demonstrated how to protect endpoints with verifyToken and roleMiddleware
- Updated the Ticket schema to include timestamps for audit trails
What’s in Next Parts
- Admin endpoints: list all tickets, fetch by user ID
- Agent actions: view assigned tickets, add replies
- Adding cloudinary database to store files
- Conversation threads: design a Message schema and link to tickets
- AI integration: outline methods for auto‑tagging and sentiment
Previous Post: To better understand this project read the previous post Part-1
🔗 Explore & Contribute
All code is on GitHub—feel free to fork, star, and suggest improvements:
Helpme github-repo
Follow me on Dev.to or LinkedIn for the next parts of this series.
Got questions or feedback? Let me know in the comments — happy to help!
Top comments (0)