Wire a Node.js / Express Backend for BuilderJS
BuilderJS is a framework-agnostic, vanilla-JavaScript drag-and-drop editor for HTML email and web/landing pages that you embed in your own app and self-host — the editor surface only, not a SaaS. The editor itself is the same client-side bundle everywhere; what makes Node.js the natural fit for many teams is the backend it talks to. BuilderJS ships reference PHP handlers for save, upload, and auth, but those are reference implementations of a small, documented contract — they port cleanly to a Node.js / Express server. This page shows exactly that: a save route that accepts the editor's JSON snapshot and an asset-upload route that signs an AWS SigV4 PUT to S3-compatible storage. The contract is tiny and HTTP-shaped, so you keep full control of persistence, auth, and storage while the browser owns the editing.
Mount it
// server.js — Express backend for BuilderJS.
// The editor is client-side (window.Builder); these routes are the
// reference PHP save + SigV4 upload handlers ported to Node.
import express from 'express';
import multer from 'multer';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { randomBytes } from 'node:crypto';
import fs from 'node:fs';
const app = express();
app.use(express.json({ limit: '8mb' }));
const upload = multer({ dest: '/tmp', limits: { fileSize: 10 * 1024 * 1024 } });
const s3 = new S3Client({
endpoint: process.env.S3_ENDPOINT || 'http://localhost:9000', // MinIO in dev
region: process.env.S3_REGION || 'us-east-1',
credentials: { accessKeyId: process.env.S3_KEY || 'minioadmin',
secretAccessKey: process.env.S3_SECRET || 'minioadmin' },
forcePathStyle: true,
});
const BUCKET = process.env.S3_BUCKET || 'builderjs-assets';
const PUBLIC = process.env.S3_PUBLIC_URL || `http://localhost:9000/${BUCKET}`;
// SAVE — receives the design (here as a hand-rolled JSON body from
// getData()). The reference PHP envelope is { status: 'success', ... };
// mirror that shape. The engine does NOT auto-send a CSRF token —
// add one yourself and validate it here before writing.
app.post('/api/builder/save', (req, res) => {
// TODO: validate your own CSRF token; persist req.body to your DB.
fs.writeFileSync('design.json', JSON.stringify(req.body));
res.json({ status: 'success', slug: 'design' });
});
// UPLOAD — SigV4 PUT to S3 (the SDK signs it); same JSON shape as the PHP handler.
app.post('/api/builder/upload', upload.single('file'), async (req, res) => {
const ext = (req.file.originalname.split('.').pop() || 'png').toLowerCase();
const d = new Date();
const key = `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}/${randomBytes(8).toString('hex')}.${ext}`;
await s3.send(new PutObjectCommand({
Bucket: BUCKET, Key: key,
Body: fs.createReadStream(req.file.path), ContentType: req.file.mimetype,
}));
res.json({ status: 'success', url: `${PUBLIC}/${key}`, key,
mime: req.file.mimetype, size: req.file.size });
});
app.listen(3000);
/* In the browser — point the mount at the Node routes:
const builder = new Builder({
mainContainer: '#MainContainer',
widgetsContainer: '#WidgetsContainer',
settingsContainer: '#SettingsContainer',
save: { url: '/api/builder/save', csrf: window.CSRF_TOKEN },
upload: { url: '/api/builder/upload', maxBytes: 10 * 1024 * 1024 },
});
builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl); // 4 args
// The built-in builder.save() posts html + data form fields to saveUrl;
// for a JSON body or a CSRF token, hand-roll the fetch with getData().
*/
The editor is client-side; Node owns persistence
BuilderJS is a DOM-mount bundle — you load builder.css and builder.js, and the script attaches a global window.Builder you instantiate in the browser. None of that changes when your backend is Node.js. The split is clean: the browser runs the editor, calls getData() for a lossless JSON snapshot, and POSTs it to your server; Express stores it. There is no Node SDK to install and no server-side render — the editor never runs under Node. What your Express app provides is two endpoints the editor calls over plain HTTP: a save URL that takes the design and an upload URL that takes a file. Everything else — auth, routing, multi-tenant isolation, which database — stays entirely yours.
The save route: accept the JSON snapshot
Persistence is plain JSON. In the browser you call builder.getData() to get the full document; to restore you feed it back to builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl) — four positional arguments in that order. The engine's built-in builder.save() POSTs html and data as form fields to your saveUrl; if you'd rather send getData() as a JSON body (the pattern shown here), hand-roll the fetch yourself. On the Node side the save route is just an Express handler: read the body, write it wherever you like (Postgres, Mongo, a file, object storage), and return a small success envelope. The reference PHP handlers reply with {"status":"success", ...} (for example {"status":"success","slug":"...","message":"Saved."}); mirror that {"status":"success"} shape in Express so the editor's save UI gets a consistent answer. CSRF is yours to add: the engine does not auto-send a token, so include one in your hand-rolled request and validate it in middleware before writing.
The upload route: port the SigV4 PHP handler to Node
This is the §9 story: the reference PHP upload handler signs an AWS SigV4 PUT and streams the file to any S3-compatible store (AWS S3, Cloudflare R2, Backblaze B2, MinIO in dev), then returns {"status":"success","url","key","mime","size"} — the url is what the canvas references as an image src. Porting it to Node is direct: accept a multipart file, build the same dated object key, and either sign the SigV4 PUT yourself or call @aws-sdk/client-s3's PutObjectCommand against the same endpoint and credentials. Keep the response JSON identical so the editor consumes it unchanged. Enforce limits server-side too — mime allowlist, max bytes, max dimensions — exactly as the PHP Validator does; the mount's upload.maxBytes is a client hint, not enforcement.
Mount the editor against your Node routes
On the page, render three containers and instantiate window.Builder client-side, pointing save and upload at your Express routes. BuilderJS is the editor only — it does not send the email or host the page it builds; your Node app brings the ESP and the hosting. Because you self-host builder.js and builder.css from your own origin, there is no external runtime dependency on a vendor service. The same vanilla engine runs identically whether the page is served by Express, a separate SPA, or a static host — only the save and upload URLs point back at Node. Guard against double-instantiation if your frontend framework re-runs effects (there is no destroy() method to undo a mount), and let unmount discard the container DOM.
Own the source with a one-time license
BuilderJS is sold under a one-time CodeCanyon license (Regular $52 / Extended $99): you get the source, self-host the bundle and your Node backend, and embed it in your own screens with no SaaS subscription, no monthly fee, no per-seat charge, no usage metering, and no phone-home. The reference PHP backends ship in the download so you have a working contract to read from — and because that contract is small and HTTP-shaped, porting save, upload, and auth to Express (or Fastify, Koa, Nest) is a short job, not a rewrite. Try the live demo at builder.emotsy.com before you build.
Frequently asked questions
-
Is there an official BuilderJS Node.js SDK or npm package?
No. BuilderJS is a self-hosted browser bundle that attaches window.Builder when builder.js loads in the page — there is no npm package and no Node SDK. Node enters the picture only as your backend: you write small Express routes (save and upload) that the editor calls over HTTP. The reference PHP handlers document that contract so you can port it to Node directly.
-
How does the editor save designs to my Express server?
In the browser you call builder.getData() for a lossless JSON snapshot. The built-in builder.save() POSTs html and data as form fields to your saveUrl; alternatively, hand-roll a fetch and POST getData() as a JSON body. Express stores it however you like and returns a small success envelope — the reference handlers reply with {"status":"success", ...} (e.g. {"status":"success","slug":"...","message":"Saved."}). To restore, call builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl) with those four positional arguments. BuilderJS never stores anything for you.
-
What does the SigV4 upload route actually do?
It receives an uploaded image, signs an AWS SigV4 PUT, and streams the file to any S3-compatible store (AWS S3, Cloudflare R2, Backblaze B2, or MinIO in dev), then returns JSON containing the public url, the object key, mime, and size. The url is what the canvas uses as the image src. The reference PHP handler does this in ~30 lines; in Node you can sign it yourself or use @aws-sdk/client-s3's PutObjectCommand and keep the same response shape.
-
Can BuilderJS send the email or host the page it builds?
No. BuilderJS is the embeddable editor only. It produces the email template markup and the page HTML and stops there — it does not send email, host or publish pages, run funnels, do CRM, or process payments. Your Node backend brings the ESP for delivery and your own hosting or CMS for publishing the exported HTML.
-
Do I have to use the reference PHP backend, or can it be all Node?
All Node is fine. The PHP handlers are reference implementations of a small HTTP contract — html plus data in for save, multipart in for upload, JSON out — not a required runtime. Porting save, upload, and auth to Express (or Fastify, Koa, Nest) is straightforward because the contract is intentionally minimal. You keep the same request and response shapes so the editor consumes your Node routes unchanged.