xmlcreate
xmlcreate is a dependency-free XML tree builder for Node.js. You create a document, add declarations, DTD nodes, elements, attributes, text, CDATA, comments, references, and processing instructions, then serialize the finished tree. Every child node has an up() method for fluent navigation back to its parent. The package escapes text and can perform basic well-formedness checks, but it does not parse XML or validate a document against a schema.
A capable small builder when you need explicit XML constructs and are comfortable owning the document structure. For new general-purpose XML work, xmlbuilder2 has broader capabilities and a healthier direct-use ecosystem.
Use it if
- You need explicit control over XML nodes and their order rather than an automatic object-to-XML mapping
- You need DTD declarations, CDATA, comments, character or entity references, and processing instructions in a small Node package
- You want included TypeScript declarations, no runtime dependencies, and configurable pretty-printing
- You already receive XmlElement instances from js2xmlparser or another xmlcreate-based component
- You need to parse, query, edit, or round-trip existing XML: xmlcreate only builds a new in-memory tree and serializes it
- You need streaming output for a huge feed or export: the API retains the full node tree and toString creates the complete XML string in memory
- You want namespace-aware APIs: prefixes and xmlns declarations can be written as names and attributes, but the library does not resolve namespace URIs or prevent prefix mistakes
- You want concise conversion from ordinary objects: every element, attribute, and text node is an explicit method call, and long up() chains are easy to miscount
- You need active feature development or a large direct-user community: 2.0.4 was published in October 2021, the last repository push was in April 2023, and the repository has 5 stars
Setup reality
`npm install xmlcreate` has no peer dependencies, runtime dependencies, native modules, credentials, or config files. It is written in TypeScript and publishes declarations through its `typings` entry, while its runtime is CommonJS. The practical friction is the tree API. Methods return the child they just created, so adding a sibling means saving parent variables or calling `up()` the correct number of times. For anything beyond a tiny document, named variables are clearer than one deep fluent chain. A document may have one declaration, one DTD, and exactly one root element in the required order when validation is enabled. Validation is basic well-formedness checking, not XSD, Relax NG, or business-rule validation. Character data and attribute text escape reserved characters automatically; CDATA, comments, processing instructions, DTD declaration fragments, entity references, and names have their own XML restrictions and can throw. Some node options can replace invalid characters with U+FFFD, but that silently changes data, so rejection is safer for identifiers and signed payloads. Empty elements self-close by default. Serialization is pretty by default with four-space indentation, newline line endings, and single quotes for attributes; set formatting explicitly when snapshots, partner systems, or signatures care about exact bytes. Namespace support is manual: use prefixed names and add matching xmlns attributes yourself. The complete tree and output string stay in memory, so this is best for small and medium documents rather than multi-gigabyte exports.
Patterns
Build a complete XML documentcreate-document
const { document } = require('xmlcreate');
const doc = document();
doc.decl({ encoding: 'UTF-8' });
const root = doc.element({ name: 'catalog' });
root.element({ name: 'title' }).charData({ charData: 'Summer' });
console.log(doc.toString());A validated document accepts exactly one root element. Keep a reference to the parent when adding several siblings.
Add escaped attribute valuesadd-attributes
const product = root.element({ name: 'product' });
product.attribute({ name: 'id' }).text({ charData: 'sku-42' });
product.attribute({ name: 'label' }).text({ charData: 'Tea & biscuits' });Attribute values are child text nodes, not a value option on attribute(). Reserved characters such as & are escaped automatically.
Navigate back to add siblingsadd-sibling-elements
const list = root.element({ name: 'items' });
list.element({ name: 'item' })
.charData({ charData: 'one' })
.up()
.up()
.element({ name: 'item' })
.charData({ charData: 'two' });charData().up() returns the item element, and the second up() returns items. Named variables are safer once chains get deeper.
Write character data safelyescape-text
root.element({ name: 'expression' }).charData({
charData: 'a < b && b > 0',
});charData escapes ampersands, opening angle brackets, and a closing angle bracket when needed to avoid the forbidden ]]> sequence.
Add a CDATA sectionwrite-cdata
root.element({ name: 'script' }).cdata({
charData: 'if (a < b) console.log(a);',
});CDATA content cannot contain ]]> as one section. Split hostile or arbitrary content yourself, or use charData for automatic escaping.
Add document and element commentsadd-comment
doc.comment({ charData: 'generated file' });
root.comment({ charData: 'items start here' });With validation enabled, declaration ordering still matters, and XML comments cannot contain invalid comment sequences such as double hyphens.
Add a processing instructionadd-processing-instruction
doc.procInst({
target: 'xml-stylesheet',
content: 'type="text/xsl" href="catalog.xsl"',
});The package writes the content as processing-instruction data. You are responsible for constructing its pseudo-attributes correctly.
Create prefixed names with a namespace declarationdeclare-namespace
const feed = doc.element({ name: 'atom:feed' });
feed.attribute({ name: 'xmlns:atom' }).text({
charData: 'http://www.w3.org/2005/Atom',
});
feed.element({ name: 'atom:title' }).charData({ charData: 'Updates' });Namespace handling is manual. xmlcreate writes the prefix and xmlns attribute but does not associate or validate the namespace URI.
Force an explicit closing tagcontrol-empty-tags
root.element({
name: 'description',
useSelfClosingTagIfEmpty: false,
});Empty elements self-close by default. This per-element option emits <description></description> when a consumer distinguishes the lexical forms.
Add character and entity referenceswrite-references
const text = root.element({ name: 'text' });
text.charData({ charData: 'Copyright ' });
text.charRef({ char: '©', hex: true });
text.charData({ charData: ' Acme ' });
text.entityRef({ name: 'registered' });An entity reference is only a reference. Declare custom entities in a DTD or ensure the receiving environment already knows them.
Build a DTD with an internal subsetdefine-internal-dtd
const doc = document();
doc.decl();
const dtd = doc.dtd({ name: 'note' });
dtd.element({ charData: 'note (to,from)' });
dtd.element({ charData: 'to (#PCDATA)' });
dtd.element({ charData: 'from (#PCDATA)' });
doc.element({ name: 'note' });DTD declaration bodies are supplied as strings. Basic validation is not a substitute for validating the resulting grammar or document against the DTD.
Control output formattingformat-xml
const xml = doc.toString({
pretty: true,
indent: ' ',
newline: '\n',
doubleQuotes: true,
});Defaults are pretty output, four-space indentation, newline line endings, and single-quoted attributes. Set all options for deterministic fixtures.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| xmlbuilder2 | npm | You want a more widely used fluent builder with namespace helpers, parsing, conversion, and callback APIs |
| xml-js | npm | You need both XML-to-JavaScript and JavaScript-to-XML conversion rather than manual tree construction |
| xml-writer | npm | You prefer a sequential writer-style API for emitting XML without navigating a retained node tree |