Self‑Contained Diagram Generator Clone

A minimal web app that renders simple SVG diagrams from JSON specs

Welcome to another official UVF IT build guide. In the next 45 minutes you’ll spin up a tiny diagram‑generator inspired by the open‑source *diagram‑design* project, using only Node.js and SQLite on the free tier.

We’ll scaffold a tiny Express app, define a JSON‑based diagram model, serve an HTML page that turns the model into SVG, and run it locally – all without any paid APIs.

Before you start

Architecture Overview
YOUR CODEBASE / RUNTIME / DB reads/writes serves Express Server SQLite DB (store diagram JSON) HTML + SVG Renderer
1

Phase 1: Scaffold the project

Create a fresh folder, init npm, and install Express and better‑sqlite3.

mkdir diagram-clone && cd diagram-clone
npm init -y
npm install express better-sqlite3
mkdir public
cat > index.js <<'EOF'
import express from 'express';
import path from 'path';
const app = express();
app.use(express.static('public'));
app.get('/api/diagram', (req,res)=>res.json({}));
app.listen(3000,()=>console.log('Server running on http://localhost:3000'));
EOF
2

Phase 2: Data model – store diagram JSON

Initialize SQLite, create a table, and seed it with one simple diagram definition.

db.js

import Database from 'better-sqlite3';
const db = new Database('diagrams.db');
// Create table if it doesn't exist
db.exec(`CREATE TABLE IF NOT EXISTS diagrams (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  spec TEXT NOT NULL
);`);
// Seed with a basic rectangle‑and‑arrow diagram
const seed = {
  nodes: [{id:'A',type:'rect',x:50,y:50,w:100,h:50},{id:'B',type:'rect',x:250,y:50,w:100,h:50}],
  edges: [{from:'A',to:'B'}]
};
const exists = db.prepare('SELECT 1 FROM diagrams WHERE name = ?').get('example');
if (!exists) {
  db.prepare('INSERT INTO diagrams (name,spec) VALUES (?,?)')
    .run('example', JSON.stringify(seed));
}
export default db;
3

Phase 3: API & UI – serve spec and static files

In this step we expose two things: a JSON endpoint that returns the diagram specification, and the static files that make up our UI. The endpoint lets the browser pull the data it needs to draw, while Express's static middleware serves the HTML, CSS, and client‑side JavaScript we wrote earlier. Keeping the spec in a separate file makes it easy to edit without touching server code.

We’ll store the spec in a file called diagram.json at the project root. The server will read that file once at startup and return it on /diagram. All files in the public folder will be served automatically, so navigating to http://localhost:3000 loads index.html which then runs our renderer.

server.js

const express = require('express');
const path = require('path');
const fs = require('fs');

const app = express();
const PORT = process.env.PORT || 3000;

// Load diagram spec once
const specPath = path.join(__dirname, 'diagram.json');
let diagramSpec = {};
try {
  const raw = fs.readFileSync(specPath, 'utf8');
  diagramSpec = JSON.parse(raw);
} catch (e) {
  console.error('Failed to load diagram.json:', e);
}

// Serve static UI files
app.use(express.static(path.join(__dirname, 'public')));

// API endpoint returns the spec
app.get('/diagram', (req, res) => {
  res.json(diagramSpec);
});

app.listen(PORT, () => {
  console.log(`🚀 http://localhost:${PORT}`);
});
4

Phase 4: Client renderer (public/renderer.js)

The renderer runs in the browser, pulls the JSON spec from /diagram, and builds an SVG element on the page. We keep it simple: each node becomes a rectangle with a label, and each edge draws a straight line with an arrow marker. This approach shows the core idea without pulling in heavy libraries, making the clone lightweight and easy to understand.

When the page loads we call fetchDiagram() which retrieves the spec, then drawDiagram() creates SVG elements and appends them to the <svg id="canvas"> placeholder in index.html. All coordinates are taken directly from the spec, so you can edit diagram.json to move boxes or add more edges.

public/renderer.js

// public/renderer.js
async function fetchDiagram() {
  const resp = await fetch('/diagram');
  if (!resp.ok) throw new Error('Failed to load diagram spec');
  return resp.json();
}

function createArrowMarker(svg) {
  const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
  marker.setAttribute('id', 'arrow');
  marker.setAttribute('markerWidth', '10');
  marker.setAttribute('markerHeight', '7');
  marker.setAttribute('refX', '10');
  marker.setAttribute('refY', '3.5');
  marker.setAttribute('orient', 'auto');
  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  path.setAttribute('d', 'M0,0 L10,3.5 L0,7 Z');
  path.setAttribute('fill', '#000');
  marker.appendChild(path);
  svg.appendChild(marker);
}

function drawDiagram(spec) {
  const svg = document.getElementById('canvas');
  // Ensure arrow marker exists
  if (!svg.querySelector('#arrow')) createArrowMarker(svg);

  // Draw nodes
  spec.nodes.forEach(node => {
    const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
    const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
    rect.setAttribute('x', node.x);
    rect.setAttribute('y', node.y);
    rect.setAttribute('width', node.width);
    rect.setAttribute('height', node.height);
    rect.setAttribute('fill', '#eef');
    rect.setAttribute('stroke', '#333');
    g.appendChild(rect);
    const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
    text.setAttribute('x', node.x + 5);
    text.setAttribute('y', node.y + 15);
    text.textContent = node.label;
    g.appendChild(text);
    svg.appendChild(g);
  });

  // Draw edges
  spec.edges.forEach(edge => {
    const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
    const from = spec.nodes.find(n => n.id === edge.from);
    const to = spec.nodes.find(n => n.id === edge.to);
    line.setAttribute('x1', from.x + from.width);
    line.setAttribute('y1', from.y + from.height / 2);
    line.setAttribute('x2', to.x);
    line.setAttribute('y2', to.y + to.height / 2);
    line.setAttribute('stroke', '#000');
    line.setAttribute('stroke-width', '2');
    line.setAttribute('marker-end', 'url(#arrow)');
    svg.appendChild(line);
  });
}

// Kick off when DOM is ready
document.addEventListener('DOMContentLoaded', async () => {
  try {
    const spec = await fetchDiagram();
    drawDiagram(spec);
  } catch (e) {
    console.error(e);
  }
});
5

Phase 5: Run & verify

Now everything is wired together. Start the server, then open a browser to the root URL. You should see a clean page with an SVG containing two labeled rectangles linked by an arrow – the exact shape described in diagram.json. If the diagram appears, the clone is functional and ready for further tweaks.

If anything looks off, check the console for errors, verify that diagram.json is valid JSON, and make sure the static files are in the public folder. Once the basics work, you can experiment by adding more nodes or changing styles.

node server.js

Verifying the Stack

CheckCommand / Why it matters
Server starts without errorsnode server.js → console prints ‘🚀 http://localhost:3000’
Browser displays SVG diagram with two boxes and an arrowOpen http://localhost:3000 in a browser and see the rendered diagram
Endpoint returns correct JSON speccurl http://localhost:3000/diagram → JSON contains nodes and edges

What’s next?

Add more node shapes, enable drag‑and‑drop editing, or export the SVG as a file. The core is there – iterate!