One-Way Data Flow
In EJS, data flows in one direction — from your Express route to the template. The template receives data and displays it, but cannot modify the original data source.
app.get('/dashboard', (req, res) => {
const user = { name: 'Bilal', score: 100 };
res.render('dashboard', { user });
});
The template reads the data but the original user object stays unchanged.
<h1>Welcome, <%= user.name %></h1>
<p>Your score: <%= user.score %></p>
Try it Yourself →
Passing Props to Includes
When you include a partial, you can pass data to it. This makes partials truly reusable because they adapt to the data they receive.
<%- include('partials/avatar', {
src: user.avatar,
alt: user.name,
size: 'large'
}) %>
The avatar partial uses these values without knowing where they come from.
<!-- partials/avatar.ejs -->
<img src="<%= src %>" alt="<%= alt %>"
class="avatar avatar-<%= size %>">
Data Nesting
You can pass nested objects and arrays to templates. Access deeply nested values using dot notation.
app.get('/order', (req, res) => {
res.render('order', {
order: {
id: 12345,
customer: {
name: 'Sara',
email: 'sara@example.com'
},
items: [
{ name: 'Laptop', price: 999 },
{ name: 'Mouse', price: 25 }
]
}
});
});
Access nested properties and iterate through arrays.
<h2>Order #<%= order.id %></h2>
<p>Customer: <%= order.customer.name %></p>
<ul>
<% order.items.forEach(item => { %>
<li><%= item.name %> - $<%= item.price %></li>
<% }); %>
</ul>
Try it Yourself →
Destructuring in Templates
You can destructure objects directly in your EJS code to keep things clean and readable.
<% const { name, email, role } = user; %>
<div class="profile">
<h3><%= name %></h3>
<p><%= email %></p>
<span class="badge"><%= role %></span>
</div>
This is especially useful when you only need a few properties from a large object.
<% const { title, excerpt, author, publishedAt } = post; %>
<article>
<h2><%= title %></h2>
<p class="meta">By <%= author %> on <%= publishedAt %></p>
<p><%= excerpt %></p>
</article>
Combining Data Sources
You can combine data from multiple sources — route parameters, query strings, and your database — into a single data object for the template.
app.get('/posts/:category', async (req, res) => {
const posts = await Post.find({ category: req.params.category });
const category = await Category.findOne({ slug: req.params.category });
res.render('posts', {
posts,
category,
page: parseInt(req.query.page) || 1
});
});
The template has everything it needs to render a complete page.
<h1><%= category.name %> Posts</h1>
<p>Page <%= page %></p>
<% posts.forEach(post => { %>
<article>
<h3><%= post.title %></h3>
<p><%= post.summary %></p>
</article>
<% }); %>