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
.ejsfiles inside aviews/folder — then everyres.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
| Setting | Purpose | Default |
|---|---|---|
'view engine' | Which engine compiles .ejs files | None (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
.ejsfiles must live inside theviews/folder (or a subfolder of it) - Subfolders are allowed and referenced with path:
res.render('partials/header') - You do not include the
.ejsextension inres.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?
| Feature | HTML File | EJS File |
|---|---|---|
| Extension | .html | .ejs |
| Dynamic data | Not possible (static) | <%= variable %> |
| Conditionals | Not possible | <% if (x) { %> ... <% } %> |
| Loops | Not possible | <% items.forEach(...) %> |
| Includes | Not built-in | <%- include('partial') %> |
| Served by | res.sendFile() or static middleware | res.render() |
| Processed by | Browser directly | EJS 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
| Mistake | Error You See | Fix |
|---|---|---|
Forgot npm install ejs | Cannot find module 'ejs' | Run npm install ejs |
Forgot app.set('view engine', 'ejs') | No default engine was specified | Add the setting |
Template not in views/ folder | Failed to lookup view "home" | Move file into views/ |
Used .html extension | Failed to lookup view "home" | Rename to .ejs |
| Passed wrong variable name | ReferenceError: x is not defined | Match names between render and template |
Included .ejs in render call | Usually works, but inconsistent | Omit 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
- Install EJS with
npm install ejs— norequire()needed in your code. - Two settings:
app.set('view engine', 'ejs')andapp.set('views', './views'). - Call
res.render('templateName', { data })to compile and send HTML. - Every key in the data object becomes a top-level variable in the template.
- Use
path.join(__dirname, 'views')for reliable paths in production.
Explain-It Challenge
Explain without notes:
- Why do you never need to
require('ejs')in your Express app? - What three things does
res.render()do internally before the browser gets HTML? - 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 ->