Controllers Made Easy
Master request handling with CalmController's super methods.
What is a Controller?#
A Controller handles HTTP requests and responses. It's the "waiter" that:
- Receives requests from routes
- Validates input using DTOs
- Calls service methods for business logic
- Sends formatted responses back to the client
The Power of CalmController#
CalmAPI provides CalmController - a base class with all CRUD operations built-in!
Built-in Methods#
JavaScript
class CalmController {
async getAll(req, res, next) // GET all items
async get(req, res, next) // GET single item
async insert(req, res, next) // CREATE item
async update(req, res, next) // UPDATE item
async delete(req, res, next) // DELETE item
}You get all these for FREE!
Basic Controller Structure#
JavaScript
const { CalmController } = require('../../../system/core/CalmController');
const { ProductService } = require('./product.service');
const { Product } = require('./product.model');
const productDTO = require('./product.dto');
const autoBind = require('auto-bind');
const productService = new ProductService(new Product().getInstance());
class ProductController extends CalmController {
constructor(service) {
super(service); // Call parent constructor
this.dto = { ...this.dto, ...productDTO }; // Merge DTOs
autoBind(this); // Bind 'this' context
}
}
module.exports = new ProductController(productService);Using super() to Call Parent Methods#
Pattern 1: Use Parent Method As-Is#
Don't write anything! The parent method works automatically:
JavaScript
class ProductController extends CalmController {
// No code needed!
// getAll, get, insert, update, delete work automatically
}Pattern 2: Add Logic Before Parent Method#
JavaScript
class ProductController extends CalmController {
async insert(req, res, next) {
// Add custom logic
console.log('Creating product...', req.body);
// Validation
if (req.body.price < 0) {
return next(new Error('Price cannot be negative'));
}
// Call parent method to do the actual work
return super.insert(req, res, next);
}
}Pattern 3: Add Logic After Parent Method#
JavaScript
class ProductController extends CalmController {
async insert(req, res, next) {
try {
// Call parent method first
await super.insert(req, res, next);
// Add custom logic after
console.log('Product created successfully!');
// Maybe send email notification
await emailService.sendNewProductAlert(req.body);
} catch (e) {
next(e);
}
}
}Pattern 4: Wrap Parent Method#
JavaScript
class ProductController extends CalmController {
async getAll(req, res, next) {
try {
console.log('Fetching products...');
// Add default filters
if (!req.query.status) {
req.query.status = 'active';
}
// Call parent
const result = await super.getAll(req, res, next);
console.log(`Returned ${result.length} products`);
return result;
} catch (e) {
next(e);
}
}
}Custom Methods#
Add your own methods for specific needs:
JavaScript
class ProductController extends CalmController {
constructor(service) {
super(service);
this.dto = { ...this.dto, ...productDTO };
autoBind(this);
}
// Custom method: Get featured products
async getFeatured(req, res, next) {
try {
const products = await this.service.model.find({
featured: true,
status: 'active'
}).limit(10);
res.sendCalmResponse(
products.map(p => new this.dto.GetDTO(p))
);
} catch (e) {
next(e);
}
}
// Custom method: Get by category
async getByCategory(req, res, next) {
try {
const { categoryId } = req.params;
const products = await this.service.model
.find({ category: categoryId, status: 'active' })
.populate('category');
res.sendCalmResponse(
products.map(p => new this.dto.GetDTO(p))
);
} catch (e) {
next(e);
}
}
// Custom method: Search
async search(req, res, next) {
try {
const { q } = req.query;
const products = await this.service.model.find({
$text: { $search: q },
status: 'active'
});
res.sendCalmResponse(
products.map(p => new this.dto.GetDTO(p))
);
} catch (e) {
next(e);
}
}
}Add custom routes:
JavaScript
// product.route.js
router.get('/featured', ProductController.getFeatured);
router.get('/search', ProductController.search);
router.get('/category/:categoryId', ProductController.getByCategory);
router.get('/', ProductController.getAll);Request & Response Objects#
Request (req)#
JavaScript
async insert(req, res, next) {
// URL params
const { id } = req.params; // /api/products/:id
// Query params
const { limit, skip } = req.query; // /api/products?limit=10&skip=0
// Body data
const data = req.body; // POST/PUT data
// User (if authenticated)
const userId = req.user._id; // Set by AuthController.checkLogin
// Headers
const contentType = req.headers['content-type'];
}Response (res)#
CalmAPI provides sendCalmResponse:
JavaScript
// Success with data
res.sendCalmResponse(data);
// Success with data and metadata
res.sendCalmResponse(data, { totalCount: 100 });
// Success with null data
res.sendCalmResponse(null, { deleted: true });Response format:
JSON
{
"status": true,
"data": { ... },
"totalCount": 100
}Error Handling#
Always use try-catch and pass errors to next():
JavaScript
async insert(req, res, next) {
try {
// Your code
const result = await this.service.insert(data);
res.sendCalmResponse(result);
} catch (e) {
next(e); // Pass to error handler
}
}Custom Errors#
JavaScript
async get(req, res, next) {
try {
const { id } = req.params;
if (!mongoose.Types.ObjectId.isValid(id)) {
const error = new Error('Invalid ID format');
error.statusCode = 400;
throw error;
}
const item = await this.service.get(id);
res.sendCalmResponse(item);
} catch (e) {
next(e);
}
}Working with DTOs in Controllers#
Applying DTOs#
JavaScript
async insert(req, res, next) {
try {
// Auto-add createdBy if user is logged in
if (req.user) {
req.body.createdBy = req.user._id;
}
// Validate with InsertDTO
const insertData = new this.dto.InsertDTO(req.body);
// Save
const response = await this.service.insert(insertData);
// Format with GetDTO
res.sendCalmResponse(new this.dto.GetDTO(response.data));
} catch (e) {
next(e);
}
}Advanced Patterns#
Pattern 1: Batch Operations#
JavaScript
async bulkInsert(req, res, next) {
try {
const items = req.body.items; // Array of items
const insertedItems = await Promise.all(
items.map(item =>
this.service.insert(new this.dto.InsertDTO(item))
)
);
res.sendCalmResponse(
insertedItems.map(item => new this.dto.GetDTO(item.data))
);
} catch (e) {
next(e);
}
}Pattern 2: Pagination with Metadata#
JavaScript
async getAll(req, res, next) {
try {
const response = await this.service.getAll(req.query);
const { skip = 0, limit = 10 } = req.query;
const totalPages = Math.ceil(response.total / limit);
const currentPage = Math.floor(skip / limit) + 1;
res.sendCalmResponse(
response.data.map(x => new this.dto.GetDTO(x)),
{
totalCount: response.total,
totalPages,
currentPage,
hasMore: currentPage < totalPages
}
);
} catch (e) {
next(e);
}
}Pattern 3: File Upload Handling#
JavaScript
async uploadImage(req, res, next) {
try {
const { id } = req.params;
const file = req.file; // From multer middleware
if (!file) {
throw new Error('No file uploaded');
}
// Upload to S3 or save locally
const imageUrl = await fileService.upload(file);
// Update product
await this.service.update(id, { images: [imageUrl] });
res.sendCalmResponse({ imageUrl });
} catch (e) {
next(e);
}
}Complete Example: Order Controller#
JavaScript
const { CalmController } = require('../../../system/core/CalmController');
const { OrderService } = require('./order.service');
const { Order } = require('./order.model');
const orderDTO = require('./order.dto');
const autoBind = require('auto-bind');
const orderService = new OrderService(new Order().getInstance());
class OrderController extends CalmController {
constructor(service) {
super(service);
this.dto = { ...this.dto, ...orderDTO };
autoBind(this);
}
// Override insert to add user and calculate total
async insert(req, res, next) {
try {
// Add user ID
req.body.user = req.user._id;
// Calculate total
req.body.total = req.body.items.reduce((sum, item) => {
return sum + (item.price * item.quantity);
}, 0);
// Call parent method
return super.insert(req, res, next);
} catch (e) {
next(e);
}
}
// Get orders for current user
async getMyOrders(req, res, next) {
try {
const orders = await this.service.model
.find({ user: req.user._id })
.populate('items.product')
.sort({ createdAt: -1 });
res.sendCalmResponse(
orders.map(o => new this.dto.GetDTO(o))
);
} catch (e) {
next(e);
}
}
// Update order status
async updateStatus(req, res, next) {
try {
const { id } = req.params;
const { status } = req.body;
const validStatuses = ['pending', 'processing', 'shipped', 'delivered', 'cancelled'];
if (!validStatuses.includes(status)) {
throw new Error('Invalid status');
}
const order = await this.service.update(id, {
status,
updatedBy: req.user._id
});
res.sendCalmResponse(new this.dto.GetDTO(order.data));
} catch (e) {
next(e);
}
}
// Cancel order
async cancel(req, res, next) {
try {
const { id } = req.params;
const order = await this.service.get(id);
// Check if can be cancelled
if (order.data.status !== 'pending') {
throw new Error('Order cannot be cancelled');
}
// Update status
await this.service.update(id, {
status: 'cancelled',
cancelledAt: new Date(),
cancelledBy: req.user._id
});
res.sendCalmResponse(null, { cancelled: true });
} catch (e) {
next(e);
}
}
}
module.exports = new OrderController(orderService);Tips & Best Practices#
1. Always Use autoBind#
JavaScript
const autoBind = require('auto-bind');
constructor(service) {
super(service);
autoBind(this); // Essential!
}2. Merge DTOs Properly#
JavaScript
this.dto = { ...this.dto, ...customDTO };3. Use Try-Catch Everywhere#
JavaScript
async myMethod(req, res, next) {
try {
// Your code
} catch (e) {
next(e); // Pass to error handler
}
}4. Validate Early#
JavaScript
if (!req.body.requiredField) {
const error = new Error('Required field missing');
error.statusCode = 400;
return next(error);
}5. Use Descriptive Method Names#
JavaScript
// Good
async getFeaturedProducts(req, res, next)
async getOrdersByStatus(req, res, next)
async cancelSubscription(req, res, next)
// Bad
async get2(req, res, next)
async doStuff(req, res, next)What's Next?#
You now understand Controllers! Let's dive into the Service layer where business logic lives.
Next: Services Layer →