Labs ICT
Pro Login

Middleware Patterns

Error pages, layouts, and data injection via middleware.

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

<%= status %>

<%= message %>

Go Home

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.