Template Engines with EJS

intermediate
14 min

Template Engines with EJS

APIs return JSON, but many sites still render HTML on the server: dashboards, blogs, admin panels, email previews. A template engine lets you write HTML files with embedded placeholders that Express fills with real data at request time. In this lesson we set up EJS, the most beginner-friendly engine, and build dynamic pages with loops, conditionals, and shared partials.

What Is Server-Side Rendering?

Instead of sending a fixed file, the server combines a template with data and sends the finished HTML. The browser receives a complete page, which is great for SEO and first-load speed. EJS (Embedded JavaScript) is popular because its syntax is just HTML plus small JavaScript tags, so there is almost nothing new to learn.

Setup

bash
npm install ejs
js
const express = require('express'); const path = require('path'); const app = express(); app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views'));

Two settings do all the work: view engine tells Express which engine to use, and views points at the folder containing your templates. With these in place you never require ejs manually; Express loads it on demand.

Rendering Your First View

Create views/home.ejs:

html
<!DOCTYPE html> <html> <head><title><%= title %></title></head> <body> <h1>Welcome, <%= username %>!</h1> </body> </html>

Then render it from a route, passing data as the second argument:

js
app.get('/', (req, res) => { res.render('home', { title: 'My Site', username: 'Aisha' }); });

res.render finds views/home.ejs, injects the values, and sends the resulting HTML with the right headers. No file extension needed in the route.

The Three EJS Tags

  • <%= value %> outputs a value, HTML-escaped. Use this for anything user-supplied; escaping blocks cross-site scripting.
  • <%- value %> outputs raw, unescaped HTML. Only for content you fully control, such as rendered partials.
  • <% code %> runs JavaScript without printing, used for loops and conditionals.

Loops and Conditionals

Templates shine when rendering lists. Create views/products.ejs:

html
<h1>Products</h1> <% if (products.length === 0) { %> <p>No products yet.</p> <% } else { %> <ul> <% products.forEach(p => { %> <li><%= p.name %> - $<%= p.price %></li> <% }) %> </ul> <% } %>
js
app.get('/products', (req, res) => { const products = [ { name: 'Keyboard', price: 49 }, { name: 'Mouse', price: 25 }, ]; res.render('products', { products }); });

The template stays declarative; the route supplies data. Resist the urge to do heavy logic inside templates, since it quickly becomes untestable.

Partials for Shared Layout

Headers, footers, and navigation belong in one place. Create views/partials/header.ejs and include it everywhere:

html
<%- include('partials/header', { active: 'home' }) %> <main>Page content here</main> <%- include('partials/footer') %>

Note the raw tag <%- for includes, because the partial returns HTML that must not be escaped. Change the header once and every page updates.

Key Takeaways

  • app.set('view engine', 'ejs') plus a views folder enables res.render.
  • <%= %> escapes output (safe default), <%- %> is raw, <% %> runs logic.
  • Pass data as the second argument to res.render; keep heavy logic in routes or services.
  • Use include for shared partials like headers and footers.
  • Server-rendered pages are SEO-friendly and fast on first load.

Next lesson: Connecting Express to MongoDB, where our data finally survives a server restart.

Template Engines with EJS - Express.js | CodeYourCraft | CodeYourCraft