BLOG POSTS
How to Use EJS to Template Your Node Application

How to Use EJS to Template Your Node Application

EJS (Embedded JavaScript) is a simple templating language that lets you generate HTML markup with plain JavaScript, making it an excellent choice for Node.js applications. Unlike template engines that introduce their own syntax, EJS sticks close to regular JavaScript, which means less learning curve and more familiar debugging. By the end of this guide, you’ll understand how to set up EJS in your Node application, create dynamic templates with partials and layouts, and handle both simple and complex data rendering scenarios.

How EJS Works Under the Hood

EJS works by compiling your templates into JavaScript functions that can be executed with data to produce HTML strings. When you call res.render() in Express, EJS takes your template file, processes any embedded JavaScript within the <% %> tags, and returns fully rendered HTML.

The engine supports three main tag types:

  • <% %> – Control flow, no output (loops, conditionals)
  • <%= %> – Escaped output (prevents XSS attacks)
  • <%- %> – Unescaped raw output (use with caution)

Under the hood, EJS compiles these templates into cached functions for better performance. The compilation process turns your template into executable JavaScript code, which is why EJS templates run fast compared to engines that parse templates at runtime.

Step-by-Step Implementation Guide

Let’s build a complete Node.js application using EJS from scratch. First, initialize your project and install the necessary dependencies:

mkdir ejs-demo
cd ejs-demo
npm init -y
npm install express ejs

Create your main application file app.js:

const express = require('express');
const path = require('path');
const app = express();

// Set EJS as the view engine
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

// Serve static files
app.use(express.static(path.join(__dirname, 'public')));

// Sample data
const users = [
  { id: 1, name: 'Alice Johnson', email: 'alice@example.com', role: 'admin' },
  { id: 2, name: 'Bob Smith', email: 'bob@example.com', role: 'user' },
  { id: 3, name: 'Carol Davis', email: 'carol@example.com', role: 'moderator' }
];

// Routes
app.get('/', (req, res) => {
  res.render('index', { 
    title: 'EJS Demo App',
    users: users,
    currentTime: new Date().toLocaleString()
  });
});

app.get('/user/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) {
    return res.status(404).render('404', { title: 'User Not Found' });
  }
  res.render('user', { title: `Profile - ${user.name}`, user: user });
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

Create the views directory structure:

mkdir views
mkdir views/partials
mkdir public
mkdir public/css

Now create your main layout in views/partials/header.ejs:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title><%= title %></title>
  <link rel="stylesheet" href="/css/style.css">
</head>
<body>
  <nav>
    <h1><%= title %></h1>
    <ul>
      <li><a href="/">Home</a></li>
      <li><a href="/about">About</a></li>
    </ul>
  </nav>
  <main>

Create the footer partial in views/partials/footer.ejs:

  </main>
  <footer>
    <p>© 2024 EJS Demo App. Generated at <%= typeof currentTime !== 'undefined' ? currentTime : new Date().toLocaleString() %></p>
  </footer>
</body>
</html>

Create your main page template in views/index.ejs:

<%- include('partials/header') %>

<h2>Welcome to Our User Management System</h2>

<% if (users && users.length > 0) { %>
  <div class="user-grid">
    <% users.forEach(user => { %>
      <div class="user-card <%= user.role %>">
        <h3><%= user.name %></h3>
        <p>Email: <%= user.email %></p>
        <p>Role: <span class="role-badge"><%= user.role %></span></p>
        <a href="/user/<%= user.id %>" class="btn">View Profile</a>
      </div>
    <% }); %>
  </div>
<% } else { %>
  <p>No users found.</p>
<% } %>

<%- include('partials/footer') %>

Create the user profile template in views/user.ejs:

<%- include('partials/header') %>

<div class="user-profile">
  <h2><%= user.name %>'s Profile</h2>
  
  <div class="profile-details">
    <p><strong>ID:</strong> <%= user.id %></p>
    <p><strong>Email:</strong> <%= user.email %></p>
    <p><strong>Role:</strong> <%= user.role %></p>
  </div>
  
  <% if (user.role === 'admin') { %>
    <div class="admin-panel">
      <h3>Admin Controls</h3>
      <button class="btn danger">Delete User</button>
      <button class="btn warning">Edit Permissions</button>
    </div>
  <% } %>
  
  <a href="/" class="btn">Back to Users</a>
</div>

<%- include('partials/footer') %>

Add some basic styling in public/css/style.css:

body { font-family: Arial, sans-serif; margin: 0; padding: 0; }
nav { background: #333; color: white; padding: 1rem; }
nav ul { list-style: none; display: flex; gap: 1rem; }
nav a { color: white; text-decoration: none; }
main { padding: 2rem; min-height: calc(100vh - 140px); }
.user-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1rem; }
.user-card { border: 1px solid #ddd; padding: 1rem; border-radius: 8px; }
.user-card.admin { border-left: 4px solid #e74c3c; }
.user-card.moderator { border-left: 4px solid #f39c12; }
.user-card.user { border-left: 4px solid #27ae60; }
.btn { display: inline-block; padding: 0.5rem 1rem; background: #3498db; color: white; text-decoration: none; border-radius: 4px; }
.btn.danger { background: #e74c3c; }
.btn.warning { background: #f39c12; }
footer { background: #f8f9fa; padding: 1rem; text-align: center; }

Real-World Examples and Use Cases

EJS shines in scenarios where you need server-side rendering with dynamic content. Here are some practical applications:

E-commerce Product Listings: EJS handles product catalogs efficiently, especially when you need SEO-friendly pages that load quickly. The template can iterate through product arrays and render different layouts based on product types.

<% products.forEach(product => { %>
  <div class="product-card" data-category="<%= product.category %>">
    <img src="<%= product.image %>" alt="<%= product.name %>" loading="lazy">
    <h3><%= product.name %></h3>
    <p class="price">$<%= product.price.toFixed(2) %></p>
    <% if (product.discount > 0) { %>
      <span class="discount"><%= product.discount %>% OFF</span>
    <% } %>
  </div>
<% }); %>

Dashboard Applications: For admin panels or analytics dashboards where you need to render charts, tables, and dynamic content based on user permissions.

<% if (user.permissions.includes('analytics')) { %>
  <div class="analytics-section">
    <h2>Analytics Overview</h2>
    <% analyticsData.forEach(metric => { %>
      <div class="metric-card">
        <h4><%= metric.title %></h4>
        <span class="value <%= metric.trend %>"><%= metric.value %></span>
      </div>
    <% }); %>
  </div>
<% } %>

Blog Systems: EJS works well for content management systems where you need to render markdown content, handle pagination, and show related posts.

<article>
  <h1><%= post.title %></h1>
  <div class="meta">
    <span>By <%= post.author %></span>
    <span><%= post.publishedAt.toDateString() %></span>
  </div>
  <div class="content">
    <%- post.contentHtml %>
  </div>
  <% if (relatedPosts.length > 0) { %>
    <section class="related-posts">
      <h3>Related Articles</h3>
      <% relatedPosts.slice(0, 3).forEach(related => { %>
        <a href="/post/<%= related.slug %>"><%= related.title %></a>
      <% }); %>
    </section>
  <% } %>
</article>

Comparison with Alternative Template Engines

Here’s how EJS stacks up against other popular Node.js template engines:

Feature EJS Handlebars Pug Mustache
Learning Curve Low (JavaScript syntax) Medium (custom helpers needed) High (indentation-based) Low (logic-less)
Performance Fast (compiled) Fast (compiled) Fast (compiled) Medium
Template Size Medium Verbose Compact Clean
Logic in Templates Full JavaScript Limited helpers Some logic allowed Logic-less
Community Support Large Large Medium Medium
Debugging Easy (familiar JS) Medium Difficult Easy

For most developers coming from a JavaScript background, EJS offers the smoothest transition. If you’re already comfortable with JavaScript arrays, objects, and control structures, you can start building EJS templates immediately without learning new syntax.

Advanced Features and Best Practices

Beyond basic templating, EJS offers several advanced features that make it production-ready:

Custom Delimiters: You can change the default <% %> delimiters if they conflict with your content:

app.set('view options', {
  delimiter: '?',
  openDelimiter: '[',
  closeDelimiter: ']'
});

// Now use [? ?] instead of <% %>

Includes with Data: Pass specific data to your partials:

<%- include('partials/user-card', {user: users[0], showAdmin: true}) %>

Template Caching: EJS automatically caches compiled templates in production. You can control this behavior:

// Force caching in development
app.set('view cache', true);

// Or disable caching completely
app.set('view cache', false);

Error Handling: Implement proper error boundaries in your templates:

<% try { %>
  <% if (user.preferences.theme) { %>
    <div class="theme-<%= user.preferences.theme %>">
      <!-- theme-specific content -->
    </div>
  <% } %>
<% } catch(err) { %>
  <div class="theme-default">
    <!-- fallback content -->
  </div>
<% } %>

Helper Functions: Create reusable functions for common template operations:

// In your app.js
app.locals.formatDate = (date) => {
  return date.toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });
};

app.locals.truncate = (text, length = 100) => {
  return text.length > length ? text.substring(0, length) + '...' : text;
};

// In your templates
<p>Published: <%= formatDate(post.createdAt) %></p>
<p><%= truncate(post.excerpt, 150) %></p>

Common Pitfalls and Troubleshooting

XSS Vulnerabilities: The biggest mistake is using unescaped output <%- %> with user-generated content. Always use escaped output <%= %> unless you’re absolutely certain the content is safe:

<!-- DANGEROUS -->
<div><%- userComment %></div>

<!-- SAFE -->
<div><%= userComment %></div>

<!-- SAFE with HTML content you control -->
<div><%- sanitizedHtmlContent %></div>

Undefined Variable Errors: EJS will throw errors if you reference undefined variables. Use defensive programming:

<!-- BAD -->
<%= user.profile.avatar %>

<!-- GOOD -->
<%= (user && user.profile && user.profile.avatar) || '/default-avatar.png' %>

<!-- BETTER -->
<%= user?.profile?.avatar || '/default-avatar.png' %>

Performance Issues: Avoid complex logic in templates. Move heavy processing to your route handlers:

// BAD - Heavy processing in template
<% 
let processedData = rawData.map(item => {
  return complexTransformation(item);  // Heavy operation
}).filter(item => item.isValid);
%>

// GOOD - Process in route handler
app.get('/data', (req, res) => {
  const processedData = rawData.map(complexTransformation).filter(item => item.isValid);
  res.render('data', { processedData });
});

File Path Issues: Use absolute paths for includes to avoid confusion:

<!-- Relative path - can break in subdirectories -->
<%- include('../partials/header') %>

<!-- Absolute path from views directory -->
<%- include('partials/header') %>

Memory Leaks: In high-traffic applications, disable view caching in development but ensure it’s enabled in production:

// Environment-specific caching
app.set('view cache', process.env.NODE_ENV === 'production');

Performance Optimization and Security

For production deployments, especially on VPS or dedicated server environments, consider these optimizations:

Template Compilation: Pre-compile templates for better performance:

const ejs = require('ejs');
const fs = require('fs');

// Pre-compile frequently used templates
const userTemplate = ejs.compile(fs.readFileSync('./views/user.ejs', 'utf8'));

app.get('/user/:id', (req, res) => {
  const user = getUserById(req.params.id);
  const html = userTemplate({ user, title: `Profile - ${user.name}` });
  res.send(html);
});

Content Security Policy: Implement CSP headers to prevent XSS attacks:

app.use((req, res, next) => {
  res.setHeader('Content-Security-Policy', 
    "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");
  next();
});

Input Validation: Always validate data before passing it to templates:

const { body, validationResult } = require('express-validator');

app.post('/user', [
  body('name').isLength({ min: 2, max: 50 }).trim().escape(),
  body('email').isEmail().normalizeEmail(),
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.render('user-form', { errors: errors.array(), formData: req.body });
  }
  // Process valid data
});

EJS remains one of the most practical choices for Node.js templating due to its JavaScript-native approach and excellent performance characteristics. The official documentation at https://ejs.co/ provides comprehensive API references and additional examples. For Express.js integration details, check the Express templating guide at https://expressjs.com/en/guide/using-template-engines.html.

The combination of familiar JavaScript syntax, robust performance, and extensive ecosystem support makes EJS an excellent choice for everything from simple websites to complex web applications requiring server-side rendering.



This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.

This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.

Leave a reply

Your email address will not be published. Required fields are marked