Tutoriales de IoT

Next.js y MQTT: patrones de integración en servidor y cliente que funcionan de verdad

Aprende las tres formas de usar MQTT en un proyecto Next.js con App Router: client components sobre WebSocket seguro, route handlers de servidor que publican con credenciales privadas y un puente con Server-Sent Events.

Diagrama de integración de Next.js con MQTT mediante client components, route handlers y un broker MQTT en la nube
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

Integrar MQTT en Next.js resulta confuso al principio porque Next.js ejecuta tu código en varios lugares: el navegador, un servidor Node.js, funciones serverless y, a veces, el edge. En esta guía aprenderás cuáles de esos lugares pueden mantener una conexión MQTT y construirás los tres patrones que cubren casi cualquier proyecto: un client component que se suscribe mediante WebSocket seguro, un route handler que publica con credenciales del lado del servidor y un puente con Server-Sent Events. Todos los ejemplos usan el endpoint WebSocket de ConnectMQ en broker.connectmq.com y MQTT.js v5 con el App Router.

¿Dónde puede ejecutarse MQTT en Next.js?

MQTT necesita una conexión de larga duración. Ese único hecho decide dónde encaja:

Entorno de ejecución ¿Conexión MQTT de larga duración? Transporte Uso típico
Navegador (client component) Sí, mientras la pestaña esté abierta wss:// puerto 8084, ruta /mqtt Dashboards en vivo, control de dispositivos
Servidor Node.js (next start, contenedor, VM) Sí mqtts:// 8883 o wss:// 8084 Suscriptor compartido, puente SSE, publicación
Función serverless No, solo por petición mqtts:// 8883 Publicar un comando y desconectar
Edge runtime No No compatible con TCP de MQTT.js Evitar para MQTT
Server Components No, se renderizan una vez - Leer los datos de una base de datos

Un Server Component se renderiza y termina; no puede mantener viva una suscripción. Las Server Actions también están ligadas a la petición. Así que la elección real está entre conectar desde el navegador y conectar desde una ruta de servidor que se ejecute en Node.js.

Instala la librería cliente una vez:

npm install mqtt

MQTT.js v5 incluye una versión para Node.js y otra para navegador, y los bundlers eligen automáticamente la correcta. La guía de MQTT.js en Node.js cubre la librería en sí; aquí nos centramos en cómo encaja en Next.js.

Patrón 1: suscribirse en un client component por WebSocket

Para dashboards en vivo, el navegador se conecta directamente al broker. No hay salto por el servidor, la latencia es mínima y tu servidor Next.js no trabaja por cada mensaje. Los navegadores no pueden abrir TCP directo, así que esto siempre usa MQTT sobre WebSocket seguro. Nuestro artículo de MQTT sobre WebSocket explica el transporte.

Crea un hook reutilizable en 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 };
}

Úsalo en una página, 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 escapa el texto en JSX, así que mostrar los payloads de esta forma es seguro frente a la inyección de scripts. Publica una lectura de prueba desde un terminal y mira cómo aparece:

mosquitto_pub -h broker.connectmq.com -p 1883 -i next-test-3b1c -t 'connectmq/demo/next-k7f3q2/sensors/room1' -m '{"t":22.4}'

Tres detalles que muerden en desarrollo

  1. Doble montaje de Strict Mode. En desarrollo, React monta, desmonta y vuelve a montar los componentes para sacar a la luz errores de limpieza. Sin client.end() en la limpieza del efecto, acabas con dos conexiones. Generar el client ID dentro del efecto también evita que ambas instancias se peleen por el mismo ID.
  2. Nunca conectes a nivel de módulo. Un mqtt.connect() en el nivel superior de un archivo de cliente también se ejecuta durante el renderizado en servidor de ese componente, donde abre una conexión TCP en tu servidor. Mantén las conexiones dentro de useEffect, que solo se ejecuta en el navegador.
  3. NEXT_PUBLIC_ significa público. Cualquier variable con ese prefijo se incrusta en el bundle de JavaScript. Pon ahí solo la URL del broker, nunca una contraseña.

Patrón 2: publicar desde un route handler con credenciales privadas

Los comandos que cambian el mundo físico, como abrir una válvula o reiniciar un dispositivo, no deberían enviarse con credenciales que cualquier visitante puede leer. En su lugar, el navegador llama a tu API, el servidor comprueba el usuario y el servidor publica con credenciales guardadas en variables de entorno.

Primero, un singleton del lado del servidor en lib/mqtt-server.js. Cachear el cliente en globalThis evita que el hot reload en desarrollo cree una nueva conexión cada vez que guardas un archivo:

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;
}

Después, el route handler en 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 });
  }
  // Comprueba aquí la sesión o el rol del usuario antes de publicar.

  await getMqttClient().publishAsync(
    `${PREFIX}/devices/${deviceId}/cmd`,
    JSON.stringify({ action, at: new Date().toISOString() }),
    { qos: 1 }
  );
  return Response.json({ ok: true });
}

Tu .env.local para un broker privado de ConnectMQ tiene este aspecto:

MQTT_URL=mqtts://YOUR_BROKER_HOST:8883
MQTT_USERNAME=YOUR_USERNAME
MQTT_PASSWORD=YOUR_PASSWORD
MQTT_CLIENT_ID=YOUR_CLIENT_ID

Validar deviceId contra un patrón estricto no es opcional. Si dejas que la entrada del usuario llegue directamente a un topic, una petición que contenga +, # o barras adicionales podría publicar en topics que nunca pretendías.

En plataformas serverless

En el alojamiento serverless, una instancia de función puede congelarse o descartarse entre peticiones, así que no puedes confiar en una conexión cacheada. En su lugar, conecta, publica y cierra en la misma petición:

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 });
}

Esto añade un handshake TLS a cada petición, lo cual está bien para comandos ocasionales pero no para publicaciones de alta frecuencia. Ten en cuenta que ConnectMQ vincula los client IDs a las credenciales emitidas en los brokers privados, así que si ejecutas muchas funciones de corta duración en paralelo, comprueba en el portal cómo se aprovisionan tus credenciales y client IDs.

Patrón 3: enviar MQTT al navegador con Server-Sent Events

A veces no quieres que el navegador hable con el broker en absoluto, por ejemplo cuando los datos del topic necesitan filtrarse por usuario. Una ruta de Node.js puede mantener una suscripción y repartir los mensajes a los navegadores mediante Server-Sent Events (SSE). Esto solo funciona en un servidor Node.js de larga duración, no en funciones serverless con timeouts cortos.

Suscríbete una vez en el singleton (añade esto dentro de getMqttClient después de crear el cliente):

globalForMqtt.__mqttClient.on("connect", () => {
  globalForMqtt.__mqttClient.subscribe("connectmq/demo/next-k7f3q2/sensors/#", { qos: 1 });
});

Después añade 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" },
  });
}

En un client component, new EventSource("/api/stream") recibe cada mensaje como un evento. Una sola conexión con el broker sirve ahora a todos los espectadores, lo que te mantiene holgadamente dentro de los límites de conexiones.

Resolución de problemas de conexiones MQTT en Next.js

Algunos problemas aparecen una y otra vez al integrar MQTT en un proyecto Next.js:

  • "WebSocket connection failed" en la consola del navegador. Revisa la URL con cuidado: debe ser wss://, puerto 8084, y terminar en /mqtt. Una página servida por HTTPS no puede usar ws:// sin cifrar.
  • "Module not found: net" o "tls" durante la build. Algo ha metido código exclusivo de servidor en un bundle de cliente, normalmente a través de un archivo compartido que también importa módulos de Node.js. Mantén lib/mqtt-server.js fuera de todo lo marcado con "use client".
  • El route handler se queda colgado y luego da timeout. El broker rechazó el login o el puerto está bloqueado desde tu host, así que publishAsync espera una conexión que nunca llega. Escucha el evento error y regístralo, y prueba las mismas credenciales con mosquitto_pub desde el servidor.
  • Los mensajes llegan dos veces al servidor. El hot reload creó un segundo cliente porque el singleton no estaba cacheado en globalThis. Reinicia el servidor de desarrollo después de corregirlo.

Elegir el patrón adecuado

Usa esta lista para decidir:

  1. Datos en vivo de solo lectura, públicos o poco sensibles: patrón 1, navegador por WebSocket con credenciales de solo lectura.
  2. Acciones con efectos secundarios: patrón 2, route handler con credenciales del lado del servidor y validación de la entrada.
  3. Filtrado por usuario o muchos espectadores en un plan pequeño: patrón 3, puente SSE en un servidor Node.js.
  4. Gráficos históricos: ninguno de los anteriores por sí solo. Guarda los mensajes con un suscriptor aparte y consulta la base de datos desde un Server Component.

La mayoría de las apps en producción combinan el 1 y el 2: el navegador se suscribe para recibir actualizaciones en vivo y cada comando pasa por un route handler. Para detalles a nivel de componente consulta el tutorial de React con MQTT, y para payloads tipados lee la guía de MQTT con TypeScript. El tutorial de app de chat con MQTT es un buen ejemplo de diseño puramente en el navegador, y la visión general de librerías cliente MQTT ayuda si parte de tu stack está en otro lenguaje. La documentación oficial de route handlers de Next.js cubre en profundidad las opciones de runtime.

Conecta tu app Next.js a ConnectMQ

ConnectMQ es un broker MQTT gestionado con WebSocket seguro en el 8084 para los client components y TLS en el 8883 para los route handlers, de modo que ambos lados de una app Next.js hablan con el mismo broker. Haz prototipos contra el servidor MQTT público gratuito y pasa después a un broker MQTT en la nube privado. Regístrate gratis en el plan Serverless, sin necesidad de tarjeta de crédito.

Preguntas frecuentes

¿Puedo usar MQTT en los Server Components de Next.js?

No para datos en vivo. Los Server Components se renderizan una vez por petición y no pueden mantener una suscripción. Suscríbete en un client component o en un route handler de Node.js, y usa los Server Components para leer datos almacenados.

¿Funciona MQTT en el Edge runtime de Next.js?

El Edge runtime no ofrece las APIs de red de Node.js que MQTT.js usa para TCP y TLS. Establece export const runtime = "nodejs" en cualquier ruta que hable con el broker.

¿Cómo mantengo las credenciales MQTT fuera del navegador en Next.js?

Guárdalas en variables de entorno no públicas y úsalas solo en route handlers o en código de servidor. Todo lo que lleva el prefijo NEXT_PUBLIC_ acaba en el bundle del cliente.

¿Por qué veo dos conexiones MQTT en desarrollo?

React Strict Mode monta los componentes dos veces en desarrollo, y el hot reload de Next.js vuelve a ejecutar los módulos del servidor. Cierra el cliente en la limpieza del efecto y cachea los clientes de servidor en globalThis.

Artículos relacionados

Las mejores herramientas CLI de MQTT para pruebas, scripts y depuración

Una comparativa práctica de mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, la CLI de MQTT.js, curl y mqttui, con la misma prueba ejecutada contra broker.connectmq.com para que elijas la herramienta adecuada para scripts y depuración.

9 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.