Tutoriais de IoT

MQTT com Next.js: Padrões de Integração no Servidor e no Cliente que Funcionam de Verdade

Conheça as três formas de usar MQTT em um projeto Next.js com App Router: client components sobre WebSocket seguro, route handlers no servidor que publicam com credenciais privadas e uma ponte com Server-Sent Events.

Diagrama de integração de MQTT com Next.js mostrando client components, route handlers e um broker MQTT na nuvem
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

Integrar MQTT com Next.js confunde no começo porque o Next.js executa o seu código em vários lugares: no navegador, em um servidor Node.js, em funções serverless e, às vezes, na edge. Neste guia você vai aprender quais desses lugares conseguem manter uma conexão MQTT e vai construir os três padrões que cobrem quase todos os projetos: um client component que assina via WebSocket seguro, um route handler que publica com credenciais do lado do servidor e uma ponte com Server-Sent Events. Todos os exemplos usam o endpoint WebSocket do ConnectMQ em broker.connectmq.com e o MQTT.js v5 com o App Router.

Onde o MQTT pode rodar no Next.js?

O MQTT precisa de uma conexão de longa duração. Esse único fato decide onde ele se encaixa:

Runtime Conexão MQTT de longa duração? Transporte Uso típico
Navegador (client component) Sim, enquanto a aba estiver aberta wss:// porta 8084, caminho /mqtt Dashboards ao vivo, controles de dispositivos
Servidor Node.js (next start, container, VM) Sim mqtts:// 8883 ou wss:// 8084 Assinante compartilhado, ponte SSE, publicação
Função serverless Não, apenas por requisição mqtts:// 8883 Publicar um comando e desconectar
Edge runtime Não TCP do MQTT.js não suportado Evite para MQTT
Server Components Não, renderizam uma vez - Leia os dados de um banco de dados

Um Server Component renderiza e termina; ele não consegue manter uma assinatura viva. Server Actions também têm escopo de requisição. Então a escolha real é entre conectar a partir do navegador ou a partir de uma rota do servidor que roda em Node.js.

Instale a biblioteca cliente uma vez:

npm install mqtt

O MQTT.js v5 traz um build para Node.js e outro para navegador, e os bundlers escolhem o correto automaticamente. O guia de MQTT.js com Node.js cobre a biblioteca em si; aqui o foco é como ela se encaixa no Next.js.

Padrão 1: assinar em um client component via WebSocket

Para dashboards ao vivo, o navegador se conecta direto ao broker. Não há salto pelo servidor, a latência é mínima e o seu servidor Next.js não faz nenhum trabalho por mensagem. Navegadores não conseguem abrir TCP puro, então esse caminho sempre usa MQTT sobre WebSocket seguro. Nosso artigo sobre MQTT sobre WebSocket explica o transporte.

Crie um hook reutilizável em 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 };
}

Use-o em uma 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>
  );
}

O React escapa o texto no JSX, então renderizar payloads dessa forma é seguro contra injeção de scripts. Publique uma leitura de teste a partir de um terminal no servidor MQTT público, que serve apenas para testes, e veja-a aparecer:

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

Três detalhes que pegam no desenvolvimento

  1. Montagem dupla do Strict Mode. Em desenvolvimento, o React monta, desmonta e remonta os componentes para revelar bugs de limpeza. Sem client.end() no cleanup do effect, você fica com duas conexões. Gerar o client ID dentro do effect também evita que as duas instâncias briguem pelo mesmo ID.
  2. Nunca conecte no escopo do módulo. Um mqtt.connect() no nível superior de um arquivo cliente também roda durante a renderização desse componente no servidor, onde abre uma conexão TCP no seu servidor. Mantenha as conexões dentro de useEffect, que só roda no navegador.
  3. NEXT_PUBLIC_ significa público. Qualquer variável com esse prefixo é embutida no bundle JavaScript. Coloque ali apenas a URL do broker, nunca uma senha.

Padrão 2: publicar a partir de um route handler com credenciais privadas

Comandos que alteram o mundo físico, como abrir uma válvula em uma estação de saneamento ou reiniciar um dispositivo, não devem ser enviados com credenciais que qualquer visitante consegue ler. Em vez disso, o navegador chama a sua API, o servidor verifica o usuário e o servidor publica com credenciais guardadas em variáveis de ambiente.

Primeiro, um singleton do lado do servidor em lib/mqtt-server.js. Guardar o cliente em globalThis impede que o hot reload em desenvolvimento crie uma nova conexão a cada vez que um arquivo é salvo:

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

Depois, o route handler em 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 });
  }
  // Verifique aqui a sessão ou o papel do usuário antes de publicar.

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

O seu .env.local para um broker ConnectMQ privado fica assim:

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

Validar deviceId com um padrão estrito não é opcional. Se você deixar a entrada do usuário ir direto para um topic, uma requisição contendo +, # ou barras extras poderia publicar em topics que você nunca pretendeu.

Em plataformas serverless

Em hospedagem serverless, uma instância de função pode ser congelada ou descartada entre requisições, então não dá para confiar em uma conexão em cache. Conecte, publique e feche na mesma requisição:

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

Isso acrescenta um handshake TLS a cada requisição, o que é aceitável para comandos ocasionais, mas não para publicação em alta frequência. Observe que o ConnectMQ vincula os client IDs às credenciais emitidas em brokers privados; portanto, se você executar muitas funções de curta duração ao mesmo tempo, verifique no portal como as suas credenciais e client IDs são provisionados.

Padrão 3: transmitir MQTT para o navegador com Server-Sent Events

Às vezes você não quer que o navegador fale com o broker, por exemplo quando os dados do topic precisam ser filtrados por usuário. Uma rota Node.js pode manter uma única assinatura e distribuir as mensagens aos navegadores via Server-Sent Events (SSE). Isso só funciona em um servidor Node.js de longa duração, não em funções serverless com timeouts curtos.

Assine uma vez no singleton (adicione isto dentro de getMqttClient, depois de criar o cliente):

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

Depois, adicione 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" },
  });
}

Em um client component, new EventSource("/api/stream") recebe cada mensagem como um evento. Uma única conexão com o broker agora atende todos os espectadores, o que mantém você com folga dentro dos limites de conexão.

Solução de problemas em conexões MQTT com Next.js

Alguns problemas aparecem repetidamente ao ligar MQTT a um projeto Next.js:

  • "WebSocket connection failed" no console do navegador. Confira a URL com cuidado: ela deve ser wss://, porta 8084, e terminar em /mqtt. Uma página servida via HTTPS não pode usar ws:// sem criptografia.
  • "Module not found: net" ou "tls" durante o build. Algo puxou código exclusivo do servidor para um bundle cliente, geralmente por meio de um arquivo compartilhado que também importa módulos do Node.js. Mantenha lib/mqtt-server.js fora de qualquer arquivo marcado com "use client".
  • O route handler trava e depois estoura o timeout. O broker rejeitou o login ou a porta está bloqueada a partir do seu host, e publishAsync fica esperando uma conexão que nunca chega. Escute o evento error, registre-o em log e teste as mesmas credenciais com mosquitto_pub a partir do servidor.
  • Mensagens chegam duas vezes no servidor. O hot reload criou um segundo cliente porque o singleton não estava em cache no globalThis. Reinicie o servidor de desenvolvimento depois de corrigir.

Escolhendo o padrão certo

Use este checklist para decidir:

  1. Dados ao vivo somente leitura, públicos ou de baixa sensibilidade: Padrão 1, navegador via WebSocket com credenciais somente leitura.
  2. Ações com efeitos colaterais: Padrão 2, route handler com credenciais do lado do servidor e validação de entrada.
  3. Filtragem por usuário ou muitos espectadores em um plano pequeno: Padrão 3, ponte SSE em um servidor Node.js.
  4. Gráficos históricos: nenhum dos anteriores sozinho. Armazene as mensagens com um assinante separado e consulte o banco de dados a partir de um Server Component.

A maioria dos apps em produção combina 1 e 2: o navegador assina para receber atualizações ao vivo, e todo comando passa por um route handler. Para detalhes no nível de componentes, veja o tutorial de React com MQTT e, para payloads tipados, leia o guia de MQTT com TypeScript. O tutorial de app de chat MQTT é um bom exemplo de design puramente do lado do navegador, e a visão geral de bibliotecas de cliente MQTT ajuda se parte da sua stack estiver em outra linguagem. A documentação oficial de route handlers do Next.js aborda as opções de runtime em profundidade.

Conecte o seu app Next.js ao ConnectMQ

O ConnectMQ é um broker MQTT gerenciado com WebSocket seguro na porta 8084 para client components e TLS na 8883 para route handlers, de modo que os dois lados de um app Next.js falam com o mesmo broker. Faça o protótipo com o servidor MQTT público (gratuito) e depois migre para um broker MQTT na nuvem privado. Cadastre-se gratuitamente no plano Serverless, sem cartão de crédito.

Perguntas frequentes

Posso usar MQTT em Server Components do Next.js?

Não para dados ao vivo. Server Components renderizam uma vez por requisição e não conseguem manter uma assinatura. Assine em um client component ou em um route handler Node.js, e use Server Components para ler dados armazenados.

O MQTT funciona no Edge runtime do Next.js?

O Edge runtime não oferece as APIs de rede do Node.js que o MQTT.js usa para TCP e TLS. Defina export const runtime = "nodejs" em toda rota que conversa com o broker.

Como manter as credenciais MQTT fora do navegador no Next.js?

Guarde-as em variáveis de ambiente não públicas e use-as apenas em route handlers ou código de servidor. Tudo o que tiver o prefixo NEXT_PUBLIC_ acaba no bundle do cliente.

Por que vejo duas conexões MQTT em desenvolvimento?

O React Strict Mode monta os componentes duas vezes em desenvolvimento, e o hot reload do Next.js reexecuta os módulos do servidor. Feche o cliente no cleanup do effect e guarde os clientes do servidor em cache no globalThis.

Artigos relacionados

Melhores Ferramentas CLI MQTT para Testes, Scripts e Depuração

Uma comparação prática entre mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, a CLI do MQTT.js, curl e mqttui, com o mesmo teste rodando no broker.connectmq.com, para você escolher a ferramenta certa para scripts e depuração.

9 min de leitura

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.