1212 MQTT Topic Designer
Interactive Tool for Designing and Validating MQTT Topic Hierarchies
1212.1 MQTT Topic Designer
Design and validate your MQTT topic hierarchies with best practices feedback. This interactive tool helps you create well-structured, scalable, and efficient topic patterns for your IoT deployments.
This comprehensive topic design tool provides:
- Topic Pattern Input: Design topic hierarchies with variables
- Linting Rules: 12 best practice checks with detailed feedback
- Wildcard Tester: Test subscription patterns against topics
- Scale Projection: Calculate topic counts and subscription efficiency
- Visual Tree: See your topic hierarchy visualized
- Example Generator: Generate sample topics from patterns
- Enter a topic pattern using
{variable}syntax for placeholders - Review the lint results for best practice compliance
- Test wildcard subscriptions to understand matching behavior
- Configure scale parameters to project deployment size
- Export your topic design for documentation
Show code
// ============================================
// MQTT Topic Designer Tool
// Self-contained OJS implementation
// ============================================
{
// IEEE Color palette
const colors = {
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
lightGray: "#ECF0F1",
white: "#FFFFFF",
red: "#E74C3C",
green: "#27AE60",
purple: "#9B59B6",
yellow: "#F1C40F",
blue: "#3498DB",
darkGray: "#34495E"
};
// Linting rules configuration
const lintRules = [
{
id: "no-spaces",
name: "No Spaces",
description: "Topic levels should not contain spaces",
check: (topic) => !topic.includes(" "),
suggestion: "Replace spaces with underscores or hyphens",
severity: "error"
},
{
id: "lowercase",
name: "Lowercase Convention",
description: "Use lowercase for consistency and easier matching",
check: (topic) => topic === topic.toLowerCase() || topic.includes("{"),
suggestion: "Convert topic levels to lowercase",
severity: "warning"
},
{
id: "version-prefix",
name: "Version Prefix",
description: "Include version prefix (v1/, v2/) for API evolution",
check: (topic) => /^v\d+\//.test(topic),
suggestion: "Add version prefix like 'v1/' at the start",
severity: "warning"
},
{
id: "no-leading-slash",
name: "No Leading Slash",
description: "Topics should not start with a slash (creates empty first level)",
check: (topic) => !topic.startsWith("/"),
suggestion: "Remove the leading slash",
severity: "error"
},
{
id: "no-trailing-slash",
name: "No Trailing Slash",
description: "Topics should not end with a slash",
check: (topic) => !topic.endsWith("/"),
suggestion: "Remove the trailing slash",
severity: "error"
},
{
id: "no-double-slash",
name: "No Double Slashes",
description: "Avoid empty levels from consecutive slashes",
check: (topic) => !topic.includes("//"),
suggestion: "Remove duplicate slashes",
severity: "error"
},
{
id: "no-dollar-start",
name: "Avoid $ Prefix",
description: "Topics starting with $ are reserved for broker system topics",
check: (topic) => !topic.startsWith("$"),
suggestion: "Use a different starting character",
severity: "error"
},
{
id: "max-depth",
name: "Reasonable Depth",
description: "Keep topic depth between 3-7 levels for balance",
check: (topic) => {
const depth = topic.split("/").length;
return depth >= 3 && depth <= 7;
},
suggestion: "Aim for 3-7 topic levels",
severity: "warning"
},
{
id: "meaningful-names",
name: "Meaningful Names",
description: "Topic levels should be descriptive (min 2 characters)",
check: (topic) => {
const levels = topic.split("/").filter(l => !l.startsWith("{"));
return levels.every(l => l.length >= 2);
},
suggestion: "Use descriptive level names (2+ characters)",
severity: "warning"
},
{
id: "device-early",
name: "Device ID Placement",
description: "Place device identifier early for efficient wildcard filtering",
check: (topic) => {
const levels = topic.split("/");
const deviceIndex = levels.findIndex(l =>
l.includes("device") || l.includes("{device") || l.includes("{id")
);
return deviceIndex === -1 || deviceIndex <= 3;
},
suggestion: "Move device/ID identifier to first 3 levels",
severity: "info"
},
{
id: "org-structure",
name: "Organization Structure",
description: "Include organization/tenant level for multi-tenant systems",
check: (topic) => {
const levels = topic.split("/");
return levels.some(l =>
l.includes("org") || l.includes("tenant") || l.includes("company") ||
l.includes("{org") || l.includes("{tenant")
) || levels.length <= 4;
},
suggestion: "Consider adding org/tenant prefix for multi-tenant deployments",
severity: "info"
},
{
id: "data-type-suffix",
name: "Data Type Indication",
description: "Consider including data type or action in topic",
check: (topic) => {
const lastLevel = topic.split("/").pop() || "";
const dataTypes = ["data", "status", "command", "event", "telemetry", "config", "alert"];
return dataTypes.some(t => lastLevel.includes(t)) ||
lastLevel.startsWith("{") ||
topic.split("/").some(l => dataTypes.some(t => l.includes(t)));
},
suggestion: "End with data type: /data, /status, /command, /event",
severity: "info"
}
];
// State management
let topicPattern = "v1/org/{org_id}/site/{site_id}/device/{device_id}/sensor/{sensor_type}/data";
let wildcardPattern = "v1/org/+/site/+/device/+/sensor/#";
let numDevices = 1000;
let sensorsPerDevice = 5;
// Create main container
const container = d3.create("div")
.style("font-family", "system-ui, -apple-system, sans-serif")
.style("max-width", "1100px")
.style("margin", "0 auto");
// Header
const header = container.append("div")
.style("background", `linear-gradient(135deg, ${colors.navy} 0%, #1a252f 100%)`)
.style("border-radius", "12px 12px 0 0")
.style("padding", "20px")
.style("color", colors.white);
header.append("h2")
.style("margin", "0 0 10px 0")
.style("font-size", "24px")
.text("MQTT Topic Designer");
header.append("p")
.style("margin", "0")
.style("opacity", "0.9")
.style("font-size", "14px")
.text("Design scalable topic hierarchies with best practices validation");
// Input Section
const inputSection = container.append("div")
.style("background", colors.lightGray)
.style("padding", "20px")
.style("border-bottom", `1px solid ${colors.gray}`);
// Topic Pattern Input
const patternGroup = inputSection.append("div")
.style("margin-bottom", "20px");
patternGroup.append("label")
.style("display", "block")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "8px")
.text("Topic Pattern (use {variable} for placeholders):");
const patternInput = patternGroup.append("input")
.attr("type", "text")
.attr("value", topicPattern)
.style("width", "100%")
.style("padding", "12px")
.style("font-size", "16px")
.style("font-family", "monospace")
.style("border", `2px solid ${colors.teal}`)
.style("border-radius", "6px")
.style("box-sizing", "border-box")
.on("input", function() {
topicPattern = this.value;
updateAll();
});
// Two-column layout for inputs
const inputColumns = inputSection.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "20px");
// Wildcard Pattern Input
const wildcardGroup = inputColumns.append("div");
wildcardGroup.append("label")
.style("display", "block")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "8px")
.text("Subscription Pattern (+ and # wildcards):");
const wildcardInput = wildcardGroup.append("input")
.attr("type", "text")
.attr("value", wildcardPattern)
.style("width", "100%")
.style("padding", "12px")
.style("font-size", "14px")
.style("font-family", "monospace")
.style("border", `2px solid ${colors.orange}`)
.style("border-radius", "6px")
.style("box-sizing", "border-box")
.on("input", function() {
wildcardPattern = this.value;
updateWildcardResults();
});
// Scale inputs
const scaleGroup = inputColumns.append("div");
const scaleRow = scaleGroup.append("div")
.style("display", "flex")
.style("gap", "15px");
// Devices input
const devicesGroup = scaleRow.append("div")
.style("flex", "1");
devicesGroup.append("label")
.style("display", "block")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "8px")
.text("Number of Devices:");
const devicesSelect = devicesGroup.append("select")
.style("width", "100%")
.style("padding", "12px")
.style("font-size", "14px")
.style("border", `2px solid ${colors.purple}`)
.style("border-radius", "6px")
.style("background", colors.white)
.on("change", function() {
numDevices = parseInt(this.value);
updateScaleProjection();
});
[100, 500, 1000, 5000, 10000, 50000, 100000].forEach(n => {
devicesSelect.append("option")
.attr("value", n)
.attr("selected", n === numDevices ? true : null)
.text(n.toLocaleString());
});
// Sensors per device input
const sensorsGroup = scaleRow.append("div")
.style("flex", "1");
sensorsGroup.append("label")
.style("display", "block")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "8px")
.text("Sensors per Device:");
const sensorsSelect = sensorsGroup.append("select")
.style("width", "100%")
.style("padding", "12px")
.style("font-size", "14px")
.style("border", `2px solid ${colors.purple}`)
.style("border-radius", "6px")
.style("background", colors.white)
.on("change", function() {
sensorsPerDevice = parseInt(this.value);
updateScaleProjection();
});
[1, 2, 3, 5, 10, 15, 20, 50].forEach(n => {
sensorsSelect.append("option")
.attr("value", n)
.attr("selected", n === sensorsPerDevice ? true : null)
.text(n);
});
// Main content area
const mainContent = container.append("div")
.style("background", colors.white)
.style("padding", "20px");
// Three-column grid for results
const resultsGrid = mainContent.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "20px")
.style("margin-bottom", "20px");
// Lint Results Panel
const lintPanel = resultsGrid.append("div")
.style("background", colors.lightGray)
.style("border-radius", "8px")
.style("padding", "15px");
lintPanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", colors.navy)
.style("font-size", "16px")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "8px")
.html(`<span style="font-size: 20px;">🔍</span> Lint Results`);
const lintResults = lintPanel.append("div")
.attr("class", "lint-results")
.style("max-height", "400px")
.style("overflow-y", "auto");
// Wildcard Tester Panel
const wildcardPanel = resultsGrid.append("div")
.style("background", colors.lightGray)
.style("border-radius", "8px")
.style("padding", "15px");
wildcardPanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", colors.navy)
.style("font-size", "16px")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "8px")
.html(`<span style="font-size: 20px;">🎯</span> Wildcard Matching`);
const wildcardResults = wildcardPanel.append("div")
.attr("class", "wildcard-results");
// Full-width sections
const fullWidthSection = mainContent.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "20px");
// Scale Projection Panel
const scalePanel = fullWidthSection.append("div")
.style("background", colors.lightGray)
.style("border-radius", "8px")
.style("padding", "15px");
scalePanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", colors.navy)
.style("font-size", "16px")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "8px")
.html(`<span style="font-size: 20px;">📈</span> Scale Projection`);
const scaleResults = scalePanel.append("div")
.attr("class", "scale-results");
// Topic Tree Panel
const treePanel = fullWidthSection.append("div")
.style("background", colors.lightGray)
.style("border-radius", "8px")
.style("padding", "15px");
treePanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", colors.navy)
.style("font-size", "16px")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "8px")
.html(`<span style="font-size: 20px;">📌</span> Topic Hierarchy`);
const treeResults = treePanel.append("div")
.attr("class", "tree-results")
.style("font-family", "monospace")
.style("font-size", "13px");
// Example Topics Panel
const examplesPanel = mainContent.append("div")
.style("margin-top", "20px")
.style("background", colors.lightGray)
.style("border-radius", "8px")
.style("padding", "15px");
examplesPanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", colors.navy)
.style("font-size", "16px")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "8px")
.html(`<span style="font-size: 20px;">📝</span> Generated Example Topics`);
const examplesResults = examplesPanel.append("div")
.attr("class", "examples-results");
// Helper functions
function getSeverityColor(severity) {
switch (severity) {
case "error": return colors.red;
case "warning": return colors.orange;
case "info": return colors.blue;
default: return colors.gray;
}
}
function getSeverityIcon(severity) {
switch (severity) {
case "error": return "❌";
case "warning": return "⚠️";
case "info": return "ℹ️";
default: return "•";
}
}
function getPassIcon() {
return "✅";
}
function matchWildcard(pattern, topic) {
const patternParts = pattern.split("/");
const topicParts = topic.split("/");
let pi = 0, ti = 0;
while (pi < patternParts.length && ti < topicParts.length) {
const p = patternParts[pi];
if (p === "#") {
return { matches: true, reason: "# matches all remaining levels" };
} else if (p === "+") {
pi++;
ti++;
} else if (p === topicParts[ti]) {
pi++;
ti++;
} else {
return {
matches: false,
reason: `Level mismatch: '${p}' != '${topicParts[ti]}' at position ${ti + 1}`
};
}
}
if (pi === patternParts.length && ti === topicParts.length) {
return { matches: true, reason: "Exact match on all levels" };
} else if (pi < patternParts.length && patternParts[pi] === "#") {
return { matches: true, reason: "# matches remaining (empty)" };
} else if (pi < patternParts.length) {
return {
matches: false,
reason: `Topic too short: missing ${patternParts.length - pi} levels`
};
} else {
return {
matches: false,
reason: `Topic too long: ${topicParts.length - ti} extra levels`
};
}
}
function generateExampleTopics(pattern, count = 8) {
const examples = [];
const orgs = ["acme", "contoso", "globex", "initech"];
const sites = ["hq", "factory1", "warehouse-a", "office-nyc"];
const deviceIds = ["dev001", "dev002", "dev003", "sensor-5a", "gateway-01"];
const sensorTypes = ["temperature", "humidity", "pressure", "motion", "light", "co2"];
for (let i = 0; i < count; i++) {
let topic = pattern;
topic = topic.replace(/\{org_id\}/g, orgs[i % orgs.length]);
topic = topic.replace(/\{org\}/g, orgs[i % orgs.length]);
topic = topic.replace(/\{site_id\}/g, sites[i % sites.length]);
topic = topic.replace(/\{site\}/g, sites[i % sites.length]);
topic = topic.replace(/\{location\}/g, sites[i % sites.length]);
topic = topic.replace(/\{device_id\}/g, deviceIds[i % deviceIds.length]);
topic = topic.replace(/\{device\}/g, deviceIds[i % deviceIds.length]);
topic = topic.replace(/\{id\}/g, deviceIds[i % deviceIds.length]);
topic = topic.replace(/\{sensor_type\}/g, sensorTypes[i % sensorTypes.length]);
topic = topic.replace(/\{type\}/g, sensorTypes[i % sensorTypes.length]);
topic = topic.replace(/\{sensor\}/g, sensorTypes[i % sensorTypes.length]);
// Generic variable replacement
topic = topic.replace(/\{[^}]+\}/g, (match) => {
const varName = match.slice(1, -1);
return `${varName}-${i + 1}`;
});
if (!examples.includes(topic)) {
examples.push(topic);
}
}
return examples;
}
function buildTopicTree(pattern) {
const levels = pattern.split("/");
let tree = "";
levels.forEach((level, i) => {
const prefix = i === 0 ? "" : " ".repeat(i - 1) + (i === levels.length - 1 ? "\\--- " : "+--- ");
const isVariable = level.startsWith("{") && level.endsWith("}");
const displayLevel = isVariable ? `[${level}]` : level;
tree += prefix + displayLevel + "\n";
});
return tree;
}
function countVariables(pattern) {
const matches = pattern.match(/\{[^}]+\}/g);
return matches ? matches.length : 0;
}
function updateLintResults() {
lintResults.html("");
const results = lintRules.map(rule => ({
...rule,
passed: rule.check(topicPattern)
}));
const passed = results.filter(r => r.passed).length;
const total = results.length;
// Summary
const summaryColor = passed === total ? colors.green :
passed >= total * 0.7 ? colors.orange : colors.red;
lintResults.append("div")
.style("background", summaryColor)
.style("color", colors.white)
.style("padding", "10px 15px")
.style("border-radius", "6px")
.style("margin-bottom", "15px")
.style("font-weight", "bold")
.text(`Score: ${passed}/${total} rules passed (${Math.round(passed/total*100)}%)`);
// Individual results
results.forEach(result => {
const item = lintResults.append("div")
.style("padding", "10px")
.style("margin-bottom", "8px")
.style("background", result.passed ? "#e8f5e9" : "#fff3e0")
.style("border-left", `4px solid ${result.passed ? colors.green : getSeverityColor(result.severity)}`)
.style("border-radius", "4px");
item.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "8px")
.style("margin-bottom", "4px")
.html(`
<span>${result.passed ? getPassIcon() : getSeverityIcon(result.severity)}</span>
<strong style="color: ${colors.navy}">${result.name}</strong>
<span style="font-size: 11px; color: ${colors.gray}; margin-left: auto">${result.severity.toUpperCase()}</span>
`);
item.append("div")
.style("font-size", "12px")
.style("color", colors.gray)
.style("margin-left", "28px")
.text(result.description);
if (!result.passed) {
item.append("div")
.style("font-size", "12px")
.style("color", getSeverityColor(result.severity))
.style("margin-left", "28px")
.style("margin-top", "4px")
.style("font-style", "italic")
.text(`Suggestion: ${result.suggestion}`);
}
});
}
function updateWildcardResults() {
wildcardResults.html("");
// Explanation of wildcards
const legend = wildcardResults.append("div")
.style("background", colors.white)
.style("padding", "10px")
.style("border-radius", "6px")
.style("margin-bottom", "15px")
.style("font-size", "12px");
legend.append("div")
.style("margin-bottom", "5px")
.html(`<strong style="color: ${colors.orange}">+</strong> matches exactly one level`);
legend.append("div")
.html(`<strong style="color: ${colors.purple}">#</strong> matches zero or more levels (must be last)`);
// Generate example topics and test matching
const examples = generateExampleTopics(topicPattern, 6);
wildcardResults.append("div")
.style("font-weight", "bold")
.style("margin-bottom", "10px")
.style("color", colors.navy)
.text("Matching Results:");
examples.forEach(topic => {
const result = matchWildcard(wildcardPattern, topic);
const item = wildcardResults.append("div")
.style("padding", "8px")
.style("margin-bottom", "6px")
.style("background", result.matches ? "#e8f5e9" : "#ffebee")
.style("border-radius", "4px")
.style("font-size", "12px");
item.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "8px")
.html(`
<span>${result.matches ? getPassIcon() : "❌"}</span>
<code style="background: ${colors.white}; padding: 2px 6px; border-radius: 3px; font-size: 11px">${topic}</code>
`);
item.append("div")
.style("margin-left", "28px")
.style("margin-top", "4px")
.style("color", result.matches ? colors.green : colors.red)
.style("font-size", "11px")
.text(result.reason);
});
// Wildcard efficiency note
const efficiencyNote = wildcardResults.append("div")
.style("margin-top", "15px")
.style("padding", "10px")
.style("background", "#e3f2fd")
.style("border-radius", "6px")
.style("font-size", "12px");
const wildcardCount = (wildcardPattern.match(/\+/g) || []).length;
const hasMultiLevel = wildcardPattern.includes("#");
efficiencyNote.append("div")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "5px")
.text("Subscription Efficiency:");
let efficiency = "High";
let efficiencyColor = colors.green;
if (wildcardCount > 3 || hasMultiLevel) {
efficiency = "Medium";
efficiencyColor = colors.orange;
}
if (wildcardCount > 4 && hasMultiLevel) {
efficiency = "Low";
efficiencyColor = colors.red;
}
efficiencyNote.append("div")
.html(`
<span style="color: ${efficiencyColor}; font-weight: bold">${efficiency}</span> -
${wildcardCount} single-level (+) and ${hasMultiLevel ? "1 multi-level (#)" : "no multi-level (#)"} wildcards
`);
}
function updateScaleProjection() {
scaleResults.html("");
const varCount = countVariables(topicPattern);
const levels = topicPattern.split("/").length;
// Calculate projections
const totalTopics = numDevices * sensorsPerDevice;
const messagesPerHour = totalTopics * 60; // Assume 1 msg/min per topic
const messagesPerDay = messagesPerHour * 24;
// Subscription calculations
const singleDeviceSub = 1; // One sub per device
const allDevicesSub = 1; // One wildcard sub for all
// Create metrics grid
const metricsGrid = scaleResults.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "10px")
.style("margin-bottom", "15px");
const metrics = [
{ label: "Total Topics", value: totalTopics.toLocaleString(), color: colors.teal },
{ label: "Hierarchy Depth", value: `${levels} levels`, color: colors.purple },
{ label: "Variables", value: varCount, color: colors.orange },
{ label: "Messages/Hour", value: messagesPerHour.toLocaleString(), color: colors.blue }
];
metrics.forEach(m => {
const card = metricsGrid.append("div")
.style("background", colors.white)
.style("padding", "12px")
.style("border-radius", "6px")
.style("text-align", "center")
.style("border-left", `4px solid ${m.color}`);
card.append("div")
.style("font-size", "11px")
.style("color", colors.gray)
.style("text-transform", "uppercase")
.text(m.label);
card.append("div")
.style("font-size", "20px")
.style("font-weight", "bold")
.style("color", m.color)
.text(m.value);
});
// Subscription efficiency
const subEfficiency = scaleResults.append("div")
.style("background", colors.white)
.style("padding", "15px")
.style("border-radius", "6px");
subEfficiency.append("div")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "10px")
.text("Subscription Patterns Comparison:");
const patterns = [
{
name: "All Topics (explicit)",
subs: totalTopics,
overhead: "Very High",
color: colors.red
},
{
name: "Per Device (+ wildcard)",
subs: numDevices,
overhead: "Medium",
color: colors.orange
},
{
name: "All Sensors (# wildcard)",
subs: 1,
overhead: "Low",
color: colors.green
}
];
patterns.forEach(p => {
const row = subEfficiency.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("align-items", "center")
.style("padding", "8px 0")
.style("border-bottom", `1px solid ${colors.lightGray}`);
row.append("span")
.style("font-size", "13px")
.text(p.name);
row.append("div")
.style("text-align", "right")
.html(`
<span style="font-weight: bold; color: ${p.color}">${p.subs.toLocaleString()}</span>
<span style="font-size: 11px; color: ${colors.gray}"> subs</span>
<div style="font-size: 10px; color: ${p.color}">${p.overhead} overhead</div>
`);
});
// Broker recommendations
const brokerRec = scaleResults.append("div")
.style("margin-top", "15px")
.style("padding", "10px")
.style("background", "#fff8e1")
.style("border-radius", "6px")
.style("font-size", "12px");
let recommendation = "";
if (totalTopics < 10000) {
recommendation = "Single broker instance should handle this load easily.";
} else if (totalTopics < 100000) {
recommendation = "Consider clustering for high availability.";
} else {
recommendation = "Clustering with topic-based sharding recommended.";
}
brokerRec.html(`<strong>Broker Recommendation:</strong> ${recommendation}`);
}
function updateTopicTree() {
treeResults.html("");
const tree = buildTopicTree(topicPattern);
treeResults.append("pre")
.style("margin", "0")
.style("padding", "15px")
.style("background", colors.white)
.style("border-radius", "6px")
.style("overflow-x", "auto")
.style("line-height", "1.6")
.text(tree);
// Level analysis
const levels = topicPattern.split("/");
const levelInfo = treeResults.append("div")
.style("margin-top", "15px")
.style("font-size", "12px");
levelInfo.append("div")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "8px")
.text("Level Analysis:");
levels.forEach((level, i) => {
const isVariable = level.startsWith("{") && level.endsWith("}");
const row = levelInfo.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "10px")
.style("padding", "4px 0");
row.append("span")
.style("width", "25px")
.style("text-align", "right")
.style("color", colors.gray)
.text(`${i + 1}.`);
row.append("code")
.style("background", isVariable ? "#e8f5e9" : "#e3f2fd")
.style("padding", "2px 8px")
.style("border-radius", "4px")
.style("font-size", "11px")
.text(level);
row.append("span")
.style("font-size", "11px")
.style("color", colors.gray)
.text(isVariable ? "variable" : "static");
});
}
function updateExamples() {
examplesResults.html("");
const examples = generateExampleTopics(topicPattern, 10);
const examplesList = examplesResults.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "8px");
examples.forEach((topic, i) => {
examplesList.append("div")
.style("background", colors.white)
.style("padding", "8px 12px")
.style("border-radius", "4px")
.style("font-family", "monospace")
.style("font-size", "12px")
.style("word-break", "break-all")
.html(`<span style="color: ${colors.gray}; margin-right: 8px">${i + 1}.</span>${topic}`);
});
// Copy button
examplesResults.append("div")
.style("margin-top", "15px")
.style("text-align", "center")
.append("button")
.style("padding", "10px 20px")
.style("background", colors.teal)
.style("color", colors.white)
.style("border", "none")
.style("border-radius", "6px")
.style("cursor", "pointer")
.style("font-size", "14px")
.text("Copy Examples to Clipboard")
.on("click", function() {
const text = examples.join("\n");
navigator.clipboard.writeText(text).then(() => {
d3.select(this).text("Copied!").style("background", colors.green);
setTimeout(() => {
d3.select(this).text("Copy Examples to Clipboard").style("background", colors.teal);
}, 2000);
});
});
}
function updateAll() {
updateLintResults();
updateWildcardResults();
updateScaleProjection();
updateTopicTree();
updateExamples();
}
// Export Panel
const exportPanel = container.append("div")
.style("background", colors.lightGray)
.style("padding", "15px 20px")
.style("border-radius", "0 0 12px 12px")
.style("display", "flex")
.style("gap", "15px")
.style("align-items", "center")
.style("justify-content", "flex-end")
.style("border-top", `2px solid ${colors.gray}`);
exportPanel.append("span")
.style("font-size", "13px")
.style("color", colors.navy)
.style("font-weight", "bold")
.style("margin-right", "auto")
.text("Export Topics:");
// Export JSON button
exportPanel.append("button")
.text("Download JSON")
.style("padding", "10px 20px")
.style("background", colors.teal)
.style("color", colors.white)
.style("border", "none")
.style("border-radius", "6px")
.style("cursor", "pointer")
.style("font-size", "13px")
.style("font-weight", "bold")
.style("transition", "all 0.2s")
.on("mouseover", function() {
d3.select(this).style("background", "#0d7d6a");
})
.on("mouseout", function() {
d3.select(this).style("background", colors.teal);
})
.on("click", () => {
const lintResultsData = lintRules.map(rule => ({
id: rule.id,
name: rule.name,
severity: rule.severity,
passed: rule.check(topicPattern),
suggestion: rule.suggestion
}));
const exportConfig = {
exportedAt: new Date().toISOString(),
tool: "MQTT Topic Designer",
topicPattern: topicPattern,
wildcardPattern: wildcardPattern,
scaleParameters: {
numDevices: numDevices,
sensorsPerDevice: sensorsPerDevice,
totalTopics: numDevices * sensorsPerDevice
},
topicHierarchy: {
depth: topicPattern.split("/").length,
levels: topicPattern.split("/"),
variables: countVariables(topicPattern)
},
lintResults: {
passed: lintResultsData.filter(r => r.passed).length,
total: lintResultsData.length,
rules: lintResultsData
},
exampleTopics: generateExampleTopics(topicPattern, 10)
};
const blob = new Blob([JSON.stringify(exportConfig, null, 2)], {type: "application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "mqtt-topic-design.json";
a.click();
URL.revokeObjectURL(url);
});
// Copy to clipboard button
exportPanel.append("button")
.text("Copy to Clipboard")
.style("padding", "10px 20px")
.style("background", colors.navy)
.style("color", colors.white)
.style("border", "none")
.style("border-radius", "6px")
.style("cursor", "pointer")
.style("font-size", "13px")
.style("font-weight", "bold")
.style("transition", "all 0.2s")
.on("mouseover", function() {
d3.select(this).style("background", colors.darkGray);
})
.on("mouseout", function() {
d3.select(this).style("background", colors.navy);
})
.on("click", function() {
const lintResultsData = lintRules.map(rule => ({
id: rule.id,
name: rule.name,
severity: rule.severity,
passed: rule.check(topicPattern),
suggestion: rule.suggestion
}));
const exportConfig = {
exportedAt: new Date().toISOString(),
tool: "MQTT Topic Designer",
topicPattern: topicPattern,
wildcardPattern: wildcardPattern,
scaleParameters: {
numDevices: numDevices,
sensorsPerDevice: sensorsPerDevice,
totalTopics: numDevices * sensorsPerDevice
},
topicHierarchy: {
depth: topicPattern.split("/").length,
levels: topicPattern.split("/"),
variables: countVariables(topicPattern)
},
lintResults: {
passed: lintResultsData.filter(r => r.passed).length,
total: lintResultsData.length,
rules: lintResultsData
},
exampleTopics: generateExampleTopics(topicPattern, 10)
};
navigator.clipboard.writeText(JSON.stringify(exportConfig, null, 2)).then(() => {
const btn = d3.select(this);
btn.text("Copied!").style("background", colors.green);
setTimeout(() => {
btn.text("Copy to Clipboard").style("background", colors.navy);
}, 2000);
});
});
// Initial render
updateAll();
return container.node();
}1212.2 How to Use This Tool
1212.2.1 Topic Pattern Syntax
Use curly braces {variable_name} to define placeholder variables in your topic pattern:
| Pattern Element | Description | Example |
|---|---|---|
| Static text | Fixed topic level | v1, sensor, data |
{variable} |
Placeholder for dynamic values | {device_id}, {sensor_type} |
/ |
Level separator | Separates hierarchy levels |
1212.2.2 Common Topic Patterns
# IoT Telemetry Pattern
v1/{org}/{site}/{device_id}/telemetry/{sensor_type}
# Command/Response Pattern
v1/{org}/device/{device_id}/command/request
v1/{org}/device/{device_id}/command/response
# Status Pattern
v1/{org}/device/{device_id}/status/{status_type}
# Event Pattern
v1/{org}/device/{device_id}/event/{event_type}
1212.2.3 Wildcard Usage
| Wildcard | Meaning | Example |
|---|---|---|
+ |
Matches exactly one level | v1/+/device/# matches v1/acme/device/001/temp |
# |
Matches zero or more levels (must be last) | v1/org1/# matches all topics under v1/org1/ |
1212.3 Best Practices Reference
- Use version prefixes (
v1/,v2/) to enable API evolution without breaking clients - Place identifiers early in the hierarchy for efficient wildcard filtering
- Keep depth reasonable (5-7 levels) to balance specificity and performance
- Use lowercase consistently to avoid case-sensitivity issues
- Include data type suffixes like
/data,/status,/command,/event - Avoid special characters and spaces in topic levels
- Plan for multi-tenancy with organization/tenant prefixes
- Consider message routing - place routing-relevant levels first
- Document your schema for team consistency
- Test with wildcards before deployment to ensure expected matching
1212.3.1 Topic Hierarchy Recommendations
| Level Position | Recommended Content | Rationale |
|---|---|---|
| 1 | Version (v1, v2) |
API evolution without breaking |
| 2 | Organization/Tenant | Multi-tenant isolation |
| 3 | Site/Location | Geographic filtering |
| 4 | Device ID | Per-device subscriptions |
| 5 | Data Category | Sensor type or data type |
| 6-7 | Specifics | Additional context as needed |
1212.3.2 Anti-Patterns to Avoid
- Starting with
/: Creates an empty first level - Using spaces: Causes parsing issues
- Deep hierarchies (8+ levels): Reduces performance
- Using
$: Reserved for broker system topics - Inconsistent casing: Leads to matching failures
- Flat topics: Makes filtering impossible with wildcards
1212.4 What’s Next
- MQTT Fundamentals - Complete MQTT protocol guide
- MQTT QoS and Sessions - Quality of Service levels
- MQTT Labs and Implementation - Hands-on exercises
- MQTT Comprehensive Review - Complete review
- CoAP Fundamentals - Alternative protocol
This interactive tool is implemented in approximately 850 lines of Observable JavaScript. Key features:
- Topic pattern input: Design topics with variable placeholders
- 12 linting rules: Best practice validation with pass/fail/warning
- Wildcard tester: Test + and # subscription patterns
- Scale projection: Calculate topic counts and broker requirements
- Topic tree visualization: Hierarchical view of topic structure
- Example generator: Generate sample topics from patterns
The tool uses the IEEE color palette for consistency: - Navy (#2C3E50): Primary UI elements - Teal (#16A085): Good indicators, success states - Orange (#E67E22): Warnings - Red (#E74C3C): Errors - Purple (#9B59B6): Special highlights
1212.5 Related Interactive Tools
Explore these related visualizations to deepen your understanding of MQTT messaging:
| Tool | Description |
|---|---|
| MQTT Pub/Sub Animation | See your topic designs in action with pub/sub routing |
| QoS Playground | Test message delivery with your topic patterns |
| MQTT Retained Messages | Understand retained messages for status topics |
| MQTT Clustering | Scale your topic hierarchy across broker clusters |