Refactoring and Optimizing Your Express Application
In this assignment, you will refactor your Express application to improve its organization, performance, and maintainability. You will implement a dynamic asset loading system that allows pages to load only the CSS and JavaScript they need, and reorganize your view files to follow consistent MVC patterns.
Refactoring is the process of restructuring existing code without changing its external behavior. This assignment focuses on eliminating inefficiencies and applying best practices that will make your application more scalable and easier to maintain as it grows.
Understanding the Problems We Need to Solve
Your current application has several organizational and performance issues. The most significant problem is inefficient asset loading. Currently, your header partial loads CSS files for every section of your site on every page. This means the home page loads catalog styles it does not use, catalog pages load faculty styles they do not need, and so on. As your application grows and you add more sections, this problem compounds: every page loads CSS for every section, regardless of whether it needs those styles.
This approach wastes bandwidth, slows down page loads, and violates the DRY (Don't Repeat Yourself) principle. When you need to add or modify section-specific styles, you have to update multiple files and remember which pages need which stylesheets. A better approach would load styles dynamically based on which route is being accessed.
The second issue is inconsistent view organization. Your faculty section has its views organized in a dedicated subdirectory (src/views/faculty/), but your catalog views are scattered in the main views folder. This inconsistency makes the codebase harder to navigate and breaks the logical grouping that MVC patterns encourage. All related views for a section should be organized together.
Finally, there is a naming inconsistency in your catalog views. The detail view is currently named course-detail.ejs, but since you are working with a catalog system, it should be named catalog-detail.ejs to match the section it belongs to. When views are moved into their proper subdirectories, this naming will become catalog/detail.ejs, which is both cleaner and more maintainable.
Assignment Instructions
1. Extend Global Middleware with Asset Management
Your application currently has global middleware in src/middleware/global.js that adds local variables to all templates. You will extend this middleware by adding asset management functionality that allows routes to dynamically add CSS and JavaScript files as needed.
First, examine your current middleware structure. Open src/middleware/global.js to see the existing addLocalVariables function. This middleware runs for every request and sets up variables like currentYear, greeting, and bodyClass that all your templates can access.
You will add a new helper function called setHeadAssetsFunctionality that creates methods for managing CSS and JavaScript assets. Add this function to your global.js file, placing it before the addLocalVariables function:
/**
* Express middleware that adds head asset management functionality to routes.
* Provides arrays for storing CSS and JS assets with priority support.
*
* Adds these methods to the response object:
* - res.addStyle(css, priority) - Add CSS/link tags to head
* - res.addScript(js, priority) - Add script tags
*
* Adds these functions to EJS templates via res.locals:
* - renderStyles() - Outputs all CSS in priority order (high to low)
* - renderScripts() - Outputs all JS in priority order (high to low)
*/
const setHeadAssetsFunctionality = (res) => {
res.locals.styles = [];
res.locals.scripts = [];
res.addStyle = (css, priority = 0) => {
res.locals.styles.push({ content: css, priority });
};
res.addScript = (js, priority = 0) => {
res.locals.scripts.push({ content: js, priority });
};
// These functions will be available in EJS templates
res.locals.renderStyles = () => {
return res.locals.styles
// Sort by priority: higher numbers load first
.sort((a, b) => b.priority - a.priority)
.map(item => item.content)
.join('\n');
};
res.locals.renderScripts = () => {
return res.locals.scripts
// Sort by priority: higher numbers load first
.sort((a, b) => b.priority - a.priority)
.map(item => item.content)
.join('\n');
};
};
Now integrate this functionality into your existing addLocalVariables middleware. Inside the addLocalVariables function, add a call to setHeadAssetsFunctionality(res) before the next() call. This ensures that every request has access to the asset management methods.
-
The
setHeadAssetsFunctionalityhelper:- Creates
res.locals.stylesandres.locals.scriptsarrays to store assets. - Adds response methods:
res.addStyle(css, priority)res.addScript(js, priority)
- Exposes template helpers via
res.locals:renderStyles()— outputs CSS sorted by priority (high → low).renderScripts()— outputs JS sorted by priority (high → low).
- Creates
- Priority controls load order when multiple assets are registered: higher numbers are emitted first so lower-priority files can override them.
2. Implement Dynamic Asset Loading in Header
Now that your middleware provides asset management functionality, you need to update your header partial to use it. Currently, your src/views/partials/header.ejs file loads all CSS files directly, including section-specific styles that are not needed on every page.
Open src/views/partials/header.ejs and locate these lines in the <head> section:
<link rel="stylesheet" href="/css/catalog.css">
<link rel="stylesheet" href="/css/faculty.css">
Remove these lines completely. Then, in the same location where these links were, add the dynamic rendering functions:
<%- renderStyles() %>
<%- renderScripts() %>
These functions will output any CSS and JavaScript assets that have been registered by routes using res.addStyle() and res.addScript(). At this point, save your changes and test your application. You will notice that catalog and faculty pages no longer have their section-specific styling. This is expected behavior since you removed the global loading but have not yet added the route-specific loading. You will fix this in the next step.
3. Add Router-Level Middleware for Section-Specific Styles
Rather than manually adding styles in every route handler, you will use router-level middleware to automatically load section-specific CSS for all routes under a given path. This approach is more maintainable and follows the DRY principle: you define the asset loading once, and it applies to all matching routes.
Router-level middleware runs after global middleware but before individual route handlers. By registering middleware for patterns like /catalog/*, you can automatically add styles for all catalog routes without modifying each route handler. This pattern is especially powerful as your application grows: adding a new catalog route automatically includes the catalog styles without any additional code.
Express processes requests through a chain: Global middleware (in server.js) runs first, then router-level middleware (in routes.js) for matching routes, and finally individual route handlers. This order ensures that res.addStyle() is available when your router middleware runs, since the global middleware set up that functionality.
Open your src/controllers/routes.js file. This file defines all your application routes using Express Router. Currently, it imports controllers and registers routes, but it does not have any router-level middleware. You need to examine the structure to understand where to add middleware.
Near the top of the file, you should see the router being created and some imports:
import { Router } from 'express';
import { homePage } from './home.js';
import { aboutPage } from './about.js';
// ... other imports
const router = Router();
// Router middleware should be added here, before route definitions
// Route definitions
router.get('/', homePage);
router.get('/about', aboutPage);
router.get('/catalog', catalogPage);
router.get('/catalog/:slugId', courseDetailPage);
// ... other routes
export default router;
Router middleware must be registered after the router is created but before the route definitions. If middleware is registered after routes, it will not run for those routes. Add this middleware after the imports but before the route definitions:
// Add catalog-specific styles to all catalog routes
router.use('/catalog', (req, res, next) => {
res.addStyle('<link rel="stylesheet" href="/css/catalog.css">');
next();
});
This middleware uses the pattern /catalog, which matches /catalog and any route that starts with /catalog/ (such as /catalog/:slugId). When a matching route is accessed, the middleware runs, calls res.addStyle() to register the catalog CSS, and then calls next() to pass control to the route handler. The registered CSS will be output when the template calls renderStyles().
Save your changes and test your catalog pages. The catalog styling should now be restored. However, your faculty pages still lack styling. Apply the same pattern for faculty routes by adding similar middleware for the /faculty pattern. Use the catalog middleware as a reference, but register faculty.css instead.
*) in router middleware patterns (e.g., /catalog/*). In modern Express, using just the base path (e.g., /catalog) is sufficient to match all sub-routes. For security and performance reasons, wildcards are no longer allowed in Express route definitions.
4. Reorganize Catalog View Files
Your faculty views are organized in a dedicated subdirectory (src/views/faculty/) with files named list.ejs and detail.ejs. Your catalog views should follow the same pattern for consistency. Organizing views by section makes the codebase easier to navigate and aligns with MVC principles that group related files together.
Create a new directory at src/views/catalog/. Then move and rename your catalog view files:
-
Move
src/views/catalog.ejstosrc/views/catalog/list.ejs -
Move
src/views/course-detail.ejstosrc/views/catalog/detail.ejs
After reorganization, your views directory structure should look like this:
[project-root]
└── src/
└── views/
├── catalog/
│ ├── detail.ejs
│ └── list.ejs
├── faculty/
│ ├── detail.ejs
│ └── list.ejs
├── partials/
│ ├── footer.ejs
│ └── header.ejs
├── about.ejs
├── demo.ejs
└── home.ejs
5. Fix Partial Include Paths
Moving the catalog view files into a subdirectory changes the relative path to your partials. The header and footer partials are in src/views/partials/, but your catalog views are now in src/views/catalog/, which is one level deeper.
Open both src/views/catalog/list.ejs and src/views/catalog/detail.ejs. Find the partial include statements at the top and bottom of each file:
<%- include('partials/header') %>
...
<%- include('partials/footer') %>
Update these paths to use ../ to navigate up one directory level:
<%- include('../partials/header') %>
...
<%- include('../partials/footer') %>
The ../ prefix tells EJS to look in the parent directory (views) rather than the current directory (catalog). This allows the includes to find the partials in views/partials/.
6. Update Controller References
After moving the view files, you need to update your catalog controller to reference the new view paths. Open src/controllers/catalog/catalog.js and find all res.render() calls.
Update the view names to match the new file locations:
-
Change
res.render('catalog', ...)tores.render('catalog/list', ...) -
Change
res.render('course-detail', ...)tores.render('catalog/detail', ...)
The view path catalog/list tells Express to look for list.ejs inside the catalog subdirectory of your views folder. Save your changes and test all catalog routes to verify that views render correctly and styles load properly.
Key Concepts Summary
This refactoring assignment introduced several important patterns for building maintainable web applications. The dynamic asset loading system you implemented allows routes to selectively load only the CSS and JavaScript they need, rather than loading everything globally. This improves performance by reducing unnecessary bandwidth usage and page load times.
The router-level middleware pattern is particularly powerful. Instead of repeating asset loading code in every route handler, you define middleware once for a route pattern, and it automatically applies to all matching routes. This follows the DRY principle and makes your code easier to maintain: when you need to add a new stylesheet to a section, you change it in one place rather than hunting through multiple route handlers.
Router-level middleware sits between global middleware and route handlers in the Express middleware chain. Global middleware runs for every request, router middleware runs for routes matching specific patterns, and route handlers contain the business logic for individual routes. Understanding this execution order helps you decide where to place different types of functionality: truly global concerns go in global middleware, section-specific concerns go in router middleware, and route-specific logic stays in handlers.
By reorganizing your view files into subdirectories, you made your application structure more consistent and easier to navigate. This organization pattern scales well: as you add more sections to your application, each section gets its own views folder, its own controller folder, and its own router middleware for section-specific concerns. This modular approach makes large applications manageable.
Experiment and Extend
Now that you have dynamic asset loading working, experiment with the priority system. Try adding multiple CSS files to a route with different priority values to see how they load in order. Remember that higher priority numbers load first, which means lower priority styles can override them.
Consider adding page-specific JavaScript files using the same pattern. Create a simple JavaScript file in your public directory and use res.addScript() in a route handler to load it only on specific pages. Place the renderScripts() call at the bottom of your layout, just before the closing </body> tag, to ensure scripts load after the page content.
Think about other uses for router-level middleware beyond asset loading. This pattern works well for any functionality that applies to all routes in a section: logging route access, checking authentication status, loading section-specific data, or setting common template variables. Any time you find yourself repeating code across multiple route handlers, consider whether router middleware could eliminate that duplication.
Troubleshooting
If your middleware does not seem to be running, verify that it was registered before your route definitions in routes.js. Middleware registered after routes will not affect those routes. The order should be: create router, register middleware, define routes.
If CSS files are not loading on catalog or faculty pages, check that you added the renderStyles() call to your header partial and that you removed the old hardcoded <link> tags. Also verify that your router middleware is calling res.addStyle() with the correct file path and calling next() to pass control to the route handler.
If you see errors about views not being found after reorganizing, double-check the paths in your controller render calls. The path catalog/list should match the file location src/views/catalog/list.ejs. Also verify that your partial includes in the catalog views use ../partials/header rather than partials/header since the views are now in a subdirectory.
If the router middleware pattern /catalog/* does not match your routes, verify that your route definitions use paths like /catalog and /catalog/:slugId. The asterisk wildcard matches the base path and any sub-paths, so both should work. If you are still having issues, try using just /catalog without the asterisk, though this would require separate middleware for the detail route.