<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Cloudmerce Engineering]]></title><description><![CDATA[Cloudmerce Engineering]]></description><link>https://cloudmerce.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 01 Nov 2026 01:17:33 GMT</lastBuildDate><atom:link href="https://cloudmerce.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Trace a Nuxt 3 Nitro server with OpenTelemetry and OTLP]]></title><description><![CDATA[An API response tells you whether a request succeeded. A trace can also show which operation failed and how that operation relates to the incoming request…]]></description><link>https://cloudmerce.hashnode.dev/trace-a-nuxt-3-nitro-server-with-opentelemetry-and-otlp</link><guid isPermaLink="true">https://cloudmerce.hashnode.dev/trace-a-nuxt-3-nitro-server-with-opentelemetry-and-otlp</guid><category><![CDATA[Nuxt]]></category><category><![CDATA[OpenTelemetry]]></category><category><![CDATA[vue]]></category><category><![CDATA[observability]]></category><dc:creator><![CDATA[Cloudmerce Agency]]></dc:creator><pubDate>Mon, 26 Oct 2026 19:25:20 GMT</pubDate><content:encoded><![CDATA[<p>An API response tells you whether a request succeeded. A trace can also show which operation failed and how that operation relates to the incoming request. The useful part is the relationship between spans: without correct context, a catalog lookup becomes an isolated event instead of a child of the request that triggered it.</p>
<p>In this tutorial, we will add explicit tracing to a Nuxt 3 API running under Nitro's Node server preset. We will preload the OpenTelemetry SDK, create a server span and a lookup span, and export them over OTLP/HTTP. A local receiver will verify the actual payload, including an incoming parent context, concurrent requests, errors, and the final shutdown flush.</p>
<p>The sample is a standalone Cloudmerce example with synthetic catalog data. It requires no hosted observability account. SigNoz and Jaeger are possible destinations later, but this exercise does not deploy either backend or claim to have verified a vendor dashboard.</p>
<h2>Run the verified project</h2>
<p>The project is a small Nuxt 3 app (<code>nuxt-otel</code>) built from the files shown in this tutorial. From its root directory:</p>
<pre><code class="language-bash">npm ci
npm run build
npm run check
</code></pre>
<p>The verified environment used Node 22.23.3, Nuxt 3.21.11, Nitro 2.13.5, OpenTelemetry API 1.9.0, SDK Node 0.208.0, and the matching 0.208.0 OTLP HTTP exporter. Resources are supplied by <code>@opentelemetry/resources</code> 2.2.0. OpenTelemetry package families do not all share one version number; use the included lockfile to reproduce this combination.</p>
<p>The sample's check starts a loopback receiver on a free port and the built Nuxt server on port 3100. It sends requests, stops the server gracefully, and checks the exported data. Keep that application port free while running it.</p>
<p>Our instrumentation covers <code>/api/catalog</code> explicitly. It does not automatically trace every Nuxt route, browser navigation, or dependency. Starting with one route makes it possible to verify context and export behavior before introducing wider instrumentation.</p>
<h2>Keep this application on Node</h2>
<p>The relevant Nitro configuration is:</p>
<pre><code class="language-ts">nitro: {
  preset: 'node-server',
  externals: { external: ['@opentelemetry/api'] }
}
</code></pre>
<p>This sample uses the Node SDK and its asynchronous context management. It is a separate deployment target from a Cloudflare Worker. Do not assume that changing the Nitro preset preserves the same SDK initialization and process lifecycle.</p>
<p>The API package is used by application code to request tracers and create spans. The SDK configures the provider that records and exports those spans. Without an initialized provider, application calls can succeed while producing no useful telemetry.</p>
<p>We initialize the SDK outside the Nitro bundle and before loading the built server. That also leaves a clear place to add Node library instrumentation later. If you introduce automatic instrumentation, review its loading requirements and verify that the libraries it patches are loaded after initialization.</p>
<h2>Preload the SDK before the application</h2>
<p>Create <code>instrumentation.mjs</code> at the project root:</p>
<pre><code class="language-js">import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { resourceFromAttributes } from '@opentelemetry/resources';

const sdk = new NodeSDK({
  resource: resourceFromAttributes({
    'service.name': process.env.OTEL_SERVICE_NAME || 'cloudmerce-nuxt-catalog'
  }),
  traceExporter: new OTLPTraceExporter()
});
sdk.start();
globalThis.__cloudmerceTelemetryShutdown = () =&gt; sdk.shutdown();
</code></pre>
<p>The resource identifies the service producing the telemetry. A stable service name helps group traces across process restarts. Do not put a request identifier into <code>service.name</code>; that would turn each request into a separate apparent service.</p>
<p><code>NodeSDK.start()</code> registers the SDK components. Its shutdown method returns a promise, which we will await during server termination. These lifecycle methods are documented in the <a href="https://open-telemetry.github.io/opentelemetry-js/classes/_opentelemetry_sdk-node.NodeSDK.html">NodeSDK API</a>.</p>
<p>The package's start script uses Node's preload option:</p>
<pre><code class="language-bash">node --import ./instrumentation.mjs .output/server/index.mjs
</code></pre>
<p>Deploy the preload file and its production dependencies alongside the built output if you package this application elsewhere. Copying only <code>.output</code> would omit the SDK setup used here. Running the ordinary output entry point without the preload also bypasses tracing initialization.</p>
<h2>Create a server span with an incoming parent</h2>
<p>The route imports the API and obtains a named tracer:</p>
<pre><code class="language-ts">import {
  context, propagation, SpanKind, SpanStatusCode, trace
} from '@opentelemetry/api'

const tracer = trace.getTracer('cloudmerce.catalog', '1.0.0')
</code></pre>
<p>At the start of the handler, extract context from the incoming headers:</p>
<pre><code class="language-ts">const parent = propagation.extract(
  context.active(), getRequestHeaders(event)
)
return tracer.startActiveSpan('GET /api/catalog', {
  kind: SpanKind.SERVER,
  attributes: {
    'http.request.method': 'GET',
    'http.route': '/api/catalog'
  }
}, parent, async (span) =&gt; {
  // The operation and span cleanup go here.
})
</code></pre>
<p>This fragment shows the request boundary; the complete executable handler is in <code>server/api/catalog.get.ts</code>. When a valid sampled <code>traceparent</code> header arrives, the server span continues its trace. Without a valid incoming context, it can begin a new trace.</p>
<p>Using a route template as the span name keeps the set of names bounded. A raw URL can contain identifiers, search terms, or tokens and can create a different name for every request. The example also avoids recording request headers and query values as attributes.</p>
<p><code>startActiveSpan</code> establishes the span as active while its callback executes. That matters when the callback crosses an <code>await</code>: a nested operation should still see the correct parent. OpenTelemetry's <a href="https://opentelemetry.io/docs/languages/js/instrumentation/">JavaScript instrumentation guide</a> explains active spans and manual instrumentation.</p>
<p>If HTTP automatic instrumentation already creates a server span in your application, adapt this boundary to avoid duplicate server spans. You may only need an internal catalog operation under the existing active span. Verify the exported tree after making that change.</p>
<h2>Trace the operation and its failure</h2>
<p>Inside the request callback, the sample creates a child operation:</p>
<pre><code class="language-ts">const items = await tracer.startActiveSpan('catalog.lookup', async (lookup) =&gt; {
  try {
    if (getQuery(event).fail === '1') {
      throw new Error('Synthetic catalog failure')
    }
    await new Promise(resolve =&gt; setTimeout(resolve, 5))
    return [{ sku: 'demo-book', available: true }]
  } catch (error) {
    lookup.recordException(error as Error)
    lookup.setStatus({
      code: SpanStatusCode.ERROR,
      message: 'Catalog lookup failed'
    })
    throw error
  } finally {
    lookup.end()
  }
})
</code></pre>
<p>The short timer represents asynchronous work; it is not a measured database latency. The explicit failure switch makes the error path reproducible without relying on an external outage.</p>
<p>Recording an exception and setting error status serve different purposes. The exception adds an event describing the failure, while the status marks the operation as failed. Ending the span in <code>finally</code> ensures both success and failure complete the span.</p>
<p>The outer callback records the HTTP response status, marks a failed request as an error, and returns a controlled 500 response through Nuxt's error helper. It ends the server span in its own <code>finally</code> block. The client receives a generic error while the lookup span contains the synthetic exception event.</p>
<p>In a real operation, review exception messages before exporting them. Database errors and upstream responses can contain sensitive fields. A tracing backend is another destination for application data, so choose attributes and events intentionally.</p>
<h2>Configure OTLP without choosing a vendor</h2>
<p>The selected <code>exporter-trace-otlp-http</code> package sends OTLP encoded as JSON over HTTP in this pinned version. The local check asserts its content type and decodes its payload. OTLP also supports HTTP/protobuf and gRPC; an HTTP port alone does not tell you which encoding the receiver accepts.</p>
<p>To inspect the demonstration manually, start the included receiver in one terminal:</p>
<pre><code class="language-bash">npm run receiver
</code></pre>
<p>In another terminal in the same directory, run:</p>
<pre><code class="language-bash">OTEL_SERVICE_NAME=cloudmerce-nuxt-catalog \
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://127.0.0.1:4318/v1/traces \
OTEL_TRACES_SAMPLER=always_on \
OTEL_METRICS_EXPORTER=none \
OTEL_LOGS_EXPORTER=none \
npm start
</code></pre>
<p>Then request the two paths:</p>
<pre><code class="language-bash">curl http://127.0.0.1:3000/api/catalog
curl 'http://127.0.0.1:3000/api/catalog?fail=1'
</code></pre>
<p>The receiver prints a span count rather than full telemetry. It is a loopback demonstration, with no storage, authentication, retry queue, or query interface. Stop both processes when finished. Export is batched, so receiving spans need not coincide immediately with the HTTP response.</p>
<p>The signal-specific <code>OTEL_EXPORTER_OTLP_TRACES_ENDPOINT</code> is a complete URL including <code>/v1/traces</code>. The generic <code>OTEL_EXPORTER_OTLP_ENDPOINT</code> has different path-appending behavior for HTTP. Check the <a href="https://opentelemetry.io/docs/specs/otel/protocol/exporter/">OTLP exporter configuration specification</a> rather than mixing the two forms.</p>
<p>For another backend, change the endpoint and provide any required authorization through secure environment configuration. A Collector can receive the app's OTLP and forward it to a destination, centralizing authentication and transport choices. Confirm that the receiver accepts this exporter's encoding; select a different exporter when it requires another protocol.</p>
<p><a href="https://www.jaegertracing.io/docs/latest/architecture/apis/">Jaeger's API documentation</a> lists OTLP write endpoints, including HTTP on port 4318. <a href="https://signoz.io/docs/ingestion/self-hosted/overview/">SigNoz</a> is another OpenTelemetry-based backend option. Neither product's web interface URL should be substituted for its ingestion endpoint. Backend installation and UI validation are outside this sample's verified scope.</p>
<h2>Flush spans during Nitro shutdown</h2>
<p>A batch exporter can still hold completed spans when the application stops. The preload exposes a shutdown callback, and <code>server/plugins/telemetry.ts</code> registers it with Nitro:</p>
<pre><code class="language-ts">export default defineNitroPlugin((nitroApp) =&gt; {
  nitroApp.hooks.hook('close', async () =&gt; {
    await (globalThis as typeof globalThis &amp; {
      __cloudmerceTelemetryShutdown?: () =&gt; Promise&lt;void&gt;
    }).__cloudmerceTelemetryShutdown?.()
  })
})
</code></pre>
<p>The optional callback permits ordinary uninstrumented development. The integration check separately proves that the preloaded production path actually exports spans, so optional access cannot hide a broken SDK setup there. Nitro documents its asynchronous close hook in the <a href="https://v2.nitro.build/guide/plugins">plugin guide</a>.</p>
<p>Use a graceful termination signal and allow sufficient shutdown time in the process manager. An immediate forced kill cannot run this hook. Exporter shutdown also cannot guarantee delivery during a network outage; it only gives pending work an opportunity to finish within its configured limits.</p>
<h2>Verify the relationships, not just span counts</h2>
<p>The automated check sends a successful request with a known remote parent and a concurrent failing request without that parent. It verifies four spans, the service resource, the server span kind, the incoming parent ID, the successful lookup's parent ID, and two error statuses isolated to the other trace. It also checks for the lookup exception event.</p>
<p>The check sets the scheduled batch delay to 60 seconds, then terminates Nitro immediately after the requests. Receiving the expected spans during graceful shutdown exercises the flush path rather than simply waiting for periodic export. These are correctness assertions, not latency or overhead benchmarks.</p>
<p>If the check passes but another environment shows no traces, first confirm that its launch command includes the preload. Then check the service name, endpoint path, receiver protocol, and sampling configuration. A healthy application response says nothing about exporter connectivity. Inspect bounded diagnostics without printing authorization headers or complete request payloads.</p>
<p>For production, choose sampling intentionally. <code>always_on</code> is useful for this tiny deterministic demonstration, but exporting every request can increase network traffic and backend storage. A parent-based sampling policy can preserve an upstream decision while controlling traces that originate in this service. Validate that policy with both sampled and unsampled incoming contexts before relying on trace counts during an incident.</p>
<p>Measure overhead against a representative workload if it matters to your deployment. The synthetic timer and four-span correctness check provide no basis for claiming a particular latency cost, throughput improvement, or storage reduction. Keep operational measurements separate from the evidence that spans have the right parents and reach an OTLP receiver.</p>
<h2>What we learned</h2>
<p>We initialized tracing before the built Node server, connected explicit request and operation spans, and exported real OTLP payloads locally. We verified parent propagation, concurrent context isolation, error recording, and shutdown delivery. That gives us a tested foundation for choosing an ingestion backend and expanding instrumentation deliberately.</p>
]]></content:encoded></item><item><title><![CDATA[Migrate a small MongoDB collection to Cloudflare D1 with Node.js]]></title><description><![CDATA[Define a strict schema mapping, stream MongoDB documents into bounded SQL batches, and verify a repeatable import with local D1.]]></description><link>https://cloudmerce.hashnode.dev/migrate-a-small-mongodb-collection-to-cloudflare-d1-with-node-js</link><guid isPermaLink="true">https://cloudmerce.hashnode.dev/migrate-a-small-mongodb-collection-to-cloudflare-d1-with-node-js</guid><category><![CDATA[Node.js]]></category><category><![CDATA[MongoDB]]></category><category><![CDATA[cloudflare]]></category><category><![CDATA[database]]></category><dc:creator><![CDATA[Cloudmerce Agency]]></dc:creator><pubDate>Mon, 26 Oct 2026 13:23:28 GMT</pubDate><content:encoded><![CDATA[<p>Moving a collection from MongoDB to Cloudflare D1 involves decisions that a format converter cannot make for you. MongoDB documents can contain BSON types, nested structures, and inconsistent fields. D1 expects a relational schema and SQLite-compatible values. A successful import needs an explicit agreement between those two models.</p>
<p>We will migrate a small customer collection using a Node.js exporter and Wrangler's local D1 runtime. The exporter streams documents, validates their shape, and writes bounded SQL statements. Replaying those statements updates existing rows by their original identifiers. The sample uses synthetic fixtures; no live MongoDB server or remote D1 database was used during verification.</p>
<p>This is a practical pattern for a modest, controlled migration in a stack like Cloudmerce's. It is not a change-data-capture system, an arbitrary BSON converter, or a promise that moving databases improves application performance. Those are separate engineering questions.</p>
<h2>Run the complete example first</h2>
<p>The project lives in <code>code/mongo-to-d1</code>. From the articles directory:</p>
<pre><code class="language-bash">cd code/mongo-to-d1
npm ci
npm run export:fixture
npm run check
</code></pre>
<p>Verification used Node 22.23.3, MongoDB's Node driver 6.21.0, and Wrangler 4.90.0. The package lockfile fixes the dependency tree. This project is plain JavaScript, so there is no compilation step: running the exporter and the integration check exercises the executable code.</p>
<p>The fixture export writes <code>generated/export/manifest.json</code> and numbered SQL files. It requires an empty output directory. To repeat the export, choose a fresh destination:</p>
<pre><code class="language-bash">OUTPUT_DIR=generated/second-export npm run export:fixture
</code></pre>
<p>The check uses a separate temporary directory and its own local D1 state. It creates the schema, imports two batches, imports them again, and compares every stored value with the expected mapping. It also exercises invalid documents and an oversized row. It cleans up its temporary state afterward.</p>
<h2>Decide what each field means</h2>
<p>Our source contract is intentionally narrow:</p>
<table>
<thead>
<tr>
<th>MongoDB field</th>
<th>D1 column</th>
<th>Mapping</th>
</tr>
</thead>
<tbody><tr>
<td><code>_id</code> as ObjectId</td>
<td><code>id</code></td>
<td>Lowercase hexadecimal text</td>
</tr>
<tr>
<td><code>email</code> as nonempty string</td>
<td><code>email</code></td>
<td>Preserve the original string</td>
</tr>
<tr>
<td><code>active</code> as boolean</td>
<td><code>active</code></td>
<td>Integer zero or one</td>
</tr>
<tr>
<td><code>createdAt</code> as Date</td>
<td><code>created_at</code></td>
<td>UTC ISO timestamp text</td>
</tr>
<tr>
<td><code>tags</code> as string array</td>
<td><code>tags_json</code></td>
<td>JSON text</td>
</tr>
<tr>
<td><code>profile</code> as flat string object</td>
<td><code>profile_json</code></td>
<td>JSON text, or SQL NULL</td>
</tr>
</tbody></table>
<p>Preserving <code>_id</code> keeps existing application references meaningful. Replacing it with an unrelated autoincrement key would require another mapping table and changes wherever those references are used.</p>
<p>We keep arrays as JSON because this example reads an entire customer record. If the application frequently filters by individual tags, consider a separate customer-tags table and suitable indexes. The schema should follow the destination access patterns rather than mechanically reproducing every document shape.</p>
<p>The sample treats a missing profile and an explicit null profile identically. It also projects away fields outside the contract. Both choices are data decisions: review them before exporting real records. An audit should identify any fields or distinctions that would be lost.</p>
<h2>Create a schema with useful constraints</h2>
<p>The accompanying <code>schema.sql</code> contains:</p>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS customers (
  id TEXT PRIMARY KEY NOT NULL,
  email TEXT NOT NULL,
  active INTEGER NOT NULL CHECK (active IN (0, 1)),
  created_at TEXT NOT NULL,
  tags_json TEXT NOT NULL CHECK (json_valid(tags_json)),
  profile_json TEXT CHECK (profile_json IS NULL OR json_valid(profile_json))
);
CREATE INDEX IF NOT EXISTS customers_email ON customers(email);
</code></pre>
<p>The email index supports lookups without imposing uniqueness that the source contract never guaranteed. If your product requires unique email addresses, first measure and resolve duplicates in the source, then make that constraint explicit.</p>
<p>SQLite's type system differs from MongoDB's BSON model. Here the exporter checks application types, while SQL constraints provide another boundary for required values, booleans, and valid JSON. The timestamp column's text declaration does not by itself validate a timestamp; that guarantee comes from the mapping function.</p>
<p><code>CREATE TABLE IF NOT EXISTS</code> makes initial setup repeatable, but does not migrate an existing, incompatible table. For a real application, put schema changes under versioned migrations and test upgrades against a copy of the existing database.</p>
<p>The <a href="https://developers.cloudflare.com/d1/best-practices/import-export-data/">D1 import guide</a> requires SQLite-compatible SQL rather than a raw SQLite database file. It also warns about transaction wrappers in imported dumps. Our generated files contain insert statements without explicit <code>BEGIN</code> or <code>COMMIT</code> wrappers.</p>
<h2>Validate before converting</h2>
<p>The full <code>mapCustomer</code> function is in <code>export.mjs</code>. Its most important property is that it rejects unexpected types instead of silently coercing them. For example:</p>
<pre><code class="language-js">if (typeof doc.active !== 'boolean') {
  throw new Error('Expected active boolean');
}
if (!(doc.createdAt instanceof Date) ||
    !Number.isFinite(doc.createdAt.getTime())) {
  throw new Error('Expected valid createdAt Date');
}
</code></pre>
<p>Using <code>Boolean(doc.active)</code> would turn the string <code>"false"</code> into true. Accepting an invalid date would defer the problem to serialization or create inconsistent output. Rejecting either record makes the migration's assumptions visible.</p>
<p>The mapping produces this ordered row:</p>
<pre><code class="language-js">return [
  doc._id.toHexString(),
  doc.email,
  doc.active ? 1 : 0,
  doc.createdAt.toISOString(),
  JSON.stringify(doc.tags),
  doc.profile == null ? null : JSON.stringify(doc.profile)
];
</code></pre>
<p>The executable version also validates ObjectIds, emails, tags, and the flat profile shape. A nested object inside <code>profile</code> is rejected. That prevents an unexpected BSON value from being flattened into a misleading string.</p>
<p>If your source includes Decimal128, binary data, or integers outside JavaScript's safe range, design additional mappings before extending the exporter. MongoDB's <a href="https://www.mongodb.com/docs/drivers/node/current/data-formats/extended-json/">Extended JSON documentation</a> explains a representation that preserves BSON type information. Ordinary JSON serialization is not a general solution to BSON fidelity.</p>
<h2>Stream from MongoDB</h2>
<p>For a real source, the exporter opens a client and obtains a cursor:</p>
<pre><code class="language-js">const documents = client.db(process.env.MONGO_DB)
  .collection(process.env.MONGO_COLLECTION)
  .find({}, {
    projection: {
      _id: 1, email: 1, active: 1,
      createdAt: 1, tags: 1, profile: 1
    }
  })
  .sort({ _id: 1 })
  .batchSize(100);
</code></pre>
<p>The surrounding function consumes it with <code>for await (const doc of documents)</code>. This avoids loading the entire collection through <code>toArray()</code>. MongoDB documents asynchronous iteration in its <a href="https://www.mongodb.com/docs/drivers/node/current/crud/query/cursor/">cursor guide</a>.</p>
<p>MongoDB's cursor batch size controls document fetching. It is independent of the SQL batch size used for D1. Keeping those settings separate lets us bound destination statements without pretending that source documents have uniform sizes.</p>
<p>The live-source branch reads <code>MONGO_URI</code>, <code>MONGO_DB</code>, and <code>MONGO_COLLECTION</code> from the process environment. Supply these through your existing secure environment mechanism, using a source account with read-only access. Run <code>node export.mjs</code> without <code>--fixture</code>. The script does not print the URI or raw driver errors, since those can expose connection details.</p>
<p>The live connection path is included for adaptation but was not verified against a running MongoDB instance. The fixture does use real driver ObjectId objects and JavaScript Dates, so the mapping runs against the intended types.</p>
<h2>Bound inserts by rows and bytes</h2>
<p>Each SQL file contains one multi-row insert followed by an upsert clause. The exporter flushes after at most 50 rows or before a statement would exceed 80,000 UTF-8 bytes. Those are conservative sample settings, not throughput recommendations.</p>
<p>D1's <a href="https://developers.cloudflare.com/d1/platform/limits/">documented limits</a> include a 100,000-byte SQL statement maximum and 100 bound parameters per query at the time of writing. This sample emits literal SQL rather than placeholders, so statement size is the relevant batching constraint. If you switch to prepared statements with six bound values per row, recalculate the row limit against the parameter budget.</p>
<p>Variable-length values make row counts alone unreliable. The exporter includes the insert prefix, separators, and upsert suffix in its byte accounting. It rejects a single row that cannot fit, rather than emitting a statement that the destination will reject later.</p>
<p>To serialize text safely into these generated files, it uses UTF-8 hex literals cast back to text:</p>
<pre><code class="language-js">function sql(value) {
  if (value === null) return 'NULL';
  if (typeof value === 'number') return String(value);
  return `CAST(X'${Buffer.from(value, 'utf8').toString('hex')}' AS TEXT)`;
}
</code></pre>
<p>Only the validated zero-or-one boolean reaches the numeric branch. Column and table names are fixed in code. Hex encoding avoids quote-escaping mistakes and preserves Unicode and embedded control characters; it is encoding, not encryption. Protect generated SQL files as carefully as the source data.</p>
<p>The fixture includes an apostrophe, non-Latin text, a newline, a null character inside a tag, and a string resembling SQL injection. Exact-value assertions confirm that those values remain data after importing.</p>
<h2>Import locally and inspect the result</h2>
<p>The Wrangler configuration contains a dummy database identifier. With <code>--local</code>, it identifies local development state and does not provision a remote database. Run:</p>
<pre><code class="language-bash">npx wrangler d1 execute migration-demo --local --file schema.sql
for file in generated/export/*.sql; do
  npx wrangler d1 execute migration-demo --local --file "$file" || break
done
npx wrangler d1 execute migration-demo --local \
  --command 'SELECT id, active, created_at FROM customers ORDER BY id'
</code></pre>
<p>The loop stops after a failed batch. In an operational migration runner, record the completed filenames and return a failing exit code on error so automation cannot mistake partial progress for completion.</p>
<p>The upsert updates destination fields when <code>id</code> already exists. That makes retrying an immutable export safe for these columns, but it can overwrite edits made in D1 after the first import. It also does not remove destination rows whose source documents were deleted. Define destination ownership and deletion behavior before using this pattern beyond a rehearsal.</p>
<h2>Plan the actual cutover</h2>
<p>Sorting by <code>_id</code> produces stable ordering; it does not create a consistent snapshot while writers continue changing the collection. For a small migration, a scheduled write pause and a fresh export may be the clearest approach. Larger systems need an explicit snapshot and change-capture strategy.</p>
<p>Export files are individually finalized through a temporary-file rename, and the manifest is written only after iteration succeeds. A failure can still leave completed files from a partial export. Require the manifest, check its counts, and never import a failed export directory as though it were complete. Start the next attempt in a fresh directory.</p>
<p>Before a remote migration, provision the destination through an authorized process, replace the dummy identifier, rehearse against representative data, and define rollback. Compare source and destination counts, selected records, timestamps, and business invariants. A matching count alone cannot detect corrupted field values.</p>
<p>Also validate how the application reads the new representation. A query returns <code>tags_json</code> as text, so the destination data layer must parse it before exposing a string array. The integer <code>active</code> value should be converted deliberately when constructing an API response. Avoid letting a database migration silently change the public response contract.</p>
<p>Keep the original export and its manifest together during reconciliation, with access restricted to the migration operators. Record which export was imported into which destination and when writes resumed. If rollback remains possible, define how destination writes will be recovered first; switching the application back cannot automatically copy new D1 data into MongoDB.</p>
<h2>What we learned</h2>
<p>We made the migration contract explicit, streamed source documents through strict validation, and generated bounded, replayable SQL. Local D1 checks verified schema compatibility and exact values, including awkward strings. A production cutover still needs a consistent source, destination ownership rules, and independent reconciliation before application traffic moves.</p>
]]></content:encoded></item><item><title><![CDATA[Deploy a Nuxt 3 app to Cloudflare Workers with Nitro and Wrangler]]></title><description><![CDATA[Build a Nuxt 3 Worker, verify SSR and static assets locally, and prepare runtime variables, secrets, and a custom domain.]]></description><link>https://cloudmerce.hashnode.dev/deploy-a-nuxt-3-app-to-cloudflare-workers-with-nitro-and-wrangler</link><guid isPermaLink="true">https://cloudmerce.hashnode.dev/deploy-a-nuxt-3-app-to-cloudflare-workers-with-nitro-and-wrangler</guid><category><![CDATA[Nuxt]]></category><category><![CDATA[cloudflare]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Cloudmerce Agency]]></dc:creator><pubDate>Sun, 25 Oct 2026 13:11:59 GMT</pubDate><content:encoded><![CDATA[<p>By Cloudmerce</p>
<p>A Nuxt application has more than one deployment target. Its Vue components run in the browser, but server rendering and API handlers need a server runtime. When that runtime is Cloudflare Workers, the build must produce a Worker entry point and make the browser assets available alongside it.</p>
<p>In this tutorial, we will build a small server-rendered page, read a runtime variable from a Nitro handler, and run the production output with Wrangler. We will also prepare the commands for a real deployment and a custom domain. The accompanying sample was built and exercised locally; account authentication, publishing, remote secrets, and DNS changes were not performed.</p>
<p>The example uses the kind of application boundary found in the Cloudmerce stack: a Nuxt frontend with a small server API. It is a standalone demonstration, with synthetic content and no production credentials or customer data.</p>
<h2>Start from the runnable sample</h2>
<p>The complete project is in <a href="code/nuxt-workers/"><code>code/nuxt-workers</code></a>. From this articles directory, run:</p>
<pre><code class="language-bash">cd code/nuxt-workers
npm ci
npm run build
npm run check
</code></pre>
<p>Use Node 22.23.3, the version used for verification, or a compatible supported Node release. The sample pins Nuxt 3.21.11 and Wrangler 4.90.0, and its lockfile records transitive dependencies. The verified build resolved Nitro 2.13.5. This is deliberately a Nuxt 3 example; copying a newer framework starter can give you a different major version and output layout.</p>
<p><code>npm run check</code> starts Wrangler locally, requests the API and rendered page, fetches the referenced client assets, and shuts the process down. For an interactive preview, use <code>npm run preview</code> and open <code>http://127.0.0.1:8787</code>. Stop that preview before running the automated check because they share a port.</p>
<p>A build only checks one part of the system. The local runtime check matters because a successful bundle can still point Wrangler at the wrong output directory, omit assets, or read a variable at the wrong time.</p>
<h2>Choose the Worker module preset</h2>
<p>The sample's <code>nuxt.config.ts</code> selects the Nitro target explicitly:</p>
<pre><code class="language-ts">export default defineNuxtConfig({
  compatibilityDate: '2026-05-01',
  devtools: { enabled: false },
  nitro: { preset: 'cloudflare_module' }
})
</code></pre>
<p>Nuxt handles the application build, while Nitro produces the server artifact for the selected platform. The <a href="https://v2.nitro.build/deploy/providers/cloudflare">Nitro 2 Cloudflare guide</a> documents <code>cloudflare_module</code> for Workers and distinguishes it from the deprecated <code>cloudflare</code> service-worker preset. It also describes generated deployment configuration; here we keep a small, explicit Wrangler file so the deployment inputs are visible.</p>
<p>After building, the important paths are <code>.output/server/index.mjs</code> for the server and <code>.output/public</code> for static files. Do not use <code>nuxt generate</code> for this exercise: we want a server handler that reads bindings when a request arrives.</p>
<p>There are two compatibility dates in the project. The Nuxt configuration guides framework output choices. The Wrangler configuration selects runtime compatibility behavior. We keep them aligned so a future change is deliberate and reviewable. Neither date upgrades the packages recorded in the lockfile.</p>
<h2>Connect the server and its assets</h2>
<p>Here is the complete <code>wrangler.jsonc</code>:</p>
<pre><code class="language-json">{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "cloudmerce-nuxt-workers-demo",
  "main": ".output/server/index.mjs",
  "compatibility_date": "2026-05-01",
  "compatibility_flags": ["nodejs_compat"],
  "assets": {
    "directory": ".output/public",
    "binding": "ASSETS"
  },
  "vars": {
    "APP_GREETING": "Hello from Cloudmerce on Workers"
  }
}
</code></pre>
<p>The entry point is relative to the project root. The <code>ASSETS</code> binding connects Nitro's asset handling to the uploaded public directory. This includes files copied from <code>public/</code> and the compiled JavaScript that hydrates the page in the browser. The sample includes a small SVG so we can verify a plain public file as well as generated assets.</p>
<p>The <code>nodejs_compat</code> flag enables the supported Node compatibility surface in Workers. It does not turn a Worker into a persistent Node server. Before bringing an existing dependency into this target, check its runtime requirements, especially filesystem assumptions, native addons, and background processes. See Cloudflare's <a href="https://developers.cloudflare.com/workers/runtime-apis/nodejs/">Node compatibility documentation</a> for the supported APIs and limitations.</p>
<p>Keep application configuration in source and build artifacts out of source control. The repository ignore file excludes <code>.nuxt</code>, <code>.output</code>, <code>node_modules</code>, and Wrangler's local state. A clean checkout should reproduce the output through <code>npm ci</code> and <code>npm run build</code>.</p>
<h2>Read bindings inside a request</h2>
<p>Create <code>server/api/health.get.ts</code>:</p>
<pre><code class="language-ts">export default defineEventHandler((event) =&gt; {
  const env = event.context.cloudflare?.env
  return {
    message: env?.APP_GREETING || 'Hello from Nuxt development',
    secretConfigured: Boolean(env?.API_TOKEN)
  }
})
</code></pre>
<p>The Cloudflare adapter places runtime bindings on the event context. Reading them inside the handler ties configuration to the current request. A module-level constant initialized during bundling would be the wrong place to resolve a deployment secret.</p>
<p>The fallback greeting makes the page usable during ordinary Nuxt development. The local Worker check asserts the configured greeting instead, so that fallback cannot quietly hide a missing binding in the verified production build.</p>
<p><code>secretConfigured</code> demonstrates access without returning the token itself. It is only a teaching aid: a real public health endpoint usually needs to reveal less about internal configuration. Never return the environment object or spread it into a response. Server execution alone does not make response data private.</p>
<p>For a larger app, you may prefer Nuxt runtime configuration with explicitly declared keys and request-scoped access. Keep private values out of <code>runtimeConfig.public</code>, which is intended for client-visible configuration. This example reads the Worker binding directly to make the platform boundary easy to inspect.</p>
<h2>Render the API result on the page</h2>
<p>The sample's <code>app.vue</code> requests that handler:</p>
<pre><code class="language-vue">&lt;script setup lang="ts"&gt;
const { data } = await useFetch('/api/health')
&lt;/script&gt;

&lt;template&gt;
  &lt;main&gt;
    &lt;h1&gt;Cloudmerce on Workers&lt;/h1&gt;
    &lt;p&gt;{{ data?.message }}&lt;/p&gt;
    &lt;img src="/mark.svg" alt="Cloudmerce demo mark" width="48" height="48"&gt;
  &lt;/main&gt;
&lt;/template&gt;
</code></pre>
<p><code>useFetch</code> integrates the request with Nuxt's rendering and payload handling. Using a relative application URL also avoids embedding a development hostname into the component.</p>
<p>The automated check looks for the configured greeting in the HTML response. This checks that the server-rendered page can reach the handler with the expected context, rather than merely proving that <code>/api/health</code> works when called directly.</p>
<p>The check also fetches client asset URLs from the HTML and confirms they return successful, non-HTML responses. A server can return a convincing page while serving an HTML fallback where JavaScript should be. Checking both paths catches that deployment mistake early. It does not replace a browser test of hydration and interaction when the application grows.</p>
<h2>Separate ordinary variables from secrets</h2>
<p>The greeting belongs in <code>vars</code> because it is harmless, reviewable configuration. Tokens belong in secret storage. For a local-only secret exercise, create an ignored <code>.dev.vars</code> file inside the sample directory with an intentionally fake value:</p>
<pre><code class="language-dotenv">API_TOKEN="local-demo-value-not-a-real-token"
</code></pre>
<p>Restart Wrangler after changing the file. Requesting <code>/api/health</code> should now report <code>secretConfigured: true</code>, while still returning no token value. Delete the file when finished. The committed sample does not require it, and automated checks do not depend on any real secret.</p>
<p>Cloudflare documents <code>.dev.vars</code> for local secrets and <code>wrangler secret put</code> for remote values in its <a href="https://developers.cloudflare.com/workers/configuration/secrets/">secrets guide</a>. A local file is not automatically copied into remote secret storage during deployment.</p>
<p>Likewise, an environment variable available to the build machine is not automatically a Worker binding. Treat build inputs and runtime inputs as separate configuration surfaces. This distinction prevents confusing situations where a project works in CI but fails after deployment because a handler cannot find its runtime credential.</p>
<p>Avoid putting secrets in package scripts, committed configuration, browser payloads, or diagnostic screenshots. When debugging, inspect whether a binding exists and whether the downstream call succeeds, without logging its contents.</p>
<h2>Prepare the deployment and domain</h2>
<p>The following commands are a future deployment procedure. They were not executed for this sample. They require an existing Cloudflare account, an appropriate authorization method, and a review of the chosen account's limits and billing configuration.</p>
<p>First choose a unique Worker name in <code>wrangler.jsonc</code>. Build the exact revision you intend to publish, then authenticate and deploy from the sample directory:</p>
<pre><code class="language-bash">npm run build
npx wrangler login
npx wrangler deploy
</code></pre>
<p>For a real server credential, use the interactive prompt rather than placing its value in the command:</p>
<pre><code class="language-bash">npx wrangler secret put API_TOKEN
</code></pre>
<p>The example handler only reports presence, so it can run before that secret exists. An application that requires a credential should validate configuration and return a controlled failure until setup is complete. Verify that the authorization context points to the intended account before any remote command.</p>
<p>For a hostname such as <code>app.example.com</code>, add this top-level field to the Wrangler configuration, replacing the example with a hostname in your own Cloudflare zone:</p>
<pre><code class="language-json">"routes": [
  { "pattern": "app.example.com", "custom_domain": true }
]
</code></pre>
<p>This is a configuration fragment to merge into the existing object. Deploying that configuration attaches the hostname. A Custom Domain makes the Worker the origin; a route pattern that proxies an existing origin serves a different purpose. Cloudflare's <a href="https://developers.cloudflare.com/workers/configuration/routing/custom-domains/">Custom Domains guide</a> explains zone prerequisites and certificate management.</p>
<p>Before attaching an existing hostname, inspect its current DNS and routing ownership. After an authorized deployment, verify the hostname over HTTPS, request the API, and inspect browser assets again. Local preview cannot validate account permissions, certificate issuance, public DNS, or behavior at Cloudflare's network edge.</p>
<h2>Diagnose failures at the right boundary</h2>
<p>If Wrangler cannot find the entry point, confirm the working directory and rebuild. If HTML loads but client scripts fail, inspect the assets directory and binding. If the greeting falls back unexpectedly, compare the runtime variable spelling with the handler and restart local preview.</p>
<p>When a package fails only under Wrangler, reproduce the issue with the production output and check whether the dependency expects an unsupported Node facility. Changing presets can make an error disappear while changing the intended deployment runtime, so keep the target explicit during investigation.</p>
<p>One failure found while preparing this sample was a compatibility date newer than the runtime bundled with the pinned Wrangler release. The application compiled, but the local Worker refused to start. Aligning both configuration dates to <code>2026-05-01</code> fixed the mismatch, and the check then passed. When updating the date later, update and test the local toolchain together rather than assuming a successful framework build proves runtime support.</p>
<p>For an existing application, extend the same check with one representative authenticated request and one expected failure response. Use synthetic credentials and fixtures. This gives you evidence about your own application's runtime assumptions before you involve public DNS or production services.</p>
<h2>What we learned</h2>
<p>We built a Nuxt 3 Worker with separate server and asset outputs, verified SSR and asset delivery locally, and read configuration from request-scoped bindings. We also prepared secret and custom-domain setup without performing remote changes. The useful deployment habit is to test the complete built artifact, then verify the account and network boundaries separately when deployment is authorized.</p>
]]></content:encoded></item></channel></rss>