Episode 3 — NodeJS MongoDB Backend Architecture / 3.5 — Template Engine EJS

3.5.b — Setting Up EJS

In one sentence: Setting up EJS takes three steps — install the package, tell Express to use it as the view engine, and create .ejs files inside a views/ folder — then every res.render() call automatically finds, compiles, and sends the right template.

Navigation: <- 3.5.a — What is a Template Engine? | 3.5.c — EJS Syntax Deep Dive ->


1. Installing EJS

# Start in your project directory
mkdir ejs-demo && cd ejs-demo
npm init -y
npm install express ejs

After installation, your package.json dependencies section looks like:

{
  "dependencies": {
    "ejs": "^3.1.9",
    "express": "^4.18.2"
  }
}

Note: You install ejs as a regular dependency, not a dev dependency. Express loads it at runtime to compile templates.


2. Configuring Express to Use EJS

Two lines of configuration are required:

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

// LINE 1: Tell Express which template engine to use
app.set('view engine', 'ejs');

// LINE 2: Tell Express where to find template files
app.set('views', './views');   // This is actually the default — but be explicit
SettingPurposeDefault
'view engine'Which engine compiles .ejs filesNone (must set)
'views'Folder path where templates live./views (process.cwd()/views)

Important: You never require('ejs') in your code. Express does that internally when you set the view engine. The ejs package just needs to be installed.


3. Project Directory Structure

ejs-demo/
├── node_modules/
├── package.json
├── package-lock.json
├── app.js                  <-- Express server
└── views/                  <-- All EJS templates go here
    ├── index.ejs
    ├── about.ejs
    ├── profile.ejs
    └── partials/           <-- Reusable components
        ├── header.ejs
        └── footer.ejs

Rules:

  • All .ejs files must live inside the views/ folder (or a subfolder of it)
  • Subfolders are allowed and referenced with path: res.render('partials/header')
  • You do not include the .ejs extension in res.render() — Express adds it automatically

4. Creating Your First .ejs File

views/index.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>
</head>
<body>
  <h1>Welcome to <%= title %></h1>
  <p>This page was rendered by EJS on the server.</p>
  <p>Current time: <%= new Date().toLocaleTimeString() %></p>
</body>
</html>

This looks exactly like HTML, except for the <%= ... %> tags. Those are EJS output tags — they get replaced with actual values when the template is compiled.


5. Rendering a View with res.render()

app.js:

const express = require('express');
const app = express();
const PORT = 3000;

// Configure EJS
app.set('view engine', 'ejs');
app.set('views', './views');

// Route that renders the template
app.get('/', (req, res) => {
  res.render('index', { title: 'My EJS App' });
  //         ^^^^^    ^^^^^^^^^^^^^^^^^^^^^^^^
  //    template name    data object passed to template
});

app.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}`);
});

What res.render() does internally:

res.render('index', { title: 'My EJS App' })
  1. Looks for: views/index.ejs
  2. Reads the file content
  3. Passes { title: 'My EJS App' } to the EJS compiler
  4. EJS replaces <%= title %> with 'My EJS App'
  5. Returns complete HTML string
  6. Express sends it with Content-Type: text/html

6. The Data Object — What You Can Pass

The second argument to res.render() is a plain JavaScript object. Every key becomes a variable available in the template.

app.get('/dashboard', (req, res) => {
  res.render('dashboard', {
    username: 'Priya',
    role: 'admin',
    notifications: 5,
    tasks: ['Deploy v2', 'Fix login bug', 'Write tests'],
    isLoggedIn: true
  });
});

Inside views/dashboard.ejs, you can use username, role, notifications, tasks, and isLoggedIn directly — no prefix, no data. needed.

<h1>Hello, <%= username %></h1>
<p>You have <%= notifications %> new notifications.</p>

7. Running and Testing

node app.js
# Server running at http://localhost:3000

Open http://localhost:3000 in your browser. You see the rendered HTML. View Page Source (Ctrl+U / Cmd+U) to confirm — there are no <%= %> tags in the source. The browser received pure HTML.


8. EJS vs Plain HTML — What is Different?

FeatureHTML FileEJS File
Extension.html.ejs
Dynamic dataNot possible (static)<%= variable %>
ConditionalsNot possible<% if (x) { %> ... <% } %>
LoopsNot possible<% items.forEach(...) %>
IncludesNot built-in<%- include('partial') %>
Served byres.sendFile() or static middlewareres.render()
Processed byBrowser directlyEJS engine on server, then sent as HTML

Key insight: An .ejs file is an HTML file with extra powers. Any valid HTML is also valid EJS. You can rename .html to .ejs and it works immediately — then gradually add dynamic tags.


9. Complete Setup Walkthrough (From Zero)

Here is every step, from an empty folder to a running EJS app:

Step 1 — Create project:

mkdir my-ejs-app && cd my-ejs-app
npm init -y

Step 2 — Install dependencies:

npm install express ejs

Step 3 — Create folder structure:

mkdir views

Step 4 — Create the template (views/home.ejs):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title><%= pageTitle %></title>
  <style>
    body { font-family: Arial, sans-serif; max-width: 600px; margin: 40px auto; }
    .card { border: 1px solid #ddd; padding: 16px; border-radius: 8px; margin: 8px 0; }
  </style>
</head>
<body>
  <h1><%= pageTitle %></h1>
  <p>Welcome, <strong><%= user.name %></strong>!</p>

  <h2>Your Projects</h2>
  <% projects.forEach(function(project) { %>
    <div class="card">
      <h3><%= project.name %></h3>
      <p>Status: <%= project.status %></p>
    </div>
  <% }); %>
</body>
</html>

Step 5 — Create the server (app.js):

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

app.set('view engine', 'ejs');
app.set('views', './views');

app.get('/', (req, res) => {
  res.render('home', {
    pageTitle: 'Project Dashboard',
    user: { name: 'Arjun', email: 'arjun@example.com' },
    projects: [
      { name: 'E-Commerce API', status: 'In Progress' },
      { name: 'Blog Platform', status: 'Completed' },
      { name: 'Chat App', status: 'Planning' }
    ]
  });
});

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

Step 6 — Run:

node app.js

Step 7 — Open browser: Visit http://localhost:3000 and see a fully rendered page with dynamic project cards.


10. Common Setup Mistakes

MistakeError You SeeFix
Forgot npm install ejsCannot find module 'ejs'Run npm install ejs
Forgot app.set('view engine', 'ejs')No default engine was specifiedAdd the setting
Template not in views/ folderFailed to lookup view "home"Move file into views/
Used .html extensionFailed to lookup view "home"Rename to .ejs
Passed wrong variable nameReferenceError: x is not definedMatch names between render and template
Included .ejs in render callUsually works, but inconsistentOmit the extension: res.render('home')

11. Using an Absolute Path for Views

If your app is started from a different working directory, the relative ./views may break. Use path.join for safety:

const path = require('path');

app.set('views', path.join(__dirname, 'views'));

__dirname always points to the folder where the current JS file lives, regardless of where node was invoked from. This is a best practice for production apps.


12. Multiple Render Calls — Multiple Pages

app.get('/', (req, res) => {
  res.render('home', { pageTitle: 'Home' });
});

app.get('/about', (req, res) => {
  res.render('about', { pageTitle: 'About Us' });
});

app.get('/contact', (req, res) => {
  res.render('contact', { pageTitle: 'Contact' });
});

Each route renders a different .ejs file from the views/ folder. This is how multi-page server-rendered apps work.


13. Key Takeaways

  1. Install EJS with npm install ejs — no require() needed in your code.
  2. Two settings: app.set('view engine', 'ejs') and app.set('views', './views').
  3. Call res.render('templateName', { data }) to compile and send HTML.
  4. Every key in the data object becomes a top-level variable in the template.
  5. Use path.join(__dirname, 'views') for reliable paths in production.

Explain-It Challenge

Explain without notes:

  1. Why do you never need to require('ejs') in your Express app?
  2. What three things does res.render() do internally before the browser gets HTML?
  3. If you see Failed to lookup view "profile", what are the three most likely causes?

Navigation: <- 3.5.a — What is a Template Engine? | 3.5.c — EJS Syntax Deep Dive ->