A minimal TypeScript‑Node.js sketch of tld’s diagram explorer
Welcome to another official UVF IT build guide. In the next 45 minutes you’ll spin up a tiny diagram explorer inspired by tld – a tool that lets you create and navigate nested maps of nodes and edges.
We’ll use Node 18, Express, SQLite, and plain TypeScript for the frontend. No paid services, just a local dev loop that you can extend into a full‑stack app.
We start by creating a fresh Node project and wiring up TypeScript so we can write modern code with type safety. Express will serve our API, while better‑sqlite3 gives us a fast, synchronous SQLite driver that works well for a tiny prototype.
All dependencies are installed locally; no global packages are required, keeping the clone self‑contained and easy to clean up later.
mkdir tld-clone && cd tld-clone
npm init -y
npm i express better-sqlite3
npm i -D typescript @types/express ts-node-dev
npx tsc --init --rootDir src --outDir dist --module commonjs --target es2022 --esModuleInterop true
mkdir srcNext we create a tiny SQLite schema that can store diagram nodes and the edges that connect them. The helper in src/db.ts abstracts opening the database and running parameterised queries, so later code can stay clean and focused on business logic.
Because better‑sqlite3 works synchronously, the wrapper can be straightforward – perfect for a minimal clone where performance isn’t a bottleneck.
src/db.ts
import Database from 'better-sqlite3';
// Initialise (or create) the local SQLite file
const db = new Database('diagram.db');
// Simple schema – run once at startup
const init = () => {
db.exec(`
CREATE TABLE IF NOT EXISTS nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT NOT NULL,
sublabel TEXT
);
CREATE TABLE IF NOT EXISTS edges (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_node INTEGER NOT NULL,
to_node INTEGER NOT NULL,
label TEXT,
FOREIGN KEY(from_node) REFERENCES nodes(id),
FOREIGN KEY(to_node) REFERENCES nodes(id)
);
`);
};
init();
// Helper to run a query and return rows (SELECT) or info (INSERT/UPDATE/DELETE)
export const run = (sql: string, params: any[] = []): any => {
const stmt = db.prepare(sql);
if (/^\s*select/i.test(sql)) {
return stmt.all(...params);
}
const info = stmt.run(...params);
return info;
};
export default db;In this step we spin up a tiny Express server that will store diagram data in memory. The server offers CRUD‑style routes for nodes and edges, but for the minimal clone we only need read (GET) endpoints that return JSON arrays. Keeping the data in a plain JavaScript object makes the code easy to understand and avoids any external database setup.
We expose /nodes and /edges routes that respond with the current collections. Later phases can add POST/PUT/DELETE handlers, but the current read‑only API is enough to feed the UI and prove the clone works.
src/server.ts
import express from 'express';
import cors from 'cors';
const app = express();
app.use(cors());
app.use(express.json());
// In‑memory store – starts empty
const store = {
nodes: [] as Array<{id:string; label:string}>,
edges: [] as Array<{from:string; to:string}>
};
// GET all nodes
app.get('/nodes', (req, res) => {
res.json(store.nodes);
});
// GET all edges
app.get('/edges', (req, res) => {
res.json(store.edges);
});
// Simple health check
app.get('/ping', (req, res) => res.send('pong'));
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`API listening on http://localhost:${PORT}`);
});Now we create a single HTML page that pulls the node and edge lists from the API and displays them as plain lists. The UI script lives in src/ui.ts; it runs in the browser, fetches JSON, and injects <li> elements into two <ul> containers.
The page is intentionally minimal – just headings and empty lists that get populated once the fetch promises resolve. This keeps the focus on wiring the front‑end to the back‑end without any heavy frameworks.
src/ui.ts
// UI logic – runs in the browser
async function loadList(url: string, ulId: string) {
const resp = await fetch(url);
const data = await resp.json();
const ul = document.getElementById(ulId) as HTMLUListElement;
ul.innerHTML = '';
data.forEach((item: any) => {
const li = document.createElement('li');
li.textContent = JSON.stringify(item);
ul.appendChild(li);
});
}
// When the page loads, fetch nodes and edges
window.addEventListener('DOMContentLoaded', () => {
const apiBase = 'http://localhost:3000';
loadList(`${apiBase}/nodes`, 'nodes-list');
loadList(`${apiBase}/edges`, 'edges-list');
});With the API and UI ready, we start the TypeScript server using ts-node and serve the static HTML page with a tiny http-server. The API runs on port 3000, while the static server runs on 8080, matching the URLs used in the checklist.
After both processes are up, open the UI page in a browser; you should see two sections titled “Nodes” and “Edges” with empty lists, confirming the end‑to‑end connection works.
# Install dev dependencies if not already present
npm install express cors ts-node typescript @types/express @types/node
# Start the API in the background
npx ts-node src/server.ts &
API_PID=$!
# Serve the UI folder (contains ui.html and compiled ui.js)
npx http-server . -p 8080 &
UI_PID=$!
# Give servers a moment to start
sleep 2
# Show the URLs for the user
echo "API running at http://localhost:3000"
echo "UI available at http://localhost:8080/ui.html"
# Clean up on exit
trap "kill $API_PID $UI_PID" EXIT| Check | Command / Why it matters |
|---|---|
| API returns empty node list | curl http://localhost:3000/nodes | jq '.' |
| API returns empty edge list | curl http://localhost:3000/edges | jq '.' |
| UI shows two sections titled 'Nodes' and 'Edges' | Open http://localhost:8080/ui.html in a browser and verify the lists are present |
You now have a working local diagram explorer. Add drag‑and‑drop, hierarchical nesting, or swap SQLite for Postgres to approach the full tld feature set.