Node.js: Template Engines and SSR
Last updated: 2026-08-26
1. Story: Charlie's Management Tool Interface
Charlie is a backend developer at a startup. The team needed an internal management tool to view orders, user statistics, and system status. His colleagues suggested using React or Vue, but Charlie felt those frameworks were too heavy for an internal tool like this—with just a few pages and simple interactions, server-side rendering with a template engine would suffice. He chose EJS because it’s essentially just HTML with a few tags, making it incredibly easy to get started. Within a day, he had built a complete admin interface with reusable layouts using Express and EJS.
You'll learn:
- The Four Core Syntax Tags in EJS and Their Uses
- How to Configure EJS and Pug Integration in Express
- Pug's Indentation Syntax, Variables, Loops, and Conditions
- The Complete Process of Template Rendering
- Strategies for Reusing Partial Views and Layouts
- How data is passed from the router to the template
- Key Differences Between SSR and CSR and Criteria for Selection
2. Template Rendering Process
The core function of a template engine is to merge template files with data to generate the final HTML string, which is then returned to the browser.
flowchart LR
A[Browser Request] --> B[Express Routing]
B --> C[Retrieve Data]
C --> D["res.render()"]
D --> E[EJS / Pug Compilation]
E --> F[Generate HTML]
F --> G[HTTP Response]
The entire process can be summarized as follows:
- The browser sends an HTTP request
- Express routes are matched to their corresponding handler functions
- The handler retrieves data from a database or API
- Call
res.render()and pass in the template name and data - The template engine compiles the template and data into HTML
- HTML is returned to the browser as an HTTP response
3. EJS Basic Syntax
EJS (Embedded JavaScript) is the most intuitive template engine—templates are simply plain HTML, with logic embedded using <% %> tags.
▶ Example: The Four Core EJS Tags
// <%= %> Output the escaped value (Safe, prevents XSS)
<p>Hello, <%= userName %>!</p>
// <% %> Execution Logic,No output
<% if (isAdmin) { %>
<span>Admin Panel</span>
<% } %>
// <%- %> Output Raw HTML (Not escaped, use with caution)
<%- articleHtml %>
// <%- include %> Importing Subtemplates
<%- include('partials/header') %>
(1) EJS Syntax Quick Reference
| Tag | Function | Output | Escape | Example |
|---|---|---|---|---|
<%= %> |
Output variable value | Yes | Yes | <%= name %> |
<%- %> |
Output original content | Yes | No | <%- html %> |
<% %> |
Execution Logic | No | — | <% if (x) { %> |
<%# %> |
Comment | No | — | <%# comment %> |
<%- include('x') %> |
Include sub-templates | Yes | No | <%- include('nav') %> |
▶ Example: EJS Loops and Conditions
<ul>
<% items.forEach(function(item) { %>
<li>
<strong><%= item.name %></strong>
<% if (item.onSale) { %>
<span class="badge">Sale</span>
<% } %>
</li>
<% }); %>
</ul>
4. Express + EJS Configuration
▶ Example: Setting Up a Complete Express + EJS Project
Project Structure:
views/
partials/
header.ejs
footer.ejs
layout.ejs
index.ejs
app.js
npm init -y
npm install express ejs
const express = require('express');
const app = express();
app.set('view engine', 'ejs');
app.set('views', './views');
app.get('/', (req, res) => {
res.render('index', {
title: 'Dashboard',
user: { name: 'Charlie', role: 'admin' },
stats: { orders: 128, users: 56 }
});
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
(1) Key API Documentation
| API | Purpose | Example |
|---|---|---|
app.set('view engine', 'ejs') |
Set the default template engine | No need to include the file extension when rendering |
app.set('views', path) |
Set template directory | Default is ./views |
res.render(view, data) |
Render the template and respond | res.render('index', {title: 'Hi'}) |
res.render(view, data, callback) |
Retrieve the HTML string after rendering | Can be used for sending emails and other scenarios |
▶ Example: Passing data as the second parameter to res.render
app.get('/profile/:id', (req, res) => {
const user = { id: req.params.id, name: 'Alice', bio: 'Full-stack dev' };
res.render('profile', {
user: user,
pageTitle: user.name + "'s Profile",
isLoggedIn: true
});
});
Accessing variables directly by name in the template:
<h1><%= pageTitle %></h1>
<p>Name: <%= user.name %></p>
<p>Bio: <%= user.bio %></p>
5. Pug Basic Syntax
Pug (formerly known as Jade) uses indentation instead of closing tags, making its syntax extremely concise, but it has a steeper learning curve than EJS.
▶ Example: Basic Pug Syntax
//- Pug Template
doctype html
html
head
title= pageTitle
body
h1= message
p Welcome to #{siteName}
Compiled HTML:
<!DOCTYPE html>
<html>
<head><title>My Page</title></head>
<body>
<h1>Hello</h1>
<p>Welcome to MySite</p>
</body>
</html>
(1) Pug Syntax Quick Reference
| Syntax | Function | Example |
|---|---|---|
tag= value |
Property Binding | h1= title |
#{expr} |
Interpolation | p Hello #{name} |
- code |
Execute JS | - const x = 1 |
if/else if/else |
Condition | if (admin) ... |
each val in arr |
loop | each item in list |
extends layout |
Inherit Layout | extends layout |
block name |
Definition/Fill Block | block content |
include path |
Import sub-templates | include header |
▶ Example: Pug Loops and Conditions
ul
each item in items
li
strong= item.name
if item.onSale
span.badge Sale
6. Reusing Partial Views and Layouts
The core value of a template engine is to avoid having to repeat the header, footer, and HTML skeleton on every page.
▶ Example: EJS Layout + Partial View
views/partials/header.ejs
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
views/partials/footer.ejs
<footer>© 2026 My App</footer>
views/layout.ejs
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<%- include('partials/header') %>
<main>
<%- body %>
</main>
<%- include('partials/footer') %>
</body>
</html>
EJS does not have a native layout mechanism; it must be implemented using the
express-ejs-layoutsmiddleware or by manually including files.
npm install express-ejs-layouts
const expressLayouts = require('express-ejs-layouts');
app.use(expressLayouts);
app.set('layout', 'layout');
▶ Example: Pug Layout Inheritance
views/layout.pug
doctype html
html
head
title= title
link(rel="stylesheet" href="/style.css")
body
include partials/header
block content
include partials/footer
views/index.pug
extends layout
block content
h1= pageTitle
p Welcome!
(1) Comparison of Layout Strategies
| Property | EJS | Pug |
|---|---|---|
| Layout Inheritance | Requires middleware express-ejs-layouts |
Native extends + block |
| Local Import | <%- include('partial') %> |
include partial |
| Passing Parameters to Child Templates | <%- include('x', {data}) %> |
Passing Data Through the Parent |
| Multi-block support | Limited | Native support for multiple blocks |
7. Comparison of Template Engines
(1) EJS vs Pug vs Handlebars
| Feature | EJS | Pug | Handlebars |
|---|---|---|---|
| Syntax Style | HTML + JS Tags | Indented, no closing tags | Mustache {{}} |
| Learning Curve | Lowest | Medium | Low |
| Logical Reasoning | Full JS | Full JS | Restricted (No arbitrary JS) |
| Architecture/Inheritance | Requires middleware | Native support | Requires additional configuration |
| Output Escape | <%= %> |
= or #{} |
{{}} |
| Original output | <%- %> |
!{} |
{{{}}} |
| Use Cases | Quick Start / Internal Tools | Simplicity First / Full-Stack Projects | Security First / Multi-Device Rendering |
| Community Activity | High | High | Medium |
- 8 SSR vs CSR Comparison
(1) Key Differences Table
| Dimension | SSR (Server-Side Rendering) | CSR (Client-Side Rendering) |
|---|---|---|
| Rendering Location | Server | Browser |
| First-screen load speed | Fast (HTML loads immediately) | Slow (must wait for JS to load and execute) |
| SEO Friendliness | High | Low (Content may not be visible to crawlers) |
| Interaction Complexity | Low (page refreshes with each request) | High (SPA, no page refresh) |
| Server Load | High | Low |
| Typical Technologies | EJS / Pug / PHP | React / Vue / Angular |
| Use Cases | Internal Tools / Blogs / SEO Pages | Complex Interactions / SPA Applications |
▶ Example: Differences in Responses Between SSR and CSR
SSR Response—The browser receives the complete HTML directly:
GET /about
→ Server-Side Rendering HTML Return in Full
→ Display directly in the browser,No need to wait JS
CSR Response—The browser receives an empty shell + JS bundle:
GET /about
→ Server Response <div id="app"></div> + bundle.js
→ Browser Download JS,Execute Rendering
→ Users view the content(Duration of the white screen)
8. Comprehensive Example: EJS Blog Page
Build a complete Express + EJS blog application, including reusable layouts, a post list, post details, and local navigation.
▶ Example: (1) Project Structure
blog/
views/
partials/
header.ejs
footer.ejs
nav.ejs
index.ejs
post.ejs
app.js
▶ Example: Application entry point app.js
const express = require('express');
const expressLayouts = require('express-ejs-layouts');
const app = express();
app.set('view engine', 'ejs');
app.set('views', './views');
app.use(expressLayouts);
app.set('layout', 'layout');
const posts = [
{ id: 1, title: 'Getting Started with Node.js', author: 'Charlie', date: '2026-06-20', excerpt: 'Learn the basics of Node.js runtime.' },
{ id: 2, title: 'Understanding Express Middleware', author: 'Alice', date: '2026-06-25', excerpt: 'Deep dive into middleware patterns.' },
{ id: 3, title: 'Template Engines Compared', author: 'Bob', date: '2026-07-01', excerpt: 'EJS vs Pug vs Handlebars showdown.' }
];
app.get('/', (req, res) => {
res.render('index', { title: 'Blog Home', posts: posts });
});
app.get('/post/:id', (req, res) => {
const post = posts.find(p => p.id === parseInt(req.params.id));
if (!post) return res.status(404).send('Post not found');
res.render('post', { title: post.title, post: post });
});
app.listen(3000, () => {
console.log('Blog running on http://localhost:3000');
});
▶ Example: Local Navigation partials/nav.ejs
<nav class="blog-nav">
<a href="/">All Posts</a>
<% posts.forEach(function(p) { %>
<a href="/post/<%= p.id %>"><%= p.title %></a>
<% }); %>
</nav>
▶ Example: Local header partials/header.ejs
<header class="site-header">
<h1><%= title %></h1>
</header>
▶ Example: Local footer partials/footer.ejs
<footer class="site-footer">
<p>© 2026 My Blog. Powered by Express + EJS.</p>
</footer>
▶ Example: Article List Page views/index.ejs
<h2>Latest Posts</h2>
<div class="post-list">
<% posts.forEach(function(post) { %>
<article class="post-card">
<h3><a href="/post/<%= post.id %>"><%= post.title %></a></h3>
<p class="meta">By <%= post.author %> on <%= post.date %></p>
<p><%= post.excerpt %></p>
</article>
<% }); %>
</div>
▶ Example: Post Details Page views/post.ejs
<h2><%= post.title %></h2>
<p class="meta">By <%= post.author %> on <%= post.date %></p>
<div class="post-body">
<p><%= post.excerpt %></p>
<p>Full article content goes here...</p>
</div>
<a href="/">← Back to all posts</a>
How it looks after launching the app:
http://localhost:3000 → Article List Page
http://localhost:3000/post/1 → Article Details Page
http://localhost:3000/post/2 → Second Article
❓ FAQ
express-ejs-layouts middleware to implement layout inheritance, or you can manually import partial views using <%- include() %>; Pug natively supports extends + block to implement layout inheritance.res.render?res.render('index', {title: 'Hi'}); in the template, you would write <%= title %>.livereload middleware to enable automatic browser refresh, eliminating the need to manually refresh the page.<%= Outputs HTML-escaped values to prevent XSS attacks; this is the secure option. <%- Outputs raw, unescaped content and should only be used when the content is trusted or when embedding sub-templates.async/await in templates?await is not supported in EJS templates. You should complete all asynchronous data retrieval in the route and then pass the results to the template using res.render().📖 Summary
- 1 Story: Key Concepts and How to Use Charlie's Management Tool Interface
- 2 Core Concepts and Usage of the Template Rendering Process
- 3 Core Concepts and Usage of Basic EJS Syntax
- 4 Core Concepts and Usage of Express + EJS Configuration
- 5 Core Concepts and Usage of Basic Pug Syntax
- 6 Core Concepts and Usage of Partial Views and Layout Reuse
- 7 Key Concepts and Usage Methods for Comparing Template Engines
- 8 Key Concepts and Usage Methods in the SSR vs. CSR Comparison
📝 Exercises
- Complete all the code examples in this lesson and make sure each one runs correctly.
- Modify the comprehensive example and add your own extensions
- Review the official documentation, identify 1–2 APIs not covered in this lesson, and write test code for them.
- Reflection: How would you apply what you’ve learned in this lesson to a real-world project?
- Try to combine what you’ve learned in this lesson with material from previous lessons to build a small project.