A minimal clone of GitDiagram using Node, Express, and SQLite
Welcome to another official UVF IT guide! In this build you’ll create a lightweight TypeScript service that fetches a public GitHub repo’s file tree and renders a basic Mermaid diagram – inspired by the GitDiagram product.
The stack stays free‑tier: Node.js, Express, TypeScript, and a local SQLite DB. No paid AI APIs – we’ll mock the diagram generation step so you can see the flow in ~45 minutes.
Initialize a TypeScript Node project and install Express and better‑sqlite3. This gives us a clean workspace with the runtime and a tiny relational store ready for our diagram cache.
We also add a tsconfig so the TypeScript compiler knows where to emit JavaScript and which module system to target. All of this can be done with a few npm commands.
mkdir gitdiagram-clone && cd gitdiagram-clone
npm init -y
npm install express better-sqlite3
npm install -D typescript @types/node @types/express ts-node-dev
npx tsc --init --rootDir src --outDir dist --esModuleInterop true --module commonjs --target es2022Create a tiny SQLite table to cache generated Mermaid strings per repo. Storing the diagram means we don’t have to recompute it on every request and we can serve it instantly later.
The helper functions below open the database (creating it if missing), ensure the table exists, and expose get/set methods that the Express route will call.
src/db.ts
import Database from 'better-sqlite3';
// Open (or create) a local SQLite file named diagrams.db in the project root
const db = new Database('diagrams.db');
// Ensure the table exists – repo is a composite key, diagram stores Mermaid text
db.exec(`CREATE TABLE IF NOT EXISTS diagrams (
repo TEXT PRIMARY KEY,
diagram TEXT NOT NULL,
updated_at INTEGER NOT NULL
);`);
/** Retrieve a cached diagram for a repo. Returns null if not found. */
export function getDiagram(repo: string): string | null {
const row = db.prepare('SELECT diagram FROM diagrams WHERE repo = ?').get(repo);
return row ? row.diagram : null;
}
/** Store or replace a diagram for a repo. */
export function setDiagram(repo: string, diagram: string): void {
const now = Date.now();
db.prepare('INSERT INTO diagrams (repo, diagram, updated_at) VALUES (?, ?, ?)
ON CONFLICT(repo) DO UPDATE SET diagram = excluded.diagram, updated_at = excluded.updated_at')
.run(repo, diagram, now);
}
// Graceful shutdown – close DB when Node exits
process.on('SIGINT', () => { db.close(); process.exit(); });
process.on('SIGTERM', () => { db.close(); process.exit(); });In this step we add two Express routes: one that pulls a GitHub repository's file tree and turns it into a Mermaid diagram, and another that returns the already‑generated diagram so the UI can cache it. The first endpoint (/api/generate) accepts a owner and repo query, calls the GitHub REST API, builds a simple tree representation, and stores the Mermaid source in memory. The second endpoint (/api/diagram/:key) serves the cached Mermaid string, letting the front‑end request it without re‑fetching the repo each time.
src/server.ts
import express from 'express';
import fetch from 'node-fetch';
import cors from 'cors';
const app = express();
app.use(cors());
app.use(express.static('public'));
// In‑memory cache: key => mermaid diagram
const diagramCache = new Map<string, string>();
// Helper: convert GitHub tree to Mermaid syntax
function treeToMermaid(nodes: any[], parent: string = ''): string {
let lines = '';
for (const node of nodes) {
const id = parent + '/' + node.path;
lines += ` ${parent || 'root'} --> ${node.path.replace(/[^a-zA-Z0-9]/g,'_') }\n`;
if (node.type === 'tree' && node.children) {
lines += treeToMermaid(node.children, node.path);
}
}
return lines;
}
app.get('/api/generate', async (req, res) => {
const {owner, repo} = req.query as any;
if (!owner || !repo) return res.status(400).send('owner and repo required');
const url = `https://api.github.com/repos/${owner}/${repo}/git/trees/main?recursive=1`;
const response = await fetch(url);
if (!response.ok) return res.status(502).send('GitHub fetch failed');
const data = await response.json();
const mermaid = `graph TD\n${treeToMermaid(data.tree)}`;
const key = `${owner}/${repo}`;
diagramCache.set(key, mermaid);
res.json({key, mermaid});
});
app.get('/api/diagram/:owner/:repo', (req, res) => {
const key = `${req.params.owner}/${req.params.repo}`;
const diagram = diagramCache.get(key);
if (!diagram) return res.status(404).send('Diagram not found');
res.type('text/plain').send(diagram);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));The UI is a tiny static page that lets a user type a GitHub owner/repo, asks the back‑end to generate a diagram, then renders the returned Mermaid source with the Mermaid CDN. We keep the markup minimal: an input box, a button, and a <div> where Mermaid will inject the SVG. The script handles the fetch, error cases, and calls mermaid.initialize so the diagram appears instantly.
public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Repo Diagram</title>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<style>
body { font-family: Arial, sans-serif; margin: 2rem; }
#diagram { margin-top: 1rem; }
</style>
</head>
<body>
<h1>GitHub Repo Diagram</h1>
<input id="repoInput" placeholder="owner/repo" />
<button id="loadBtn">Load</button>
<pre id="error" style="color:red"></pre>
<div id="diagram"></div>
<script>
mermaid.initialize({ startOnLoad: false });
document.getElementById('loadBtn').onclick = async () => {
const [owner, repo] = document.getElementById('repoInput').value.split('/');
const errEl = document.getElementById('error');
errEl.textContent = '';
try {
const genRes = await fetch(`/api/generate?owner=${owner}&repo=${repo}`);
if (!genRes.ok) throw new Error('Generation failed');
const {key} = await genRes.json();
const diagRes = await fetch(`/api/diagram/${owner}/${repo}`);
const mermaidSrc = await diagRes.text();
document.getElementById('diagram').innerHTML = `<pre class="mermaid">${mermaidSrc}</pre>`;
mermaid.run();
} catch (e) {
errEl.textContent = e.message;
}
};
</script>
</body>
</html>Now we compile the TypeScript server, start it, and open the UI in a browser. The npm run build script uses tsc to emit JavaScript into the dist folder, then node dist/server.js launches the Express app. Visiting http://localhost:3000 shows the input page; entering a known public repo like facebook/react should produce a Mermaid diagram of its file tree.
# install dependencies (run once)
npm install express cors node-fetch@2 typescript @types/express @types/node
# compile TypeScript
npx tsc --project tsconfig.json
# start the server
node dist/server.js| Check | Command / Why it matters |
|---|---|
| Server starts without errors | node dist/server.js should log ‘Server running on http://localhost:3000’ in the console |
| UI loads in browser | Open http://localhost:3000 in Chrome; the page with input box appears |
| Diagram renders for a repo | Enter vercel/next.js and click Load – a Mermaid graph of the repo tree appears |
Your minimal GitDiagram clone is up and running. Feel free to extend it: add authentication, cache expiration, or plug in an LLM to generate richer explanations.