Passing Data from Routes to Views
Pass data from Express route handlers to templates using res.render, res.locals, and app.locals
The Three Ways to Pass Data
Express provides three mechanisms to pass data to your views:
| Method | Scope | When to Use |
|---|---|---|
res.render() options |
Per-request | Page-specific data (user, posts, etc.) |
res.locals |
Per-request | Middleware-set data (auth, flash messages) |
app.locals |
Application-wide | Globals (site name, version, constants) |
app.locals Global data (available everywhere)
│
├─ route /home
│ ├─ res.locals Request data (set by middleware)
│ └─ res.render('home', { ... }) Route-specific data
│
├─ route /profile
│ ├─ res.locals
│ └─ res.render('profile', { ... })
│
└─ route /about
├─ res.locals
└─ res.render('about', { ... })
1. Passing Data via res.render()
The most common way - pass an object as the second argument to res.render():
app.get('/profile/:id', (req, res) => {
const user = {
id: req.params.id,
name: 'Alice',
email: '[email protected]',
bio: 'Full-stack developer',
joined: new Date('2024-01-15')
};
const stats = {
posts: 42,
followers: 128,
following: 89
};
const recentPosts = [
{ title: 'Express Tips', slug: 'express-tips', views: 1200 },
{ title: 'Node.js Best Practices', slug: 'node-best-practices', views: 3400 }
];
res.render('profile', {
title: `${user.name}'s Profile`,
user,
stats,
recentPosts,
isOwnProfile: req.user?.id === user.id,
currentYear: new Date().getFullYear()
});
});
Accessing in Templates
EJS:
<h1><%= user.name %></h1>
<p><%= user.email %></p>
<p><%= stats.posts %> posts published</p>
<% if (isOwnProfile) { %>
<a href="/settings">Edit Profile</a>
<% } %>
Pug:
h1= user.name
p= user.email
p #{stats.posts} posts published
if isOwnProfile
a(href="/settings") Edit Profile
Handlebars:
<h1>{{user.name}}</h1>
<p>{{user.email}}</p>
<p>{{stats.posts}} posts published</p>
{{#if isOwnProfile}}
<a href="/settings">Edit Profile</a>
{{/if}}
2. Passing Data via res.locals
res.locals is an object that exists for the duration of a single request. Data set there is available to all templates rendered during that request.
Setting in Middleware
// Auth middleware - attach user to every view
function attachUser(req, res, next) {
if (req.session?.userId) {
res.locals.user = getUser(req.session.userId);
res.locals.isAuthenticated = true;
} else {
res.locals.user = null;
res.locals.isAuthenticated = false;
}
next();
}
app.use(attachUser);
// Now every template can access user and isAuthenticated
// without passing them explicitly in res.render()
Flash Messages Middleware
function flashMessages(req, res, next) {
res.locals.success = req.session?.success || null;
res.locals.error = req.session?.error || null;
// Clear after reading
if (req.session) {
delete req.session.success;
delete req.session.error;
}
next();
}
app.use(flashMessages);
// Route
app.post('/login', (req, res) => {
if (validLogin(req.body)) {
req.session.success = 'Logged in successfully!';
res.redirect('/dashboard');
} else {
req.session.error = 'Invalid credentials';
res.redirect('/login');
}
});
Template Usage
<!-- Available in any view without passing explicitly -->
<% if (isAuthenticated) { %>
<p>Welcome, <%= user.name %>!</p>
<% } %>
<% if (success) { %>
<div class="alert alert-success"><%= success %></div>
<% } %>
<% if (error) { %>
<div class="alert alert-error"><%= error %></div>
<% } %>
3. Passing Data via app.locals
app.locals holds data that’s available across all requests - it persists for the app’s lifetime.
Setting Globals
app.locals.siteName = 'My Express App';
app.locals.siteVersion = '2.0.0';
app.locals.supportEmail = '[email protected]';
app.locals.currentYear = new Date().getFullYear();
app.locals.features = {
darkMode: true,
comments: true,
analytics: false
};
app.locals.menu = [
{ label: 'Home', path: '/' },
{ label: 'About', path: '/about' },
{ label: 'Contact', path: '/contact' }
];
Dynamic app.locals (on startup)
async function initializeApp() {
const config = await loadConfig();
const theme = await getThemeSettings();
const categories = await getCategories();
app.locals.config = config;
app.locals.theme = theme;
app.locals.categories = categories;
app.locals.startupTime = new Date();
}
initializeApp();
Accessing Globals in Templates
<!-- EJS -->
<footer>
© <%= currentYear %> <%= siteName %>. All rights reserved.
<a href="mailto:<%= supportEmail %>">Support</a>
</footer>
<!-- Pug -->
footer
| © #{currentYear} #{siteName}. All rights reserved.
a(href='mailto:' + supportEmail) Support
<!-- Handlebars -->
<footer>
© {{currentYear}} {{siteName}}. All rights reserved.
<a href="mailto:{{supportEmail}}">Support</a>
</footer>
Precedence and Merging
When the same key exists in multiple places, more specific wins:
app.locals.theme = 'dark' # Base default
res.locals.theme = 'light' # Override for this request
res.render('page', { theme: 'custom' }) # Override for this render
Precedence order (highest wins):
res.render()optionsres.localsapp.locals
Practical Example: Full Data Flow
// app.js
const express = require('express');
const path = require('path');
const app = express();
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// Global data
app.locals.siteName = 'TechBlog';
app.locals.currentYear = new Date().getFullYear();
app.locals.categories = ['JavaScript', 'Node.js', 'Express', 'Database'];
// Middleware: request data
app.use((req, res, next) => {
res.locals.currentPath = req.path;
res.locals.query = req.query;
res.locals.isHome = req.path === '/';
next();
});
// Middleware: auth data
app.use((req, res, next) => {
const token = req.cookies?.token;
if (token) {
res.locals.user = { name: 'Alice', email: '[email protected]' };
res.locals.isAuthenticated = true;
} else {
res.locals.user = null;
res.locals.isAuthenticated = false;
}
next();
});
// Route: page-specific data
app.get('/blog/:slug', (req, res) => {
const post = {
title: 'Understanding Express Middleware',
slug: req.params.slug,
body: 'Express middleware is...',
author: 'Alice',
published: new Date('2026-06-10'),
tags: ['express', 'middleware'],
views: 1542
};
res.render('blog/post', {
title: post.title,
post,
relatedPosts: getRelatedPosts(post.tags)
});
});
views/blog/post.ejs
<!DOCTYPE html>
<html>
<head>
<title><%= title %> - <%= siteName %></title>
</head>
<body>
<header>
<h1><a href="/"><%= siteName %></a></h1>
<nav>
<% categories.forEach(cat => { %>
<a href="/category/<%= cat %>" class="<%= currentPath.includes(cat) ? 'active' : '' %>">
<%= cat %>
</a>
<% }) %>
</nav>
<% if (isAuthenticated) { %>
<span>Welcome, <%= user.name %></span>
<% } %>
</header>
<main>
<article>
<h1><%= post.title %></h1>
<p class="meta">
By <%= post.author %> on <%= post.published.toDateString() %>
- <%= post.views %> views
</p>
<div class="content"><%= post.body %></div>
</article>
</main>
<footer>
<p>© <%= currentYear %> <%= siteName %></p>
</footer>
</body>
</html>
Best Practices
1. Use app.locals for truly global data
// ✅ Good
app.locals.siteName = 'MyApp';
// ❌ Not global
app.locals.currentUser = getUser(); // Different per request!
2. Use res.locals for middleware data
// ✅ Good
app.use((req, res, next) => {
res.locals.csrfToken = generateToken(req);
next();
});
// ❌ Don't set request-specific data in app.locals
app.locals.csrfToken = generateToken(); // Same for all users - defeats purpose
3. Decompose complex data in the route
// ✅ Good - prepare data before passing
app.get('/dashboard', async (req, res) => {
const [user, posts, notifications] = await Promise.all([
getUser(req.userId),
getUserPosts(req.userId),
getNotifications(req.userId)
]);
res.render('dashboard', {
user: user.toJSON(),
posts: posts.map(p => p.toJSON()),
notifications: notifications.slice(0, 5),
unreadCount: notifications.filter(n => !n.read).length
});
});
4. Set defaults, then override
app.use((req, res, next) => {
// Default values for every view
res.locals.title = 'MyApp';
res.locals.description = 'Default description';
res.locals.ogImage = '/images/default-og.png';
next();
});
// Routes override as needed
app.get('/about', (req, res) => {
res.render('about', {
title: 'About Us - MyApp',
description: 'Learn more about our team and mission'
});
});
Debugging Data in Templates
Temporarily dump all available data during development:
EJS
<pre><%= JSON.stringify(locals, null, 2) %></pre>
Pug
pre= JSON.stringify(locals, null, 2)
Handlebars
<pre>{{json locals}}</pre>
hbs.registerHelper('json', (context) => JSON.stringify(context, null, 2));
Key Takeaways
res.render('view', { data })passes page-specific datares.localspasses request-scoped data (ideal for middleware)app.localspasses application-wide globals- Precedence:
res.render()>res.locals>app.locals - Use middleware to attach common data (user, flash messages, CSRF tokens)
- Decompose and transform data before passing to views
- Dump
localsduring development to debug available data
Keep learning
Related tutorials from across the library.
Using EJS, Pug, or Handlebars
Set up and use template engines in Express - EJS, Pug, and Handlebars compared with examples
Web Development / Backend / Express / Templating & Views
Layouts & Partials
Create reusable layouts and partial templates in EJS, Pug, and Handlebars to keep your views DRY
Web Development / Backend / Express / Templating & Views
Rendering Dynamic Content
Render dynamic HTML in Express using template engines - conditionals, loops, helpers, and more
Web Development / Backend / Express / Templating & Views