Uygulamalı Eğitimler

Next.js ile MQTT: Client Component'ler ve Sunucu Route'ları

Next.js App Router'da MQTT: Client Component'te güvenli WebSocket ile canlı veri, Route Handler'lardan publish, önbelleğe alınmış sunucu client'ı ve SSE.

Güncellendi · 9 dk okuma

Next.js ile MQTT: Client Component'ler ve Sunucu Route'ları

Next.js ile MQTT en iyi ikiye bölündüğünde çalışır: tarayıcı, bir Client Component'ten güvenli WebSocket üzerinden canlı verilere abone olur; gizli bilgi gerektiren veya cihazlara yazan her şey ise sunucudan publish eden bir Route Handler üzerinden geçer. Bu rehber iki tarafı da App Router'da MQTT.js 5 ile kuruyor; ayrıca tarayıcının broker ile hiç konuşmaması gereken durumlar için isteğe bağlı bir Server-Sent Events route'u gösteriyor.

Örnekler broker.connectmq.com (/mqtt yoluyla WebSocket 8084, TLS 8883) ve testmqtt/nextjs/... altındaki test topic'lerini kullanır. Kendi host'unuzu ve kimlik bilgilerinizi yerine koyun.

Bir Next.js uygulamasında MQTT nerede çalışır

KodÇalıştığı yerTaşımaKullanım amacı
Client Component ("use client")Tarayıcıwss://…:8084/mqttCanlı dashboard'lar, bildirimler
Route Handler (app/**/route.ts)Node.js sunucusumqtts://…:8883Komutlar, yazma işlemleri, gizli kimlik bilgileri
Server ComponentSunucu, her render'daMQTT'den kaçınınVeritabanı veya API'den ilk veri

Server Component'ler istek başına bir kez (ya da build sırasında) render edilip biter; bu, bir subscription için yanlış yaşam süresidir. İlk durumu veritabanınızdan alın, güncellemeleri ise bir Client Component'e bırakın.

Client'ı kurun:

Install MQTT.js
npm install mqtt server-only

Client Component'te canlı veri

Tarayıcı TCP soketi açamaz, bu yüzden tek seçenek WebSocket üzerinden MQTT'dir. Sunucu render'ı sırasında asla çalışmaması için bağlantıyı useEffect içinde oluşturun ve cleanup fonksiyonunda kapatın:

app/devices/[id]/live-telemetry.tsx
"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>
  );
}

Bunu normal bir Server Component sayfasından render edin; güncel Next.js'te params bir promise'tir:

app/devices/[id]/page.tsx
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>
  );
}

Bu tarafta sık takılınan noktalar:

  • Geliştirmede iki bağlantı. React Strict Mode effect'i, cleanup'ı ve ardından effect'i yeniden çalıştırır. Cleanup'ta client.end(true) varsa ilk bağlantı anında kapanır. Yoksa her hot reload açıkta bir bağlantı bırakır.
  • Mixed content. HTTPS ile sunulan bir sayfa yalnızca wss:// soketleri açabilir. ws://broker...:8083 http://localhost üzerinde çalışır, production'da ise WebSocket connection to 'ws://…' failed hatasıyla başarısız olur.
  • Yanlış yol. WebSocket üzerinden MQTT, broker'ın yolunu ister (burada /mqtt). Yol eksikse upgrade genellikle HTTP 404 ile biter; bkz. WebSocket path 404 ve WebSocket connection failed.
  • connect içinde subscribe. Her yeniden bağlanmadan sonra tekrar çalışır; session kalıcı değilse istediğiniz de tam olarak budur. Aynı filtreye iki kez abone olmak sorun yaratmaz.

Aynı akışa birden fazla bileşen ihtiyaç duyuyorsa, her bileşen için ayrı bağlantı açmak yerine client'ı bir context provider'a taşıyın; desen React ile MQTT yazısındakiyle aynıdır.

Tarayıcının yapmasına izin verilenler

NEXT_PUBLIC_ önekli her şey build sırasında bundle'a gömülür; bu kimlik bilgilerini herkese açık sayın. Tarayıcıya, ACL'i dashboard'un gösterdiği topic'lerde yalnızca subscribe izni veren ayrı bir kullanıcı tanımlayın. Komut publish etmek, aşağıda anlatıldığı gibi sunucu üzerinden yapılır. Kullanıcı bazlı erişim gerekiyorsa kısa ömürlü token'lar tarayıcıda WebSocket kimlik doğrulaması yazısında anlatılıyor.

Route Handler'dan publish etmek

Route Handler'lar Node.js runtime'ında (varsayılan) çalışır, dolayısıyla 8883 portunda TLS üzerinden MQTT kullanabilir. Her istekte yeni bağlantı açmak her seferinde bir TCP ve TLS handshake'i demektir; bu yüzden sunucu süreci başına bir client'ı önbellekte tutun. globalThis üzerinde saklamak, next dev'deki hot module reload'lardan da sağ çıkmasını sağlar:

lib/mqtt-server.ts
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", bir Client Component'ten yanlışlıkla yapılan importu build hatasına çevirir; böylece publish kimlik bilgileri asla tarayıcıya ulaşmaz. Rastgele client ID önemlidir: aynı ID'yi paylaşan birden fazla sunucu instance'ı birbirini sürekli broker'dan atar (yinelenen client ID).

Handler girdiyi doğrular, QoS 1 ile publish eder ve yanıt vermeden önce PUBACK'i bekler:

app/api/devices/[id]/command/route.ts
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 });
  }
}

Cihaz ID kontrolü süs değildir: /, + veya # içeren bir değer, çağıranın amaçladığınızdan farklı bir topic'e yazmasına izin verirdi. Filtrelerinizi topic matcher ile kontrol edin.

Server Action da aynı şekilde çalışır: bir sunucu dosyasındaki fonksiyonu "use server" ile işaretleyin, içinde getMqtt() çağırın ve publish edin. Form işleme yapınıza hangisi uyuyorsa onu kullanın; MQTT kısmı aynıdır.

Serverless ve uzun ömürlü sunucular

next start ile, Docker'da veya uzun ömürlü herhangi bir Node.js host'unda önbellekteki client bağlı kalır ve her publish tek bir gidiş-dönüştür. Serverless platformlarda bir fonksiyon instance'ı istekler arasında dondurulabilir; uyandığında soket ölmüş olabilir ve MQTT.js publish'i göndermeden önce yeniden bağlanır. Bu çalışır, ancak bir aradan sonraki ilk istek yeniden bağlanmanın bedelini öder. Komutlar seyrekse sorun yok. Sıksa uzun ömürlü bir deployment tercih edin.

Broker bağlantısı olmadan tarayıcıya streaming

Bazen tarayıcının broker ile hiç konuşmaması gerekir: katı firewall'lar, tarayıcıda hiç kimlik bilgisi olmaması ya da verileri kullanıcı bazında filtreleme ihtiyacı. Bir Route Handler sunucuda abone olup mesajları Server-Sent Events olarak iletebilir:

app/api/devices/[id]/stream/route.ts
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" },
  });
}

Tarayıcıda new EventSource("/api/devices/dev-1/stream") her mesajı bir event olarak alır. Bu yöntem açık her sekme için bir MQTT bağlantısı tutar; bu yüzden uzun ömürlü bir sunucuda dahili araçlara ve küçük kitlelere uygundur. Büyük kitlelerde doğrudan WebSocket yaklaşımı daha iyi ölçeklenir, çünkü fan-out'u broker yapar.

Uçtan uca test edin

  1. next dev'i çalıştırın ve /devices/dev-1 sayfasını açın. Durum connected göstermelidir.
  2. Online MQTT client'ı başka bir sekmede açın, broker.connectmq.com'a bağlanın ve testmqtt/nextjs/dev-1/telemetry topic'ine {"temp":21.4} publish edin. Sayfa anında güncellenir.
  3. Online client'ta testmqtt/nextjs/+/command topic'ine abone olun, ardından Route Handler'ı çağırın:
Trigger a command
curl -X POST http://localhost:3000/api/devices/dev-1/command \
  -H "Content-Type: application/json" \
  -d '{"action":"identify"}'

Komut online client'ta görünür ve yanıt {"ok":true} olur. Handler'ın bilinmeyen aksiyonları reddettiğini doğrulamak için {"action":"format-disk"} deneyin; ağı kısa süreliğine keserek durumun reconnecting'e geçip geri döndüğünü izleyin.

Next.js test topic'lerini canlı izleyin

Subscription'ları güvenli WebSocket üzerinden Client Component'lerde, gizli bilgileri ve yazma işlemlerini önbellekteki tek bir sunucu client'ıyla Route Handler'larda tutun; SSE'ye yalnızca tarayıcının broker'dan uzak durması gerektiğinde başvurun. Bu ayrımla aynı MQTT.js kodu next dev'de, CI'da ve production'da yalnızca ortam değişkenleri değişerek çalışır.

Sıkça sorulan sorular

Bir Next.js sayfası 1883 portu üzerinden MQTT'ye bağlanabilir mi?

Tarayıcıdan bağlanamaz. Tarayıcılar ham TCP soketi açamaz, bu yüzden bir Client Component WebSocket üzerinden MQTT kullanmalıdır (wss://broker.connectmq.com:8084/mqtt). Route Handler'lardaki sunucu kodu Node.js üzerinde çalışır ve 1883'te mqtt:// ya da 8883'te mqtts:// kullanabilir.

Geliştirme ortamında neden iki MQTT bağlantısı görüyorum?

React Strict Mode, geliştirme ortamında her bileşeni mount eder, unmount eder ve yeniden mount eder; bu yüzden useEffect iki kez çalışır. Cleanup fonksiyonu client.end() çağırıyorsa ilk bağlantı hemen kapanır ve yalnızca biri açık kalır. Production build'lerde bileşen bir kez mount edilir.

MQTT kimlik bilgilerini NEXT_PUBLIC_ değişkenlerine koymak güvenli mi?

Yalnızca her ziyaretçinin okumasında sakınca olmayan bilgiler için. NEXT_PUBLIC_ değerleri JavaScript bundle'ına gömülür. Tarayıcıya ACL ile salt okunur topic'lerle sınırlandırılmış bir kullanıcı verin; publish kimlik bilgilerini Route Handler'ların kullandığı, yalnızca sunucuda bulunan ortam değişkenlerinde tutun.