Skip to content

Express Template Engines

Template engines let Express generate dynamic HTML on the server by combining a template file with data, rather than sending static markup or building HTML strings by hand. This lesson covers the general mechanism before diving into specific engines.

How res.render() Works

Configuring app.set('view engine', name) and app.set('views', dir) tells Express which templating library to use and where template files live. res.render(templateName, data) then finds the matching file, injects the data, and sends the resulting HTML as the response.

Express supports many template engines through a common interface, EJS, Pug, and Handlebars are the three most widely used, each covered in its own lesson.

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

app.get('/profile', (req, res) => {
  res.render('profile', { name: 'Ada', email: 'ada@example.com' });
});

This looks for views/profile.ejs, renders it with the given data object, and sends the resulting HTML.

The Template Engine Contract

app.set('view engine', 'ejs' | 'pug' | 'hbs');
app.set('views', './views');

res.render(templateName, dataObject, callback?);
  • Template files live in the directory set by app.set('views', ...).
  • The file extension must match the configured view engine (.ejs, .pug, .hbs).
  • Data passed as the second argument to res.render() becomes available as variables inside the template.
  • Variables set on res.locals are automatically merged into every rendered template's data.

Template Engine Comparison

A quick comparison of the three engines covered in this course.

Engine Syntax Style File Extension
EJS Plain HTML with embedded <%= %> tags .ejs
Pug Indentation-based, no closing tags .pug
Handlebars HTML with {{ }} mustache-style placeholders .hbs

Sharing Data Across Templates With res.locals

Instead of passing the same value (like the current logged-in user, or a site title) into every single res.render() call, middleware can attach it to res.locals, and every subsequently rendered template picks it up automatically.

app.use((req, res, next) => {
  res.locals.siteName = 'My Shop';
  res.locals.user = req.user || null;
  next();
});

app.get('/', (req, res) => {
  res.render('home'); // siteName and user are already available
});

Layouts and Partials

Most real applications reuse a common header, footer, and navigation across many pages. Each engine has its own mechanism, <%- include(...) %> in EJS, include/extends in Pug, {{> partial}} in Handlebars, for composing smaller template fragments into a full page.

Common Mistakes

  • Forgetting to set app.set('views', ...), causing Express to look in the wrong directory.
  • Mismatching the file extension with the configured view engine.
  • Rebuilding the same data object (like site-wide settings) manually in every route instead of using res.locals.
  • Mixing multiple template engines in one project without a clear reason, increasing maintenance overhead.

Key Takeaways

  • app.set('view engine', ...) and res.render() form the core template rendering contract in Express.
  • Template files must live in the configured views directory with the matching extension.
  • res.locals shares data with every rendered template without repeating it in each route.
  • EJS, Pug, and Handlebars are the three most common engines, each with a different syntax style.

Pro Tip

Set shared, cross-page data (site name, current user, navigation state) on res.locals in one piece of middleware rather than repeating it as an argument to every res.render() call across your route files.