Skip to content

EJS with Includes and require()

When to use

Use this pattern when rendering server-side HTML (or other structured text like SQL, XML, YAML) in Node.js and you need templates with editor syntax highlighting, require() support for importing utilities, composable includes, and useful error messages. Standard EJS lacks require() inside templates and its built-in include mechanism has limited path resolution. This pattern wraps EJS with enhancements that solve both problems.

The pattern

Build a render() function that wraps ejs.render() and injects two extra helpers into the template data: a scoped require() and a recursive include().

Rendering a template

Call render() with a template path and data object:

javascript
import express from "express";
import { render } from "./template.js";

const app = express();

app.get("/", (req, res) => {
  const html = render("./views/layout.ejs", {
    title: "Home",
    content: "<p>Welcome</p>",
  });
  res.send(html);
});

Using require() in templates

Templates call require() to import utilities directly, without passing everything through the data object:

html
<%
const { formatDate } = require("./utils/dates.js");
const config = require("./config.json");
%>
<h1><%= config.title %></h1>
<p>Generated on <%= formatDate(new Date()) %></p>

Using include() to compose templates

The include() function renders partials with merged data. Here is a layout that includes a header and footer:

layout.ejs:

html
<!DOCTYPE html>
<html>
<head><title><%= title %></title></head>
<body>
  <%- include("./header.ejs") %>
  <%- content %>
  <%- include("./footer.ejs") %>
</body>
</html>

Defining the render function

The render() function wraps ejs.render() and injects a scoped require() and recursive include() into every template's data context, so templates get both helpers without callers having to wire them manually:

javascript
import { createRequireFromPath as createRequire } from "module";
import ejs from "ejs";
import ejsLint from "ejs-lint";

export function render(templatePath, data) {
  const absolutePath = resolve(templatePath);
  const template = readFileSync(absolutePath, "utf-8");
  
  try {
    return ejs.render(template, {
      ...data,
      require: createRequire(absolutePath),
      include: createInclude(absolutePath, data),
    });
  } catch (error) {
    console.error(error);
    console.error(ejsLint(template));
  }
}

Key points:

  • createRequire(absolutePath) gives the template a require() scoped to its own directory, so relative imports resolve correctly
  • include is a custom function (not the EJS built-in) that supports recursive rendering with merged data
  • ejsLint(template) runs on errors to produce clearer diagnostics than raw EJS error messages

Defining the include helper

The include() helper re-enters render() so included templates also get require() and include() support:

javascript
function createInclude(parentPath, data) {
  return function (includePath, includeData) {
    const templatePath = resolve(includePath, parentPath);
    
    return render(templatePath, {
      ...data,
      ...includeData,
    });
  };
}

Include data merges with parent data, so child templates inherit context while accepting overrides.

Defining path resolution

Support three path styles so templates can reference files naturally:

javascript
import path from "path";

function resolve(pathString, callerPath) {
  const { dir } = path.parse(pathString);
  const isAbsolute = path.isAbsolute(pathString);
  const isRelative = !isAbsolute && dir.startsWith(".");
  const isModule = !isAbsolute && !isRelative;
  
  if (isAbsolute) {
    return pathString;
  } else if (isRelative) {
    const callerDir = path.dirname(callerPath);
    return path.resolve(callerDir, pathString);
  } else if (isModule) {
    return require.resolve(pathString);
  }
}
  1. Absolute (/foo/bar.ejs) — used as-is
  2. Relative (./header.ejs) — resolved from the calling template's directory
  3. Module (my-pkg/template.ejs) — resolved through Node's module system

Project structure

A typical layout keeps templates in a views/ directory with the render helper and server entry point at the project root:

views/
├── layout.ejs
├── header.ejs
├── footer.ejs
└── pages/
    ├── home.ejs
    └── about.ejs
utils/
└── dates.js
template.js          # render() and helpers
server.js

Trade-offs

Benefits:

  • Templates keep their native file extension (.ejs, .html) so editors provide syntax highlighting, linting, and autocompletion
  • require() lets templates pull in JavaScript utilities without passing everything through the data object
  • Recursive include() means partials compose naturally at any nesting depth
  • ejs-lint integration surfaces template syntax errors with clear messages instead of cryptic EJS stack traces

Costs:

  • Synchronous file reads at render time — fine for server-side rendering but not suitable for client-side use
  • createRequireFromPath is a Node.js-specific API, so templates are not portable to browsers
  • Custom include() replaces EJS's built-in include syntax, so EJS documentation examples using <%- include('file') %> behave differently
  • Error handling swallows the original error after logging; production code should rethrow or return an error page

See also