L'intégration de MQTT dans Next.js déroute au début, car Next.js exécute votre code à plusieurs endroits : le navigateur, un serveur Node.js, des fonctions serverless et parfois l'edge. Dans ce guide, vous verrez lesquels de ces endroits peuvent maintenir une connexion MQTT, et vous construirez les trois patterns qui couvrent presque tous les projets : un composant client qui s'abonne en WebSocket sécurisé, un route handler qui publie avec des identifiants côté serveur, et une passerelle Server-Sent Events. Tous les exemples utilisent l'endpoint WebSocket de ConnectMQ sur broker.connectmq.com et MQTT.js v5 avec l'App Router.
Où MQTT peut-il s'exécuter dans Next.js ?
MQTT a besoin d'une connexion de longue durée. Ce seul fait détermine où il trouve sa place :
| Environnement d'exécution | Connexion MQTT de longue durée ? | Transport | Usage typique |
|---|---|---|---|
| Navigateur (composant client) | Oui, tant que l'onglet est ouvert | wss:// port 8084, chemin /mqtt |
Tableaux de bord en direct, commandes d'appareils |
Serveur Node.js (next start, conteneur, VM) |
Oui | mqtts:// 8883 ou wss:// 8084 |
Subscriber partagé, passerelle SSE, publication |
| Fonction serverless | Non, uniquement par requête | mqtts:// 8883 |
Publier une commande, puis se déconnecter |
| Edge runtime | Non | Non pris en charge pour le TCP de MQTT.js | À éviter pour MQTT |
| Server Components | Non, ils sont rendus une seule fois | - | Lire plutôt les données depuis une base |
Un Server Component est rendu puis se termine ; il ne peut pas maintenir un abonnement actif. Les Server Actions sont elles aussi limitées à la requête. Le vrai choix se fait donc entre une connexion depuis le navigateur et une connexion depuis une route serveur qui tourne sur Node.js.
Installez la bibliothèque cliente une fois :
npm install mqtt
MQTT.js v5 fournit à la fois une version Node.js et une version navigateur, et les bundlers choisissent automatiquement la bonne. Le guide MQTT.js avec Node.js couvre la bibliothèque elle-même ; ici, nous nous concentrons sur son intégration dans Next.js.
Pattern 1 : s'abonner dans un composant client en WebSocket
Pour les tableaux de bord en direct, le navigateur se connecte directement au broker. Pas de détour par un serveur, une latence minimale, et votre serveur Next.js n'effectue aucun travail par message. Les navigateurs ne peuvent pas ouvrir de TCP brut : ce pattern utilise donc toujours MQTT over WebSocket sécurisé. Notre article MQTT over WebSocket explique ce transport.
Créez un hook réutilisable dans app/hooks/useMqtt.js :
"use client";
import { useEffect, useRef, useState } from "react";
import mqtt from "mqtt";
const BROKER_URL = process.env.NEXT_PUBLIC_MQTT_URL ?? "wss://broker.connectmq.com:8084/mqtt";
export function useMqtt(topic) {
const clientRef = useRef(null);
const [status, setStatus] = useState("connecting");
const [messages, setMessages] = useState([]);
useEffect(() => {
const clientId = "next-" + Math.random().toString(16).slice(2, 10);
const client = mqtt.connect(BROKER_URL, {
clientId,
protocolVersion: 5,
clean: true,
keepalive: 30,
reconnectPeriod: 3000,
});
clientRef.current = client;
client.on("connect", () => {
setStatus("connected");
client.subscribe(topic, { qos: 1 });
});
client.on("reconnect", () => setStatus("reconnecting"));
client.on("close", () => setStatus("disconnected"));
client.on("message", (t, payload) => {
setMessages((prev) => [...prev.slice(-99), { topic: t, text: payload.toString(), at: Date.now() }]);
});
return () => {
client.end(true);
clientRef.current = null;
};
}, [topic]);
const publish = (t, payload) => clientRef.current?.publish(t, payload, { qos: 1 });
return { status, messages, publish };
}
Utilisez-le dans une page, app/live/page.jsx :
"use client";
import { useMqtt } from "../hooks/useMqtt";
const PREFIX = "connectmq/demo/next-k7f3q2";
export default function LivePage() {
const { status, messages } = useMqtt(`${PREFIX}/sensors/#`);
return (
<main style={{ padding: 24 }}>
<h1>Live sensors</h1>
<p>Broker: {status}</p>
<ul>
{messages.map((m) => (
<li key={m.at + m.topic}>{m.topic}: {m.text}</li>
))}
</ul>
</main>
);
}
React échappe le texte dans le JSX : afficher des payloads de cette manière est donc protégé contre l'injection de script. Publiez une mesure de test depuis un terminal et regardez-la apparaître :
mosquitto_pub -h broker.connectmq.com -p 1883 -i next-test-3b1c -t 'connectmq/demo/next-k7f3q2/sensors/room1' -m '{"t":22.4}'
Trois détails qui piègent en développement
- Le double montage du Strict Mode. En développement, React monte, démonte puis remonte les composants pour révéler les bugs de cleanup. Sans
client.end()dans le cleanup de l'effet, vous obtenez deux connexions. Générer le client ID à l'intérieur de l'effet évite aussi que les deux instances se disputent le même ID. - Ne vous connectez jamais au niveau du module. Un
mqtt.connect()de premier niveau dans un fichier client s'exécute aussi pendant le rendu côté serveur de ce composant, où il ouvre une connexion TCP sur votre serveur. Gardez les connexions dansuseEffect, qui ne s'exécute que dans le navigateur. NEXT_PUBLIC_signifie public. Toute variable portant ce préfixe est intégrée en clair dans le bundle JavaScript. N'y placez que l'URL du broker, jamais un mot de passe.
Pattern 2 : publier depuis un route handler avec des identifiants privés
Les commandes qui agissent sur le monde physique, comme ouvrir une vanne ou redémarrer un appareil, ne doivent pas être envoyées avec des identifiants que chaque visiteur peut lire. Le navigateur appelle plutôt votre API, le serveur vérifie l'utilisateur, puis publie avec des identifiants conservés dans des variables d'environnement.
D'abord, un singleton côté serveur dans lib/mqtt-server.js. Mettre le client en cache sur globalThis évite que le hot reload ne crée une nouvelle connexion à chaque enregistrement de fichier en développement :
import mqtt from "mqtt";
const globalForMqtt = globalThis;
export function getMqttClient() {
if (!globalForMqtt.__mqttClient) {
globalForMqtt.__mqttClient = mqtt.connect(process.env.MQTT_URL ?? "mqtts://broker.connectmq.com:8883", {
clientId: process.env.MQTT_CLIENT_ID ?? "next-server-" + Math.random().toString(16).slice(2, 10),
username: process.env.MQTT_USERNAME,
password: process.env.MQTT_PASSWORD,
protocolVersion: 5,
clean: true,
reconnectPeriod: 5000,
});
}
return globalForMqtt.__mqttClient;
}
Puis le route handler dans app/api/command/route.js :
import { getMqttClient } from "@/lib/mqtt-server";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
const PREFIX = "connectmq/demo/next-k7f3q2";
const ALLOWED = new Set(["on", "off"]);
export async function POST(request) {
const { deviceId, action } = await request.json();
if (!/^[a-z0-9-]{1,32}$/.test(deviceId ?? "") || !ALLOWED.has(action)) {
return Response.json({ error: "invalid command" }, { status: 400 });
}
// Vérifiez ici la session ou le rôle de l'utilisateur avant de publier.
await getMqttClient().publishAsync(
`${PREFIX}/devices/${deviceId}/cmd`,
JSON.stringify({ action, at: new Date().toISOString() }),
{ qos: 1 }
);
return Response.json({ ok: true });
}
Votre fichier .env.local pour un broker ConnectMQ privé ressemble à ceci :
MQTT_URL=mqtts://YOUR_BROKER_HOST:8883
MQTT_USERNAME=YOUR_USERNAME
MQTT_PASSWORD=YOUR_PASSWORD
MQTT_CLIENT_ID=YOUR_CLIENT_ID
Valider deviceId selon un motif strict n'est pas facultatif. Si vous laissez une saisie utilisateur arriver directement dans un topic, une requête contenant +, # ou des barres obliques supplémentaires pourrait publier sur des topics que vous n'aviez jamais prévus.
Sur les plateformes serverless
En hébergement serverless, une instance de fonction peut être gelée ou supprimée entre deux requêtes : on ne peut donc pas compter sur une connexion en cache. Connectez-vous, publiez et fermez plutôt dans la même requête :
import mqtt from "mqtt";
export const runtime = "nodejs";
export async function POST(request) {
const { message } = await request.json();
const client = await mqtt.connectAsync(process.env.MQTT_URL, {
clientId: "next-fn-" + Math.random().toString(16).slice(2, 10),
username: process.env.MQTT_USERNAME,
password: process.env.MQTT_PASSWORD,
});
try {
await client.publishAsync("connectmq/demo/next-k7f3q2/notices", String(message), { qos: 1 });
} finally {
await client.endAsync();
}
return Response.json({ ok: true });
}
Cela ajoute un handshake TLS à chaque requête, ce qui convient pour des commandes occasionnelles mais pas pour de la publication à haute fréquence. Notez que ConnectMQ lie les client ID aux identifiants délivrés sur les brokers privés : si vous exécutez de nombreuses fonctions éphémères en parallèle, vérifiez dans le portail comment vos identifiants et vos client ID sont provisionnés.
Pattern 3 : diffuser MQTT vers le navigateur avec les Server-Sent Events
Il arrive que vous ne vouliez pas du tout que le navigateur parle au broker, par exemple quand les données d'un topic doivent être filtrées par utilisateur. Une route Node.js peut maintenir un seul abonnement et redistribuer les messages aux navigateurs via les Server-Sent Events (SSE). Cela ne fonctionne que sur un serveur Node.js de longue durée, pas sur des fonctions serverless aux délais d'expiration courts.
Abonnez-vous une seule fois dans le singleton (ajoutez ceci dans getMqttClient après la création du client) :
globalForMqtt.__mqttClient.on("connect", () => {
globalForMqtt.__mqttClient.subscribe("connectmq/demo/next-k7f3q2/sensors/#", { qos: 1 });
});
Ajoutez ensuite app/api/stream/route.js :
import { getMqttClient } from "@/lib/mqtt-server";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function GET(request) {
const client = getMqttClient();
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
const onMessage = (topic, payload) => {
const data = JSON.stringify({ topic, payload: payload.toString() });
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
};
client.on("message", onMessage);
request.signal.addEventListener("abort", () => {
client.off("message", onMessage);
controller.close();
});
},
});
return new Response(stream, {
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" },
});
}
Dans un composant client, new EventSource("/api/stream") reçoit chaque message sous forme d'événement. Une seule connexion au broker sert désormais tous les spectateurs, ce qui vous maintient confortablement sous les limites de connexions.
Dépannage des connexions MQTT dans Next.js
Quelques problèmes reviennent sans cesse quand on intègre MQTT dans un projet Next.js :
- « WebSocket connection failed » dans la console du navigateur. Vérifiez soigneusement l'URL : elle doit commencer par
wss://, utiliser le port 8084 et se terminer par/mqtt. Une page servie en HTTPS ne peut pas utiliserws://en clair. - « Module not found: net » ou « tls » pendant le build. Du code réservé au serveur s'est retrouvé dans un bundle client, généralement via un fichier partagé qui importe aussi des modules Node.js. Tenez
lib/mqtt-server.jsà l'écart de tout ce qui est marqué"use client". - Le route handler se bloque, puis expire. Le broker a refusé le login ou le port est bloqué depuis votre hébergeur :
publishAsyncattend alors une connexion qui n'arrive jamais. Écoutez l'événementerror, journalisez-le et testez les mêmes identifiants avecmosquitto_pubdepuis le serveur. - Les messages arrivent en double sur le serveur. Le hot reload a créé un second client parce que le singleton n'était pas mis en cache sur
globalThis. Redémarrez le serveur de développement après la correction.
Choisir le bon pattern
Utilisez cette liste pour décider :
- Données en direct en lecture seule, publiques ou peu sensibles : pattern 1, navigateur en WebSocket avec des identifiants en lecture seule.
- Actions avec effets de bord : pattern 2, route handler avec identifiants côté serveur et validation des entrées.
- Filtrage par utilisateur ou nombreux spectateurs sur une petite offre : pattern 3, passerelle SSE sur un serveur Node.js.
- Graphiques historiques : aucun des patterns ci-dessus seul. Stockez les messages avec un subscriber séparé et interrogez la base depuis un Server Component.
La plupart des applications de production combinent les patterns 1 et 2 : le navigateur s'abonne pour les mises à jour en direct, et chaque commande passe par un route handler. Pour les détails au niveau des composants, consultez le tutoriel React MQTT, et pour des payloads typés, lisez le guide MQTT avec TypeScript. Le tutoriel d'application de chat MQTT est un bon exemple de conception entièrement côté navigateur, et la vue d'ensemble des bibliothèques clientes MQTT vous aidera si une partie de votre stack est dans un autre langage. La documentation officielle des route handlers Next.js détaille les options d'environnement d'exécution.
Connectez votre application Next.js à ConnectMQ
ConnectMQ est un broker MQTT managé qui propose le WebSocket sécurisé sur le port 8084 pour les composants client et TLS sur le port 8883 pour les route handlers : les deux côtés d'une application Next.js parlent ainsi au même broker. Prototypez sur le serveur MQTT public gratuit, réservé aux tests, puis passez à un broker MQTT privé dans le cloud. Inscrivez-vous gratuitement à l'offre Serverless, sans carte bancaire.
Questions fréquentes
Peut-on utiliser MQTT dans les Server Components de Next.js ?
Pas pour des données en direct. Les Server Components sont rendus une fois par requête et ne peuvent pas maintenir d'abonnement. Abonnez-vous dans un composant client ou un route handler Node.js, et utilisez les Server Components pour lire les données stockées.
MQTT fonctionne-t-il sur l'Edge runtime de Next.js ?
L'Edge runtime ne fournit pas les API réseau de Node.js que MQTT.js utilise pour TCP et TLS. Définissez export const runtime = "nodejs" sur toute route qui communique avec le broker.
Comment tenir les identifiants MQTT à l'écart du navigateur dans Next.js ?
Stockez-les dans des variables d'environnement non publiques et ne les utilisez que dans des route handlers ou du code serveur. Tout ce qui est préfixé par NEXT_PUBLIC_ se retrouve dans le bundle client.
Pourquoi vois-je deux connexions MQTT en développement ?
Le Strict Mode de React monte les composants deux fois en développement, et le hot reload de Next.js réexécute les modules serveur. Fermez le client dans le cleanup de l'effet et mettez les clients serveur en cache sur globalThis.



