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ığı yer | Taşıma | Kullanım amacı |
|---|---|---|---|
Client Component ("use client") | Tarayıcı | wss://…:8084/mqtt | Canlı dashboard'lar, bildirimler |
Route Handler (app/**/route.ts) | Node.js sunucusu | mqtts://…:8883 | Komutlar, yazma işlemleri, gizli kimlik bilgileri |
| Server Component | Sunucu, her render'da | MQTT'den kaçının | Veritabanı 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:
npm install mqtt server-onlyClient 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:
"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:
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...:8083http://localhostüzerinde çalışır, production'da iseWebSocket connection to 'ws://…' failedhatası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. connectiç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:
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:
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:
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
next dev'i çalıştırın ve/devices/dev-1sayfasını açın. Durumconnectedgöstermelidir.- Online MQTT client'ı başka bir sekmede açın,
broker.connectmq.com'a bağlanın vetestmqtt/nextjs/dev-1/telemetrytopic'ine{"temp":21.4}publish edin. Sayfa anında güncellenir. - Online client'ta
testmqtt/nextjs/+/commandtopic'ine abone olun, ardından Route Handler'ı çağırın:
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.


