JavaScript vs XQuery - When to Choose Which

Making the Right Language Choice

personClever Llamas
CleverLlamasMinimum Llamaverse Version: 2
databaseMinimum MarkLogic Version: 8

Short version first. If your workload is XML-heavy, path-heavy, or transformation-heavy, XQuery is usually the sharper tool. If your workload is JSON-heavy, service-heavy, and aligned with modern web engineering skills, JavaScript usually wins.

Long version: this is exactly where expensive mistakes creep in quietly.

The deeper rule is this: in MarkLogic, JavaScript and XQuery are peers in capability more often than people think, but they are not peers in ergonomics. You can reach similar server functionality from both, yet the day-to-day developer experience is not the same. Teams that treat this as a style debate usually pay for it later through rewrites, bugs, and slower delivery.

The choice affects how clearly you express queries, how maintainable modules remain six months from now, how quickly engineers diagnose data issues, and how often you need cross-language bridging. None of that is theoretical. It is the difference between a steady release cadence and "why is this module terrifying" moments.

The Practical Starting Point

Before anybody starts a language argument, ask four questions: What is the dominant data shape? What is the dominant transformation style? What can the team support in production at 2am? Are there version-specific platform constraints? Those four questions settle most debates fast.

ConcernJavaScriptXQueryWhat usually wins
JSON document manipulationExcellentGoodJavaScript
XML construction and transformationPossible, but verboseExcellentXQuery
XPath-style navigationIndirectNativeXQuery
FLWOR-style data reshapingNot nativeNativeXQuery
Modern application syntaxExcellentLimited by language designJavaScript
Module familiarity for web teamsExcellentUsually lowerJavaScript
Cross-language interoperabilityAvailableAvailableTie
Schema-oriented XML workflowsPossibleMore naturalXQuery
Team onboarding from Node or browser workEasyHarderJavaScript

There is no trophy for "one language everywhere". There is also no trophy for linguistic chaos. The best teams pick a deliberate default, then use the other language only where it creates a clear, defensible advantage.

In other words: no dogma, no drama, no accidental llama stampede in production.

It is less dramatic than a grand unification speech, but it works better in production, and that is the only scoreboard that really matters.

XQuery's Strengths

XQuery Is Built for Structured Content Work

XQuery was designed to navigate, filter, reshape, and construct hierarchical content. XPath is not bolted on — it is the native navigation model. FLWOR is not an imitation of SQL or array chaining — it is the native reshaping model. Element constructors are not a helper library — they are part of the language. When the problem is document-shaped, XQuery usually reads like the problem statement.

XML and XPath Processing

In XQuery, paths are compact, expressive, and easy to refine. Predicates, namespace handling, structural constraints, and construction live in one mental model. When you need to say "find this thing, then derive this thing, then emit a new shape", XQuery often compresses ten minutes of JavaScript object handling into three readable lines.

xquery version "1.0-ml";

let $profile :=
  <llama-profile xmlns="http://cleverllamas.com/llamas">
    <name>Bradley</name>
    <species>suri</species>
    <skills>
      <skill>poetry</skill>
      <skill>chess</skill>
      <skill>foraging</skill>
    </skills>
  </llama-profile>

return
  <summary>
    <name>{ $profile/*:name/fn:string() }</name>
    <featured-skills>
      {
        for $skill in $profile/*:skills/*:skill[fn:position() le 2]
        return <skill>{ fn:string($skill) }</skill>
      }
    </featured-skills>
  </summary>
'use strict';

const profile = xdmp.unquote(`
  <llama-profile xmlns="http://cleverllamas.com/llamas">
    <name>Bradley</name>
    <species>suri</species>
    <skills>
      <skill>poetry</skill>
      <skill>chess</skill>
      <skill>foraging</skill>
    </skills>
  </llama-profile>
`);

const ns = 'declare default element namespace "http://cleverllamas.com/llamas"; ';

const extracted = fn.head(
  xdmp.xqueryEval(
    `${ns}
     declare variable $p external;
     object-node {
       "name": xs:string($p/llama-profile/name),
       "featuredSkills": array-node {
         for $s in $p/llama-profile/skills/skill[position() le 2]
         return xs:string($s)
       }
     }`,
    { p: profile },
    { update: 'false' }
  )
);

const name = extracted.name;
const featuredSkills = JSON.parse(xdmp.toJsonString(extracted.featuredSkills));

const summary = new NodeBuilder();
summary.startElement('summary');
summary.startElement('name');
summary.addText(name);
summary.endElement();
summary.startElement('featured-skills');
for (const skill of featuredSkills) {
  summary.startElement('skill');
  summary.addText(skill);
  summary.endElement();
}
summary.endElement();
summary.endElement();
summary.toNode();
<summary>
  <name>Bradley</name>
  <featured-skills>
    <skill>poetry</skill>
    <skill>chess</skill>
  </featured-skills>
</summary>

The JavaScript version is not wrong. It is just more mechanical for this specific problem shape. Repeat that pattern across a large codebase and the maintenance bill will find you.

FLWOR Expressions

FLWOR is still one of the cleanest ways to express structured transformations in MarkLogic. The for, let, where, order by, and return flow is especially strong when the transformation is both content-aware and shape-aware. You can search, bind intermediate values, sort by derived expressions, and emit new XML in one readable pipeline. For reporting, indexing preparation, XML normalisation, and pipeline-style enrichment, this matters a lot.

xquery version "1.0-ml";

(: Query the wild-llamas collection and emit XML for the first 3 results,     :)
(: sorted alphabetically by llama name.                                       :)
for $doc in cts:search(fn:collection("wild-llamas"), cts:true-query())[1 to 3]
let $name := fn:string(($doc//name)[1])
let $uri  := xdmp:node-uri($doc)
order by fn:lower-case($name)
return
  <llama uri="{$uri}">
    <name>{ $name }</name>
  </llama>
'use strict';

// Query the wild-llamas collection and emit XML for the first 3 results,
// sorted alphabetically by llama name.
const rows = [];
let count = 0;
for (const doc of cts.search(cts.collectionQuery('wild-llamas'))) {
  rows.push({ uri: xdmp.nodeUri(doc), name: String(doc.root.name) });
  if (++count >= 3) { break; }
}

rows.sort((a, b) => a.name.localeCompare(b.name));

const builder = new NodeBuilder();
builder.startElement('llamas');
for (const row of rows) {
  builder.startElement('llama');
  builder.addAttribute('uri', row.uri);
  builder.startElement('name');
  builder.addText(row.name);
  builder.endElement();
  builder.endElement();
}
builder.endElement();
builder.toNode();
<llama uri="/cleverllamas/llamaverse/raw/wild-llamas/llamas/...json">
  <name>Aaron</name>
</llama>
<llama uri="/cleverllamas/llamaverse/raw/wild-llamas/llamas/...json">
  <name>Bradley</name>
</llama>
<llama uri="/cleverllamas/llamaverse/raw/wild-llamas/llamas/...json">
  <name>Chloe</name>
</llama>

(: The three results are sorted by name ascending.                            :)
(: Actual URIs depend on which documents appear first in the collection.      :)

In a transformation-heavy codebase, FLWOR is not just elegant syntax. It is a genuine maintenance advantage.

Built-in XML Functions and Element Constructors

MarkLogic's XML tooling feels most natural in XQuery because the core language and the server APIs share the same conceptual vocabulary: node identity, typed values, QName handling, path evaluation, element construction, and document order. Direct element construction remains one of XQuery's superpowers — the output shape is visible in the code without ceremony. That is especially valuable for envelope creation, metadata documents, configuration artefacts, and content harmonisation pipelines.

Schema Validation

Both languages can reach validation functionality, but XML schema validation feels more natural in XQuery because the inputs, namespaces, node handling, and downstream XML reshaping are already in the same idiom.

xquery version "1.0-ml";

let $candidate :=
  <llama xmlns="http://cleverllamas.com/schema/llama">
    <name>Aurora</name>
    <species>suri</species>
  </llama>

return xdmp:validate($candidate)
'use strict';

const candidate = xdmp.unquote(`
  <llama xmlns="http://cleverllamas.com/schema/llama">
    <name>Aurora</name>
    <species>suri</species>
  </llama>
`);

xdmp.validate(candidate);
<llama xmlns="http://cleverllamas.com/schema/llama">
  <name>Aurora</name>
  <species>suri</species>
</llama>

(: On validation success, the validated node is returned.                     :)
(: On failure, a XDMP-VALIDATE-FAIL error is thrown.                         :)

The point is not that JavaScript cannot validate XML — it can. The point is that the surrounding code is usually more coherent in XQuery.

JavaScript's Strengths

JSON-Native Handling

This is JavaScript's clearest win. When the application boundary is already JSON, there is no mental or structural impedance mismatch. You insert, transform, enrich, and return data in the same model your service code already speaks.

Native JSON vs MarkLogic JSON Nodes

This trips up smart teams all the time. In Server-Side JavaScript, there are two related but different things:

  • native JavaScript objects and arrays,
  • MarkLogic JSON nodes (document/object/array node representations in the database model).

They look similar in logs, but they do not behave identically in code. Native objects work directly with JavaScript property access and array methods. JSON nodes often need explicit conversion when you want ordinary JavaScript manipulation.

Practical rule: if a value came from node-centric database APIs, pause for ten seconds and verify whether it is a native object or a MarkLogic node representation before applying normal JavaScript assumptions.

'use strict';

const nativeDoc = {
  name: 'Bradley',
  traits: ['poetry', 'chess', 'foraging']
};

const nodeDoc = xdmp.toJSON(nativeDoc);
const roundTripDoc = JSON.parse(xdmp.toJsonString(nodeDoc));

const payload = {
  nativeName: nativeDoc.name,
  nativeTraitCount: nativeDoc.traits.length,
  nodeNameDirect: typeof nodeDoc.name === 'undefined' ? null : nodeDoc.name,
  nodeDescribe: xdmp.describe(nodeDoc),
  convertedNodeName: roundTripDoc.name,
  convertedTraitCount: roundTripDoc.traits.length
};

payload;
{
  "nativeName": "Bradley",
  "nativeTraitCount": 3,
  "nodeNameDirect": null,
  "nodeDescribe": "Document({\"name\":\"Bradley\", \"traits\":[\"poetry\", \"chess\", \"foraging\"]})",
  "convertedNodeName": "Bradley",
  "convertedTraitCount": 3
}
xquery version "1.0-ml";

let $doc := map:map() =>
  map:with("name", "Bradley") =>
  map:with("species", "suri") =>
  map:with("traits", json:array-values(json:to-array(("poetry", "chess", "foraging"))))

return map:map() =>
  map:with("name", map:get($doc, "name")) =>
  map:with("featuredTraits", json:to-array(
    let $all := map:get($doc, "traits")
    return $all[. = ("poetry", "chess")]
  ))
'use strict';

const doc = {
  name: 'Bradley',
  species: 'suri',
  traits: ['poetry', 'chess', 'foraging']
};

({
  name: doc.name,
  featuredTraits: doc.traits.filter(t => ['poetry', 'chess'].includes(t))
});
{
  "name": "Bradley",
  "featuredTraits": [
    "poetry",
    "chess"
  ]
}

The XQuery version is solid. The JavaScript version is what most web engineers can read at full speed without mental context switching. That matters when release cadence is real and deadlines are not hypothetical.

V8 Engine, Modern Syntax, and Module System

MarkLogic's Server-Side JavaScript runs on V8. The language surface feels familiar to modern JavaScript developers: arrow functions, template literals, array methods, objects and destructuring, and a module system based on require. The significance is not performance marketing — it is developer fluency. If your team already lives in Node, browser code, or TypeScript-transpiled ecosystems, Server-Side JavaScript in MarkLogic is much less of a context switch than XQuery. The require model makes code organisation intuitive: search helpers, validation helpers, response formatters, and Optic wrappers all feel like familiar patterns.

Version Note

This article is written against MarkLogic 11.3 because that is the LTS release.

However, MarkLogic 12 introduces many mind-altering advances worth exploring. We cover those in Migrating Server-Side JavaScript to MarkLogic 12.

Async Patterns — Useful at the Boundary

JavaScript as a language is better aligned with the async patterns most web teams know. Note that MarkLogic Server-Side APIs are not Promise-based in the same way Node developers may expect — you still reason primarily in terms of transactions, invokes, and request boundaries. The advantage is that JavaScript teams can express orchestration code, callback-free control flow, and module interactions in a familiar style. That is a human advantage more than a runtime one.

When FLWOR Becomes Less Relevant

In JavaScript-first service code, there are many cases where the data has already been materialised as rows or objects. At that point, FLWOR-style shaping is usually replaced by ordinary array pipelines (filter, map, sort, slice) plus straightforward object construction.

The point is not that FLWOR is weak. The point is that when the application boundary is already JSON-oriented, a JavaScript pipeline often becomes the more natural and maintainable expression.

'use strict';

// In many JS service layers, row-shaped data is already available as objects.
// At that point, array pipelines can express the transformation directly.
const llamaRows = [
  { name: 'Aaron', species: 'suri', active: true, score: 92 },
  { name: 'Bradley', species: 'huacaya', active: true, score: 88 },
  { name: 'Chloe', species: 'suri', active: false, score: 97 },
  { name: 'Daphne', species: 'huacaya', active: true, score: 95 },
  { name: 'Elliot', species: 'suri', active: true, score: 84 }
];

const topActive = llamaRows
  .filter((row) => row.active)
  .sort((a, b) => b.score - a.score)
  .slice(0, 3)
  .map((row) => ({
    llamaName: row.name,
    species: row.species,
    score: row.score
  }));

const payload = {
  generatedBy: 'javascript-array-pipeline',
  results: topActive
};

payload;
{
  "generatedBy": "javascript-array-pipeline",
  "results": [
    {
      "llamaName": "Daphne",
      "species": "huacaya",
      "score": 95
    },
    {
      "llamaName": "Aaron",
      "species": "suri",
      "score": 92
    },
    {
      "llamaName": "Bradley",
      "species": "huacaya",
      "score": 88
    }
  ]
}

This is where FLWOR becomes less central to day-to-day coding decisions: you are no longer shaping XML-first document flows, you are shaping service-level objects.

Feature Availability in MarkLogic 11

The biggest mistake here is vague language. "Both languages support MarkLogic" is true, but it is too vague to guide implementation decisions that have to hold up under pressure.

CapabilityJavaScriptXQueryNotes
Core document search APIsYesYescts.search and cts:search are equivalent capabilities
Basic document updatesYesYesBoth can insert, replace, delete, patch content
Cross-language module invocationYesYesJS uses xdmp.invoke; XQuery uses xdmp:invoke and xdmp:javascript-eval
Native XPath step expressionsLimitedYesXQuery is the natural language for deep XML navigation
FLWOR expressionsNoYesNo JavaScript equivalent with the same expressive density
Direct XML element constructorsNoYesJS can build XML, but not with XQuery constructor syntax
JSON object/array ergonomicsYesGoodXQuery supports JSON nodes, but JS is more natural
require module systemYesNoXQuery uses module imports, not CommonJS-style require
Optic query readingYesYesAvailable in both languages for read scenarios
XML schema validationYesYesMore natural in XQuery workflows
declareUpdate() transaction switchYesNoJavaScript-specific transaction declaration

"Available" is not the same as "equally pleasant." XML schema validation is available in both languages and is still usually cleaner in XQuery-centric workflows.

Interoperability: Using Both Without Losing Your Mind

MarkLogic lets you bridge between the two languages. That is a major advantage — the platform does not force a single-language religion. You can call XQuery from JavaScript and JavaScript from XQuery, using each language where it is strongest while keeping the broader application coherent. The key is to bridge intentionally. Do not bounce between languages casually inside routine request paths. Every bridge adds one more place to debug parameters, transaction options, error propagation, and module resolution.

JavaScript Calling XQuery via xdmp.invoke

This is the most common bridge when a JavaScript application needs an XQuery module for XML-heavy work. The module extension determines which language MarkLogic executes.

xquery version "1.0-ml";

(: This is the XQuery module stored at /lib/llama-name-from-xml.xqy           :)
(: It accepts a document URI and returns an XML element with the llama name.  :)
declare variable $uri as xs:string external;

let $doc := fn:doc($uri)
let $name :=
	if ($doc/node() instance of object-node())
	then fn:string(map:get($doc/node(), "name"))
	else fn:string(($doc//name)[1])
return <llama-name>{ $name }</llama-name>
'use strict';

// Call a stored XQuery module from JavaScript via xdmp.invoke.
// MarkLogic determines the language from the .xqy extension.
const result = xdmp.invoke(
  '/lib/llama-name-from-xml.xqy',
  { uri: '/cleverllamas/llamaverse/raw/wild-llamas/llamas/0c8bdb0d-ac62-49b7-ac74-94dbba46efa5.json' },
  { update: 'false' }
);

result;
<llama-name>Aaron</llama-name>

JavaScript Evaluating Inline XQuery with xdmp.xqueryEval

Useful for small, contained bridges — diagnostics, tactical XML transforms, or migration utilities. It is not how you should build a large codebase.

'use strict';

// Evaluate inline XQuery from JavaScript — useful for narrow utility cases,
// diagnostics, or when a JS codebase needs a single XML construction.
xdmp.xqueryEval(
  'xquery version "1.0-ml"; declare variable $name external; <featured-llama>{$name}</featured-llama>',
  { name: 'Sparkle' },
  { update: 'false' }
);

XQuery Calling JavaScript via xdmp:javascript-eval

The usual bridge when an XQuery-heavy codebase needs a JavaScript implementation for a specific module or integration point.

xquery version "1.0-ml";

(: Call inline JavaScript from XQuery via xdmp:javascript-eval.               :)
(: Useful when an XQuery codebase needs a JavaScript-only API.               :)
xdmp:javascript-eval(
  'const llama = {name: "Aurora", verified: true}; llama;',
  (),
  <options xmlns="xdmp:eval">
    <update>false</update>
  </options>
)

Interoperability Rules

Use module invocation (xdmp.invoke) for stable logic. Use inline eval mostly for utility cases, diagnostics, or migration scripts. Pass data in simple shapes. If you find yourself passing large, deeply structured values repeatedly across the bridge, you are probably compensating for the wrong language choice upstream.

Be explicit about update, commit, and isolation options when transaction semantics matter. xdmp.invoke and the eval family default to different-transaction isolation unless you override it. That matters enormously when reasoning about consistency.

Performance Considerations

Performance discussions often go off the rails because teams compare syntax speed instead of workload shape. The dominant performance factor is rarely "JavaScript versus XQuery" in the abstract. It is whether your code shape actually matches your data and transformation shape.

XQuery tends to shine when the work is dominated by XML navigation, XPath resolution, XML construction, and expression-oriented transformations. The code is denser, the compiler has direct visibility into the path-centric intent, and you allocate less scaffolding. JavaScript tends to shine when the workload is dominated by JSON object handling, application orchestration, and familiar module structure. The bigger win is usually developer efficiency rather than raw query speed — less translation in the programmer's head means fewer bugs and faster delivery.

ScenarioUsually better fitWhy
Heavy XML transformationXQueryLess ceremony, direct path navigation, natural constructors
JSON response shapingJavaScriptNative objects and arrays
Complex XPath-driven extractionXQueryNative XPath model
Utility-heavy service layerJavaScriptFamiliar module patterns and syntax
Mixed XML/JSON with one dominant formatDepends on dominant formatChoose the language that matches the dominant representation

Do not chase hypothetical microbenchmarks. Choose the language that makes dominant work straightforward. That usually gives the best combined outcome for performance, maintainability, and correctness.

Team Skills Matter More Than Most Architecture Diagrams Admit

A perfectly reasonable XQuery design can fail if the delivery team cannot maintain it. A perfectly reasonable JavaScript design can become clumsy if it is forced to do XML-heavy work all day. Language choice is partly a talent decision. That is not surrender, it is engineering realism.

If your developers are overwhelmingly JavaScript engineers and the system is mostly JSON, defaulting to JavaScript is usually correct. If your developers are experienced in XML technologies, content modelling, and XPath-heavy work, defaulting to XQuery can be a major productivity advantage. If the team is mixed, choose a dominant language and document the exceptions. A codebase with no language policy drifts, and then every module becomes a local preference contest.

Many strong MarkLogic teams use this policy:

RuleWhy it works
Default service and endpoint logic to JavaScriptEasier onboarding for app teams
Allow XQuery for XML-heavy transforms and path-heavy logicUses XQuery where it has a real advantage
Bridge intentionally through small, stable modulesLimits cross-language sprawl
Document where each language is expectedReduces guesswork and style drift

Quick Decision Guide

Use CaseRecommended LanguageReason
JSON API endpoint over document contentJavaScriptNative JSON handling and familiar service-layer code
XML transformation for publishing or interchangeXQueryCleaner path navigation and constructors
Search endpoint returning lightweight JSON summariesJavaScriptEasier response shaping
XML envelope inspection with namespace-sensitive predicatesXQueryXPath and namespace handling are much more direct
Schema validation plus XML remediationXQueryValidation and reshape workflow stays in one idiom
Mixed workflow with one XML-heavy hot spotJavaScript overall, XQuery for the hot spotBest of both without whole-app complexity
Team new to MarkLogic but strong in NodeJavaScriptFastest path to maintainable delivery
Legacy XQuery codebase with XML-centric dataXQueryLowest migration risk and strongest existing fit

Common Mistakes

The most common mistake is choosing by preference instead of workload shape. Forcing JavaScript onto xml-heavy logic usually starts with "we only want one language" and ends with verbose node handling and developers quietly avoiding those modules. Forcing XQuery onto a JSON-first service platform can leave app engineers locked out of routine delivery.

Mixing languages everywhere is also a mistake. Interoperability is a feature, not a substitute for design discipline. Cross-language bridges should solve specific problems — they should not become your default programming model.

Finally: do not ignore version-specific product behaviour. If your tested environment shows practical limitations in one API surface, design for reality instead of ideals. That is how teams avoid unnecessary wrappers and rescue work later.

Final Checklist Before You Decide

QuestionIf the answer is yesBias
Is the dominant payload JSON?Most code will manipulate objects and arraysJavaScript
Is the dominant problem XML transformation?Path-heavy logic will dominate maintenance costXQuery
Is the team mostly Node or web developers?Developer fluency will materially affect speedJavaScript
Is namespace-sensitive XPath routine?The code will be cleaner and saferXQuery
Do you only need the other language in a few hotspots?Bridge at those hotspots onlyMixed, intentionally

Answer these honestly, decide once, and document the default so your codebase stays calm under pressure.

Need Some Help?


Looking for more information on this subject or any other topic related to MarkLogic? Contact Us (info@cleverllamas.com) to find out how we can assist you with consulting or training!