Appearance
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 arequire()scoped to its own directory, so relative imports resolve correctlyincludeis a custom function (not the EJS built-in) that supports recursive rendering with merged dataejsLint(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);
}
}- Absolute (
/foo/bar.ejs) — used as-is - Relative (
./header.ejs) — resolved from the calling template's directory - 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.jsTrade-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-lintintegration 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
createRequireFromPathis 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
- Vite HTML Includes Plugin: Build-time HTML composition as an alternative to server-side rendering.
- HTML Includes: Runtime HTML composition via web components for browser-side page assembly.