Color Mode

Refactoring Your Course Catalog to MVC Architecture

As your Express application grows, keeping all your code in server.js becomes difficult to maintain. Professional web applications use the Model-View-Controller (MVC) architecture to organize code into logical, manageable pieces.

In this assignment, you will refactor your existing course catalog system to follow MVC principles. This will make your code more organized, easier to understand, and simpler to expand with new features.

Understanding MVC Architecture

MVC separates your application into three main components:

From this point forward, your application structure will use a src/ directory to organize all your core application code, while static assets remain in public/.

Why Refactor?

Refactoring is the process of restructuring existing code without changing its functionality. This is a valuable skill in professional development, as it helps maintain code quality and makes applications easier to work with as they grow.

Assignment Instructions

1. Clean Up and Prepare Your Code

Before restructuring your application, you need to clean up features that are no longer needed. In professional development, requirements often change and features get removed or redesigned.

Remove the Products Feature: During previous assignments, you likely created a products page and related functionality. This feature is no longer part of our application design and should be completely removed.

Look through your current code and remove all traces of:

Simplify and Clean: As you work, also look for opportunities to:

If something isn't working, don't proceed to the final step. Debug the issue first. Common problems include incorrect file paths in import statements or missing files in the new directory structure.

2. Create the MVC Directory Structure

Create the directory structure for your MVC architecture. You need to organize your code into logical folders within each MVC layer:

Your project structure should look similar to the following when you have completed this assignment. Some files have been omitted for brevity:


        your-project/
        ├── public/
        │   ├── css/
        │   └── images/
        ├── src/
        │   ├── controllers/
        │   │   ├── catalog/
        │   │   │    └── catalog.js
        │   │   ├── index.js
        │   │   └── routes.js
        │   ├── middleware/
        │   │   └── demo/
        │   │   │    └── headers.js
        │   ├── models/
        │   │   └── catalog/
        │   │   │    └── catalog.js
        │   └── views/
        └── server.js
    

3. Create Your Course Model

The Model handles all data-related operations. Create src/models/catalog/catalog.js and add this enhanced course data and access functions.

Export Methods: JavaScript modules offer two common ways to export functions. Here's a quick comparison:


        // Method 1: Individual exports (export as you define)
        export const getAllCourses = () => { /* ... */ };

        // Method 2: Define functions, then export at the end
        const getAllCourses = () => { /* ... */ };

        export { getAllCourses };
    

Both methods function the same way when it comes to importing. In our examples, we'll use Method 2 (define first, then export), but you're free to choose whichever style you prefer. Be sure to stay consistent throughout your code, pick one style and stick with it going forward.

Add this content to your new catalog model file:


        // Enhanced course data object
        const courses = {
            'CS121': {
                id: 'CS121',
                title: 'Introduction to Programming',
                department: 'Computer Science',
                description: 'Learn programming fundamentals using JavaScript and basic web development concepts.',
                credits: 3,
                sections: [
                    { time: '9:00 AM', room: 'STC 392', professor: 'Brother Jack' },
                    { time: '2:00 PM', room: 'STC 394', professor: 'Sister Enkey' },
                    { time: '11:00 AM', room: 'STC 390', professor: 'Brother Keers' }
                ]
            },
            'CS162': {
                id: 'CS162',
                title: 'Introduction to Computer Science',
                department: 'Computer Science', 
                description: 'Object-oriented programming concepts and software development practices.',
                credits: 3,
                sections: [
                    { time: '10:00 AM', room: 'STC 392', professor: 'Brother Jack' },
                    { time: '1:00 PM', room: 'STC 394', professor: 'Sister Enkey' }
                ]
            },
            'MATH113': {
                id: 'MATH113',
                title: 'College Algebra',
                department: 'Mathematics',
                description: 'Fundamental algebra concepts including functions, polynomials, and equations.',
                credits: 3,
                sections: [
                    { time: '8:00 AM', room: 'STC 290', professor: 'Sister Peterson' },
                    { time: '11:00 AM', room: 'STC 292', professor: 'Brother Thompson' },
                    { time: '3:00 PM', room: 'STC 290', professor: 'Sister Anderson' }
                ]
            },
            'MATH119': {
                id: 'MATH119',
                title: 'Calculus I',
                department: 'Mathematics',
                description: 'Introduction to differential and integral calculus with applications.',
                credits: 4,
                sections: [
                    { time: '9:00 AM', room: 'STC 290', professor: 'Brother Thompson' },
                    { time: '2:00 PM', room: 'STC 292', professor: 'Sister Anderson' }
                ]
            },
            'ENG101': {
                id: 'ENG101',
                title: 'College Writing',
                department: 'English',
                description: 'Develop writing skills for academic and professional communication.',
                credits: 3,
                sections: [
                    { time: '10:00 AM', room: 'GEB 201', professor: 'Sister Anderson' },
                    { time: '12:00 PM', room: 'GEB 205', professor: 'Brother Davis' },
                    { time: '4:00 PM', room: 'GEB 203', professor: 'Sister Enkey' }
                ]
            },
            'ENG102': {
                id: 'ENG102', 
                title: 'Composition and Literature',
                department: 'English',
                description: 'Advanced writing skills through the study of literature and critical analysis.',
                credits: 3,
                sections: [
                    { time: '11:00 AM', room: 'GEB 201', professor: 'Brother Davis' },
                    { time: '1:00 PM', room: 'GEB 205', professor: 'Sister Enkey' }
                ]
            },
            'HIST105': {
                id: 'HIST105',
                title: 'World History',
                department: 'History',
                description: 'Survey of world civilizations from ancient times to the present.',
                credits: 3,
                sections: [
                    { time: '9:00 AM', room: 'GEB 301', professor: 'Brother Wilson' },
                    { time: '2:00 PM', room: 'GEB 305', professor: 'Sister Roberts' }
                ]
            }
        };

        // Model functions that handle all data access

        const getAllCourses = () => {
            return courses;
        };

        const getCourseById = (courseId) => {
            return courses[courseId] || null;
        };

        const getSortedSections = (sections, sortBy) => {
            const sortedSections = [...sections];
            
            switch (sortBy) {
                case 'professor':
                    return sortedSections.sort((a, b) => a.professor.localeCompare(b.professor));
                case 'room':
                    return sortedSections.sort((a, b) => a.room.localeCompare(b.room));
                case 'time':
                default:
                    return sortedSections; // Keep original order
            }
        };

        const getCoursesByDepartment = () => {
            const departments = {};
            
            Object.values(courses).forEach(course => {
                if (!departments[course.department]) {
                    departments[course.department] = [];
                }
                departments[course.department].push(course);
            });
            
            return departments;
        };

        export { getAllCourses, getCourseById, getSortedSections, getCoursesByDepartment };
    

Notice how the model only handles data. It doesn't know about HTTP requests, responses, or error handling. It just provides clean functions to access course data.

Alternative: Individual Export Method

If you prefer to export functions as you define them, you can use this approach instead:


            // Export functions individually as you define them
            export const getAllCourses = () => {
                return courses;
            };

            export const getCourseById = (courseId) => {
                return courses[courseId] || null;
            };

            export const getSortedSections = (sections, sortBy) => {
                const sortedSections = [...sections];
                
                switch (sortBy) {
                    case 'professor':
                        return sortedSections.sort((a, b) => a.professor.localeCompare(b.professor));
                    case 'room':
                        return sortedSections.sort((a, b) => a.room.localeCompare(b.room));
                    case 'time':
                    default:
                        return sortedSections;
                }
            };

            export const getCoursesByDepartment = () => {
                const departments = {};
                
                Object.values(courses).forEach(course => {
                    if (!departments[course.department]) {
                        departments[course.department] = [];
                    }
                    departments[course.department].push(course);
                });
                
                return departments;
            };
        

4. Move and Organize Your Middleware

Move your middleware functions into src/middleware/global.js. This middleware will add data to res.locals, making it available to all your templates.

When you add data to res.locals, every template in your application can access those values. Templates simply use the values they need and ignore the rest. This is useful for data like the current year, environment settings, or query parameters that many templates might use.

Create your global middleware file with this consolidated function:


        /**
         * Helper function to get the current greeting based on the time of day.
         */
        const getCurrentGreeting = () => {
            const currentHour = new Date().getHours();

            if (currentHour < 12) {
                return 'Good Morning!';
            }
            
            if (currentHour < 18) {
                return 'Good Afternoon!';
            }

            return 'Good Evening!';
        };

        /**
         * Middleware to add local variables to res.locals for use in all templates.
         * Templates can access these values but are not required to use them.
         */
        const addLocalVariables = (req, res, next) => {
            // Set current year for use in templates
            res.locals.currentYear = new Date().getFullYear();

            // Make NODE_ENV available to all templates
            res.locals.NODE_ENV = process.env.NODE_ENV?.toLowerCase() || 'production';

            // Make req.query available to all templates
            res.locals.queryParams = { ...req.query };

            // Set greeting based on time of day
            res.locals.greeting = `<p>${getCurrentGreeting()}</p>`;

            // Randomly assign a theme class to the body
            const themes = ['blue-theme', 'green-theme', 'red-theme'];
            const randomTheme = themes[Math.floor(Math.random() * themes.length)];
            res.locals.bodyClass = randomTheme;

            // Continue to the next middleware or route handler
            next();
        };

        export { addLocalVariables };
    

Next, create src/middleware/demo/headers.js for route-specific middleware:


        /**
         * Middleware to add custom headers for demo purposes.
         */
        const addDemoHeaders = (req, res, next) => {
            // Add a header called 'X-Demo-Page' with value 'true'
            res.setHeader('X-Demo-Page', 'true');

            // Add a header called 'X-Middleware-Demo' with any message you want
            res.setHeader('X-Middleware-Demo', 'This is a middleware demo header');

            next();
        };

        export { addDemoHeaders };
    
Alternative: Individual Export Method

If you prefer individual exports, you can write your middleware like this:


            /**
             * Helper function to get the current greeting based on the time of day.
             */
            const getCurrentGreeting = () => {
                const currentHour = new Date().getHours();

                if (currentHour < 12) {
                    return 'Good Morning!';
                }
                
                if (currentHour < 18) {
                    return 'Good Afternoon!';
                }

                return 'Good Evening!';
            };

            /**
             * Middleware to add local variables to res.locals for use in all templates.
             */
            export const addLocalVariables = (req, res, next) => {
                res.locals.currentYear = new Date().getFullYear();
                res.locals.NODE_ENV = process.env.NODE_ENV?.toLowerCase() || 'production';
                res.locals.queryParams = { ...req.query };
                res.locals.greeting = `<p>${getCurrentGreeting()}</p>`;
                const themes = ['blue-theme', 'green-theme', 'red-theme'];
                const randomTheme = themes[Math.floor(Math.random() * themes.length)];
                res.locals.bodyClass = randomTheme;
                next();
            };
        

            /**
             * Middleware to add custom headers for demo purposes.
             */
            export const addDemoHeaders = (req, res, next) => {
                res.setHeader('X-Demo-Page', 'true');
                res.setHeader('X-Middleware-Demo', 'This is a middleware demo header');
                next();
            };
        
Understanding res.locals

The res.locals object is Express's way of passing data to templates. Any property you add to res.locals becomes available to all templates that are rendered during that request. Think of it as a shared data container that travels with the response through your application.

Templates do not need to use every value in res.locals. They simply access the values they need. For example, your footer might use currentYear, while your demo page might use greeting and bodyClass. Other templates might not use these values at all, and that is perfectly fine.

5. Create Your Route Handlers (Controllers)

Route handlers (also called controllers) are middleware functions that handle HTTP requests and send responses. In MVC architecture, they coordinate between models and views.

Create src/controllers/catalog/catalog.js for course catalog functionality:


        import { getAllCourses, getCourseById, getSortedSections } from '../../models/catalog/catalog.js';

        // Route handler for the course catalog list page
        const catalogPage = (req, res) => {
            const courses = getAllCourses();
            
            res.render('catalog', {
                title: 'Course Catalog',
                courses: courses
            });
        };

        // Route handler for individual course detail pages
        const courseDetailPage = (req, res, next) => {
            const courseId = req.params.courseId;
            const course = getCourseById(courseId);
            
            // If course doesn't exist, create 404 error
            if (!course) {
                const err = new Error(`Course ${courseId} not found`);
                err.status = 404;
                return next(err);
            }
            
            // Handle sorting if requested
            const sortBy = req.query.sort || 'time';
            const sortedSections = getSortedSections(course.sections, sortBy);
            
            res.render('course-detail', {
                title: `${course.id} - ${course.title}`,
                course: { ...course, sections: sortedSections },
                currentSort: sortBy
            });
        };

        export { catalogPage, courseDetailPage };
    

Now create src/controllers/index.js for your basic pages:


        // Route handlers for static pages
        const homePage = (req, res) => {
            res.render('home', { title: 'Home' });
        };

        const aboutPage = (req, res) => {
            res.render('about', { title: 'About' });
        };

        const demoPage = (req, res) => {
            res.render('demo', { title: 'Middleware Demo Page' });
        };

        const testErrorPage = (req, res, next) => {
            const err = new Error('This is a test error');
            err.status = 500;
            next(err);
        };

        export { homePage, aboutPage, demoPage, testErrorPage };
    
Understanding Controllers

Notice how these route handlers (controller functions) use the model to get data, handle any errors (like 404s), and then render the appropriate view. They don't know about implementation details like file paths or database connections; they just focus on application logic.

Alternative: Individual Export Method

For controllers, individual exports might look like this:


            import { getAllCourses, getCourseById, getSortedSections } from '../../models/catalog/catalog.js';

            export const catalogPage = (req, res) => {
                const courses = getAllCourses();
                res.render('catalog', { title: 'Course Catalog', courses: courses });
            };

            export const courseDetailPage = (req, res, next) => {
                const courseId = req.params.courseId;
                const course = getCourseById(courseId);
                
                if (!course) {
                    const err = new Error(`Course ${courseId} not found`);
                    err.status = 404;
                    return next(err);
                }
                
                const sortBy = req.query.sort || 'time';
                const sortedSections = getSortedSections(course.sections, sortBy);
                
                res.render('course-detail', {
                    title: `${course.id} - ${course.title}`,
                    course: { ...course, sections: sortedSections },
                    currentSort: sortBy
                });
            };
        

            // Route handlers for static pages
            export const homePage = (req, res) => {
                res.render('home', { title: 'Home' });
            };

            export const aboutPage = (req, res) => {
                res.render('about', { title: 'About' });
            };

            export const demoPage = (req, res) => {
                res.render('demo', { title: 'Middleware Demo Page' });
            };

            export const testErrorPage = (req, res, next) => {
                const err = new Error('This is a test error');
                err.status = 500;
                next(err);
            };
        

6. Build Your Routes File

Routes connect URLs to route handler functions. The Express Router helps organize routes into manageable modules. We'll build this file step by step so you understand how it works.

Create src/controllers/routes.js and start with the basic setup:


        import { Router } from 'express';

        // Create a new router instance
        const router = Router();

        // TODO: Add import statements for controllers and middleware
        // TODO: Add route definitions

        export default router;
    

Now add the import statements for your controllers and middleware. You'll need to import:

Add these imports after the Router import:


        import { addDemoHeaders } from '../middleware/demo/headers.js';
        import { catalogPage, courseDetailPage } from './catalog/catalog.js';
        import { homePage, aboutPage, demoPage, testErrorPage } from './index.js';
    

Finally, add your route definitions. Routes follow the pattern router.get(path, handler) or router.get(path, middleware, handler):


        // Home and basic pages
        router.get('/', homePage);
        router.get('/about', aboutPage);

        // Course catalog routes
        router.get('/catalog', catalogPage);
        router.get('/catalog/:courseId', courseDetailPage);

        // Demo page with special middleware
        router.get('/demo', addDemoHeaders, demoPage);

        // Route to trigger a test error
        router.get('/test-error', testErrorPage);
    

Once you have completed your routes.js file, take a moment to understand how each piece works:

Understanding Express Router

The Router is like a mini Express application. It can handle middleware and routes but needs to be mounted on the main app to work. This lets you organize related routes together and then attach them all at once.

7. Update Your server.js

Now update your server.js to use the MVC structure. Your server file should focus only on configuration and setup, while the application logic lives in the MVC components.

Replace your existing server.js content with:


        import express from 'express';
        import path from 'path';
        import { fileURLToPath } from 'url';

        // Import MVC components
        import routes from './src/controllers/routes.js';
        import { addLocalVariables } from './src/middleware/global.js';

        /**
         * Server configuration
         */
        const __filename = fileURLToPath(import.meta.url);
        const __dirname = path.dirname(__filename);
        const NODE_ENV = process.env.NODE_ENV?.toLowerCase() || 'production';
        const PORT = process.env.PORT || 3000;

        /**
         * Setup Express Server
         */
        const app = express();

        /**
         * Configure Express
         */
        app.use(express.static(path.join(__dirname, 'public')));
        app.set('view engine', 'ejs');
        app.set('views', path.join(__dirname, 'src/views'));

        /**
         * Global Middleware
         */
        app.use(addLocalVariables);

        /**
         * Routes
         */
        app.use('/', routes);

        /**
         * Error Handling
         */
        
        // 404 handler
        app.use((req, res, next) => {
            const err = new Error('Page Not Found');
            err.status = 404;
            next(err);
        });

        // Global error handler
        app.use((err, req, res, next) => {
            // Prevent infinite loops, if a response has already been sent, do nothing
            if (res.headersSent || res.finished) {
                return next(err);
            }
            
            // Determine status and template
            const status = err.status || 500;
            const template = status === 404 ? '404' : '500';

            // Prepare data for the template
            const context = {
                title: status === 404 ? 'Page Not Found' : 'Server Error',
                error: NODE_ENV === 'production' ? 'An error occurred' : err.message,
                stack: NODE_ENV === 'production' ? null : err.stack,
                NODE_ENV // Our WebSocket check needs this and its convenient to pass along
            };

            // Render the appropriate error template with fallback
            try {
                res.status(status).render(`errors/${template}`, context);
            } catch (renderErr) {
                // If rendering fails, send a simple error page instead
                if (!res.headersSent) {
                    res.status(status).send(`<h1>Error ${status}</h1><p>An error occurred.</p>`);
                }
            }
        });

        /**
         * Start WebSocket Server in Development Mode; used for live reloading
         */
        if (NODE_ENV.includes('dev')) {
            const ws = await import('ws');

            try {
                const wsPort = parseInt(PORT) + 1;
                const wsServer = new ws.WebSocketServer({ port: wsPort });

                wsServer.on('listening', () => {
                    console.log(`WebSocket server is running on port ${wsPort}`);
                });

                wsServer.on('error', (error) => {
                    console.error('WebSocket server error:', error);
                });
            } catch (error) {
                console.error('Failed to start WebSocket server:', error);
            }
        }

        /**
         * Start Server
         */
        app.listen(PORT, () => {
            console.log(`Server is running on http://127.0.0.1:${PORT}`);
        });
    

Notice how much cleaner server.js is now. It focuses only on configuration and setup, while the actual application logic has been moved to the MVC components.

Understanding the Error Handler

The global error handler has a special four-parameter signature (err, req, res, next) that tells Express to use it for errors. The res.headersSent and res.finished checks prevent attempting to send a response twice, which would crash the server. The try-catch block around res.render() provides a fallback in case the error template itself has problems, ensuring users always see something rather than a blank page.

In production, we hide error details from users for security reasons, while in development we show the full error message and stack trace to help with debugging.

8. Test Your Refactored Application

Checkpoint: Before proceeding, test your application to make sure the refactoring worked correctly. Start your server and test all the functionality:

Everything should work exactly the same as before, but now your code is organized using MVC principles!

If something isn't working, don't proceed to the final step. Debug the issue first. Common problems include incorrect file paths in import statements or missing files in the new directory structure.

9. Final Cleanup and Code Polish

Now that your MVC refactoring is working, it's time for the final cleanup. This step will help you develop good professional habits around code maintenance and quality.

Your tasks for this step:

Professional developers spend significant time on code cleanup and documentation. Clean, well-organized code is easier to maintain, debug, and extend with new features. Look carefully at the updates we made throughout the project. Can you spot all the refactoring that was done?

Understanding What You've Accomplished

You've successfully refactored your application to use MVC architecture. Here's what each component now does:

MVC in Action
  • Models (src/models/catalog/catalog.js): Store course data and provide functions to access it. They know nothing about HTTP or web requests.
  • Views (src/views/): Your EJS templates that display data. They receive data from controllers and present it to users.
  • Controllers (src/controllers/catalog/, src/controllers/index.js): Handle web requests, use models to get data, and decide which views to render.

This separation makes your code easier to understand, maintain, and expand. When you need to add new features, you know exactly where different types of code belong.

The directory structure you've created also follows professional conventions. Related functionality is grouped together, and each file has a clear, descriptive name that indicates its purpose.

Key Concepts Summary

MVC architecture provides clear separation of concerns that makes applications more maintainable. Your course data lives in models, your presentation logic lives in views, and your request handling lives in controllers.

The refactoring process you just completed is a valuable professional skill. You took working code and reorganized it to be more maintainable without changing its functionality. This type of code organization becomes crucial as applications grow in size and complexity.

You also learned to use Express Router, which allows you to organize routes into logical modules. This makes it easy to manage different areas of your application and keeps your main server file focused on configuration rather than business logic.

With this foundation in place, you're ready to build more sophisticated features and eventually connect to real databases, as models provide a clean interface that can be upgraded without affecting the rest of your application.

Alternative Approach: Domain-Driven MVC

In professional development, you might encounter Domain-Driven Design, where each business domain (like courses, accounts, or admin) has its own complete MVC structure. Instead of organizing by layers (all controllers together), everything for one domain stays together.

Layer-based MVC (what you just built):


            src/
            ├── controllers/
            │   ├── catalog/
            │   └── index.js
            ├── models/
            │   └── catalog/
            └── views/
                └── catalog/
        

Domain-based MVC (alternative approach):


            src/
            ├── catalog/
            │   ├── controllers/
            │   ├── models/
            │   └── views/
            └── accounts/
                ├── controllers/
                ├── models/
                └── views/
        

Both approaches use MVC principles but organize files differently. The layer-based approach (what you learned) is more common for learning MVC concepts, while domain-based can be useful for large applications with distinct business areas.

Experiment and Extend

Now that you have MVC structure in place, consider these enhancements to deepen your understanding:

  1. Add a Departments Page: Use the getCoursesByDepartment() function to create a page that lists courses grouped by department. You'll need a new controller, route, and view.
  2. Expand Your Controllers: Add more sophisticated logic for handling different types of requests or user preferences, such as filtering by credit hours or professor.
  3. Practice More Refactoring: We left the error handlers in the server.js file. Move these into controllers somewhere logical and then import and register them in the routes section of your server.js file.

These experiments help you understand how MVC architecture scales to support more complex features while keeping your code organized and maintainable. Each addition should fit cleanly into the structure you've created.

Conclusion

You've successfully transformed a monolithic Express application into a well-organized MVC architecture. This refactoring demonstrates several important professional development skills: code organization, separation of concerns, and maintaining functionality while improving structure.

The MVC pattern you've implemented provides a solid foundation for building larger, more complex web applications. As you continue developing your skills, you'll find that this organized approach makes debugging easier, feature development more predictable, and team collaboration more effective.

In future assignments, you'll build upon this MVC foundation to add more sophisticated features like database integration, user authentication, and advanced routing patterns.