Embed a Drag-and-Drop Builder in Flask with Jinja2 and a JSON Route
BuilderJS is a framework-agnostic, vanilla-JavaScript drag-and-drop editor for HTML email and web pages that you self-host and embed directly inside your own Flask application. Because it ships as a compiled browser bundle that attaches a global window.Builder — not a Python package, not an npm import — wiring it into Flask is refreshingly boring: you serve builder.js and builder.css as ordinary static files, render three container divs in a Jinja2 template, mount the editor in a small client-side script, and POST the design JSON back to a regular @app.route. This page lays out that full Python loop end to end: serve the assets from Flask static, render the canvas in your template, save with builder.getData() to a POST route, persist the JSON in SQLite or Postgres, and re-hydrate the editor on the next page load with load(). Everything matches the real BuilderJS API so a Python developer can paste it and ship. One honest boundary up front: BuilderJS is the editor only. It builds and exports the markup; your Flask app owns persistence, auth, file uploads, and — for email — the sending. It does not send mail, host pages, or store designs for you.
Mount it
{# templates/editor.html — Jinja2 renders the shell; the client mounts the engine #}
<link rel="stylesheet" href="{{ url_for('static', filename='builderjs/builder.css') }}">
<div id="bjs-widgets"></div>
<div id="bjs-canvas"></div>
<div id="bjs-settings"></div>
{# Inject the saved design (or a starter template) from Flask into the page #}
<script>
window.THEME_JSON = {{ design_json|tojson }}; // from getData(), or a template
window.THEME_TEMPLATES = {{ templates_json|tojson }};
window.THEME_CONFIG_DATA = {{ config_json|tojson }};
window.MEDIA_URL = {{ media_url|tojson }};
window.SAVE_URL = "{{ url_for('save_design', design_id=design_id) }}";
</script>
{# Loading builder.js attaches window.Builder — there is NO npm/pip import #}
<script src="{{ url_for('static', filename='builderjs/builder.js') }}"></script>
<script>
const builder = new window.Builder({
mainContainer: '#bjs-canvas', // REQUIRED — CSS selector string
widgetsContainer: '#bjs-widgets', // optional — CSS selector string
settingsContainer: '#bjs-settings', // optional — CSS selector string
});
// load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl) — 4 positional args
builder.load(window.THEME_JSON, window.THEME_TEMPLATES, window.THEME_CONFIG_DATA, window.MEDIA_URL);
// Save: getData() -> POST JSON to your own Flask route
document.querySelector('#save').addEventListener('click', async () => {
await fetch(window.SAVE_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(builder.getData()),
});
});
</script>
# app.py — serve the template, persist the JSON, re-hydrate on next render
import json, sqlite3
from flask import Flask, render_template, request, jsonify
app = Flask(__name__) # static_folder='static' serves /static/builderjs/*
@app.route('/designs/<design_id>/edit')
def edit_design(design_id):
row = sqlite3.connect('app.db').execute(
'SELECT data FROM designs WHERE id = ?', (design_id,)
).fetchone()
design = json.loads(row[0]) if row else {} # {} -> fresh editor
return render_template(
'editor.html',
design_id=design_id,
design_json=design,
templates_json={}, # your starter templates map
config_json={}, # fonts/colors/sizes
media_url='/static/uploads/',
)
@app.route('/designs/<design_id>/save', methods=['POST'])
def save_design(design_id):
data = request.get_json() # the builder.getData() snapshot
conn = sqlite3.connect('app.db')
conn.execute(
'INSERT INTO designs (id, data) VALUES (?, ?) '
'ON CONFLICT(id) DO UPDATE SET data = excluded.data',
(design_id, json.dumps(data)),
)
conn.commit()
return jsonify(ok=True)
Serve /dist/builder.js + builder.css from Flask static
BuilderJS is delivered as a self-hosted browser bundle: two files, builder.js and builder.css, from the dist folder of the package you license. There is no pip install and no Python binding — Flask never imports BuilderJS, it just serves the files. Drop the two assets into your static directory (for example static/builderjs/builder.js and static/builderjs/builder.css) and reference them with the standard url_for('static', ...) helper in your template. Loading builder.js attaches a single global, window.Builder, which is the constructor your client script will call. Because the bundle lives on your own origin, the editor has no external runtime dependency on a vendor service — no CDN you don't control, no phone-home, no API key. That is the whole point of a one-time, self-hosted source license: Flask serves the bytes, the browser runs them, and nothing leaves your infrastructure. If you prefer, you can also serve the dist folder from a CDN or object storage you own; the only requirement is that the <script> and <link> resolve to files you host.
Render the three container divs in a Jinja2 template
BuilderJS is a DOM-mount editor: it queries the DOM and builds its chrome the moment you construct it, so it only runs in the browser against live elements. In Flask that means your Jinja2 template renders the static scaffolding and a small client-side <script> does the mount. Render exactly three container nodes — <div id="bjs-widgets"></div>, <div id="bjs-canvas"></div>, and <div id="bjs-settings"></div> — then, after both the DOM and builder.js have loaded, call new window.Builder({ mainContainer: '#bjs-canvas', widgetsContainer: '#bjs-widgets', settingsContainer: '#bjs-settings' }). These options are CSS selector strings, not DOM nodes: BuilderJS resolves them with document.querySelector. mainContainer is required (it resolves the canvas); widgetsContainer and settingsContainer are optional, so a selector that matches nothing simply leaves that sub-panel unmounted. Jinja2's role is purely to emit the HTML shell — you can inject server data (a saved design, a media base URL, a CSRF token) into the page via a small inline script or data attributes, then read them in the mount script. The server renders the divs; the client attaches the engine to them.
POST builder.getData() to a Flask @app.route and persist it
Saving is plain JSON over plain HTTP — exactly what Flask is good at. In the browser, call const json = builder.getData() to get a serializable snapshot of the full design tree, then fetch() it to your own route as JSON. On the server, define a route like @app.route('/designs/<id>/save', methods=['POST']) that reads request.get_json() and writes it wherever you like. The reference handlers shipped with BuilderJS are PHP, but the contract is trivial to port to Python: receive JSON, store JSON. For a small app, SQLite via the stdlib sqlite3 module is enough — store the design in a TEXT/JSON column keyed by user and document id. For something larger, a Postgres jsonb column gives you indexable, queryable design data. Either way your backend owns the schema, the keys, and the access rules; there is no usage meter, no per-seat counter, and no proprietary store you can be evicted from. Add your own auth check and CSRF protection on this route the same way you would for any other Flask form POST — BuilderJS does not impose an authentication model, your app does.
Re-hydrate the editor on next render with load()
To restore a saved design, read the stored JSON in your Flask view, pass it into the Jinja2 template, and feed it to builder.load() in the mount script. The signature is exactly four positional arguments, in this order: builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl) — the saved design/page tree JSON, the theme templates map, the theme config data (fonts, colors, sizes), and the base media URL for resolving relative asset paths. Passing the snapshot you previously captured with getData() back as the first argument reconstructs the exact editor state — the round-trip is lossless, so save → reload → keep editing with no drift. The same load() call also seeds a brand-new editor with a starter template on first paint; you just pass a template instead of a saved design. Because the data is plain JSON, you control storage entirely: render it from SQLite or Postgres, embed it in the page, or fetch it from a second JSON route after mount. BuilderJS hands you JSON and consumes JSON; the storage layer between is yours.
Editor only — Flask owns persistence, auth, uploads, and sending
It is worth being precise about the division of labor, because it is what keeps the integration clean. BuilderJS is the embeddable editor: the drag-drop canvas, the element palette, the JSON model, and the dual output that exports either modern-CSS page HTML for the browser or Outlook-safe email HTML through its EmailExportPipeline (flexbox-to-table conversion, SVG-to-PNG rasterization, @media mobile-stacking, MSO conditional comments, and CSS inlining, with templates Litmus-tested across 22 email clients). What it deliberately does not do: it does not send email, host or publish pages, manage lists, or run a CRM — and it does not persist your designs or authenticate your users. Those are your Flask app's jobs. You expose a save route, a load route, and an upload route; you check sessions and CSRF; you write to SQLite or Postgres; and when a design is finished, you hand the exported email HTML to your own ESP or SMTP, or the page HTML to your own hosting. BuilderJS ships as a one-time CodeCanyon source license (Regular $52 / Extended $99) — no SaaS, no monthly fee, no per-seat charge — so you self-host the bundle and embed it in as many of your own Flask screens as the license tier allows.
Frequently asked questions
-
Is there a pip package or Python binding for BuilderJS?
No. BuilderJS is a vanilla-JavaScript browser bundle, not a Python library. Flask never imports it — you serve builder.js and builder.css as static files and the browser loads them, which attaches the global window.Builder constructor. There is no pip install and no npm import either; the mount happens entirely in a client-side <script>, and Flask's role is to serve the assets and the Jinja2 template that holds the three container divs.
-
How do I save a design from the browser to Flask?
In the browser call const json = builder.getData() to get a JSON snapshot of the design, then fetch() it to one of your own routes (e.g. @app.route('/designs/<id>/save', methods=['POST'])). On the server read request.get_json() and store it — a TEXT/JSON column in SQLite for small apps, or a Postgres jsonb column for larger ones. BuilderJS does not store designs for you; your Flask backend owns the schema and the storage.
-
How do I re-hydrate a saved design on the next page load?
Read the stored JSON in your Flask view, pass it to the Jinja2 template, and call builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl) in the mount script with those four positional arguments in that exact order. Pass the snapshot you saved from getData() as the first argument and BuilderJS rebuilds the exact editor state. The same call also seeds a fresh editor with a starter template on first load.
-
Does BuilderJS handle file uploads, auth, or sending in Flask?
No — your Flask app does. BuilderJS is the editor only. It builds and exports the markup, but it does not authenticate users, store uploads, send email, or host pages. You provide a Flask upload route, your own session/CSRF auth on the save and load routes, your own database, and your own sending path (ESP or SMTP) for email and hosting for pages. The package includes reference PHP backends as working examples, and the same receive-JSON / store-JSON contract ports directly to Flask.
-
Can I build both HTML email and web pages in a Flask app with this?
Yes — from one editor and one JSON model. BuilderJS renders modern-CSS page HTML for the browser, and separately exports Outlook-safe email HTML through its EmailExportPipeline (tables, inlined CSS, MSO conditional comments, @media mobile-stacking, SVG-to-PNG). You choose the output mode at render time based on the destination. BuilderJS builds the markup; your Flask app brings the hosting for pages and the sending service for email.