File Organization
Keep your templates in a views folder with clear subdirectories. Group related templates together.
views/
layouts/
main.ejs
admin.ejs
partials/
header.ejs
footer.ejs
sidebar.ejs
pages/
home.ejs
about.ejs
contact.ejs
emails/
welcome.ejs
reset-password.ejs
errors/
404.ejs
500.ejs
macros/
form-elements.ejs
cards.ejs
This structure scales with your app. Anyone joining the project can find templates quickly. Keep naming consistent — lowercase with hyphens for multi-word names.
Naming Conventions
Consistent naming prevents confusion. Follow these patterns.
views/pages/user-profile.ejs page name: kebab-case
views/partials/nav-bar.ejs partial name: kebab-case
views/emails/account-created.ejs descriptive names
views/errors/not-found.ejs HTTP status as name
Use descriptive names that tell you what the template does. user-profile.ejs is better than profile.ejs which could be a product profile, user profile, or anything else.
For partials, prefix with the component name: card-header.ejs, card-body.ejs, card-footer.ejs.
Keep Logic Minimal
Templates should display data, not process it. Move logic to route handlers or helper functions.
<% const total = items.reduce((sum, i) => sum + i.price * i.qty, 0) %>
<% const formatted = '$' + total.toFixed(2) %>
Total: <%= formatted %>
res.render('cart', {
items: cart.items,
total: cart.getTotal(),
formattedTotal: cart.getFormattedTotal()
})
Complex expressions make templates hard to read and debug. If a template needs more than simple property access or a single function call, the data should be pre-processed.
Separation of Concerns
Each template should handle one thing. Don't build a template that's a layout, a page, and a component all at once.
// views/partials/product-card.ejs
<%= product.name %>
<%= product.price %>
// views/pages/products.ejs
<% products.forEach(product => { %>
<%- include('../partials/product-card', { product }) %>
<% }) %>
The card partial doesn't know it's on a products page. The products page doesn't know how cards are styled. Each piece is independent and reusable.
Try it Yourself →Escape by Default
Use <%= to escape HTML. Only use <%- when you intentionally want raw HTML.
<%= userInput %>
<%- userInput %>
<%- include('partials/header') %>
<%- defineContent() %>
XSS vulnerabilities happen when unescaped user input enters the page. Always ask yourself: "Is this data trusted?" If not, escape it.
Production Tips
A few settings make EJS faster and more reliable in production.
// Disable debugging in production
app.set('view cache', true)
// Set the views directory once
app.set('views', './views')
// Set the template engine
app.set('view engine', 'ejs')
// Set default render options
app.locals.pretty = false
app.locals.cache = true
Template caching is critical in production. Without it, EJS reads and compiles templates on every request. Enable caching and your response times drop significantly.
// Environment-specific settings
if (process.env.NODE_ENV === 'production') {
app.set('view cache', true)
ejs.cache = new (require('lru-cache'))(100)
}
if (process.env.NODE_ENV === 'development') {
app.set('view cache', false)
app.set('strict routing', false)
}
In development, disable caching so template changes show up immediately. In production, enable everything that improves performance.
Avoid Common Mistakes
Here are patterns that cause problems in production.
<%= undefinedVar.property %>
<%= typeof undefinedVar !== 'undefined' ? undefinedVar.property : 'N/A' %>
<% if (user) { %>
<% if (user.role === 'admin') { %>
<% if (user.permissions.includes('edit')) { %>
<% } %>
<% } %>
<% } %>
<%- include('partials/admin-controls', { user }) %>
<%- include('layouts/main', { body: content }) %>
Small, focused templates are easier to test, debug, and maintain. When a template exceeds 100 lines, consider splitting it.