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: <script>alert("xss")</script> -->
<!-- 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