Error Pages with Middleware
Custom error pages make your app feel polished. Use Express error-handling middleware to render EJS templates when things go wrong.
app.use((req, res, next) => {
const err = new Error('Page Not Found')
err.status = 404
next(err)
})
app.use((err, req, res, next) => {
const status = err.status || 500
res.status(status).render('errors/error', {
title: `Error ${status}`,
message: err.message,
status: status
})
})
Keep your error templates simple. They render when something is already wrong, so they should be lightweight with no external dependencies.
// views/errors/error.ejs
Layouts via Middleware
Instead of wrapping every route handler, use middleware to inject layout functionality into res.render.
function layoutMiddleware(layoutFile) {
return (req, res, next) => {
const originalRender = res.render.bind(res)
res.render = (view, data = {}, callback) => {
res.render = originalRender
data.layout = layoutFile
res.render(view, data, callback)
}
next()
}
}
app.use(layoutMiddleware('layouts/main'))
A simpler approach patches res.locals so every template has access to layout data.
app.use((req, res, next) => {
res.locals.layout = 'layouts/main'
res.locals.siteName = 'My App'
res.locals.user = req.user || null
next()
})
app.get('/', (req, res) => {
res.render('home', { pageTitle: 'Home' })
})
Now every template automatically has siteName and user available without passing them explicitly.
Data Injection Middleware
Middleware can fetch data and attach it to the response before the template renders.
async function injectNotifications(req, res, next) {
if (req.user) {
res.locals.notifications = await Notification.find({
userId: req.user.id,
read: false
})
res.locals.unreadCount = res.locals.notifications.length
}
next()
}
app.use(injectNotifications)
Every template now has notifications and unreadCount available. The template doesn't know where the data came from — it just uses it.
// In any template
<% if (unreadCount > 0) { %>
<%= unreadCount %>
<% } %>
Try it Yourself →
Authentication Guards in Templates
Middleware handles authentication, but templates need to know what the user can see. Pass role and permission data through middleware.
function injectPermissions(req, res, next) {
if (req.user) {
res.locals.permissions = {
canEdit: req.user.role === 'admin' || req.user.role === 'editor',
canDelete: req.user.role === 'admin',
canManageUsers: req.user.role === 'admin'
}
} else {
res.locals.permissions = {}
}
next()
}
app.use(injectPermissions)
Templates check permissions cleanly without knowing the role system.
// In templates
<% if (permissions.canEdit) { %>
Edit
<% } %>
<% if (permissions.canDelete) { %>
<% } %>
Flash Messages Middleware
Flash messages give users feedback after actions. Middleware reads them from the session and clears them after rendering.
function flashMiddleware(req, res, next) {
res.locals.flash = {
success: req.session.flashSuccess,
error: req.session.flashError,
info: req.session.flashInfo
}
req.session.flashSuccess = null
req.session.flashError = null
req.session.flashInfo = null
next()
}
app.use(flashMiddleware)
// Setting a flash message in a route
app.post('/profile', (req, res) => {
req.session.flashSuccess = 'Profile updated!'
res.redirect('/profile')
})
// Displaying it in the layout
<% if (flash.success) { %>
<%= flash.success %>
<% } %>
<% if (flash.error) { %>
<%= flash.error %>
<% } %>
Rate Limiting and Maintenance Mode
Middleware can protect your app and show appropriate EJS pages during maintenance or abuse.
function maintenanceMode(req, res, next) {
if (process.env.MAINTENANCE === 'true') {
return res.status(503).render('errors/maintenance', {
title: 'Under Maintenance',
message: 'We\'ll be back soon!'
})
}
next()
}
app.use(maintenanceMode)
This catches all requests and shows a maintenance page without touching any route handler. Toggle it by setting an environment variable.