Skip to content

Templating

Server-side rendering is old school until you realize how much faster it is for SEO-critical pages and how much simpler it is when you don't need a React build pipeline
Express supports multiple template engines through a consistent interface. Set the view engine , create .ejs or .pug files , and call res.render(). Three steps , infinite HTML

setup - view engine

const express = require('express')
const app = express()

app.set('view engine' , 'ejs')        // use EJS templates
app.set('views' , './views')          // template directory (default: ./views)

Express locates views in the views directory by default. Change it if you're organized:

app.set('views' , './src/views')

EJS - embedded JavaScript

The most popular Express template engine. Write HTML with JavaScript interpolation

npm install ejs
// app.js
app.set('view engine' , 'ejs')
<!-- views/profile.ejs -->
<!DOCTYPE html>
<html>
<head>
  <title><%= title %></title>
</head>
<body>
  <h1>Welcome , <%= user.name %></h1>
  <ul>
    <% users.forEach(function(u) { %>
      <li><%= u.name %> - <%= u.email %></li>
    <% }) %>
  </ul>
</body>
</html>
// route handler
app.get('/profile' , (req , res) => {
  res.render('profile' , {
    title: 'User Profile',
    user: { name: 'mahmoud' },
    users: [
      { name: 'ali' , email: 'ali@test.com' },
      { name: 'khaled' , email: 'khaled@test.com' }
    ]
  })
})

EJS tags:

  • <%= value %> - escapes HTML (safe , use this)
  • <%- value %> - raw output (DANGEROUS , XSS risk)
  • <% code %> - execute code without output
  • <%# comment %> - EJS comment (not in output)

Pug - whitespace-sensitive

Pug (formerly Jade) uses indentation instead of closing tags. Love it or hate it

npm install pug
app.set('view engine' , 'pug')
<!-- views/profile.pug -->
doctype html
html
  head
    title= title
  body
    h1 Welcome , #{user.name}
    ul
      each u in users
        li= u.name + ' - ' + u.email

Pug pros: minimal syntax , clean templates Pug cons: adds learning curve , harder for non-devs to read , indentation errors break everything

Handlebars - logic-less templates

Handlebars enforces strict separation of logic from presentation

npm install express-handlebars
const { engine } = require('express-handlebars')

app.engine('hbs' , engine({ extname: '.hbs' }))
app.set('view engine' , 'hbs')
<!-- views/profile.hbs -->
<!DOCTYPE html>
<html>
<head>
  <title>{{title}}</title>
</head>
<body>
  <h1>Welcome , {{user.name}}</h1>
  <ul>
    {{#each users}}
      <li>{{this.name}} - {{this.email}}</li>
    {{/each}}
  </ul>
</body>
</html>

passing data to templates

app.get('/dashboard' , (req , res) => {
  res.render('dashboard' , {
    title: 'Dashboard',
    user: req.user,
    stats: { visits: 420 , revenue: 1337 },
    isAdmin: req.user?.role === 'admin',
    csrfToken: req.csrfToken?.()   // pass CSRF token for forms
  })
})

partials and layouts

EJS partials:

<!-- views/partials/header.ejs -->
<header>
  <nav>
    <a href="/">Home</a>
    <a href="/profile">Profile</a>
  </nav>
</header>
<!-- views/index.ejs -->
<%- include('partials/header') %>
<h1>Content</h1>
<%- include('partials/footer') %>

EJS layouts (using express-ejs-layouts):

npm install express-ejs-layouts
const expressLayouts = require('express-ejs-layouts')
app.use(expressLayouts)
app.set('layout' , 'layouts/main')  // default layout

XSS prevention in templates

This is non-negotiable. Escape everything by default

<!-- SAFE - <%= %> escapes HTML -->
<p><%= user.name %></p>
<!-- user.name = '<script>alert("xss")</script>' -->
<!-- Output: &lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt; -->

<!-- DANGEROUS - <%- %> outputs raw HTML -->
<p><%- user.name %></p>
<!-- user.name = '<script>alert("xss")</script>' -->
<!-- Output: <script>alert("xss")</script>  - EXECUTED -->

Rules:

  • Always use <%= %> for user-supplied data
  • Only use <%- %> when you explicitly trust the source AND sanitized the content
  • Never pass unsanitized user input to template variables
  • Use a sanitization library like DOMPurify if you must render user HTML

complete template example with security

app.get('/profile/:id' , async (req , res) => {
  const user = await db.findUser(req.params.id)

  // NEVER pass raw user input to template
  // Don't do: res.render('profile' , { ...user })

  res.render('profile' , {
    title: 'User Profile',
    user: {
      name: user.name,           // escaped by <%= %>
      bio: user.bio,             // escaped by <%= %>
      joinDate: user.created_at.toDateString()
    },
    isOwnProfile: req.user?.id === user.id
  })
})

prerequisites

express_06_error_handling.md - error handling , async wrappers , custom errors


next → express_08_form_data.md