This document outlines the comprehensive plan for integrating a standardized controller architecture into the SMS-CO2 project. The implementation of the IController interface will establish consistent API endpoints and response formats, improving maintainability and developer experience across the application.
The SMS-CO2 project follows a domain-driven design with a clear separation of concerns. The current controller implementation lacks standardization, with routes being defined separately from controllers.
F:\Github\Capstone_Xin\sms-co2\co2-emission-system\
├── packages
│ ├── backend
│ │ ├── src
│ │ │ ├── app.ts
│ │ │ ├── server.ts
│ │ │ ├── domain
│ │ │ ├── infrastructure
│ │ │ └── interfaces
│ │ │ └── http
│ │ │ ├── controllers
│ │ │ │ └── emissionController.ts
│ │ │ ├── middlewares
│ │ │ │ └── errorHandler.ts
│ │ │ └── routes
│ │ │ └── emissionRoutes.ts
│ ├── frontend
│ └── shared
The current controller pattern:
Example from existing code:
// Current EmissionController implementation
export class EmissionController {
constructor(private emissionService: EmissionService) {}
async createEmission(req: Request, res: Response): Promise<void> {
try {
// Implementation details
res.status(201).json({
success: true,
message: 'Emission created successfully'
});
} catch (error) {
// Error handling
}
}
// Other methods...
}
Route registration:
// Current route registration
const router = Router();
const emissionController = new EmissionController(emissionService);
router.post('/', (req, res) => emissionController.createEmission(req, res));
router.get('/:id', (req, res) => emissionController.getEmissionById(req, res));
// Other routes...
The first phase establishes the foundation for the standardized controller architecture.
// src/interfaces/http/controllers/base/IController.ts
import { Router } from 'express';
export interface IController {
// Base path for controller routes
path: string;
// Express router instance
router: Router;
// Initialize and register routes
initializeRoutes(): void;
}
// src/interfaces/http/controllers/base/BaseController.ts
import { Router, Request, Response, NextFunction } from 'express';
import { IController } from './IController';
export abstract class BaseController implements IController {
public path: string;
public router: Router;
constructor(path: string) {
this.path = path;
this.router = Router();
this.initializeRoutes();
}
// Must be implemented by derived controllers
abstract initializeRoutes(): void;
// Standard response formats
protected sendSuccess(res: Response, data: any, statusCode: number = 200): Response {
return res.status(statusCode).json({
success: true,
data,
});
}
protected sendError(
res: Response,
message: string,
statusCode: number = 400,
errors?: any
): Response {
return res.status(statusCode).json({
success: false,
message,
errors,
});
}
// Pagination support
protected sendPaginated(
res: Response,
data: any[],
page: number,
limit: number,
total: number,
statusCode: number = 200
): Response {
return res.status(statusCode).json({
success: true,
data,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
},
});
}
// Error handling wrapper for async methods
protected catchAsync(fn: (req: Request, res: Response, next: NextFunction) => Promise<any>) {
return (req: Request, res: Response, next: NextFunction) => {
fn(req, res, next).catch(next);
};
}
}
Ensure the error handling middleware works with the new controller pattern:
// src/interfaces/http/middlewares/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
export class HttpException extends Error {
status: number;
message: string;
constructor(status: number, message: string) {
super(message);
this.status = status;
this.message = message;
}
}
export const errorHandler = (
error: Error,
req: Request,
res: Response,
next: NextFunction
): void => {
const status = error instanceof HttpException ? error.status : 500;
const message = error.message || 'Something went wrong';
res.status(status).json({
success: false,
message,
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined,
});
};
// src/interfaces/http/controllers/emissionController.ts
import { Request, Response } from 'express';
import { BaseController } from './base/BaseController';
import { EmissionService } from '../../../domain/services/emissionService';
import { CreateEmissionDto } from '../../../domain/models/emission';
import multer from 'multer';
export class EmissionController extends BaseController {
private upload = multer({ storage: multer.memoryStorage() });
constructor(private emissionService: EmissionService) {
super('/api/emissions');
}
initializeRoutes(): void {
// Register all routes with their handlers
this.router.post(this.path, this.catchAsync(this.createEmission.bind(this)));
this.router.get(`${this.path}/:id`, this.catchAsync(this.getEmissionById.bind(this)));
this.router.get(this.path, this.catchAsync(this.getAllEmissions.bind(this)));
this.router.post(
`${this.path}/import`,
this.upload.single('file'),
this.catchAsync(this.importEmissions.bind(this))
);
}
// Controller methods - refactored to use standardized responses
private async createEmission(req: Request, res: Response): Promise<void> {
// Parse request body to CreateEmissionDto
var finaldate = new Date(req.body.date);
const utcDate = new Date(finaldate.getUTCFullYear(), finaldate.getUTCMonth(), finaldate.getUTCDate());
const emissionData: CreateEmissionDto = {
contributorId: parseInt(req.body.contributorId),
date: utcDate,
location: req.body.location,
value: parseFloat(req.body.value),
unit: req.body.unit,
emission_type: req.body.type,
hours: parseFloat(req.body.hours)
};
// Call service to create the emission
const success = await this.emissionService.createEmission(emissionData);
if (success) {
this.sendSuccess(res, { message: 'Emission created successfully' }, 201);
} else {
this.sendError(res, 'Failed to create emission', 500);
}
}
private async getEmissionById(req: Request, res: Response): Promise<void> {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return this.sendError(res, 'Invalid emission ID', 400);
}
const emission = await this.emissionService.getEmissionById(id);
if (!emission) {
return this.sendError(res, 'Emission not found', 404);
}
this.sendSuccess(res, emission);
}
private async getAllEmissions(req: Request, res: Response): Promise<void> {
const emissions = await this.emissionService.getAllEmissions();
this.sendSuccess(res, emissions);
}
private async importEmissions(req: Request, res: Response): Promise<void> {
// Check if file exists in request
if (!req.file) {
return this.sendError(res, 'No file uploaded', 400);
}
// Get file buffer and process it
const csvBuffer = req.file.buffer;
const importResult = await this.emissionService.importEmissionsFromCsv(csvBuffer);
// Respond with results
if (importResult.success) {
this.sendSuccess(res, {
message: 'CSV imported successfully',
totalProcessed: importResult.totalProcessed,
totalSucceeded: importResult.totalSucceeded,
totalFailed: importResult.totalFailed
});
} else {
this.sendError(res, 'CSV validation failed', 400, {
errors: importResult.errors,
validRecords: importResult.validRecords,
totalProcessed: importResult.totalProcessed
});
}
}
}
Update app.ts to use the new controller pattern:
// src/app.ts
import express from 'express';
import cors from 'cors';
import { errorHandler } from './interfaces/http/middlewares/errorHandler';
import { EmissionController } from './interfaces/http/controllers/emissionController';
import { EmissionService } from './domain/services/emissionService';
import { EmissionRepository } from './infrastructure/database/repositories/emissionRepository';
import { IController } from './interfaces/http/controllers/base/IController';
export class App {
public app: express.Application;
constructor(controllers: IController[]) {
this.app = express();
this.initializeMiddlewares();
this.initializeControllers(controllers);
this.initializeErrorHandling();
}
private initializeMiddlewares(): void {
this.app.use(cors());
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
}
private initializeControllers(controllers: IController[]): void {
controllers.forEach((controller) => {
this.app.use('/', controller.router);
});
}
private initializeErrorHandling(): void {
this.app.use(errorHandler);
}
// Health check endpoint
private initializeHealthCheck(): void {
this.app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});
}
}
// Initialize repositories and services
const emissionRepository = new EmissionRepository();
const emissionService = new EmissionService(emissionRepository);
// Initialize controllers
const controllers: IController[] = [
new EmissionController(emissionService),
// Add other controllers here
];
// Create app instance
const app = new App(controllers).app;
export default app;
// src/server.ts
import app from './app';
import config from './config';
const PORT = config.server.port || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
// tests/unit/controllers/baseController.test.ts
import { BaseController } from '../../../src/interfaces/http/controllers/base/BaseController';
import { Request, Response } from 'express';
// Test implementation class
class TestController extends BaseController {
constructor() {
super('/test');
}
initializeRoutes(): void {
// Empty for testing purposes
}
// Expose protected methods for testing
public testSendSuccess(res: Response, data: any, statusCode?: number): Response {
return this.sendSuccess(res, data, statusCode);
}
public testSendError(res: Response, message: string, statusCode?: number, errors?: any): Response {
return this.sendError(res, message, statusCode, errors);
}
}
describe('BaseController', () => {
let controller: TestController;
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
beforeEach(() => {
controller = new TestController();
mockRequest = {};
mockResponse = {
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
};
});
describe('sendSuccess', () => {
it('should return success response with status 200 by default', () => {
const data = { id: 1, name: 'Test' };
controller.testSendSuccess(mockResponse as Response, data);
expect(mockResponse.status).toHaveBeenCalledWith(200);
expect(mockResponse.json).toHaveBeenCalledWith({
success: true,
data
});
});
it('should return success response with custom status code', () => {
const data = { id: 1, name: 'Test' };
controller.testSendSuccess(mockResponse as Response, data, 201);
expect(mockResponse.status).toHaveBeenCalledWith(201);
expect(mockResponse.json).toHaveBeenCalledWith({
success: true,
data
});
});
});
describe('sendError', () => {
it('should return error response with status 400 by default', () => {
const message = 'Test error';
controller.testSendError(mockResponse as Response, message);
expect(mockResponse.status).toHaveBeenCalledWith(400);
expect(mockResponse.json).toHaveBeenCalledWith({
success: false,
message
});
});
it('should return error response with custom status code and errors', () => {
const message = 'Test error';
const errors = { field: 'Invalid value' };
controller.testSendError(mockResponse as Response, message, 422, errors);
expect(mockResponse.status).toHaveBeenCalledWith(422);
expect(mockResponse.json).toHaveBeenCalledWith({
success: false,
message,
errors
});
});
});
});
Add JSDoc comments to all controller methods and interfaces, for example:
/**
* Interface for standardizing controller implementation
* across the application
*/
export interface IController {
/**
* Base path for the controller routes
*/
path: string;
/**
* Express router instance for registering routes
*/
router: Router;
/**
* Initialize and register all routes for this controller
*/
initializeRoutes(): void;
}
// src/interfaces/http/controllers/factory/controllerFactory.ts
import { IController } from '../base/IController';
import { EmissionController } from '../emissionController';
import { EmissionService } from '../../../../domain/services/emissionService';
import { EmissionRepository } from '../../../../infrastructure/database/repositories/emissionRepository';
export class ControllerFactory {
static createControllers(): IController[] {
// Create repositories
const emissionRepository = new EmissionRepository();
// Create services with repositories
const emissionService = new EmissionService(emissionRepository);
// Create controllers with services
return [
new EmissionController(emissionService),
// Add other controllers as needed
];
}
}
Update app.ts to use the factory:
// src/app.ts
import { ControllerFactory } from './interfaces/http/controllers/factory/controllerFactory';
// Create app instance
const controllers = ControllerFactory.createControllers();
const app = new App(controllers).app;
Add Swagger annotations to controller methods:
/**
* @swagger
* /api/emissions:
* get:
* summary: Get all emissions
* tags: [Emissions]
* responses:
* 200:
* description: List of emissions
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* type: array
* items:
* $ref: '#/components/schemas/Emission'
*/
private async getAllEmissions(req: Request, res: Response): Promise<void> {
const emissions = await this.emissionService.getAllEmissions();
this.sendSuccess(res, emissions);
}
| Phase | Task | Duration | Dependencies |
|---|---|---|---|
| 1. Infrastructure | Create IController Interface | 0.5 days | None |
| Implement BaseController | 1.5 days | IController | |
| Update Error Handling | 1 day | BaseController | |
| 2. Controller Migration | Refactor EmissionController | 2 days | Phase 1 |
| Update App Bootstrap | 1 day | Refactored Controller | |
| Update Server.ts | 0.5 days | Updated App | |
| Manual Testing | 1.5 days | Phase 2 | |
| 3. Testing & Documentation | Create Unit Tests | 2 days | Phase 2 |
| Add JSDoc Comments | 1 day | None | |
| Test Coverage Analysis | 1 day | Unit Tests | |
| 4. Further Improvements | Implement Controller Factory | 1 day | Phase 3 |
| Add Swagger Documentation | 2 days | None |
Total Duration: 15 working days
| Risk | Impact | Probability | Mitigation |
|---|---|---|---|
| Incompatible existing controllers | High | Medium | Create adapter pattern for legacy controllers |
| Regression in API behavior | High | Low | Comprehensive test suite before/after changes |
| Extended development timeline | Medium | Medium | Phased approach allows partial implementation |
| Inconsistent adoption | Medium | Low | Code review process to enforce new pattern |
The standardization of controllers through the IController interface will significantly improve the maintainability, consistency, and developer experience of the SMS-CO2 application. This refactoring focuses on enhancing the structure without disrupting functionality, allowing for a smooth transition to the new architecture.
By following this phased approach over a 15-day period, we can implement these changes methodically while minimizing risk to the existing application. The resulting controller standardization will provide a solid foundation for future development and scaling of the application.