easy-template-x: filling Word docx templates from Node or the browser
Generate docx documents from templates, in Node or in the browser.
At a glance
- What is it?
- easy-template-x takes a .docx file with tagged placeholders and a plain JavaScript object, and produces a filled document without going through Word or a headless Office install. It runs the same in Node and in a browser tab, and its plugin model covers text, loops, images, links, charts and raw XML insertion.
- Who is it for?
- Adopt easy-template-x if you produce Word documents from templates in a Node service or a browser, and you want the same template file to render identically in both places. Do not adopt it if you need PDF output, if you need to author the template in code rather than fill one in Word, or if you are on Node 18, since the package requires Node 20 or newer.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 29 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Fill a docx template instead of building a document
easy-template-x generates docx documents from templates, and it runs in Node or in the browser. The unit of work is a .docx file that someone has authored in Word, containing tagged placeholders, plus a plain JavaScript object describing the values.
The distinction from a document generation library is worth being precise about. A generator library produces a document by describing it in code, layout and all. easy-template-x takes the opposite approach: a human or a designer produces the template in Word, with the formatting, the header, the table borders and the styles, and the library only fills in the tagged spots. The formatting is inherited from the template rather than reproduced by the code.
The example is short enough to read as the whole API. You read the template file, build a data object, create a TemplateHandler, process the template against the data, and write the result out:
import * as fs from 'fs';
import { TemplateHandler } from 'easy-template-x';
// 1. read template file
const templateFile = fs.readFileSync('myTemplate.docx');
// 2. process the template
const data = {
posts: [
{ author: 'Alon Bar', text: 'Very important\ntext here!' },
{ author: 'Alon Bar', text: 'Forgot to mention that...' }
]
};
const handler = new TemplateHandler();
const doc = await handler.process(templateFile, data);
// 3. save output
fs.writeFileSync('myTemplate - output.docx', doc);The data is untyped plain JSON. There is no schema, no template compiler step and no intermediate representation. The object you pass is matched against the tags in the document.
The audience is a developer who has a Word template from a customer or a colleague and a JSON payload, and needs the merged .docx back. The README points at a CodeSandbox live demo for the browser case, and the project also advertises a hosted platform, templatedocs.io, offering workflow automation, email delivery, Zapier and Make integration, a REST API, PDF output and live preview.
Those hosted features are the natural place to look if you do not want to run the library yourself, and the PDF output in particular is worth noting as a gap in the library itself.
The same handler in a browser tab
The browser example is the interesting one, because it shows that the processing step is environment-independent. The README states that the actual template processing, step 2, is exactly the same as in the Node example, and the difference is entirely in how the template file arrives and how the result leaves.
The template comes in as a Blob. The example fetches it over HTTP and converts the response to a blob, and notes that an HTML file input is the other common way to get hold of a Blob. Then the same TemplateHandler processes it, and the output Blob is saved by creating an object URL, making a temporary anchor element with the download attribute, clicking it, and cleaning up the link and the object URL on a timeout.
That is the whole browser story, and it is deliberately dependency-free. No file system access, no server round trip, no rendering service. A user can pick a .docx in a web form, the browser fills it, and the browser downloads the result. That is a genuinely useful shape for a privacy-sensitive document tool, and it is the reason the library ships no Node-specific code path in the handler.
The build configuration supports that. The package is marked as type module with sideEffects false, and the exports map carries both a require branch pointing at dist/cjs/easy-template-x.cjs with .d.cts types and an import branch pointing at dist/es/easy-template-x.mjs with .d.mts types, plus a top-level typings entry at dist/types/index.d.ts. So CommonJS, ESM and TypeScript consumers are all served from one package, and the rollup configuration plus a separate dts rollup config produce the two builds.
The package is published as easy-template-x at version 8.0.0, requires Node 20 or newer, and ships both dist and src in the published files.
Six plugins and the tag syntax
The library uses a plugin model for its template manipulation capabilities, with built-in plugins and a documented path for writing your own. The bundled set covers the operations that documents actually need.
The Text plugin is the most basic one, replacing a single tag with custom text and preserving the original text style. The Loop plugin iterates text, table rows, table columns and lists, and handles simple conditions. The Image plugin embeds images, the Link plugin creates hyperlinks, the Chart plugin handles charts, and the Raw xml plugin is for custom XML insertion.
The tag syntax is where the design decisions sit. Loops require an opening tag that starts with # and a closing tag that starts with /, and the delimiters are configurable. The closing tag does not need to have the same name as the opening tag, or a name at all, so {#loop}{/loop} works, and so does {#loop}{/}, and so does {#loop}{/something else}.
That is a deliberate relaxation, and it cuts both ways. It makes templates easier to author, since a designer does not have to repeat a name in two places, and it makes them easier to get wrong, since a mismatched closing tag is not an error you are likely to notice in Word.
The text plugin takes a flat object of tag names to values:
{
"First Tag": "Quis et ducimus voluptatum\nipsam id.",
"Second Tag": "Dolorem sit voluptas magni dolorem molestias."
}The loop plugin takes an array of objects, and the loop body is repeated once per element:
{
"Beers": [
{ "Brand": "Carlsberg", "Price": 1 },
{ "Brand": "Leaf Blonde", "Price": 2 },
{ "Brand": "Weihenstephan", "Price": 1.5 }
]
}Conditions reuse the loop syntax, with a boolean value deciding whether the block renders. The example data for a condition is a list of objects each carrying a truthy key, one visible and one invisible, so two lines render with different content depending on the values. Nested conditions are supported, and you can nest other tags inside them, including loop tags, with the data shaped to match.
How a tag is matched to a value
The scope resolution rules are the part of this library that most often decides whether a document comes out right, and they are documented as a separate topic in the README's table of contents, alongside advanced syntax and custom resolvers and custom tag delimiters.
The basic idea is that a tag in the document names a path into your data. A plain tag name like First Tag matches a top-level key. A tag inside a loop body resolves against the current loop item, which is why the loop data can be an array of flat objects rather than a nested structure. That is the same shape as a template engine you would write yourself, and it is what lets one data object drive a table with one row per beer.
The rules get more involved than that in the corners, and the README covers custom resolvers and advanced syntax for those cases. The listing of tags feature exists so you can ask a document what it expects, which is a useful thing to have when a template comes from a designer rather than from you.
The template handler options are the other half of the configuration surface, and the one explicitly called out in the table of contents is custom tag delimiters. That option exists because # and / are not the only sensible conventions, and because a document may already contain text that looks like a tag. Changing the delimiters is the escape hatch for the second problem, though it means the template author and the data have to agree on a convention that is not the default.
One thing to keep in mind when reading the docs is that a docx file is a zip of XML, and the tags live inside Word runs. A tag that Word has split across two runs, which happens when a user types part of it in bold or changes the spelling of one word in it, is no longer a single tag in the file. That is the failure mode behind most of the support questions this kind of library receives, and the reason to keep template authoring controlled.
Where this is the wrong choice
Three boundaries are worth naming before you build on it.
The first is PDF. The library's output is a .docx, and the README does not document PDF conversion. The hosted platform lists PDF output support among its features, which tells you that the capability is a product decision made upstream of the library, not something the open source package does. If your workflow ends in a PDF for a customer, you need that extra step and it is not free.
The second is code-generated documents. If you want to produce a document whose structure itself varies with the data, for example a report where the number of sections is decided at runtime, you are fighting the model. The template is fixed, and the data fills it in. A generator library, where the document is described in code, is the right tool for that, and this is the difference that decides which of the two you want.
The third is the shape of the data. Tags are resolved by name against the object you pass, and that means the data has to be organised around the template rather than around your domain. When the same payload feeds both a document and an API, you will end up reshaping it into the document's naming, and that mapping is code you maintain.
There is also a version consideration. The package is at version 8.0.0, and a major version of a library that has shipped under that name for years will have had breaking changes along the way. The CHANGELOG.md is in the repository, and the version you are reading is not necessarily the version you will be reading in a year.
Licence, tooling and what the repository shows
The licence is MIT, stated in the package metadata and with a LICENSE file in the repository, and the author is Alon Bar. That is permissive, with no copyleft obligation, so a template service built on it can be closed source.
The repository layout shows a serious build setup rather than a script collection. There is a rollup.config.js and a separate rollup.dts.config.js for the two build outputs, a vitest.config.ts with the test script running vitest, an eslint.config.js, a .markdownlint.json for the documentation, a .yarn directory with a .yarnrc.yml, and a dist-verify/ directory that the release script runs against. The scripts section spells out the release pipeline in order: clean, quality, build, dist-verify, then package-lint, where quality is typecheck plus lint plus test, and package-lint runs attw and publint. Those last two are the packaging linters that check the published tarball matches the exports map and the Node engines field, which is a sign the author cares about consumers installing the package correctly.
The CI badge at the top of the README points at a ci.yaml workflow. There are no GitHub releases, so the version history is in CHANGELOG.md, and the last push was on 2026-09-24.
The README also documents an internal API section, marked as internal in the table of contents, and a community extensions section, which tells you the plugin surface is meant to grow outside the core. The philosophy section is the last thing in the table of contents before the changelog, and reading it tells you what the author thinks the library is for, which is usually the fastest way to know whether it matches your problem.
Editorial conclusion
Adopt easy-template-x if you produce Word documents from templates in a Node service or a browser, and you want the same template file to render identically in both places. Do not adopt it if you need PDF output, if you need to author the template in code rather than fill one in Word, or if you are on Node 18, since the package requires Node 20 or newer. Verify first that your placeholder tags sit inside runs the library can reach, since a tag split across formatting runs is the usual reason a replacement silently does nothing, and confirm the shape of your data against the scope resolution rules before you build the template.
Frequently asked questions
How do I generate a docx from a template with easy-template-x?
Read the .docx file, build a plain JavaScript object, create a TemplateHandler and call process with the template and the data, then write the returned document out. The same call works in Node and in the browser, where the template is loaded as a Blob and the result is saved as a Blob.
Which plugins does easy-template-x ship with?
The bundled plugins are Text for simple text replacement, Loop for iterating text, table rows, table columns and lists as well as simple conditions, Image for embedding images, Link for hyperlinks, Chart for handling charts, and Raw xml for custom XML insertion. You can also write your own plugins.
What is the loop and condition tag syntax in easy-template-x?
A loop needs an opening tag starting with # and a closing tag starting with /, and the delimiters are configurable. The closing tag does not have to match the opening name, so {#loop}{/loop}, {#loop}{/} and {#loop}{/something else} all work, and conditions reuse the same syntax with a boolean value deciding whether the block renders.
Can easy-template-x run in a browser?
Yes, the README gives a browser example and states that the actual template processing is exactly the same as in the Node example, with only the template loading and result saving differing. The package is published as easy-template-x at version 8.0.0 and requires Node 20 or newer.
Does easy-template-x produce PDF output?
The library produces .docx documents and the README does not document PDF conversion. PDF output is listed among the features of the hosted templatedocs.io platform, so if you need it you should look at that route rather than assuming the open source package covers it.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/alonrbar-easy-template-x)Was this useful?