MQTT in Next.js funktioniert am besten zweigeteilt: Der Browser abonniert Live-Daten über sichere WebSockets aus einem Client Component, und alles, was Secrets braucht oder an Geräte schreibt, läuft über einen Route Handler, der vom Server aus publiziert. Diese Anleitung baut beide Hälften mit MQTT.js 5 im App Router, dazu eine optionale Server-Sent-Events-Route für den Fall, dass der Browser gar nicht mit dem Broker sprechen soll.
Die Beispiele nutzen broker.connectmq.com (WebSocket 8084 mit Pfad /mqtt, TLS 8883) und Test-Topics unter testmqtt/nextjs/.... Ersetze Host und Zugangsdaten durch deine eigenen.
Wo MQTT in einer Next.js-App läuft
| Code | Läuft auf | Transport | Einsatz |
|---|---|---|---|
Client Component ("use client") | Browser | wss://…:8084/mqtt | Live-Dashboards, Benachrichtigungen |
Route Handler (app/**/route.ts) | Node.js-Server | mqtts://…:8883 | Befehle, Schreibzugriffe, geheime Zugangsdaten |
| Server Component | Server, pro Render | MQTT vermeiden | Initiale Daten aus Datenbank oder API |
Server Components rendern einmal pro Request (oder beim Build) und sind dann fertig – die falsche Lebensdauer für eine Subscription. Hole den Anfangszustand aus deiner Datenbank und lass ein Client Component die Updates streamen.
Installiere den Client:
npm install mqtt server-onlyLive-Daten in einem Client Component
Der Browser kann keinen TCP-Socket öffnen, also bleibt nur MQTT über WebSockets. Baue die Verbindung in useEffect auf, damit sie nie beim Server-Rendering läuft, und schließe sie in der Cleanup-Funktion:
"use client";
import { useEffect, useState } from "react";
import mqtt from "mqtt";
const MQTT_URL =
process.env.NEXT_PUBLIC_MQTT_URL ?? "wss://broker.connectmq.com:8084/mqtt";
type Status = "connecting" | "connected" | "reconnecting" | "offline";
export function LiveTelemetry({ deviceId }: { deviceId: string }) {
const [status, setStatus] = useState<Status>("connecting");
const [last, setLast] = useState<string | null>(null);
useEffect(() => {
const topic = `testmqtt/nextjs/${deviceId}/telemetry`;
const client = mqtt.connect(MQTT_URL, {
clientId: `web-${crypto.randomUUID().slice(0, 8)}`,
username: process.env.NEXT_PUBLIC_MQTT_USER,
password: process.env.NEXT_PUBLIC_MQTT_PASS,
reconnectPeriod: 2000,
connectTimeout: 10_000,
});
client.on("connect", () => {
setStatus("connected");
client.subscribe(topic, { qos: 1 });
});
client.on("reconnect", () => setStatus("reconnecting"));
client.on("offline", () => setStatus("offline"));
client.on("error", (err) => console.error("MQTT error:", err.message));
client.on("message", (t, payload) => {
if (t === topic) setLast(payload.toString());
});
return () => {
client.end(true);
};
}, [deviceId]);
return (
<section>
<p>MQTT: {status}</p>
<pre>{last ?? "waiting for the first message…"}</pre>
</section>
);
}Gerendert wird es aus einer normalen Server-Component-Seite; params ist im aktuellen Next.js ein Promise:
import { LiveTelemetry } from "./live-telemetry";
export default async function DevicePage({ params }: PageProps<"/devices/[id]">) {
const { id } = await params;
return (
<main>
<h1>Device {id}</h1>
<LiveTelemetry deviceId={id} />
</main>
);
}Typische Stolperfallen in dieser Hälfte:
- Zwei Verbindungen in der Entwicklung. React Strict Mode führt den Effect, sein Cleanup und den Effect erneut aus. Mit
client.end(true)im Cleanup schließt die erste Verbindung sofort. Ohne das hinterlässt jeder Hot Reload eine verwaiste Verbindung. - Mixed Content. Eine per HTTPS ausgelieferte Seite darf nur
wss://-Sockets öffnen.ws://broker...:8083funktioniert aufhttp://localhostund scheitert in Produktion mitWebSocket connection to 'ws://…' failed. - Falscher Pfad. MQTT über WebSockets braucht den Pfad des Brokers (hier
/mqtt). Fehlt er, endet das Upgrade meist mit HTTP 404; siehe WebSocket path 404 und WebSocket connection failed. - Subscribe im
connect-Handler. Er läuft nach jedem Reconnect erneut – genau richtig, wenn die Session nicht persistent ist. Zweimal denselben Filter zu abonnieren schadet nicht.
Brauchen mehrere Komponenten denselben Stream, verschiebe den Client in einen Context Provider, statt pro Komponente eine Verbindung zu öffnen; das Muster ist dasselbe wie bei MQTT mit React.
Was der Browser darf
Alles mit dem Präfix NEXT_PUBLIC_ wird beim Build ins Bundle eingebettet, behandle diese Zugangsdaten also als öffentlich. Gib dem Browser einen eigenen Benutzer, dessen ACL nur Subscribe auf die Topics erlaubt, die das Dashboard zeigt. Befehle werden über den Server publiziert, siehe unten. WebSocket-Authentifizierung im Browser behandelt kurzlebige Tokens, falls du Zugriff pro Benutzer brauchst.
Publizieren aus einem Route Handler
Route Handlers laufen in der Node.js-Runtime (Standard) und können daher MQTT über TLS auf Port 8883 nutzen. Eine neue Verbindung pro Request kostet jedes Mal einen TCP- und TLS-Handshake, also cache einen Client pro Serverprozess. Auf globalThis abgelegt übersteht er auch Hot Module Reloads in next dev:
import "server-only";
import mqtt, { type MqttClient } from "mqtt";
const g = globalThis as unknown as { mqttClient?: Promise<MqttClient> };
export function getMqtt(): Promise<MqttClient> {
g.mqttClient ??= mqtt
.connectAsync("mqtts://broker.connectmq.com:8883", {
clientId: `nextjs-${crypto.randomUUID().slice(0, 8)}`,
username: process.env.MQTT_USER,
password: process.env.MQTT_PASS,
reconnectPeriod: 2000,
})
.catch((err) => {
g.mqttClient = undefined; // retry on the next request
throw err;
});
return g.mqttClient;
}import "server-only" macht aus einem versehentlichen Import in ein Client Component einen Build-Fehler, sodass die Publish-Zugangsdaten nie im Browser landen. Die zufällige Client-ID ist wichtig: Mehrere Serverinstanzen mit derselben ID würden sich gegenseitig vom Broker werfen (doppelte Client-ID).
Der Handler validiert die Eingabe, publiziert mit QoS 1 und wartet auf das PUBACK, bevor er antwortet:
import { getMqtt } from "@/lib/mqtt-server";
const ACTIONS = new Set(["reboot", "identify", "set-interval"]);
export async function POST(request: Request, ctx: RouteContext<"/api/devices/[id]/command">) {
const { id } = await ctx.params;
if (!/^[\w-]{1,64}$/.test(id)) {
return Response.json({ error: "invalid device id" }, { status: 400 });
}
const body = await request.json().catch(() => null);
if (!body || !ACTIONS.has(body.action)) {
return Response.json({ error: "unknown action" }, { status: 400 });
}
try {
const client = await getMqtt();
await client.publishAsync(
`testmqtt/nextjs/${id}/command`,
JSON.stringify({ action: body.action, at: Date.now() }),
{ qos: 1 },
);
return Response.json({ ok: true });
} catch (err) {
console.error("publish failed", err);
return Response.json({ error: "broker unavailable" }, { status: 503 });
}
}Die Prüfung der Geräte-ID ist keine Kosmetik: Ein Wert mit /, + oder # würde dem Aufrufer erlauben, in ein anderes Topic zu schreiben als vorgesehen. Prüfe deine Filter mit dem Topic Matcher.
Eine Server Action funktioniert genauso: Markiere eine Funktion in einer Server-Datei mit "use server", rufe darin getMqtt() auf und publiziere. Nimm, was besser zu deiner Formularverarbeitung passt; der MQTT-Teil ist identisch.
Serverless vs. langlebige Server
Mit next start, in Docker oder auf jedem langlebigen Node.js-Host bleibt der gecachte Client verbunden, und jeder Publish ist ein einziger Roundtrip. Auf Serverless-Plattformen kann eine Funktionsinstanz zwischen Requests eingefroren werden; beim Aufwachen ist der Socket womöglich tot, und MQTT.js verbindet sich neu, bevor der Publish rausgeht. Das funktioniert, aber der erste Request nach einer Pause bezahlt den Reconnect. Bei seltenen Befehlen ist das in Ordnung. Bei häufigen bevorzuge ein langlebiges Deployment.
Streaming zum Browser ohne Broker-Verbindung
Manchmal soll der Browser gar nicht mit dem Broker sprechen: strenge Firewalls, keine Zugangsdaten im Browser oder Daten, die pro Benutzer gefiltert werden müssen. Ein Route Handler kann auf dem Server abonnieren und Nachrichten als Server-Sent Events weiterreichen:
import mqtt from "mqtt";
export async function GET(request: Request, ctx: RouteContext<"/api/devices/[id]/stream">) {
const { id } = await ctx.params;
const topic = `testmqtt/nextjs/${id}/telemetry`;
const encoder = new TextEncoder();
const client = await mqtt.connectAsync("mqtts://broker.connectmq.com:8883", {
username: process.env.MQTT_USER,
password: process.env.MQTT_PASS,
});
await client.subscribeAsync(topic, { qos: 0 });
const stream = new ReadableStream({
start(controller) {
client.on("message", (_t, payload) => {
controller.enqueue(encoder.encode(`data: ${payload.toString()}\n\n`));
});
request.signal.addEventListener("abort", () => {
client.end();
controller.close();
});
},
});
return new Response(stream, {
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" },
});
}Im Browser empfängt new EventSource("/api/devices/dev-1/stream") jede Nachricht als Event. Das hält eine MQTT-Verbindung pro offenem Tab, passt also zu internen Tools und überschaubarem Publikum auf einem langlebigen Server; für großes Publikum skaliert der direkte WebSocket-Weg besser, weil der Broker das Fan-out übernimmt.
End-to-End testen
- Starte
next devund öffne/devices/dev-1. Der Status sollteconnectedzeigen. - Öffne den Online-MQTT-Client in einem zweiten Tab, verbinde dich mit
broker.connectmq.comund publiziere{"temp":21.4}auftestmqtt/nextjs/dev-1/telemetry. Die Seite aktualisiert sich sofort. - Abonniere im Online-Client
testmqtt/nextjs/+/commandund rufe dann den Route Handler auf:
curl -X POST http://localhost:3000/api/devices/dev-1/command \
-H "Content-Type: application/json" \
-d '{"action":"identify"}'Der Befehl erscheint im Online-Client, die Antwort lautet {"ok":true}. Probiere {"action":"format-disk"}, um zu prüfen, dass der Handler unbekannte Aktionen ablehnt, und trenne kurz das Netzwerk, um zu sehen, wie der Status auf reconnecting und zurück wechselt.
Next.js-Test-Topics live beobachten
Halte Subscriptions in Client Components über sichere WebSockets, Secrets und Schreibzugriffe in Route Handlers mit einem gecachten Server-Client, und greif nur dann zu SSE, wenn der Browser vom Broker fernbleiben muss. Mit dieser Aufteilung läuft derselbe MQTT.js-Code in next dev, in CI und in Produktion – nur mit anderen Umgebungsvariablen.
Häufig gestellte Fragen
Kann sich eine Next.js-Seite über Port 1883 mit MQTT verbinden?
Aus dem Browser nicht. Browser können keine rohen TCP-Sockets öffnen, deshalb muss ein Client Component MQTT über WebSockets nutzen (wss://broker.connectmq.com:8084/mqtt). Server-Code in Route Handlers läuft auf Node.js und kann mqtt:// auf 1883 oder mqtts:// auf 8883 verwenden.
Warum sehe ich in der Entwicklung zwei MQTT-Verbindungen?
React Strict Mode mountet, unmountet und mountet jede Komponente in der Entwicklung erneut, daher läuft dein useEffect zweimal. Ruft die Cleanup-Funktion client.end() auf, schließt sich die erste Verbindung sofort und nur eine bleibt offen. Produktions-Builds mounten nur einmal.
Ist es sicher, MQTT-Zugangsdaten in NEXT_PUBLIC_-Variablen abzulegen?
Nur Zugangsdaten, die jeder Besucher lesen darf. NEXT_PUBLIC_-Werte werden ins JavaScript-Bundle eingebettet. Gib dem Browser einen Benutzer, der per ACL auf lesende Topics beschränkt ist, und halte Publish-Zugangsdaten in reinen Server-Umgebungsvariablen, die nur Route Handlers verwenden.


