<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Streams - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Streams - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 26 Jul 2026 22:33:39 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/streams/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Use Streams in Flutter ]]>
                </title>
                <description>
                    <![CDATA[ Flutter, Google's open-source UI software development toolkit, has rapidly become a preferred choice for building natively compiled, cross-platform applications from a single codebase. Its declarative UI paradigm, coupled with robust performance, hel... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-streams-in-flutter/</link>
                <guid isPermaLink="false">69028db72c8d547cd3a4cb95</guid>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Dart ]]>
                    </category>
                
                    <category>
                        <![CDATA[ flutter-aware ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Streams ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Atuoha Anthony ]]>
                </dc:creator>
                <pubDate>Wed, 29 Oct 2025 21:57:11 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761774911652/ca6749c7-391b-4a9f-9264-5f15e54855ee.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Flutter, Google's open-source UI software development toolkit, has rapidly become a preferred choice for building natively compiled, cross-platform applications from a single codebase. Its declarative UI paradigm, coupled with robust performance, helps developers craft beautiful and highly responsive user experiences.</p>
<p>But in order to build such dynamic and efficient applications in Flutter, you’ll need a profound understanding of asynchronous programming. And within that domain, <strong>streams</strong> are an indispensable tool.</p>
<p>This comprehensive guide will delve deep into the world of streams in Flutter, demystifying their core concepts, illustrating their practical applications, and providing a wealth of code examples to solidify your understanding.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-challenge-of-asynchronous-operations">The Challenge of Asynchronous Operations</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-are-streams-the-flow-of-asynchronous-events">What are Streams? The Flow of Asynchronous Events</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-analogy-the-river-of-data">Analogy: The River of Data</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-why-streams-are-crucial-in-flutter">Why Streams are Crucial in Flutter</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-key-concepts-of-streams">Key Concepts of Streams</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-streamcontroller">1. StreamController</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-types-of-streams-single-subscription-vs-broadcast">Types of Streams: Single-Subscription vs. Broadcast</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-streambuilder">2. StreamBuilder</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-streamsubscription">3. StreamSubscription</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-async-programming-with-streams-async-and-yield">Async Programming with Streams: async* and yield</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-work-with-streams-practical-scenarios">How to Work with Streams: Practical Scenarios</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-transforming-streams-map-where-take-skip-and-so-on">1. Transforming Streams: map, where, take, skip, and so on.</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-combining-streams">2. Combining Streams</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-real-world-examples-in-flutter">Real-World Examples in Flutter</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-fetching-data-from-a-network-with-live-updates">1. Fetching Data from a Network with Live Updates</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-user-input-handling-debouncing-a-search-field">2. User Input Handling: Debouncing a Search Field</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-best-practices-and-considerations">Best Practices and Considerations</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-advanced-concepts-brief-introduction">Advanced Concepts (Brief Introduction)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-references">References</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-official-documentation">Official Documentation</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-key-packages">Key Packages</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-articles-and-tutorials-general">Articles and Tutorials (General)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-related-state-management-patterns">Related State Management Patterns</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-http-package-for-network-examples">HTTP Package (for Network Examples)</a></p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before we embark on this journey, make sure you have a basic understanding of:</p>
<ol>
<li><p><strong>Dart programming language:</strong> Familiarity with Dart's syntax, variables, functions, and object-oriented concepts.</p>
</li>
<li><p><strong>Flutter fundamentals:</strong> Knowledge of Flutter widgets, <code>StatefulWidget</code> vs. <code>StatelessWidget</code>, and basic UI layout.</p>
</li>
<li><p><strong>Asynchronous programming basics (Dart's</strong> <code>Future</code>): An understanding of what <code>Future</code> represents and how the <code>async</code>/<code>await</code> keywords work to handle single asynchronous operations. If you're new to <code>Future</code>, think of it as a placeholder for a value that will be available at some point in the future.</p>
</li>
</ol>
<p>If you're comfortable with these concepts, you're well-prepared to explore the power of streams.</p>
<h2 id="heading-the-challenge-of-asynchronous-operations">The Challenge of Asynchronous Operations</h2>
<p>In modern applications, blocking the UI is bad. Imagine an app freezing while it fetches data from the internet, processes a large file, or performs a complex calculation. This leads to a frustrating user experience.</p>
<p>Traditional synchronous programming executes tasks sequentially. When a long-running task is encountered, the entire program waits for it to complete. Asynchronous programming, on the other hand, allows tasks to run in the background without blocking the main execution thread, particularly the UI thread.</p>
<p>Dart's <code>Future</code> class is excellent for handling single asynchronous events (for example, a single network request that returns a single piece of data). But what if you have a continuous flow of events? What if you need to listen for data updates over time, like real-time chat messages, sensor readings, or continuous user input? This is where <code>Streams</code> shine.</p>
<h2 id="heading-what-are-streams-the-flow-of-asynchronous-events">What are Streams? The Flow of Asynchronous Events</h2>
<p>In Flutter (and Dart), a <strong>stream</strong> is fundamentally a sequence of asynchronous events. Think of it as a conveyor belt carrying data items over time. These events can be:</p>
<ol>
<li><p><strong>Data values:</strong> The actual information being transmitted (for example, integers, strings, custom objects).</p>
</li>
<li><p><strong>Errors:</strong> Signals that something went wrong during the event sequence.</p>
</li>
<li><p><strong>Stream termination:</strong> A signal indicating that no more events will be sent.</p>
</li>
</ol>
<p>Streams provide a powerful reactive programming paradigm, allowing your application to react to events as they occur, without blocking the user interface. This enables the creation of highly responsive and efficient applications.</p>
<h3 id="heading-analogy-the-river-of-data">Analogy: The River of Data</h3>
<p>Imagine a river. The water flowing in the river is like the data (events) in a stream.</p>
<ul>
<li><p>You can set up a <strong>listener</strong> (like a fishing net) to catch fish (data) as they flow by.</p>
</li>
<li><p>Sometimes, debris (errors) might come down the river.</p>
</li>
<li><p>Eventually, the river might dry up (stream termination).</p>
</li>
</ul>
<p>This continuous flow is what makes streams distinct from <code>Future</code> objects, which represent a single "delivery" rather than a continuous "flow."</p>
<h3 id="heading-why-streams-are-crucial-in-flutter">Why Streams are Crucial in Flutter</h3>
<ol>
<li><p><strong>Real-time updates:</strong> Ideal for chat applications, live data feeds (stocks, weather), and sensor data.</p>
</li>
<li><p><strong>Event handling:</strong> Managing continuous user input (for example, search bar suggestions), gestures, or notifications.</p>
</li>
<li><p><strong>Decoupling logic:</strong> Separating data fetching/processing from UI rendering, leading to cleaner, more maintainable code.</p>
</li>
<li><p><strong>State management:</strong> Many advanced Flutter state management solutions (like BLoC, Provider's <code>StreamProvider</code>) leverage Streams extensively.</p>
</li>
</ol>
<p>Here's a visual representation of how a stream works:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761638371213/7029f337-d63d-4178-b24c-6678173349ed.png" alt="a visual representation of how a stream works" class="image--center mx-auto" width="1078" height="857" loading="lazy"></p>
<h2 id="heading-key-concepts-of-streams">Key Concepts of Streams</h2>
<p>To effectively work with Streams, you need to understand a few core components:</p>
<h3 id="heading-1-streamcontroller">1. <code>StreamController</code></h3>
<p>A <code>StreamController</code> is your primary tool for creating and managing streams. It acts as both a <strong>sink</strong> (where you add data/events to the stream) and a <strong>source</strong> (from which you can get the stream to listen to). It's the mechanism that allows you to "control" the flow of events into your stream.</p>
<p>The purpose of a <code>StreamController</code> is to create, manage, and add events (data, errors, done signals) to a stream.</p>
<p><strong>Code sample:</strong></p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:async'</span>; <span class="hljs-comment">// Required for StreamController</span>

<span class="hljs-keyword">void</span> main() {
  <span class="hljs-comment">// 1. Create a StreamController for String data</span>
  <span class="hljs-comment">//    The type argument &lt;String&gt; specifies the type of data this stream will emit.</span>
  <span class="hljs-keyword">final</span> streamController = StreamController&lt;<span class="hljs-built_in">String</span>&gt;();

  <span class="hljs-comment">// 2. Get the stream from the controller</span>
  <span class="hljs-comment">//    This is the stream that other parts of your application will listen to.</span>
  Stream&lt;<span class="hljs-built_in">String</span>&gt; myStream = streamController.stream;

  <span class="hljs-comment">// 3. Listen to the stream</span>
  <span class="hljs-comment">//    The .listen() method registers a callback to handle incoming data.</span>
  <span class="hljs-comment">//    It returns a StreamSubscription, which can be used to manage the listener.</span>
  <span class="hljs-keyword">var</span> subscription = myStream.listen((data) {
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'Received data: <span class="hljs-subst">$data</span>'</span>);
  });

  <span class="hljs-comment">// 4. Add data to the stream</span>
  <span class="hljs-comment">//    Use the sink property of the controller to add events.</span>
  streamController.sink.add(<span class="hljs-string">'Hello'</span>);
  streamController.sink.add(<span class="hljs-string">'Flutter'</span>);
  streamController.sink.add(<span class="hljs-string">'Streams!'</span>);

  <span class="hljs-comment">// 5. Simulate an error</span>
  <span class="hljs-comment">//    You can also add errors to the stream.</span>
  streamController.sink.addError(<span class="hljs-string">'Something went wrong!'</span>);

  <span class="hljs-comment">// 6. Close the stream when you're done</span>
  <span class="hljs-comment">//    It's crucial to close the stream controller to prevent memory leaks.</span>
  <span class="hljs-comment">//    This also sends a "done" event to all listeners.</span>
  streamController.close();

  <span class="hljs-comment">// Optionally cancel the subscription if no longer needed before stream closes</span>
  <span class="hljs-comment">// subscription.cancel();</span>
}
</code></pre>
<p>In this code:</p>
<p>The line <code>final streamController = StreamController&lt;String&gt;();</code> initializes a <code>StreamController</code> designed to handle <code>String</code> data, though it can be created for any data type (for example, <code>int</code>, custom classes, and so on). The <code>Stream&lt;String&gt; myStream = streamController.stream;</code> statement retrieves the actual <code>Stream</code> that consumers, such as <code>StreamBuilder</code> widgets or other listeners, can subscribe to.</p>
<p>By calling <code>myStream.listen((data) { ... });</code>, you set up a listener that executes the provided callback function each time <code>streamController.sink.add()</code> is invoked with new data. To emit data, you use <code>streamController.sink.add('Hello');</code>, while <code>streamController.sink.addError('Something went wrong!');</code> allows you to emit error events that listeners can respond to.</p>
<p>Finally, calling <code>streamController.close();</code> is essential, as it notifies all listeners that the stream is complete and will emit no further events, while also freeing resources. Neglecting to close a controller can cause memory leaks, especially in long-running applications.</p>
<h3 id="heading-types-of-streams-single-subscription-vs-broadcast">Types of Streams: Single-Subscription vs. Broadcast</h3>
<p>Streams come in two flavors, each suited for different use cases:</p>
<ol>
<li><p><strong>Single-Subscription Streams (Default):</strong></p>
<ul>
<li><p><strong>Purpose:</strong> Designed for a single listener. Once you <code>listen()</code> to it, you cannot listen again unless the first subscription is cancelled or the stream is created as a broadcast stream.</p>
</li>
<li><p><strong>Use Cases:</strong> Data fetches (like a file read), HTTP responses where you only need one component to consume the result.</p>
</li>
<li><p><strong>Example:</strong> When you call <code>http.get(...).asStream()</code>, you get a single-subscription stream.</p>
</li>
</ul>
</li>
<li><p><strong>Broadcast Streams:</strong></p>
<ul>
<li><p><strong>Purpose:</strong> Allows multiple listeners to subscribe and receive events simultaneously. Events are delivered to all active listeners.</p>
</li>
<li><p><strong>Use Cases:</strong> Real-time data updates where multiple UI widgets or logic components need the same information (for example, a global authentication status, real-time notifications).</p>
</li>
<li><p><strong>Creation:</strong> You create a broadcast stream by passing <code>broadcast: true</code> to the <code>StreamController</code> constructor.</p>
</li>
</ul>
</li>
</ol>
<p><strong>Code sample (Broadcast Stream):</strong></p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:async'</span>;

<span class="hljs-keyword">void</span> main() <span class="hljs-keyword">async</span> {
  <span class="hljs-comment">// Create a StreamController that supports multiple listeners</span>
  <span class="hljs-keyword">final</span> broadcastController = StreamController&lt;<span class="hljs-built_in">int</span>&gt;.broadcast();

  <span class="hljs-comment">// Listener 1</span>
  broadcastController.stream.listen((event) {
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'Listener 1 received: <span class="hljs-subst">$event</span>'</span>);
  }, onError: (e) =&gt; <span class="hljs-built_in">print</span>(<span class="hljs-string">'Listener 1 error: <span class="hljs-subst">$e</span>'</span>));

  <span class="hljs-comment">// Listener 2 (can listen even while Listener 1 is active)</span>
  broadcastController.stream.listen((event) {
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'  Listener 2 received: <span class="hljs-subst">$event</span>'</span>);
  }, onError: (e) =&gt; <span class="hljs-built_in">print</span>(<span class="hljs-string">'  Listener 2 error: <span class="hljs-subst">$e</span>'</span>));

  broadcastController.sink.add(<span class="hljs-number">1</span>);
  <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(milliseconds: <span class="hljs-number">500</span>)); <span class="hljs-comment">// Simulate delay</span>
  broadcastController.sink.add(<span class="hljs-number">2</span>);
  <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(milliseconds: <span class="hljs-number">500</span>));
  broadcastController.sink.addError(<span class="hljs-string">'Broadcast error!'</span>);
  <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(milliseconds: <span class="hljs-number">500</span>));
  broadcastController.sink.add(<span class="hljs-number">3</span>);

  <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>)); <span class="hljs-comment">// Give time for events to process</span>
  broadcastController.close(); <span class="hljs-comment">// Close the controller, notifying all listeners</span>
}
</code></pre>
<p>In <code>final broadcastController = StreamController&lt;int&gt;.broadcast();</code>, the key is <code>.broadcast()</code>. This ensures that multiple <code>listen()</code> calls on <code>broadcastController.stream</code> will all receive events. Both <code>Listener 1</code> and <code>Listener 2</code> independently subscribe and receive <code>1</code>, <code>2</code>, the error, and <code>3</code>.</p>
<p>Choose the stream type carefully based on your application's needs. When in doubt, start with a single-subscription stream and convert to broadcast only if truly necessary, as broadcast streams can sometimes make debugging event flow more complex.</p>
<h3 id="heading-2-streambuilder">2. <code>StreamBuilder</code></h3>
<p>The <code>StreamBuilder</code> widget is Flutter's dedicated tool for integrating Streams directly into your UI. It's a <code>StatefulWidget</code> under the hood that listens to a stream and rebuilds its UI whenever new data, errors, or completion signals arrive. This makes your UI reactive to data changes without manually calling <code>setState()</code>.</p>
<p><code>StreamBuilder</code> automatically rebuilds a part of the UI in response to new data from a stream.</p>
<p><strong>Code sample:</strong></p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'dart:async'</span>;

<span class="hljs-keyword">void</span> main() =&gt; runApp(MyApp());

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyApp</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> MaterialApp(
      title: <span class="hljs-string">'StreamBuilder Demo'</span>,
      theme: ThemeData(primarySwatch: Colors.blue),
      home: StreamBuilderPage(),
    );
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">StreamBuilderPage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  _StreamBuilderPageState createState() =&gt; _StreamBuilderPageState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_StreamBuilderPageState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">StreamBuilderPage</span>&gt; </span>{
  <span class="hljs-keyword">final</span> _dataController = StreamController&lt;<span class="hljs-built_in">int</span>&gt;();
  <span class="hljs-built_in">int</span> _counter = <span class="hljs-number">0</span>;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    <span class="hljs-comment">// Start adding data to the stream every second</span>
    Timer.periodic(<span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>), (timer) {
      _counter++;
      _dataController.sink.add(_counter);
      <span class="hljs-keyword">if</span> (_counter &gt;= <span class="hljs-number">5</span>) {
        timer.cancel(); <span class="hljs-comment">// Stop adding after 5 events</span>
        _dataController.close(); <span class="hljs-comment">// Close the stream</span>
      }
    });
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(title: Text(<span class="hljs-string">'StreamBuilder Example'</span>)),
      body: Center(
        <span class="hljs-comment">// StreamBuilder is the core widget here</span>
        child: StreamBuilder&lt;<span class="hljs-built_in">int</span>&gt;(
          stream: _dataController.stream, <span class="hljs-comment">// The stream to listen to</span>
          <span class="hljs-comment">// initialData: 0, // Optional: A value to display before any stream data arrives</span>
          builder: (context, snapshot) {
            <span class="hljs-comment">// The builder function is called every time the stream emits a new event.</span>
            <span class="hljs-comment">// 'snapshot' contains the latest state of the stream.</span>

            <span class="hljs-keyword">if</span> (snapshot.connectionState == ConnectionState.waiting) {
              <span class="hljs-comment">// Show a loading indicator while waiting for the first event</span>
              <span class="hljs-keyword">return</span> CircularProgressIndicator();
            } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (snapshot.hasError) {
              <span class="hljs-comment">// Display an error message if the stream emits an error</span>
              <span class="hljs-keyword">return</span> Text(<span class="hljs-string">'Error: <span class="hljs-subst">${snapshot.error}</span>'</span>, style: TextStyle(color: Colors.red));
            } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (snapshot.hasData) {
              <span class="hljs-comment">// Display the received data</span>
              <span class="hljs-keyword">return</span> Text(
                <span class="hljs-string">'Received Data: <span class="hljs-subst">${snapshot.data}</span>'</span>,
                style: TextStyle(fontSize: <span class="hljs-number">24</span>, fontWeight: FontWeight.bold),
              );
            } <span class="hljs-keyword">else</span> {
              <span class="hljs-comment">// This case might occur if the stream closes without sending data</span>
              <span class="hljs-comment">// or initialData wasn't provided and no data has arrived yet.</span>
              <span class="hljs-keyword">return</span> Text(<span class="hljs-string">'No data yet or stream closed.'</span>);
            }
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          <span class="hljs-comment">// You could also add data from a button press, for instance</span>
          <span class="hljs-comment">// _dataController.sink.add(99);</span>
        },
        child: Icon(Icons.add),
      ),
    );
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    <span class="hljs-comment">// IMPORTANT: Close the StreamController when the widget is disposed</span>
    <span class="hljs-comment">// to prevent memory leaks.</span>
    _dataController.close();
    <span class="hljs-keyword">super</span>.dispose();
  }
}
</code></pre>
<p>This is a lot – so let’s explore what’s going on in this code:</p>
<p>A <code>StreamBuilder&lt;int&gt;(stream: _dataController.stream, builder: (context, snapshot) { ... })</code> widget listens to a stream and rebuilds the UI in response to new events or connection state changes.</p>
<p>The <code>stream</code> parameter specifies the stream to listen to, while the <code>builder</code> function is called every time the stream emits a new event or changes state. It receives the <code>BuildContext</code> and an <code>AsyncSnapshot&lt;T&gt;</code>, which encapsulates the latest stream data and status.</p>
<p>The <code>snapshot</code> provides key details about the stream:</p>
<ul>
<li><p><code>snapshot.connectionState</code> shows the current connection state, <code>none</code> (no stream connected), <code>waiting</code> (connected but no data yet), <code>active</code> (actively receiving events), and <code>done</code> (stream closed).</p>
</li>
<li><p><code>snapshot.hasData</code> and <code>snapshot.data</code> indicate whether the stream has emitted data and provide access to the most recent value.</p>
</li>
<li><p><code>snapshot.hasError</code> and <code>snapshot.error</code> handle errors emitted by the stream.</p>
</li>
</ul>
<p>In the <code>builder</code>, conditional rendering (using <code>if</code> or <code>switch</code> statements) allows you to display appropriate UI for each state, such as loading indicators, error messages, or the actual data.</p>
<p>You can also specify <code>initialData</code> to provide a starting value before the first event arrives, avoiding unnecessary loading indicators if you already have a known initial state.</p>
<p>Finally, always close your <code>StreamController</code> in the widget’s <code>dispose()</code> method to prevent memory leaks when the widget is removed from the widget tree.</p>
<h3 id="heading-3-streamsubscription">3. <code>StreamSubscription</code></h3>
<p>When you call <code>stream.listen()</code>, it returns a <code>StreamSubscription</code> object. This object represents the active connection between your listener and the stream. It's essential for managing the lifecycle of your listener.</p>
<p><code>StreamSubscription</code> manages an active listener on a stream, primarily for cancelling it.</p>
<p><strong>Code sample (already shown partially in</strong> <code>StreamController</code> example, but emphasizing <code>StreamSubscription</code>):</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:async'</span>;

<span class="hljs-keyword">void</span> main() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> streamController = StreamController&lt;<span class="hljs-built_in">String</span>&gt;();

  StreamSubscription&lt;<span class="hljs-built_in">String</span>&gt;? subscription; <span class="hljs-comment">// Declare it nullable</span>

  <span class="hljs-comment">// Listen to the stream and store the subscription object</span>
  subscription = streamController.stream.listen(
    (data) {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Received data: <span class="hljs-subst">$data</span>'</span>);
      <span class="hljs-comment">// After receiving 'Stop', cancel the subscription</span>
      <span class="hljs-keyword">if</span> (data == <span class="hljs-string">'Stop'</span>) {
        <span class="hljs-built_in">print</span>(<span class="hljs-string">'Cancelling subscription...'</span>);
        subscription?.cancel(); <span class="hljs-comment">// Use null-safe call</span>
        streamController.close(); <span class="hljs-comment">// Close the controller after stopping</span>
      }
    },
    onError: (error) {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Error: <span class="hljs-subst">$error</span>'</span>);
    },
    onDone: () {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Stream is done (closed)!'</span>);
    },
    cancelOnError: <span class="hljs-keyword">false</span>, <span class="hljs-comment">// Don't cancel subscription if an error occurs</span>
  );

  streamController.sink.add(<span class="hljs-string">'Start'</span>);
  <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(milliseconds: <span class="hljs-number">500</span>));
  streamController.sink.add(<span class="hljs-string">'Continue'</span>);
  <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(milliseconds: <span class="hljs-number">500</span>));
  streamController.sink.add(<span class="hljs-string">'Stop'</span>); <span class="hljs-comment">// This will trigger cancellation</span>

  <span class="hljs-comment">// If the stream wasn't closed by 'Stop' logic, ensure it's closed here after a delay</span>
  <span class="hljs-comment">// await Future.delayed(Duration(seconds: 2));</span>
  <span class="hljs-comment">// if (!streamController.isClosed) {</span>
  <span class="hljs-comment">//   streamController.close();</span>
  <span class="hljs-comment">// }</span>
}
</code></pre>
<p>In this code, a <code>StreamSubscription&lt;String&gt;? subscription;</code> variable is declared to hold the subscription to a stream. When <code>subscription = streamController.stream.listen(...)</code> is called, the <code>listen</code> method returns a <code>StreamSubscription</code> object that allows you to control the stream’s behavior.</p>
<p>The <code>subscription?.cancel();</code> method is the most crucial part: it detaches the listener from the stream, preventing it from receiving further events. This is especially important for single-subscription streams or when you need to stop listening to a broadcast stream temporarily. Forgetting to cancel subscriptions, particularly in <code>StatefulWidgets</code>, can lead to memory leaks.</p>
<p>The <code>listen</code> method accepts several parameters:</p>
<ul>
<li><p>The first positional argument is the <code>onData</code> callback (triggered when new data arrives)</p>
</li>
<li><p><code>onError</code> is an optional callback for handling errors</p>
</li>
<li><p><code>onDone</code> is an optional callback for when the stream closes</p>
</li>
<li><p>And <code>cancelOnError</code> is a boolean that, when true, automatically cancels the subscription after the first error, stopping all further events.</p>
</li>
</ul>
<h3 id="heading-async-programming-with-streams-async-and-yield">Async Programming with Streams: <code>async*</code> and <code>yield</code></h3>
<p>While <code>StreamController</code> gives you fine-grained control over adding events, Dart also provides a more declarative way to create streams using <code>async*</code> and <code>yield</code>. This syntax is similar to <code>async</code>/<code>await</code> for <code>Future</code>s but for continuous streams of data.</p>
<ol>
<li><p><code>async*</code> (async-generator function): A function marked with <code>async*</code> returns a <code>Stream</code>.</p>
</li>
<li><p><code>yield</code>: Inside an <code>async*</code> function, <code>yield</code> is used to emit data events to the stream.</p>
</li>
</ol>
<p>We use <code>async*</code> and <code>yield</code> to easily create streams by iteratively yielding data without manually managing a <code>StreamController</code>.</p>
<p><strong>Code sample:</strong></p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:async'</span>;

<span class="hljs-comment">// A function that returns a Stream of integers</span>
Stream&lt;<span class="hljs-built_in">int</span>&gt; countStream(<span class="hljs-built_in">int</span> max) <span class="hljs-keyword">async</span>* {
  <span class="hljs-keyword">for</span> (<span class="hljs-built_in">int</span> i = <span class="hljs-number">1</span>; i &lt;= max; i++) {
    <span class="hljs-comment">// Simulate some asynchronous work</span>
    <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(milliseconds: <span class="hljs-number">500</span>));
    <span class="hljs-comment">// Yield (emit) the current value to the stream</span>
    <span class="hljs-keyword">yield</span> i;
  }
  <span class="hljs-comment">// No explicit close() needed; the stream closes automatically when the function completes.</span>
}

<span class="hljs-keyword">void</span> main() {
  <span class="hljs-built_in">print</span>(<span class="hljs-string">'Starting stream...'</span>);
  <span class="hljs-comment">// Listen to the stream generated by countStream</span>
  <span class="hljs-keyword">final</span> subscription = countStream(<span class="hljs-number">5</span>).listen(
    (data) {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Received: <span class="hljs-subst">$data</span>'</span>);
    },
    onDone: () {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Stream is done!'</span>);
    },
    onError: (error) {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Error in stream: <span class="hljs-subst">$error</span>'</span>);
    },
  );

  <span class="hljs-comment">// You can still cancel the subscription manually if needed</span>
  <span class="hljs-comment">// Future.delayed(Duration(seconds: 2), () =&gt; subscription.cancel());</span>
}
</code></pre>
<p>In this code, the <code>Stream&lt;int&gt; countStream(int max) async*</code> function uses the <code>async*</code> keyword to indicate that it returns a stream. Inside it, <code>await Future.delayed(Duration(milliseconds: 500));</code> demonstrates that <code>await</code> can still be used within an <code>async*</code> function to pause execution until a future completes, enabling asynchronous operations during stream generation.</p>
<p>The <code>yield i;</code> statement is what adds each value to the stream. Every time it’s called, the value <code>i</code> is emitted as an event, and the function pauses until the next value is ready or requested.</p>
<p>When the function completes (for example, when the <code>for</code> loop finishes), the stream automatically closes and emits an <code>onDone</code> event to all listeners, making stream management simpler than using a <code>StreamController</code> manually.</p>
<p>This <code>async*</code>/<code>yield</code> syntax is particularly elegant for generating streams of data where the sequence is known or can be computed iteratively.</p>
<h2 id="heading-how-to-work-with-streams-practical-scenarios">How to Work with Streams: Practical Scenarios</h2>
<p>Let's explore common patterns and operations with streams.</p>
<h3 id="heading-1-transforming-streams-map-where-take-skip-and-so-on">1. Transforming Streams: <code>map</code>, <code>where</code>, <code>take</code>, <code>skip</code>, and so on.</h3>
<p>Streams are powerful because they are iterable, meaning you can apply various transformations to their data flow using methods similar to those found on Dart's <code>Iterable</code>s (<code>List</code>, <code>Set</code>).</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:async'</span>;

<span class="hljs-keyword">void</span> main() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> numbersController = StreamController&lt;<span class="hljs-built_in">int</span>&gt;();

  <span class="hljs-comment">// Create a stream that emits squares of numbers from another stream,</span>
  <span class="hljs-comment">// but only for even numbers, and only takes the first 3 results.</span>
  numbersController.stream
      .where((number) =&gt; number % <span class="hljs-number">2</span> == <span class="hljs-number">0</span>) <span class="hljs-comment">// Only let even numbers pass</span>
      .map((evenNumber) =&gt; evenNumber * evenNumber) <span class="hljs-comment">// Transform even numbers to their squares</span>
      .take(<span class="hljs-number">3</span>) <span class="hljs-comment">// Only take the first 3 squared even numbers</span>
      .listen(
        (squaredEven) {
          <span class="hljs-built_in">print</span>(<span class="hljs-string">'Transformed data: <span class="hljs-subst">$squaredEven</span>'</span>);
        },
        onDone: () {
          <span class="hljs-built_in">print</span>(<span class="hljs-string">'Transformed stream is done!'</span>);
        },
        onError: (e) {
          <span class="hljs-built_in">print</span>(<span class="hljs-string">'Transformed stream error: <span class="hljs-subst">$e</span>'</span>);
        }
      );

  <span class="hljs-comment">// Add some numbers to the source stream</span>
  numbersController.sink.add(<span class="hljs-number">1</span>);
  numbersController.sink.add(<span class="hljs-number">2</span>); <span class="hljs-comment">// Passes where, maps to 4, taken (1st)</span>
  numbersController.sink.add(<span class="hljs-number">3</span>);
  numbersController.sink.add(<span class="hljs-number">4</span>); <span class="hljs-comment">// Passes where, maps to 16, taken (2nd)</span>
  numbersController.sink.add(<span class="hljs-number">5</span>);
  numbersController.sink.add(<span class="hljs-number">6</span>); <span class="hljs-comment">// Passes where, maps to 36, taken (3rd)</span>
  numbersController.sink.add(<span class="hljs-number">7</span>);
  numbersController.sink.add(<span class="hljs-number">8</span>); <span class="hljs-comment">// Will not be processed due to .take(3)</span>
  <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(milliseconds: <span class="hljs-number">100</span>)); <span class="hljs-comment">// Allow events to process</span>

  numbersController.close();
}
</code></pre>
<p>In Dart streams, several transformation and filtering methods are available:</p>
<ul>
<li><p><code>.where(bool test(T element))</code> filters events based on a condition</p>
</li>
<li><p><code>.map&lt;R&gt;(R convert(T event))</code> transforms each event from one type to another</p>
</li>
<li><p><code>.take(int count)</code> emits only the first specified number of events</p>
</li>
<li><p><code>.skip(int count)</code> ignores the first few events and emits the rest</p>
</li>
<li><p><code>.distinct()</code> allows only unique consecutive events to pass</p>
</li>
<li><p><code>.first</code>, <code>.last</code>, and <code>.single</code> return a <code>Future</code> that completes with the first, last, or single event respectively</p>
</li>
<li><p><code>.fold&lt;R&gt;(R initialValue, R combine(R previous, T element))</code> accumulates values like <code>reduce</code></p>
</li>
<li><p><code>.asyncMap&lt;R&gt;(FutureOr&lt;R&gt; convert(T event))</code> applies asynchronous transformations to each event, making it useful for async operations on stream items.</p>
</li>
</ul>
<p>These operators are incredibly powerful for manipulating and refining the data flow within your application.</p>
<h3 id="heading-2-combining-streams">2. Combining Streams</h3>
<p>Sometimes you need to combine events from multiple streams.</p>
<ol>
<li><p><code>Stream.fromFutures(Iterable&lt;Future&lt;T&gt;&gt; futures)</code>: Creates a stream that emits the results of multiple <code>Future(s)</code> as they complete.</p>
</li>
<li><p><code>StreamGroup</code> (from <code>package:async</code>): A utility for combining multiple streams into a single stream, preserving the order of events from the original streams.</p>
</li>
</ol>
<p><strong>Code sample (Stream.fromFutures):</strong></p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:async'</span>;

Future&lt;<span class="hljs-built_in">String</span>&gt; fetchUserData(<span class="hljs-built_in">String</span> userId) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>));
  <span class="hljs-keyword">return</span> <span class="hljs-string">'User Data for <span class="hljs-subst">$userId</span>'</span>;
}

Future&lt;<span class="hljs-built_in">String</span>&gt; fetchProductData(<span class="hljs-built_in">String</span> productId) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">await</span> Future.delayed(<span class="hljs-built_in">Duration</span>(milliseconds: <span class="hljs-number">500</span>));
  <span class="hljs-keyword">return</span> <span class="hljs-string">'Product Data for <span class="hljs-subst">$productId</span>'</span>;
}

<span class="hljs-keyword">void</span> main() {
  <span class="hljs-keyword">final</span> userFuture = fetchUserData(<span class="hljs-string">'user123'</span>);
  <span class="hljs-keyword">final</span> productFuture = fetchProductData(<span class="hljs-string">'prod456'</span>);

  <span class="hljs-comment">// Create a stream from these two futures</span>
  Stream.fromFutures([userFuture, productFuture]).listen(
    (data) {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Received: <span class="hljs-subst">$data</span>'</span>);
    },
    onDone: () {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'All futures completed and stream is done.'</span>);
    },
    onError: (e) {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Error: <span class="hljs-subst">$e</span>'</span>);
    }
  );
}
</code></pre>
<p>The stream created by <code>Stream.fromFutures</code> will emit "Product Data for prod456" first (because it resolves faster), and then "User Data for user123". This demonstrates that events are emitted as their respective futures complete, not necessarily in the order they were provided in the list.</p>
<h2 id="heading-real-world-examples-in-flutter">Real-World Examples in Flutter</h2>
<h3 id="heading-1-fetching-data-from-a-network-with-live-updates">1. Fetching Data from a Network with Live Updates</h3>
<p>Imagine an app displaying a list of news articles that should refresh automatically.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'dart:async'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'dart:convert'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:http/http.dart'</span> <span class="hljs-keyword">as</span> http; <span class="hljs-comment">// Add http: ^0.13.0 to pubspec.yaml</span>

<span class="hljs-comment">// Model for a simple Article</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Article</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> title;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> description;

  Article({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.title, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.description});

  <span class="hljs-keyword">factory</span> Article.fromJson(<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; json) {
    <span class="hljs-keyword">return</span> Article(
      title: json[<span class="hljs-string">'title'</span>] ?? <span class="hljs-string">'No Title'</span>,
      description: json[<span class="hljs-string">'body'</span>] ?? <span class="hljs-string">'No Description'</span>, <span class="hljs-comment">// Using 'body' for simplicity</span>
    );
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NewsService</span> </span>{
  <span class="hljs-keyword">final</span> _articleController = StreamController&lt;<span class="hljs-built_in">List</span>&lt;Article&gt;&gt;.broadcast();
  Stream&lt;<span class="hljs-built_in">List</span>&lt;Article&gt;&gt; <span class="hljs-keyword">get</span> articlesStream =&gt; _articleController.stream;

  Timer? _refreshTimer;

  NewsService() {
    _startAutoRefresh();
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _fetchArticles() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.<span class="hljs-keyword">get</span>(<span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'https://jsonplaceholder.typicode.com/posts?_limit=5'</span>)); <span class="hljs-comment">// Fake API</span>
      <span class="hljs-keyword">if</span> (response.statusCode == <span class="hljs-number">200</span>) {
        <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">dynamic</span>&gt; jsonList = json.decode(response.body);
        <span class="hljs-built_in">List</span>&lt;Article&gt; fetchedArticles = jsonList.map((json) =&gt; Article.fromJson(json)).toList();
        _articleController.sink.add(fetchedArticles);
      } <span class="hljs-keyword">else</span> {
        _articleController.sink.addError(<span class="hljs-string">'Failed to load articles: <span class="hljs-subst">${response.statusCode}</span>'</span>);
      }
    } <span class="hljs-keyword">catch</span> (e) {
      _articleController.sink.addError(<span class="hljs-string">'Network Error: <span class="hljs-subst">$e</span>'</span>);
    }
  }

  <span class="hljs-keyword">void</span> _startAutoRefresh() {
    _fetchArticles(); <span class="hljs-comment">// Fetch immediately</span>
    _refreshTimer = Timer.periodic(<span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">10</span>), (timer) {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Auto-refreshing articles...'</span>);
      _fetchArticles(); <span class="hljs-comment">// Fetch every 10 seconds</span>
    });
  }

  <span class="hljs-keyword">void</span> dispose() {
    _refreshTimer?.cancel();
    _articleController.close();
  }
}

<span class="hljs-keyword">void</span> main() =&gt; runApp(MyApp());

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyApp</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> MaterialApp(
      title: <span class="hljs-string">'Live News Feed'</span>,
      theme: ThemeData(primarySwatch: Colors.deepPurple),
      home: NewsFeedPage(),
    );
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NewsFeedPage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  _NewsFeedPageState createState() =&gt; _NewsFeedPageState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_NewsFeedPageState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">NewsFeedPage</span>&gt; </span>{
  <span class="hljs-keyword">final</span> NewsService _newsService = NewsService();

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    _newsService.dispose(); <span class="hljs-comment">// Important: dispose the service when widget is gone</span>
    <span class="hljs-keyword">super</span>.dispose();
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(title: Text(<span class="hljs-string">'Live News Feed'</span>)),
      body: StreamBuilder&lt;<span class="hljs-built_in">List</span>&lt;Article&gt;&gt;(
        stream: _newsService.articlesStream,
        builder: (context, snapshot) {
          <span class="hljs-keyword">if</span> (snapshot.connectionState == ConnectionState.waiting) {
            <span class="hljs-keyword">return</span> Center(child: CircularProgressIndicator());
          } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (snapshot.hasError) {
            <span class="hljs-keyword">return</span> Center(
              child: Padding(
                padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">16.0</span>),
                child: Text(<span class="hljs-string">'Error: <span class="hljs-subst">${snapshot.error}</span>'</span>, style: TextStyle(color: Colors.red, fontSize: <span class="hljs-number">18</span>)),
              ),
            );
          } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (snapshot.hasData) {
            <span class="hljs-keyword">final</span> articles = snapshot.data!;
            <span class="hljs-keyword">if</span> (articles.isEmpty) {
              <span class="hljs-keyword">return</span> Center(child: Text(<span class="hljs-string">'No articles found.'</span>));
            }
            <span class="hljs-keyword">return</span> ListView.builder(
              itemCount: articles.length,
              itemBuilder: (context, index) {
                <span class="hljs-keyword">final</span> article = articles[index];
                <span class="hljs-keyword">return</span> Card(
                  margin: EdgeInsets.all(<span class="hljs-number">8.0</span>),
                  elevation: <span class="hljs-number">4.0</span>,
                  child: Padding(
                    padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">16.0</span>),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(article.title, style: TextStyle(fontSize: <span class="hljs-number">18</span>, fontWeight: FontWeight.bold)),
                        SizedBox(height: <span class="hljs-number">8</span>),
                        Text(article.description, style: TextStyle(fontSize: <span class="hljs-number">14</span>, color: Colors.grey[<span class="hljs-number">700</span>])),
                      ],
                    ),
                  ),
                );
              },
            );
          } <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">return</span> Center(child: Text(<span class="hljs-string">'Waiting for news...'</span>));
          }
        },
      ),
    );
  }
}
</code></pre>
<p>In this code, the <code>NewsService</code> class encapsulates the logic for fetching articles. It uses a <code>StreamController.broadcast()</code> to allow multiple widgets to listen for article updates, even though in this example only the <code>NewsFeedPage</code> does.</p>
<p>The <code>_fetchArticles()</code> method handles the actual HTTP request, while <code>_startAutoRefresh()</code> initiates an immediate fetch and uses a <code>Timer.periodic</code> to trigger new fetches every 10 seconds, adding each new list of articles to <code>_articleController.sink</code>. The <code>dispose()</code> method is essential for cancelling the timer and closing the stream controller to prevent memory leaks.</p>
<p>On the UI side, the <code>NewsFeedPage</code> creates an instance of <code>NewsService</code>, and in its <code>dispose()</code> method, it calls <code>_newsService.dispose()</code> to release resources. A <code>StreamBuilder&lt;List&lt;Article&gt;&gt;</code> listens to <code>_newsService.articlesStream</code>, and its builder function updates the UI dynamically, displaying a loading indicator, an error message, or the list of articles as new events arrive from the stream.</p>
<p>This pattern is a robust way to handle dynamic, asynchronously updating data in your Flutter applications.</p>
<h3 id="heading-2-user-input-handling-debouncing-a-search-field">2. User Input Handling: Debouncing a Search Field</h3>
<p>Imagine a search bar where you don't want to perform a search API call on every keystroke, but rather after the user pauses typing for a short duration (debouncing).</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'dart:async'</span>;

<span class="hljs-keyword">void</span> main() =&gt; runApp(MyApp());

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyApp</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> MaterialApp(
      title: <span class="hljs-string">'Debounced Search'</span>,
      theme: ThemeData(primarySwatch: Colors.green),
      home: DebouncedSearchPage(),
    );
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DebouncedSearchPage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  _DebouncedSearchPageState createState() =&gt; _DebouncedSearchPageState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_DebouncedSearchPageState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">DebouncedSearchPage</span>&gt; </span>{
  <span class="hljs-keyword">final</span> TextEditingController _searchController = TextEditingController();
  <span class="hljs-keyword">final</span> _searchQueryController = StreamController&lt;<span class="hljs-built_in">String</span>&gt;.broadcast();

  <span class="hljs-built_in">String</span> _lastSearchedTerm = <span class="hljs-string">''</span>;
  StreamSubscription&lt;<span class="hljs-built_in">String</span>&gt;? _debouncedSubscription;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();

    <span class="hljs-comment">// Listen to changes in the text field</span>
    _searchController.addListener(() {
      _searchQueryController.sink.add(_searchController.text);
    });

    <span class="hljs-comment">// Debounce the stream of search queries</span>
    _debouncedSubscription = _searchQueryController.stream
        .distinct() <span class="hljs-comment">// Only emit if the value is different from the previous</span>
        .debounce(<span class="hljs-built_in">Duration</span>(milliseconds: <span class="hljs-number">500</span>)) <span class="hljs-comment">// Wait 500ms after the last event</span>
        .listen((query) {
          <span class="hljs-keyword">if</span> (query.isNotEmpty) {
            _performSearch(query);
          } <span class="hljs-keyword">else</span> {
            setState(() {
              _lastSearchedTerm = <span class="hljs-string">''</span>;
         });
          }
        });
     }

  <span class="hljs-keyword">void</span> _performSearch(<span class="hljs-built_in">String</span> query) {
    <span class="hljs-comment">// In a real app, this would be an API call</span>
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'Performing search for: "<span class="hljs-subst">$query</span>"'</span>);
    setState(() {
      _lastSearchedTerm = query;
    });
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    _searchController.dispose();
    _searchQueryController.close();
    _debouncedSubscription?.cancel(); <span class="hljs-comment">// Cancel the subscription</span>
    <span class="hljs-keyword">super</span>.dispose();
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(title: Text(<span class="hljs-string">'Debounced Search'</span>)),
      body: Padding(
        padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">16.0</span>),
        child: Column(
          children: [
            TextField(
              controller: _searchController,
              decoration: InputDecoration(
                labelText: <span class="hljs-string">'Search'</span>,
                hintText: <span class="hljs-string">'Type to search...'</span>,
                prefixIcon: Icon(Icons.search),
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(<span class="hljs-number">8.0</span>),
                ),
              ),
              onChanged: (text) {
                <span class="hljs-comment">// The addListener already handles adding to the stream</span>
                <span class="hljs-comment">// You could also add directly here if not using addListener</span>
              },
            ),
            SizedBox(height: <span class="hljs-number">20</span>),
            Text(
              _lastSearchedTerm.isEmpty
                  ? <span class="hljs-string">'Start typing to search.'</span>
                  : <span class="hljs-string">'Last performed search: "<span class="hljs-subst">${_lastSearchedTerm}</span>"'</span>,
              style: TextStyle(fontSize: <span class="hljs-number">18</span>),
            ),
            SizedBox(height: <span class="hljs-number">10</span>),
            Text(
              <span class="hljs-string">'A search is triggered 500ms after you stop typing.'</span>,
              style: TextStyle(fontSize: <span class="hljs-number">14</span>, color: Colors.grey),
            ),
          ],
        ),
      ),
    );
  }
}

<span class="hljs-comment">// Extension to add a debounce operator to any Stream&lt;T&gt;</span>
<span class="hljs-keyword">extension</span> DebounceExtension&lt;T&gt; <span class="hljs-keyword">on</span> Stream&lt;T&gt; {
  Stream&lt;T&gt; debounce(<span class="hljs-built_in">Duration</span> duration) =&gt; transform(
    _DebounceStreamTransformer(duration),
  );
}

<span class="hljs-comment">// Custom StreamTransformer for debouncing</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_DebounceStreamTransformer</span>&lt;<span class="hljs-title">T</span>&gt; <span class="hljs-keyword">extends</span> <span class="hljs-title">StreamTransformerBase</span>&lt;<span class="hljs-title">T</span>, <span class="hljs-title">T</span>&gt; </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">Duration</span> duration;

  _DebounceStreamTransformer(<span class="hljs-keyword">this</span>.duration);

  <span class="hljs-meta">@override</span>
  Stream&lt;T&gt; bind(Stream&lt;T&gt; stream) {
    StreamController&lt;T&gt; controller = StreamController&lt;T&gt;();
    Timer? _timer;
    StreamSubscription&lt;T&gt;? _subscription;

    controller.onListen = () {
      _subscription = stream.listen(
        (data) {
          _timer?.cancel(); <span class="hljs-comment">// Cancel previous timer</span>
          _timer = Timer(duration, () {
            controller.add(data); <span class="hljs-comment">// Add data after duration</span>
            _timer = <span class="hljs-keyword">null</span>;
          });
        },
        onError: controller.addError,
        onDone: () {
          _timer?.cancel(); <span class="hljs-comment">// Ensure timer is cancelled if stream done</span>
          controller.close();
        },
      );
    };

    controller.onPause = () =&gt; _subscription?.pause();
    controller.onResume = () =&gt; _subscription?.resume();
    controller.onCancel = () {
      _timer?.cancel(); <span class="hljs-comment">// Cancel any pending timer</span>
      <span class="hljs-keyword">return</span> _subscription?.cancel();
    };

    <span class="hljs-keyword">return</span> controller.stream;
  }
}
</code></pre>
<p>In this code, the <code>TextEditingController _searchController</code> is a standard Flutter controller that manages the text within a <code>TextField</code>. Alongside it, the <code>StreamController&lt;String&gt; _searchQueryController</code> serves as the source stream for all raw text input changes. It’s a broadcast stream, allowing multiple listeners, such as the debouncing logic, to receive events whenever text input changes.</p>
<p>Every time the user types, <code>_searchController.addListener(() { _searchQueryController.sink.add(_searchController.text); });</code> adds the latest text value to the <code>_searchQueryController</code> stream. This ensures that every input change emits an event into the stream.</p>
<p>The <code>debouncedSubscription = _searchQueryController.stream ... .listen(...);</code> line contains the main debouncing logic. The <code>.distinct()</code> operator ensures that duplicate inputs (like typing “apple,” deleting it, and retyping “apple”) don’t trigger redundant events. The <code>.debounce(Duration(milliseconds: 500))</code> operator, implemented as a custom stream transformer, waits for 500 milliseconds of inactivity before emitting the most recent value, resetting its timer with each new event. Once the debounced query is finally emitted, <code>.listen((query) { performSearch(query); });</code> executes the <code>performSearch</code> method with that query.</p>
<p>The <code>DebounceExtension</code> and <code>_DebounceStreamTransformer</code> make this possible by defining a custom <code>StreamTransformer</code>. The core logic resides in <code>bind(Stream&lt;T&gt; stream)</code>, which takes the original stream and produces a transformed one. Inside, a new <code>StreamController</code> is created to manage the output stream, while the input stream is listened to with <code>stream.listen(...)</code>.</p>
<p>The debouncing behavior is achieved by canceling any existing timer and starting a new one (<code>timer?.cancel(); timer = Timer(duration, () { ... });</code>). When the timer completes without new events, the data is emitted via <code>controller.add(data)</code>. Lifecycle methods like <code>onCancel</code>, <code>onPause</code>, and <code>onResume</code> handle proper cleanup and control, ensuring efficient resource management when listeners are paused, resumed, or canceled.</p>
<p>This debounce pattern is incredibly useful for optimizing expensive operations tied to rapid user input.</p>
<h2 id="heading-best-practices-and-considerations">Best Practices and Considerations</h2>
<p>Keep the following in mind when you’re working with streams:</p>
<ol>
<li><p><strong>Always close</strong> <code>StreamControllers</code><strong>:</strong> This is paramount. Forgetting to call <code>_controller.close()</code> (especially in <code>dispose()</code> methods of <code>StatefulWidgets</code> or when a service is no longer needed) leads to memory leaks. If using <code>async*</code>/<code>yield</code>, the stream closes automatically when the generator function finishes.</p>
</li>
<li><p><strong>Cancel</strong> <code>StreamSubscriptions</code><strong>:</strong> If you manually call <code>stream.listen()</code>, remember to store the returned <code>StreamSubscription</code> and call <code>subscription.cancel()</code> when you no longer need to listen. Again, this is typically done in <code>dispose()</code>. <code>StreamBuilder</code> handles its internal subscriptions automatically.</p>
</li>
<li><p><strong>Choose the right stream type:</strong></p>
<ul>
<li><p><strong>Single-Subscription:</strong> For one-time data flows, like a file read or a single HTTP response.</p>
</li>
<li><p><strong>Broadcast:</strong> For multiple UI widgets or logic components needing to react to the same ongoing stream of events. Use <code>StreamController.broadcast()</code>.</p>
</li>
</ul>
</li>
<li><p><strong>Error handling:</strong> Always implement <code>onError</code> callbacks for <code>listen()</code> and handle <code>snapshot.hasError</code> in <code>StreamBuilder</code> to provide a robust user experience.</p>
</li>
<li><p><code>initialData</code> <strong>with</strong> <code>StreamBuilder</code><strong>:</strong> Use <code>initialData</code> when you have a meaningful value to display before the first stream event arrives. This can prevent brief loading indicators if the initial state is known.</p>
</li>
<li><p><strong>Avoid excessive</strong> <code>StreamBuilder</code> <strong>nesting:</strong> While convenient, having too many nested <code>StreamBuilders</code> can lead to complex code and potential performance issues if not managed well. Consider consolidating related stream logic.</p>
</li>
<li><p><strong>Testing streams:</strong> Mock <code>StreamControllers</code> or use <code>Stream.fromIterable</code> to create test streams for your widgets and business logic.</p>
</li>
<li><p><strong>Reactive extensions (RxDart):</strong> For more advanced stream operations (combining, throttling, buffering, and so on), consider using the rxdart package. It provides a rich set of operators inspired by ReactiveX, making complex asynchronous logic more manageable and declarative.</p>
</li>
</ol>
<h2 id="heading-advanced-concepts-brief-introduction">Advanced Concepts (Brief Introduction)</h2>
<p>If you want to go further with streams, there are some key concepts you’ll need to understand. Here’s a brief introduction so you know where to go from here:</p>
<ol>
<li><p><strong>RxDart:</strong> As mentioned, RxDart extends Dart's Stream API with powerful operators. If you find yourself needing more complex stream manipulation than what the core Dart Stream API offers, RxDart is the next logical step. It introduces concepts like <code>BehaviorSubject</code> (a <code>StreamController</code> that remembers the last emitted value and emits it immediately to new listeners) and <code>PublishSubject</code>.</p>
</li>
<li><p><strong>BLoC/Cubit pattern:</strong> Many popular Flutter state management solutions, like the BLoC (Business Logic Component) pattern, are heavily built on streams. BLoCs expose streams (often using <code>StreamController</code>s internally) for UI to listen to state changes, completely decoupling presentation from business logic.</p>
</li>
<li><p><strong>Stream generators with</strong> <code>sync*</code> <strong>and</strong> <code>yield</code> <strong>(for Iterables):</strong> While <code>async*</code>/<code>yield</code> create Streams, Dart also has <code>sync*</code>/<code>yield</code> for creating Iterables (synchronous sequences). This is not directly related to asynchronous streams but uses similar syntax.</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Streams are a cornerstone of modern asynchronous programming in Flutter. By understanding <code>StreamController</code>, <code>StreamBuilder</code>, <code>StreamSubscription</code>, and the <code>async*</code>/<code>yield</code> syntax, you gain the power to build highly reactive, efficient, and dynamic applications.</p>
<p>From handling network data to real-time user interactions, streams provide a flexible and robust mechanism for managing sequences of asynchronous events. Embrace them, and you'll unlock a new level of responsiveness and elegance in your Flutter development.</p>
<h2 id="heading-references">References</h2>
<h3 id="heading-official-documentation">Official Documentation</h3>
<ol>
<li><p><a target="_blank" href="https://dart.dev/tutorials/language/streams">Dart Streams Tutorial (Official Dart Website)</a><strong>:</strong> This is the fundamental resource. It covers the core concepts of streams in Dart, including <code>StreamController</code>, <code>listen</code>, <code>async*</code>/<code>yield</code>, and basic transformations.</p>
</li>
<li><p><a target="_blank" href="https://api.dart.dev/stable/dart-async/Stream-class.html"><code>Stream</code> Class API Documentation (Dart)</a>: The comprehensive reference for all methods and properties of the <code>Stream</code> class itself. Essential for understanding transformation methods like <code>map</code>, <code>where</code>, <code>take</code>, <code>skip</code>, and so on.</p>
</li>
<li><p><a target="_blank" href="https://api.dart.dev/stable/dart-async/StreamController-class.html"><code>StreamController</code> Class API Documentation (Dart)</a>: Details on how to create and manage <code>StreamController</code>s, including single-subscription vs. broadcast.</p>
</li>
<li><p><a target="_blank" href="https://api.dart.dev/stable/dart-async/StreamSubscription-class.html"><code>StreamSubscription</code> Class API Documentation (Dart)</a>: Information on managing your listeners and cancelling subscriptions.</p>
</li>
<li><p><a target="_blank" href="https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html"><code>StreamBuilder</code> Widget API Documentation (Flutter)</a>: The official Flutter documentation for the <code>StreamBuilder</code> widget, explaining its properties (<code>stream</code>, <code>builder</code>, <code>initialData</code>) and the <code>AsyncSnapshot</code>.</p>
</li>
</ol>
<h3 id="heading-key-packages">Key Packages</h3>
<ol>
<li><p><a target="_blank" href="https://pub.dev/packages/async"><code>async</code> package</a>: Provides utilities for asynchronous programming in Dart, including <code>StreamGroup</code> which is useful for combining multiple streams.</p>
</li>
<li><p><a target="_blank" href="https://pub.dev/packages/rxdart"><code>rxdart</code> package</a>: Extends Dart's streams with powerful Rx (ReactiveX) operators, making complex asynchronous event handling much easier and more declarative. A must-have for advanced stream usage.</p>
</li>
</ol>
<h3 id="heading-articles-and-tutorials-general">Articles and Tutorials (General)</h3>
<ol>
<li><a target="_blank" href="https://dart.dev/guides/language/language-tour#asynchrony-support">Asynchronous programming</a>: Futures, async, await (Official Dart Guide): While not directly about streams, a solid understanding of <code>Future</code>s is a prerequisite.</li>
</ol>
<h3 id="heading-related-state-management-patterns">Related State Management Patterns</h3>
<ol>
<li><p><a target="_blank" href="https://pub.dev/packages/flutter_bloc">BLoC Pattern</a><strong>:</strong> Streams are fundamental to the BLoC (Business Logic Component) pattern for state management in Flutter. <code>flutter_bloc</code> package.</p>
</li>
<li><p><a target="_blank" href="https://bloclibrary.dev/">Bloc Library Documentation</a></p>
</li>
</ol>
<h3 id="heading-http-package-for-network-examples">HTTP Package (for Network Examples)</h3>
<ol>
<li><a target="_blank" href="https://pub.dev/packages/http"><code>http</code> package</a>: For making HTTP requests, as shown in the network example</li>
</ol>
<p>By exploring these resources, you'll gain an even deeper and more authoritative understanding of streams in the Dart and Flutter ecosystem.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Telehealth App Using Stream Video and Chat SDK in React ]]>
                </title>
                <description>
                    <![CDATA[ Remember when the COVID-19 pandemic moved everything online – doctor’s visits included – and staying home became the safest option?  That moment kicked off a massive shift in how healthcare gets delivered.  Telehealth became more than a workaround. I... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-telehealth-app-using-stream-video-and-chat-sdk-in-react/</link>
                <guid isPermaLink="false">687ad06430967c1524c4663e</guid>
                
                    <category>
                        <![CDATA[ streams SDK ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Streams ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tailwind CSS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ MERN Stack ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Mern Stack Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Okoro Emmanuel Nzube ]]>
                </dc:creator>
                <pubDate>Fri, 18 Jul 2025 22:53:24 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1752879185676/ab0574c8-d16b-41d0-8883-7df0f4bd0eb5.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Remember when the COVID-19 pandemic moved everything online – doctor’s visits included – and staying home became the safest option? </p>
<p>That moment kicked off a massive shift in how healthcare gets delivered. </p>
<p><a target="_blank" href="https://getstream.io/chat/solutions/healthcare/">Telehealth</a> became more than a workaround. It’s now a core part of modern care. As demand grows, developers are stepping up to build secure, real-time platforms that connect patients and providers from anywhere.</p>
<p>In this article, you’ll learn how to build a telehealth application with Stream’s <a target="_blank" href="https://getstream.io/video/sdk/react/">React Video</a> and <a target="_blank" href="https://getstream.io/chat/sdk/react/">Chat</a> SDKs. You’ll set up authentication, create video calls, enable messaging, and design a functional user interface that mimics real-world telehealth workflows.</p>
<p>Let’s dive in.</p>
<h2 id="heading-outline">Outline</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-introduction">Introduction</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-app-structure">App Structure</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-backend-setup">Backend Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-frontend-setup">Frontend Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-stream-chat-and-video-integration">Stream Chat and Video Integration</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-chat-and-video-function-\(frontend\)">Chat and Video Function (Frontend)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-demo">Project Demo</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-Conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you start this tutorial, make sure you have:</p>
<ul>
<li><p>A basic understanding of React.</p>
</li>
<li><p>Node.js and npm/yarn installed on your computer</p>
</li>
<li><p>A <a target="_blank" href="https://getstream.io/try-for-free/">free account with Stream</a></p>
</li>
<li><p>Familiarity with Stream SDKs</p>
</li>
<li><p>A basic understanding of Tailwind CSS for styling</p>
</li>
<li><p>Experience with VS Code and Postman (for testing APIs)</p>
</li>
</ul>
<h2 id="heading-app-structure"><strong>App Structure</strong></h2>
<p>Before diving into the code, it’s helpful to understand how the app is structured.</p>
<pre><code class="lang-markdown"><span class="hljs-section"># App Flow Structure</span>

<span class="hljs-bullet">-</span> Landing Page  
<span class="hljs-bullet">  -</span> Navigation  
<span class="hljs-bullet">    -</span> Home  
<span class="hljs-bullet">    -</span> About  
<span class="hljs-bullet">    -</span> Sign Up  
<span class="hljs-bullet">      -</span> Verify Account  
<span class="hljs-bullet">        -</span> Log In  
<span class="hljs-bullet">          -</span> Dashboard  
<span class="hljs-bullet">            -</span> Stream Chat  
<span class="hljs-bullet">            -</span> Stream Video  
<span class="hljs-bullet">            -</span> Log Out
</code></pre>
<h2 id="heading-project-setup"><strong>Project Setup</strong></h2>
<p>Before getting started, create two folders: “Frontend” to handle the client-side code and “Backend” for the server-side logic. This separation allows you to manage both parts of your application efficiently.</p>
<h3 id="heading-set-up-react-for-the-frontend"><strong>Set Up React for the Frontend</strong></h3>
<p>Once the folders are created, you can set up the React application in the Frontend folder.</p>
<p>First, navigate to the Frontend directory using the command <code>cd Frontend</code>.</p>
<p>Now you can initialize your React project. You’ll use Vite, a fast build tool for React applications.</p>
<p>To create your React project, run the following command:</p>
<p><code>npm create vite@latest [project name] -- --template react</code></p>
<p> Next, navigate to your new project folder, using the command:</p>
<p><code>cd [project name]</code></p>
<p>Once there, install the required dependencies by running:</p>
<p><code>npm install</code></p>
<p>This command installs both the <code>node_modules</code> folder (which contains all your project's packages) and the <code>package-lock.json</code> file (which records exact versions of installed packages).</p>
<p>Next, you’ll need to install Tailwind CSS for styling. Follow the <a target="_blank" href="https://v3.tailwindcss.com/docs/guides/vite">Tailwind Docs</a> for step-by-step instructions.</p>
<p>Then, it’s time to set up the website. Using React, you’ll create the home sign-in/log-in pages. Both will be nested together using <code>React-router-dom</code>.</p>
<p>Here’s what the <a target="_blank" href="https://github.com/Derekvibe/Telehealth_Frontend/tree/main/src/pages/Home">home page</a> looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752705718347/ed5dd289-2998-41f7-8d10-352aa35fe614.gif" alt="Telehealth Home Page" class="image--center mx-auto" width="1415" height="666" loading="lazy"></p>
<p>Now, the user has a place to land whenever they visit the website.</p>
<p>Let’s set up the backend.</p>
<h2 id="heading-backend-setup"><strong>Backend Setup</strong></h2>
<h3 id="heading-installing-required-packages"><strong>Installing Required Packages</strong></h3>
<p>Before setting up your project's backend, it's important to define what your project needs to offer. This will help you install all the necessary packages in one go.</p>
<p>Start by moving into the backend folder using the command: <code>cd Backend</code></p>
<p>Inside the Backend directory, initialize your Node.js project using <code>npm install</code></p>
<p>This will create a <code>package.json</code> file, which stores metadata and dependencies for your project.</p>
<p>Next, install all the dependencies needed to build your backend. Run the following command:</p>
<p><code>npm i bcryptjs cookie-parser cors dotenv express jsonwebtoken mongoose nodemailer validator nodemon</code></p>
<p>Here’s a brief overview of what each package does:</p>
<ul>
<li><p>bcryptjs: Encrypts user passwords for secure storage.</p>
</li>
<li><p>Cookie-parser: Handles cookies in your application.</p>
</li>
<li><p>CORS: Middleware that enables cross-origin requests – essential for frontend-backend communication.</p>
</li>
<li><p>dotenv: Loads environment variables from a <code>.env</code> file into process.env.</p>
</li>
<li><p>Express: The core framework for building your server and API routes.</p>
</li>
<li><p>jsonwebtoken: Generates and verifies JWT tokens for authentication.</p>
</li>
<li><p>Mongoose: Connects your app to a MongoDB database.</p>
</li>
<li><p>nodemailer: Handles sending emails from your application.</p>
</li>
<li><p>Validator: Validates user inputs like email, strings, and so on.</p>
</li>
<li><p>nodemon: Automatically restarts your server when changes are made to files.</p>
</li>
</ul>
<p>Once your packages are installed, create two key files in the backend directory: <code>App.js</code>, which contains your app logic, middleware, and route handlers, and <code>server.js</code>, responsible for initializing and configuring your server.</p>
<p>Next, you have to update your <code>package.json</code> start script. Head to the <code>package.json</code> file in your backend directory and replace the default script:</p>
<pre><code class="lang-json"><span class="hljs-string">"scripts"</span>: {
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"echo\”Error: no test specified\" &amp;&amp; exit 1”
  }</span>
</code></pre>
<p>with this:</p>
<pre><code class="lang-json"><span class="hljs-string">"scripts"</span>: {
    <span class="hljs-attr">"start"</span>: <span class="hljs-string">"nodemon server.js"</span>
  }
</code></pre>
<p>This setup allows you to run your server using <code>nodemon</code>, automatically reloading it when changes are made. This helps boost productivity during development.</p>
<p>To check if your backend setup is correct, open the <code>server.js</code> file and add a test log: <code>console.log (“Any of your Log Message”)</code>. Then, head to your terminal in the backend directory, and run npm start. You should see the log message in the terminal, confirming that your backend is running.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752703046663/dc06ce5a-3b6c-4846-bd33-53c423a57235.png" alt="Backend Server Testing" class="image--center mx-auto" width="1120" height="986" loading="lazy"></p>
<h3 id="heading-appjs-setup"><code>App.js</code> <strong>Setup</strong></h3>
<p>In the <code>App.js</code> file, start by importing the packages you initially installed.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express"</span>);

<span class="hljs-keyword">const</span> cors = <span class="hljs-built_in">require</span>(<span class="hljs-string">"cors"</span>);

<span class="hljs-keyword">const</span> cookieParser = <span class="hljs-built_in">require</span>(<span class="hljs-string">"cookie-parser"</span>);

<span class="hljs-keyword">const</span> app = express();


app.use(

  cors({

<span class="hljs-attr">origin</span>: [

   <span class="hljs-string">"http://localhost:5173"</span>,

],

<span class="hljs-attr">credentials</span>: <span class="hljs-literal">true</span>,

  })

);

app.use(express.json({ <span class="hljs-attr">limit</span>: <span class="hljs-string">"10kb"</span> }));

app.use(cookieParser());

<span class="hljs-built_in">module</span>.exports = app;
</code></pre>
<p>Here’s what the code above does: </p>
<p>The require statements import <code>express</code>, <code>cors</code>, and <code>cookie-parser</code>, which are essential for creating your backend server, handling cross-origin requests, and parsing cookies.</p>
<p>The <code>const app = express();</code> command sets up a new instance of an Express application. </p>
<p><code>app.use(cors({ origin: ["</code><a target="_blank" href="http://localhost:5173"><code>http://localhost:5173</code></a><code>"], credentials: true }));</code> grants permission or allows requests from your frontend and enables cookie sharing between the frontend and backend of your application. This is important for authentication.</p>
<p>The <code>app.use(express.json({ limit: "10kb" }));</code> command is a middleware function that ensures the server can process incoming JSON payloads and protects against overly large requests, which could be used in DoS attacks. </p>
<p>The <code>app.use(cookieParser());</code> command makes cookies available via <code>req.cookies</code>.</p>
<p>Last, the <code>module.exports = app;</code> command allows the app to be imported in other files, especially in <code>server.js</code>, which is where the app will be started.</p>
<h3 id="heading-serverjs-setup"><code>Server.js</code> <strong>Setup</strong></h3>
<p>Once <code>App.js</code> is set up, the next step is to create and configure your server in a new file called <code>server.js</code>.</p>
<p>Before doing that, ensure you have a <strong>MongoDB database</strong> set up. If you don’t have one yet, you can <a target="_blank" href="https://youtu.be/pO6m0nmo1k0?si=Rqi_50fnsfQrM-ww">follow this video tutorial</a> to set up a MongoDB database.</p>
<p>After setting up MongoDB, you will receive a <code>username</code> and <code>password</code>. Copy the password, head to your backend directory, and create a <code>.env</code> file to store it.</p>
<p>After you have stored the password, head back to complete your database setup.</p>
<p>Next, click on the “Create Database User” button, then click on the <code>choose connection method</code> option. Since we are using Node.js for this project, choose the “Drivers” option. This gives you the database connection string (you should see it at No. 3).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752706253668/ad0cdbb4-453c-4291-ab4c-395d14ce297c.gif" alt="Database-String-Auth" class="image--center mx-auto" width="1280" height="671" loading="lazy"></p>
<p>Then head to your <code>.env</code> and paste it there, and add <code>auth</code> immediately after you have “.net/”.</p>
<p>Here’s what it looks like:</p>
<p><code>mongodb+srv://&lt;username&gt;:&lt;password&gt;@cluster0.qrrtmhs.mongodb.net/auth?retryWrites=true&amp;w=majority&amp;appName=Cluster0</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752767020758/aee4f54c-e562-4916-a8f5-b97590a671d1.png" alt="Backend config.env file" class="image--center mx-auto" width="1632" height="426" loading="lazy"></p>
<p>Lastly, whitelist your IP address. This ensures your backend can connect to MongoDB from your local machine or any environment during development.</p>
<p>To allow your application to connect to the database:</p>
<ul>
<li><p>Go to the "Network Access" section in the Security sidebar of your MongoDB dashboard.</p>
</li>
<li><p>Click on “ADD IP ADDRESS.”</p>
</li>
<li><p>Choose “Allow Access from Anywhere”, then click on Confirm.</p>
</li>
</ul>
<p>At this point, you can set up your <code>server.js</code></p>
<pre><code class="lang-javascript"><span class="hljs-comment">//server.js</span>
<span class="hljs-built_in">require</span>(<span class="hljs-string">"dotenv"</span>).config();
<span class="hljs-keyword">const</span> mongoose = <span class="hljs-built_in">require</span>(<span class="hljs-string">"mongoose"</span>);
<span class="hljs-keyword">const</span> dotenv = <span class="hljs-built_in">require</span>(<span class="hljs-string">"dotenv"</span>); <span class="hljs-comment">//to Manage our environment variable</span>

dotenv.config({ <span class="hljs-attr">path</span>: <span class="hljs-string">"./config.env"</span> });
<span class="hljs-comment">// console.log(process.env.NODE_ENV);</span>

<span class="hljs-keyword">const</span> app = <span class="hljs-built_in">require</span>(<span class="hljs-string">"./app"</span>);

<span class="hljs-keyword">const</span> db = process.env.DB;
<span class="hljs-comment">//connect the application to database using MongoDB</span>

mongoose
  .connect(db)
  .then(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"DB connection Successful"</span>);
  })
  .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(err);
  });

<span class="hljs-keyword">const</span> port = process.env.PORT || <span class="hljs-number">3000</span>;
<span class="hljs-comment">// console.log(process.env.PORT)</span>

app.listen(port, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`App running on port <span class="hljs-subst">${port}</span>`</span>);
});
</code></pre>
<p>The <code>server.js</code> file is responsible for handling all server-related functions and logic. From the code above, the <code>server.js</code> file loads the environment variables using <code>dotenv</code>, connects your backend to MongoDB using <code>mongoose</code>, and starts the Express server. It gets the database URL and port from the <code>config.env</code> file, connects to the database, then runs your application on the specified port (<code>8000</code>).</p>
<p>If the specified port is not found, it falls back to port <code>3000</code> and a confirmation message is printed to the console indicating that the server is up and running on the specified port.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752703203108/a94c724c-ad9c-4653-9081-894ac6e44dd6.png" alt="server-js Telehealth App" class="image--center mx-auto" width="991" height="800" loading="lazy"></p>
<h3 id="heading-connect-the-database-to-mongodb-compass">Connect the Database to MongoDB Compass</h3>
<p>First, download the MongoDB Compass app. (Go here to download and install: <a target="_blank" href="https://www.mongodb.com/try/download/compass">https://www.mongodb.com/try/download/compass</a>). The MongoDB Compass app makes it easy for us to manage our data.</p>
<p>Once the installation is complete, open the app and click on <code>Click to add new connection</code>. Go to your <code>.env</code> file, copy the connection string you initially got when setting up MongoDB, paste it in the URL section, and then click on “connect.” This setup helps you manage your data when you create and delete users.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752703344533/8dff0ff6-66e9-4359-a2c0-fe7a4bd5e4ba.png" alt="Mongo-DB-Compass" class="image--center mx-auto" width="1263" height="790" loading="lazy"></p>
<h3 id="heading-set-up-an-advanced-error-handling-method"><strong>Set up an Advanced Error Handling Method</strong></h3>
<p>You’ll now create an advanced error-handling mechanism. To do so, create a utils folder in your backend, a <code>catchAsync.js</code> file in the utils folder, and add this code:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//catchAsync.js</span>
<span class="hljs-built_in">module</span>.exports = <span class="hljs-function">(<span class="hljs-params">fn</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> <span class="hljs-function">(<span class="hljs-params">req, res, next</span>) =&gt;</span> {
    fn(req, res, next).catch(next);
  };
};
</code></pre>
<p>Next, create an <code>appError.js</code> file still in your utils folder. In the <code>appError.js</code> file, add the following command:</p>
<pre><code class="lang-javascript"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppError</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Error</span> </span>{
  <span class="hljs-keyword">constructor</span>(message, statusCode) {
    <span class="hljs-built_in">super</span>(message);

    <span class="hljs-built_in">this</span>.statusCode = statusCode;
    <span class="hljs-built_in">this</span>.status = <span class="hljs-string">`<span class="hljs-subst">${statusCode}</span>`</span>.startsWith(<span class="hljs-string">"4"</span>) ? <span class="hljs-string">"fail"</span> : <span class="hljs-string">"error"</span>;
    <span class="hljs-built_in">this</span>.isOperational = <span class="hljs-literal">true</span>;

    <span class="hljs-built_in">Error</span>.captureStackTrace(<span class="hljs-built_in">this</span>, <span class="hljs-built_in">this</span>.constructor);
  }
}

<span class="hljs-built_in">module</span>.exports = AppError;
</code></pre>
<p>The code above is helpful in tracking and tracing errors. It also provides you with the URL and file location where your error might be occurring, which helps with cleaner error handling and debugging.</p>
<p>Next, let’s create a global error handler. Start by creating a new folder in the backend directory, and name it “controller”. In the controller folder, create your global error handling file. You can name it anything you like. In this example, it’s called <code>globalErrorHandler.js</code>.</p>
<p>Your <code>globalErrorHandler</code> file will define several functions that handle specific error types, such as database issues, validation failures, or even JWT problems and return a nicely formatted error response for users. For the <code>globalErrorHandler</code> to work properly, you have to create a controller function.</p>
<p>So, next, create an <code>errorController.js</code> file (still inside the controller folder). The <code>errorController.js</code> file responds to the user whenever an error is caught, sending the error in JSON format.</p>
<p><code>globalErrorHandler.js</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// globalErrorHandler.js</span>
<span class="hljs-keyword">const</span> AppError = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../utils/appError"</span>);

<span class="hljs-keyword">const</span> handleCastErrorDB = <span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> message = <span class="hljs-string">`Invalid <span class="hljs-subst">${err.path}</span>: <span class="hljs-subst">${err.value}</span>.`</span>;
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> AppError(message, <span class="hljs-number">400</span>);
};

<span class="hljs-keyword">const</span> handleDuplicateFieldsDB = <span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> value = err.keyValue ? <span class="hljs-built_in">JSON</span>.stringify(err.keyValue) : <span class="hljs-string">"duplicate field"</span>;
  <span class="hljs-keyword">const</span> message = <span class="hljs-string">`Duplicate field value: <span class="hljs-subst">${value}</span>. Please use another value!`</span>;
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> AppError(message, <span class="hljs-number">400</span>);
};

<span class="hljs-keyword">const</span> handleValidationErrorDB = <span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> errors = <span class="hljs-built_in">Object</span>.values(err.errors).map(<span class="hljs-function">(<span class="hljs-params">el</span>) =&gt;</span> el.message);
  <span class="hljs-keyword">const</span> message = <span class="hljs-string">`Invalid input: <span class="hljs-subst">${errors.join(<span class="hljs-string">". "</span>)}</span>`</span>;
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> AppError(message, <span class="hljs-number">400</span>);
};

<span class="hljs-keyword">const</span> handleJWTError = <span class="hljs-function">() =&gt;</span>
  <span class="hljs-keyword">new</span> AppError(<span class="hljs-string">"Invalid token. Please log in again!"</span>, <span class="hljs-number">401</span>);
<span class="hljs-keyword">const</span> handleJWTExpiredError = <span class="hljs-function">() =&gt;</span>
  <span class="hljs-keyword">new</span> AppError(<span class="hljs-string">"Your token has expired! Please log in again."</span>, <span class="hljs-number">401</span>);

<span class="hljs-built_in">module</span>.exports = {
  handleCastErrorDB,
  handleDuplicateFieldsDB,
  handleValidationErrorDB,
  handleJWTError,
  handleJWTExpiredError,
};
</code></pre>
<p>Here’s what the code above does:</p>
<p>The <code>const handleCastErrorDB = (err) =&gt;..</code> section handles MongoDB CastError which usually happens when an invalid ID is passed, and returns a <code>400 Bad Request</code> error response using your <code>AppError</code> class.</p>
<p>The <code>const handleDuplicateFieldsDB = (err) =&gt;...</code> checks and handles MongoDB duplicate key errors, such as trying to register an email or username that’s already taken and returns a <code>400 Bad Request</code> error.</p>
<p>The <code>const handleValidationErrorDB = (err) =&gt;...</code> handles Mongoose validation errors (like required fields missing or wrong data types). It gathers all the individual validation error messages and combines them into one.</p>
<p>The <code>const handleJWTError = () =&gt;</code> and <code>const handleJWTExpiredError = () =&gt;</code> handle errors which might occur as a result of invalid, tampered, or expired JWT tokens and return a <code>401 Unauthorized</code> error response.</p>
<p>The <code>module.exports = {……};</code> section exports all the individual error handlers so they can be used in the <code>errorController.js</code> file.</p>
<p><code>errorController.js</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//errorController.js</span>
<span class="hljs-keyword">const</span> errorHandlers = <span class="hljs-built_in">require</span>(<span class="hljs-string">"./globalErrorHandler"</span>);

<span class="hljs-keyword">const</span> {
  handleCastErrorDB,
  handleDuplicateFieldsDB,
  handleValidationErrorDB,
  handleJWTError,
  handleJWTExpiredError,
} = errorHandlers;

<span class="hljs-built_in">module</span>.exports = <span class="hljs-function">(<span class="hljs-params">err, req, res, next</span>) =&gt;</span> {
  err.statusCode = err.statusCode || <span class="hljs-number">500</span>;
  err.status = err.status || <span class="hljs-string">"error"</span>;

  <span class="hljs-keyword">let</span> error = { ...err, <span class="hljs-attr">message</span>: err.message };

  <span class="hljs-keyword">if</span> (err.name === <span class="hljs-string">"CastError"</span>) error = handleCastErrorDB(err);
  <span class="hljs-keyword">if</span> (err.code === <span class="hljs-number">11000</span>) error = handleDuplicateFieldsDB(err);
  <span class="hljs-keyword">if</span> (err.name === <span class="hljs-string">"ValidationError"</span>) error = handleValidationErrorDB(err);
  <span class="hljs-keyword">if</span> (err.name === <span class="hljs-string">"JsonWebTokenError"</span>) error = handleJWTError();
  <span class="hljs-keyword">if</span> (err.name === <span class="hljs-string">"TokenExpiredError"</span>) error = handleJWTExpiredError();

  res.status(error.statusCode).json({
    <span class="hljs-attr">status</span>: error.status,
    <span class="hljs-attr">message</span>: error.message,
    ...(process.env.NODE_ENV === <span class="hljs-string">"production"</span> &amp;&amp; { error, <span class="hljs-attr">stack</span>: err.stack }),
  });
};
</code></pre>
<p>To ensure your error-handling function works properly, head to your <code>App.js</code> and add the command:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//import command</span>
<span class="hljs-keyword">const</span> globalErrorHandler = <span class="hljs-built_in">require</span>(<span class="hljs-string">"./controller/errorController"</span>);
<span class="hljs-keyword">const</span> AppError = <span class="hljs-built_in">require</span>(<span class="hljs-string">"./utils/appError"</span>);

<span class="hljs-comment">//Catch unknown routes</span>
app.all(<span class="hljs-string">"/{*any}"</span>, <span class="hljs-function">(<span class="hljs-params">req, res, next</span>) =&gt;</span> {
  next(<span class="hljs-keyword">new</span> AppError(<span class="hljs-string">`Can't find <span class="hljs-subst">${req.originalUrl}</span> on this server!`</span>, <span class="hljs-number">404</span>)); });

app.use(globalErrorHandler);
</code></pre>
<p>This ensures that all errors are properly handled and sends the error response to the user.</p>
<h3 id="heading-create-user-model"><strong>Create User Model</strong></h3>
<p>To build a user model, create a new folder in the backend directory and name it “model.” Inside the model folder, create a new file named <code>userModel.js</code>.</p>
<p>The <code>userModel.js</code> file is built essentially for user authentication and security. It provides a validation-rich schema for managing users using Mongoose, which maps how user data is structured in the MongoDB database. It includes validations, password hashing, and methods to compare user passwords securely.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//userModel.js</span>
<span class="hljs-keyword">const</span> mongoose = <span class="hljs-built_in">require</span>(<span class="hljs-string">"mongoose"</span>);
<span class="hljs-keyword">const</span> validator = <span class="hljs-built_in">require</span>(<span class="hljs-string">"validator"</span>);
<span class="hljs-keyword">const</span> bcrypt = <span class="hljs-built_in">require</span>(<span class="hljs-string">"bcryptjs"</span>);

<span class="hljs-keyword">const</span> userSchema = <span class="hljs-keyword">new</span> mongoose.Schema(
  {
    <span class="hljs-attr">username</span>: {<span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>, <span class="hljs-attr">required</span>: [<span class="hljs-literal">true</span>, <span class="hljs-string">"Please provide username"</span>], <span class="hljs-attr">trim</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">minlength</span>: <span class="hljs-number">3</span>, <span class="hljs-attr">maxlength</span>: <span class="hljs-number">30</span>, <span class="hljs-attr">index</span>: <span class="hljs-literal">true</span>,},
    <span class="hljs-attr">email</span>: {<span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>, <span class="hljs-attr">required</span>: [<span class="hljs-literal">true</span>, <span class="hljs-string">"Please Provide an email"</span>], <span class="hljs-attr">unique</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">lowercase</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">validate</span>: [validator.isEmail, <span class="hljs-string">"Please provide a valid email"</span>],},
    <span class="hljs-attr">password</span>: {<span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>, <span class="hljs-attr">required</span>: [<span class="hljs-literal">true</span>, <span class="hljs-string">"Please provide a Password"</span>], <span class="hljs-attr">minlength</span>: <span class="hljs-number">8</span>, <span class="hljs-attr">select</span>: <span class="hljs-literal">false</span>,},
    <span class="hljs-attr">passwordConfirm</span>: {<span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>, <span class="hljs-attr">required</span>: [<span class="hljs-literal">true</span>, <span class="hljs-string">"Please confirm your Password"</span>],
     <span class="hljs-attr">validate</span>: {<span class="hljs-attr">validator</span>: <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">el</span>) </span>{<span class="hljs-keyword">return</span> el === <span class="hljs-built_in">this</span>.password;},
        <span class="hljs-attr">message</span>: <span class="hljs-string">"Passwords do not match"</span>,
      },
    },
    <span class="hljs-attr">isVerified</span>: {<span class="hljs-attr">type</span>: <span class="hljs-built_in">Boolean</span>, <span class="hljs-attr">default</span>: <span class="hljs-literal">false</span>,}, <span class="hljs-attr">otp</span>: <span class="hljs-built_in">String</span>,
    <span class="hljs-attr">otpExpires</span>: <span class="hljs-built_in">Date</span>,
     <span class="hljs-attr">resetPasswordOTP</span>: <span class="hljs-built_in">String</span>,
      <span class="hljs-attr">resetPasswordOTPExpires</span>: <span class="hljs-built_in">Date</span>,
    <span class="hljs-attr">createdAt</span>: {<span class="hljs-attr">type</span>: <span class="hljs-built_in">Date</span>, <span class="hljs-attr">default</span>: <span class="hljs-built_in">Date</span>.now,},}, { <span class="hljs-attr">timestamps</span>: <span class="hljs-literal">true</span> });

<span class="hljs-comment">// Hash password before saving</span>
userSchema.pre(<span class="hljs-string">"save"</span>, <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">next</span>) </span>{
  <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">this</span>.isModified(<span class="hljs-string">"password"</span>)) <span class="hljs-keyword">return</span> next();

  <span class="hljs-built_in">this</span>.password = <span class="hljs-keyword">await</span> bcrypt.hash(<span class="hljs-built_in">this</span>.password, <span class="hljs-number">12</span>);
  <span class="hljs-built_in">this</span>.passwordConfirm = <span class="hljs-literal">undefined</span>; <span class="hljs-comment">// Remove passwordConfirm before saving</span>
  next();
});

<span class="hljs-keyword">const</span> User = mongoose.model(<span class="hljs-string">"User"</span>, userSchema);
<span class="hljs-built_in">module</span>.exports = User;
</code></pre>
<h3 id="heading-auth-controller"><strong>Auth Controller</strong></h3>
<p>Now, you can create logic that regulates your user's authentication process. This authentication logic consists of the sign-up, sign-in (log-in), OTP, and so on. </p>
<p>To do so, first head to your controller folder and create a new file. Call it <code>authController.js</code> because it handles the authentication flow of your project.</p>
<p>After you’ve created the file, you’ll create your sign-up function.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//import</span>
<span class="hljs-keyword">const</span> User = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../model/userModel"</span>);
<span class="hljs-keyword">const</span> AppError = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../utils/appError"</span>);
<span class="hljs-keyword">const</span> catchAsync = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../utils/catchAsync"</span>);
<span class="hljs-keyword">const</span> generateOtp = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../utils/generateOtp"</span>);
<span class="hljs-keyword">const</span> jwt = <span class="hljs-built_in">require</span>(<span class="hljs-string">"jsonwebtoken"</span>);
<span class="hljs-keyword">const</span> sendEmail = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../utils/email"</span>)

<span class="hljs-built_in">exports</span>.signup = catchAsync(<span class="hljs-keyword">async</span> (req, res, next) =&gt; {
  <span class="hljs-keyword">const</span> { email, password, passwordConfirm, username } = req.body;

  <span class="hljs-keyword">const</span> existingUser = <span class="hljs-keyword">await</span> User.findOne({ email });

  <span class="hljs-keyword">if</span> (existingUser) <span class="hljs-keyword">return</span> next(<span class="hljs-keyword">new</span> AppError(<span class="hljs-string">"Email already registered"</span>, <span class="hljs-number">400</span>));

  <span class="hljs-keyword">const</span> otp = generateOtp();

  <span class="hljs-keyword">const</span> otpExpires = <span class="hljs-built_in">Date</span>.now() + <span class="hljs-number">24</span> <span class="hljs-number">60</span> <span class="hljs-number">60</span> * <span class="hljs-number">1000</span>; <span class="hljs-comment">//when thhe otp will expire (1 day)</span>

  <span class="hljs-keyword">const</span> newUser = <span class="hljs-keyword">await</span> User.create({
    username,
    email,
    password,
    passwordConfirm,
    otp,
    otpExpires,
  });

  <span class="hljs-comment">//configure email sending functionality</span>
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> sendEmail({
      <span class="hljs-attr">email</span>: newUser.email,
      <span class="hljs-attr">subject</span>: <span class="hljs-string">"OTP for email Verification"</span>,
      <span class="hljs-attr">html</span>: <span class="hljs-string">`&lt;h1&gt;Your OTP is : <span class="hljs-subst">${otp}</span>&lt;/h1&gt;`</span>,
    });

    createSendToken(newUser, <span class="hljs-number">200</span>, res, <span class="hljs-string">"Registration successful"</span>);
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Email send error:"</span>, error);
    <span class="hljs-keyword">await</span> User.findByIdAndDelete(newUser.id);
    <span class="hljs-keyword">return</span> next(
      <span class="hljs-keyword">new</span> AppError(<span class="hljs-string">"There is an error sending the email. Try again"</span>, <span class="hljs-number">500</span>)
    );
  }
});
</code></pre>
<p><code>const { email, password, passwordConfirm, username } = req.body;</code> extracts the necessary registration details from the incoming request: email, password, password confirmation, and username during user sign-up.</p>
<p><code>const existingUser = await User.findOne({ email });</code> checks the database to see if a user already exists with the given email. If yes, it sends an error response using your <code>AppError</code> utility.</p>
<p><code>const otp = generateOtp();</code> generates the OTP, and <code>const otpExpires =</code> <a target="_blank" href="http://date.now"><code>Date.now</code></a><code>()…..</code> is used to set the OTP to expire at a specified time or day.</p>
<p>With <code>const newUser = await User.create({…});</code>, the new user is saved in MongoDB with their credentials and the OTP info, with the password automatically hashed.</p>
<p><code>await sendEmail({…});</code> sends an email to the user. This email contains their sign-in OTP. If the email is sent successfully, <code>createSendToken(newUser, 200, res, "Registration successful");</code> (which is a utility function) generates a JWT token and sends it in the response with a success message.</p>
<p>If the email fails to send or something goes wrong, <code>await User.findByIdAndDelete(</code><a target="_blank" href="http://newuser.id"><code>newUser.id</code></a><code>);</code> deletes the user from the database to keep things clean, and an error message of <code>There is an error sending the email. Try again", 500</code> is returned.</p>
<h3 id="heading-generate-otp"><strong>Generate OTP</strong></h3>
<p>To ensure that your users' OTP is successfully sent to them, in the utils folder, create a new file and name it <code>generateOtp.js</code>. Then add the code:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">module</span>.exports = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">Math</span>.floor(<span class="hljs-number">1000</span> + <span class="hljs-built_in">Math</span>.random() * <span class="hljs-number">9000</span>).toString();
};
</code></pre>
<p>The code above is a function that generates a user's random 4-digit OTP and returns it as a string.</p>
<p>After completing the code above, go to your authController.js and ensure you import the <code>generateOtp.js</code> in the import section.</p>
<h3 id="heading-create-user-token"><strong>Create User Token</strong></h3>
<p>Next, the user sign-in token will be created, and it will be assigned to the user upon sign-in.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> signToken = <span class="hljs-function">(<span class="hljs-params">userId</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> jwt.sign({ <span class="hljs-attr">id</span>: userId }, process.env.JWT_SECRET, {
    <span class="hljs-attr">expiresIn</span>: process.env.JWT_EXPIRES_IN || <span class="hljs-string">"90d"</span>,
  });
};

<span class="hljs-comment">//function to create the token</span>
<span class="hljs-keyword">const</span> createSendToken = <span class="hljs-function">(<span class="hljs-params">user, statusCode, res, message</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> token = signToken(user._id);

  <span class="hljs-comment">//function to generate the cookie</span>
  <span class="hljs-keyword">const</span> cookieOptions = {
    <span class="hljs-attr">expires</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(
      <span class="hljs-built_in">Date</span>.now() + process.env.JWT_COOKIE_EXPIRES_IN <span class="hljs-number">24</span> <span class="hljs-number">60</span> <span class="hljs-number">60</span> <span class="hljs-number">1000</span>
    ),

    <span class="hljs-attr">httponly</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">secure</span>: process.env.NODE_ENV === <span class="hljs-string">"production"</span>, <span class="hljs-comment">//only secure in production</span>
    <span class="hljs-attr">sameSite</span>: process.env.NODE_ENV === <span class="hljs-string">"production"</span> ? <span class="hljs-string">"none"</span> : <span class="hljs-string">"Lax"</span>,
  };

  res.cookie(<span class="hljs-string">"token"</span>, token, cookieOptions);

  user.password = <span class="hljs-literal">undefined</span>;
  user.passwordConfirm = <span class="hljs-literal">undefined</span>;
  user.otp = <span class="hljs-literal">undefined</span>;
</code></pre>
<p>Before the code above can work perfectly, create a JWT in your <code>.env</code> file.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//.env</span>
JWT_SECRET = kaklsdolrnnhjfsnlsoijfbwhjsioennbandksd;
JWT_EXPIRES_IN = <span class="hljs-number">90</span>d
JWT_COOKIE_EXPIRES_IN = <span class="hljs-number">90</span>
</code></pre>
<p>The code above is how the <code>.env</code> file should look. Your <code>JWT_SECRET</code> can be anything, just as you can see in the code.</p>
<p>Note: The user token creation logic should run before the sign-in logic. So in that case, the <code>signToken</code> and <code>createSendToken</code> logic should be placed at the top before the <code>signup</code> logic.</p>
<h3 id="heading-send-email"><strong>Send Email</strong></h3>
<p>Next, you need to configure your email sending functionality so you can automatically send the user's OTP to their email whenever they sign in. To configure the email, head to the utils folder, create a new file, and give it a name. In this example, the name is <code>email.js</code>.</p>
<p>In <code>email.js,</code> we will send emails using the <code>nodemailer</code> package and Gmail as a provider.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//email.js</span>
<span class="hljs-keyword">const</span> nodemailer = <span class="hljs-built_in">require</span>(<span class="hljs-string">'nodemailer'</span>);

<span class="hljs-keyword">const</span> sendEmail = <span class="hljs-keyword">async</span> (options) =&gt; {
  <span class="hljs-keyword">const</span> transporter = nodemailer.createTransport({
    <span class="hljs-attr">service</span>: <span class="hljs-string">'Gmail'</span>,
    <span class="hljs-attr">auth</span>: {
      <span class="hljs-attr">user</span>: process.env.HOST_EMAIL,
      <span class="hljs-attr">pass</span>: process.env.EMAIL_PASS
    }
  })

  <span class="hljs-comment">//defining email option and structure</span>

  <span class="hljs-keyword">const</span> mailOptions = {
    <span class="hljs-attr">from</span>: <span class="hljs-string">`"{HOST Name}" &lt;{HOST Email} &gt;`</span>,
    <span class="hljs-attr">to</span>: options.email,
    <span class="hljs-attr">subject</span>: options.subject,
    <span class="hljs-attr">html</span>: options.html,
  };
  <span class="hljs-keyword">await</span> transporter.sendMail(mailOptions);
};

<span class="hljs-built_in">module</span>.exports = sendEmail;
</code></pre>
<p>From the code above, the <code>const nodemailer = require('nodemailer');</code> command imports the <code>nodemailer</code> package. This is a popular Node.js library for sending emails.</p>
<p>The <code>const transporter = nodemailer.createTransport({…..})</code> is an email transporter. Since we will be using the Gmail service provider, <code>service</code> will be assigned to <code>Gmail</code> and <code>auth</code> pulls your Gmail address and password from the <code>.env</code> file where it’s stored.</p>
<p>Note: The password is not your actual Gmail password but rather your Gmail app password. You can see how you can get your <a target="_blank" href="https://youtu.be/MkLX85XU5rU?si=yBIj4MJDLY7-k-c4">Gmail password here</a>.</p>
<p>Once you’ve successfully gotten your Gmail app password, store it in your <code>.env</code> file.</p>
<h3 id="heading-route-creation"><strong>Route Creation</strong></h3>
<p>At this point, you have finished setting up your project's signup function. Next, you need to test whether your signup works properly using Postman. But before that, let’s set up and define a route where the signup function will be executed.</p>
<p>To set up your route, create a new folder in your backend directory named "routes" and a file named <code>userRouter.js</code>.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express"</span>);
<span class="hljs-keyword">const</span> {signup} = <span class="hljs-built_in">require</span>(“../controller/authController”);

<span class="hljs-keyword">const</span> router = express.Router();
router.post(<span class="hljs-string">"/signup"</span>, signup);

<span class="hljs-built_in">module</span>.exports = router;
</code></pre>
<p>Next, go to your <code>App.js</code> file and add the router to it.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> userRouter = <span class="hljs-built_in">require</span>(<span class="hljs-string">"./routes/userRouters"</span>); <span class="hljs-comment">//Route import statement</span>
app.use(<span class="hljs-string">"/api/v1/users"</span>, userRouter) <span class="hljs-comment">//common route for all auth, i.e sign up, log in, forget password, etc.</span>
</code></pre>
<p>After setting up your routes, you can test your signup to see if it works. This is a post request, and the route URL will be <a target="_blank" href="http://localhost:8000/api/v1/users/signup%60"><code>http://localhost:8000/api/v1/users/signup</code></a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752704061383/380d8480-9997-4678-8ca7-0ed86ea24481.png" alt="New user Sign up Testing" class="image--center mx-auto" width="1280" height="716" loading="lazy"></p>
<p>The image above shows that the signup function works perfectly with a <code>statusCode</code> of <code>200</code> and an OTP code being sent to the user’s email.</p>
<p>Congratulations on reaching this point! You can check your MongoDB database to see if the user is displayed there.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752703565468/0f23f8ab-d17e-4555-8347-475bb6483b8a.png" alt="Mongo-DB-Compass-User-Display" class="image--center mx-auto" width="1280" height="495" loading="lazy"></p>
<p>From the image above, you can see that the user details are obtained and the password is in an encrypted form, which ensures the user credentials are safe.</p>
<h3 id="heading-create-a-verify-account-controller-function"><strong>Create a Verify Account Controller Function</strong></h3>
<p>In this section, you’ll create a Verify Account controller function. This function verifies a user’s account using the OTP code sent to their email address.</p>
<p>First, go to your <code>authController.js</code> file and add:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">exports</span>.verifyAccount = catchAsync(<span class="hljs-keyword">async</span> (req, res, next) =&gt; {
  <span class="hljs-keyword">const</span> { email, otp } = req.body;

  <span class="hljs-keyword">if</span> (!email || !otp) {
    <span class="hljs-keyword">return</span> next(<span class="hljs-keyword">new</span> AppError(<span class="hljs-string">"Email and OTP are required"</span>, <span class="hljs-number">400</span>));
  }

  <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> User.findOne({ email });

  <span class="hljs-keyword">if</span> (!user) {
    <span class="hljs-keyword">return</span> next(<span class="hljs-keyword">new</span> AppError(<span class="hljs-string">"No user found with this email"</span>, <span class="hljs-number">404</span>));
  }

  <span class="hljs-keyword">if</span> (user.otp !== otp) {
    <span class="hljs-keyword">return</span> next(<span class="hljs-keyword">new</span> AppError(<span class="hljs-string">"Invalid OTP"</span>, <span class="hljs-number">400</span>));
  }

  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">Date</span>.now() &gt; user.otpExpires) {
    <span class="hljs-keyword">return</span> next(
      <span class="hljs-keyword">new</span> AppError(<span class="hljs-string">"OTP has expired. Please request a new OTP."</span>, <span class="hljs-number">400</span>)
    );
  }

  user.isVerified = <span class="hljs-literal">true</span>;
  user.otp = <span class="hljs-literal">undefined</span>;
  user.otpExpires = <span class="hljs-literal">undefined</span>;

  <span class="hljs-keyword">await</span> user.save({ <span class="hljs-attr">validateBeforeSave</span>: <span class="hljs-literal">false</span> });

  <span class="hljs-comment">// ✅ Optionally return a response without logging in</span>
  res.status(<span class="hljs-number">200</span>).json({
    <span class="hljs-attr">status</span>: <span class="hljs-string">"success"</span>,
    <span class="hljs-attr">message</span>: <span class="hljs-string">"Email has been verified"</span>,
  });
});
</code></pre>
<p>Next, create a middleware function to authenticate the currently logged-in user.</p>
<p>In your backend directory, create a new folder called <code>middlewares</code>. Inside the <code>middlewares</code> folder, create a file named <code>isAuthenticated.js</code>.</p>
<p>Add the following code:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//isAuthenticated.js</span>
<span class="hljs-keyword">const</span> jwt = <span class="hljs-built_in">require</span>(<span class="hljs-string">"jsonwebtoken"</span>);
<span class="hljs-keyword">const</span> catchAsync = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../utils/catchAsync"</span>);
<span class="hljs-keyword">const</span> AppError = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../utils/appError"</span>);
<span class="hljs-keyword">const</span> User = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../model/userModel"</span>);

<span class="hljs-keyword">const</span> isAuthenticated = catchAsync(<span class="hljs-keyword">async</span> (req, res, next) =&gt; {
  <span class="hljs-keyword">let</span> token;

  <span class="hljs-comment">// 1. Retrieve token from cookies or Authorization header</span>
  <span class="hljs-keyword">if</span> (req.cookies?.token) {
    token = req.cookies.token;
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (req.headers.authorization?.startsWith(<span class="hljs-string">"Bearer"</span>)) {
    token = req.headers.authorization.split(<span class="hljs-string">" "</span>)[<span class="hljs-number">1</span>];
  }

  <span class="hljs-keyword">if</span> (!token) {
    <span class="hljs-keyword">return</span> next(
      <span class="hljs-keyword">new</span> AppError(
        <span class="hljs-string">"You are not logged in. Please log in to access this resource."</span>,
        <span class="hljs-number">401</span>
      )
    );
  }

  <span class="hljs-comment">// 2. Verify token</span>
  <span class="hljs-keyword">let</span> decoded;
  <span class="hljs-keyword">try</span> {
    decoded = jwt.verify(token, process.env.JWT_SECRET);
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-keyword">return</span> next(
      <span class="hljs-keyword">new</span> AppError(<span class="hljs-string">"Invalid or expired token. Please log in again."</span>, <span class="hljs-number">401</span>)
    );
  }

<span class="hljs-comment">// 3. Confirm user still exists in database</span>
  <span class="hljs-keyword">const</span> currentUser = <span class="hljs-keyword">await</span> User.findById(decoded._id);
  <span class="hljs-keyword">if</span> (!currentUser) {
    <span class="hljs-keyword">return</span> next(
      <span class="hljs-keyword">new</span> AppError(<span class="hljs-string">"User linked to this token no longer exists."</span>, <span class="hljs-number">401</span>)
    );
  }

  <span class="hljs-comment">// 4. Attach user info to request</span>
  req.user = currentUser;
  req.user = {
    <span class="hljs-attr">id</span>: currentUser.id,
    <span class="hljs-attr">name</span>: currentUser.name,
  };

  next();
});

<span class="hljs-built_in">module</span>.exports = isAuthenticated;
<span class="hljs-string">``</span><span class="hljs-string">`
Now, go to your `</span>userRouter.js<span class="hljs-string">` file and add the route for the verify account function:
`</span><span class="hljs-string">``</span>
<span class="hljs-keyword">const</span> { verifyAccount} = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../controller/authController"</span>);
<span class="hljs-keyword">const</span> isAuthenticated = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../middlewares/isAuthenticated"</span>);
router.post(<span class="hljs-string">"/verify"</span>, isAuthenticated, verifyAccount);
</code></pre>
<p>Here is what these two sets of code are doing:</p>
<p>When a user sends a request to the <code>/verify</code> route, the <code>isAuthenticated</code> middleware runs first. It checks whether a valid token exists in the cookie or authorization header. If no token is found, it throws an error: <code>You are not logged in. Please log in to access this resource.</code></p>
<p>If a token is found, it verifies the token and checks if the associated user still exists. If not, another error is thrown: <code>"User linked to this token no longer exists."</code></p>
<p>If the user exists and the token is valid, their details are attached to the request (<code>req.user</code>). The request then proceeds to the <code>verifyAccount</code> controller, which handles OTP verification.</p>
<p>You can test this endpoint using Postman with a POST request to: <a target="_blank" href="http://localhost:8000/api/v1/users/verify%60"><code>http://localhost:8000/api/v1/users/verify</code></a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752703870392/a534d04f-7cb9-4f84-92e1-e9844cfa6921.png" alt="Verify-Account" class="image--center mx-auto" width="1280" height="723" loading="lazy"></p>
<p>The image above shows that the verify token function is working well, and a status code of <code>200</code> is displayed.</p>
<h3 id="heading-login-function"><strong>Login Function</strong></h3>
<p>If you’ve reached this point, you’ve successfully signed up and verified a user’s account. </p>
<p>Now it’s time to create the login function, which allows a verified user to access their account. Here’s how you can do that:</p>
<p>Go to your <code>authController.js</code> file and create your login function by adding the following:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">exports</span>.login = catchAsync(<span class="hljs-keyword">async</span> (req, res, next) =&gt; {
  <span class="hljs-keyword">const</span> { email, password } = req.body;

  <span class="hljs-comment">// 1. Validate email &amp; password presence</span>
  <span class="hljs-keyword">if</span> (!email || !password) {
    <span class="hljs-keyword">return</span> next(<span class="hljs-keyword">new</span> AppError(<span class="hljs-string">"Please provide email and password"</span>, <span class="hljs-number">400</span>));
  }

  <span class="hljs-comment">// 2. Check if user exists and include password</span>
  <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> User.findOne({ email }).select(<span class="hljs-string">"+password"</span>);
  <span class="hljs-keyword">if</span> (!user || !(<span class="hljs-keyword">await</span> user.correctPassword(password, user.password))) {
    <span class="hljs-keyword">return</span> next(<span class="hljs-keyword">new</span> AppError(<span class="hljs-string">"Incorrect email or password"</span>, <span class="hljs-number">401</span>));
  }

  <span class="hljs-comment">// 3. Create JWT token</span>
  <span class="hljs-keyword">const</span> token = signToken(user._id);

  <span class="hljs-comment">// 4. Configure cookie options</span>
  <span class="hljs-keyword">const</span> cookieOptions = {
    <span class="hljs-attr">expires</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(
      <span class="hljs-built_in">Date</span>.now() +
        (<span class="hljs-built_in">parseInt</span>(process.env.JWT_COOKIE_EXPIRES_IN, <span class="hljs-number">10</span>) || <span class="hljs-number">90</span>) <span class="hljs-number">24</span> <span class="hljs-number">60</span> <span class="hljs-number">60</span> <span class="hljs-number">1000</span>
    ),
    <span class="hljs-attr">httpOnly</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-comment">// secure: process.env.NODE_ENV === "production",</span>
    <span class="hljs-comment">// sameSite: process.env.NODE_ENV === "production" ?</span>
    <span class="hljs-comment">//  "None" : "Lax",</span>

    <span class="hljs-comment">//set to false during or for local HTTP and cross-origin</span>
    <span class="hljs-attr">secure</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">sameSite</span>: <span class="hljs-string">"Lax"</span>,
  };

  <span class="hljs-comment">// 5. Send cookie</span>
  res.cookie(<span class="hljs-string">"token"</span>, token, cookieOptions);

});
</code></pre>
<p><code>if (!email || !password) {…}</code> checks if the user actually provided both an email and a password. If not, it returns the error: <code>Please provide email and password", 400</code>.</p>
<p><code>const user = await User.findOne({ email }).select("+password");</code> searches the database for a user with the provided email and explicitly includes the password field, since it’s normally hidden by default in the schema.</p>
<p><code>if (!user || !(await user.correctPassword(…))) {…}</code> checks if the user exists and if the password entered matches the one stored in the database (after hashing comparison). If either is wrong, it throws: <code>Incorrect email or password</code>.</p>
<p>The line <code>signToken(user._id)</code> generates a JWT using the user's unique ID. The <code>cookieOptions</code> object configures how the cookie behaves – it sets the cookie to expire after a specific number of days defined in the <code>.env</code> file, marks it as <code>httpOnly</code> to prevent JavaScript access for security, sets <code>secure</code> to <code>false</code> since the app is currently in development, and uses <code>sameSite: "Lax"</code> to allow cross-origin requests during local testing. </p>
<p>Finally, <code>res.cookie(...)</code> sends the token as a cookie attached to the HTTP response, enabling the client to store the token for authentication purposes.</p>
<p>From the code above, you may have noticed that the password stored in the database is hashed for security reasons. This means it looks completely different from the user's password when logging in. So, even if a user types in the correct password, it won't match the stored hash directly through a simple comparison. </p>
<p>To fix this, you need to compare the entered password with the hashed one using the <code>bcryptjs</code> package.</p>
<p>Head over to your <code>userModel.js</code> file and create a method that handles password comparison. This method will take the plain text password provided by the user and compare it to the hashed password stored in the database.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//userModel.js</span>
<span class="hljs-comment">//create a method responsible for comparing the password stored in the database</span>

userSchema.methods.correctPassword = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">password, userPassword</span>) </span>{
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> bcrypt.compare(password, userPassword);
};
</code></pre>
<p>This <code>correctPassword</code> method uses <code>bcrypt.compare()</code>, which internally hashes the plain password and checks if it matches the stored hashed version. This ensures that login validation works correctly and securely, even though the actual password is not stored in plain text.</p>
<p>Next, add the Login functionality to the <code>userRouter.js</code> file.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> {login} = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../controller/authController"</span>);
<span class="hljs-keyword">const</span> isAuthenticated = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../middlewares/isAuthenticated"</span>);

router.post(<span class="hljs-string">"/login"</span>, login);
</code></pre>
<p>You can test this endpoint using Postman with a <code>POST</code> request to: <a target="_blank" href="http://localhost:8000/api/v1/users/login%60"><code>http://localhost:8000/api/v1/users/login</code></a></p>
<h3 id="heading-logout-function"><strong>Logout Function</strong></h3>
<p>At this point, you can implement the logout function to end a user's session securely. To do this, navigate to your <code>authController.js</code> file and add the following function:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//creating a log out function</span>
<span class="hljs-built_in">exports</span>.logout = catchAsync(<span class="hljs-keyword">async</span> (req, res, next) =&gt; {
  res.cookie(<span class="hljs-string">"token"</span>, <span class="hljs-string">"loggedout"</span>, {
    <span class="hljs-attr">expires</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(<span class="hljs-number">0</span>),
    <span class="hljs-attr">httpOnly</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">secure</span>: process.env.NODE_ENV === <span class="hljs-string">"production"</span>,
  });

  res.status(<span class="hljs-number">200</span>).json({
    <span class="hljs-attr">status</span>: <span class="hljs-string">"success"</span>,
    <span class="hljs-attr">message</span>: <span class="hljs-string">"Logged out successfully"</span>,
  });
});
</code></pre>
<p>This function works by overwriting the authentication cookie named <code>token</code> with the value <code>"loggedout"</code> and setting its expiration time to the past using <code>new Date(0)</code>. This effectively invalidates the cookie and removes it from the browser. </p>
<p>The <code>httpOnly: true</code> flag ensures that the cookie cannot be accessed via JavaScript, which protects it from XSS attacks, while the <code>secure</code> flag ensures that the cookie is only sent over HTTPS in a production environment. Once the cookie is cleared, a success response is returned with the message "Logged out successfully" to confirm the action.</p>
<p>Next, add the <code>logout</code> functionality to your route:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> {logout} = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../controller/authController"</span>);
<span class="hljs-keyword">const</span> isAuthenticated = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../middlewares/isAuthenticated"</span>);

router.post(<span class="hljs-string">"/logout"</span>, logout);
</code></pre>
<p>Then, head to Postman to test your logout function and see if it works.</p>
<h2 id="heading-frontend-setup"><strong>Frontend Setup</strong></h2>
<p>Now that your backend is up and running, you can integrate it into your frontend application.</p>
<p>First, navigate to the frontend directory using the command <code>cd Frontend</code>.</p>
<p>Create a new folder in the <code>src</code> folder where your authentication-related files will live. Depending on your preference or app structure, you can name it something like <code>auth</code> or <code>pages</code>. Then, create a new file called <code>NewUser. js</code>. This file will handle user signup functionality.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;
<span class="hljs-keyword">import</span> React, { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { Link, useNavigate } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-router-dom'</span>;
<span class="hljs-keyword">import</span> { Loader } <span class="hljs-keyword">from</span> <span class="hljs-string">'lucide-react'</span>;
<span class="hljs-keyword">import</span> { useDispatch } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-redux'</span>;
<span class="hljs-keyword">import</span> { setAuthUser, setPendingEmail } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../../../store/authSlice'</span>;

<span class="hljs-keyword">const</span> API_URL = <span class="hljs-keyword">import</span>.meta.env.VITE_API_URL;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">NewUser</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> dispatch = useDispatch();
  <span class="hljs-keyword">const</span> navigate = useNavigate();

  <span class="hljs-keyword">const</span> [loading, setLoading] = useState(<span class="hljs-literal">false</span>);

  <span class="hljs-keyword">const</span> [formData, setFormData] = useState({
    <span class="hljs-attr">username</span>: <span class="hljs-string">''</span>,
    <span class="hljs-attr">email</span>: <span class="hljs-string">''</span>,
    <span class="hljs-attr">password</span>: <span class="hljs-string">''</span>,
    <span class="hljs-attr">passwordConfirm</span>: <span class="hljs-string">''</span>,
  });

  <span class="hljs-keyword">const</span> handleChange = <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> { name, value } = e.target;
    setFormData(<span class="hljs-function">(<span class="hljs-params">prev</span>) =&gt;</span> ({ ...prev, [name]: value }));
  };

  <span class="hljs-keyword">const</span> submitHandler = <span class="hljs-keyword">async</span> (e) =&gt; {
    e.preventDefault();
    setLoading(<span class="hljs-literal">true</span>);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> axios.post(<span class="hljs-string">`<span class="hljs-subst">${API_URL}</span>/users/signup`</span>, formData, {
        <span class="hljs-attr">withCredentials</span>: <span class="hljs-literal">true</span>,
      });
      <span class="hljs-keyword">const</span> user = response.data.data.user;
      dispatch(setAuthUser(user));
      dispatch(setPendingEmail(formData.email)); <span class="hljs-comment">// Save email for OTP</span>
      navigate(<span class="hljs-string">'/verifyAcct'</span>); <span class="hljs-comment">// Navigate to OTP verification page</span>
    } <span class="hljs-keyword">catch</span> (error) {
      alert(error.response?.data?.message || <span class="hljs-string">'Signup failed'</span>);
    } <span class="hljs-keyword">finally</span> {
      setLoading(<span class="hljs-literal">false</span>);
    }
  };

  <span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
// visit the frontend Github repository to see the remaining code for the OTP Verification

https://github.com/Derekvibe/Telehealth_Frontend/blob/main/src/pages/Auth/Join/NewUser.jsx
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> NewUser;
</code></pre>
<p>The code above renders a signup form with fields for <code>username</code>, <code>email</code>, <code>password</code> and <code>passwordConfirm</code>. When the user submits the form, the frontend sends a <code>POST</code> request to the backend’s <code>/users/signup</code> endpoint using <code>Axios</code>. The <code>withCredentials: true</code> option ensures cookies like the <code>auth token</code> are properly set by the backend.</p>
<p>If the signup is successful, the user data is dispatched into Redux using <code>setAuthUser()</code>, and their email is saved with <code>setPendingEmail()</code> so it can be used during <code>OTP</code> verification. Then, the user is redirected to the <code>/verifyAcct</code> route, where they can enter their <code>OTP</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752704266192/0d1d5891-000a-48dc-a1d8-306a0103824a.png" alt="Frontend-Sign-Up" class="image--center mx-auto" width="815" height="675" loading="lazy"></p>
<h3 id="heading-otp-verification-page"><strong>OTP Verification Page</strong></h3>
<p>The OTP verification page is the next step in the user authentication process. Once a user signs up, they are redirected to enter the 4-digit OTP sent to their email. This verifies their account before allowing login access.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> React, { useState, useRef, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { useSelector, useDispatch } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-redux'</span>;
<span class="hljs-keyword">import</span> { useNavigate, Link } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-router-dom'</span>;
<span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;
<span class="hljs-keyword">import</span> { clearPendingEmail } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../../../store/authSlice'</span>;

<span class="hljs-keyword">const</span> API_URL = <span class="hljs-keyword">import</span>.meta.env.VITE_API_URL || <span class="hljs-string">'http://localhost:5000/api'</span>; <span class="hljs-comment">// adjust as needed</span>

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">VerifyAcct</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [code, setCode] = useState([<span class="hljs-string">''</span>, <span class="hljs-string">''</span>, <span class="hljs-string">''</span>, <span class="hljs-string">''</span>]);
  <span class="hljs-keyword">const</span> [loading, setLoading] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [resendLoading, setResendLoading] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [timer, setTimer] = useState(<span class="hljs-number">60</span>);

  <span class="hljs-keyword">const</span> inputsRef = useRef([]);
  <span class="hljs-keyword">const</span> dispatch = useDispatch();
  <span class="hljs-keyword">const</span> navigate = useNavigate();
  <span class="hljs-keyword">const</span> email = useSelector(<span class="hljs-function">(<span class="hljs-params">state</span>) =&gt;</span> state.auth.pendingEmail);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">let</span> interval;
    <span class="hljs-keyword">if</span> (timer &gt; <span class="hljs-number">0</span>) {
      interval = <span class="hljs-built_in">setInterval</span>(<span class="hljs-function">() =&gt;</span> setTimer(<span class="hljs-function">(<span class="hljs-params">prev</span>) =&gt;</span> prev - <span class="hljs-number">1</span>), <span class="hljs-number">1000</span>);
    }
    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">clearInterval</span>(interval);
  }, [timer]);

  <span class="hljs-keyword">const</span> handleChange = <span class="hljs-function">(<span class="hljs-params">value, index</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (!<span class="hljs-regexp">/^\d*$/</span>.test(value)) <span class="hljs-keyword">return</span>;
    <span class="hljs-keyword">const</span> newCode = [...code];
<span class="hljs-comment">// visit the frontend Github repository to see the remaining code for the OTP Verification</span>
https:<span class="hljs-comment">//github.com/Derekvibe/Telehealth_Frontend/blob/main/src/pages/Auth/login/VerifyAcct.jsx</span>
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> VerifyAcct;
</code></pre>
<p>Here’s what the code does: </p>
<p>The OTP is stored as an array of 4 characters (<code>[‘ ‘, ‘ ‘, ‘ ‘, ‘ ‘]</code>). Each box only accepts digits, and focus automatically moves to the next input as the user types in the digit. The focus returns to the previous input box if the user presses the backspace button on an empty box.</p>
<p>When the OTP has been added and the form is submitted, the 4-digit code is joined into a string and an <code>HTTP POST</code> request is made to the backend <code>/user/verify/</code> endpoint along with the stored email and OTP. If the verification is successful, the user is alerted and redirected to the login page, and if not, an error is shown.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752704448954/8ea46e32-c6d9-42e1-a016-f04b259eb0e7.png" alt="Frontend-OTP" class="image--center mx-auto" width="798" height="631" loading="lazy"></p>
<h3 id="heading-log-in"><strong>Log In</strong></h3>
<p>Now you can create the login interface for your application. First, create a <code>Login.jsx</code> file and input the code:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//Login.Jsx</span>

<span class="hljs-keyword">import</span> React, { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { Link, useNavigate } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-router-dom'</span>;
<span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;

<span class="hljs-keyword">const</span> API_URL = <span class="hljs-keyword">import</span>.meta.env.VITE_API_URL || <span class="hljs-string">'https://telehealth-backend-2m1f.onrender.com/api/v1'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Join</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [formData, setFormData] = useState({ <span class="hljs-attr">email</span>: <span class="hljs-string">''</span>, <span class="hljs-attr">password</span>: <span class="hljs-string">''</span> });
  <span class="hljs-keyword">const</span> [loading, setLoading] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [error, setError] = useState(<span class="hljs-string">''</span>);
  <span class="hljs-keyword">const</span> navigate = useNavigate();

  <span class="hljs-keyword">const</span> handleChange = <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
    setFormData({ ...formData, [e.target.name]: e.target.value });
  };

  <span class="hljs-keyword">const</span> handleLogin = <span class="hljs-keyword">async</span> (e) =&gt; {
    e.preventDefault();
    setLoading(<span class="hljs-literal">true</span>);
    setError(<span class="hljs-string">''</span>);

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> axios.post(<span class="hljs-string">`<span class="hljs-subst">${API_URL}</span>/users/login`</span>, formData, {
        <span class="hljs-attr">withCredentials</span>: <span class="hljs-literal">true</span>,
      });

      <span class="hljs-keyword">if</span> (res.data.status === <span class="hljs-string">'success'</span>) {
        <span class="hljs-keyword">const</span> { token, user, streamToken } = res.data;

        <span class="hljs-comment">// Save to localStorage</span>
        <span class="hljs-built_in">localStorage</span>.setItem(<span class="hljs-string">'authToken'</span>, token);
        <span class="hljs-built_in">localStorage</span>.setItem(<span class="hljs-string">'user'</span>, <span class="hljs-built_in">JSON</span>.stringify(user));
        <span class="hljs-built_in">localStorage</span>.setItem(<span class="hljs-string">'streamToken'</span>, streamToken);

        navigate(<span class="hljs-string">'/dashboard'</span>);
      }
    } <span class="hljs-keyword">catch</span> (err) {
      <span class="hljs-built_in">console</span>.error(err);
      setError(
        err.response?.data?.message || <span class="hljs-string">'Something went wrong. Please try again.'</span>
      );
    } <span class="hljs-keyword">finally</span> {
      setLoading(<span class="hljs-literal">false</span>);
    }
  };

  <span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
{// visit the frontend Github repository to see the remaining code for the OTP Verification
https://github.com/Derekvibe/Telehealth_Frontend/blob/main/src/pages/Auth/login/Login.jsx
 <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
);
}
</code></pre>
<p>The <code>Export default Join;</code> component allows a registered and verified user to log into your application using their email and password. It handles form submission, talks to the backend, and securely stores user data if login is successful. </p>
<p><code>handleChange()</code> updates the email or password field as the user types. </p>
<p><code>handleLogin()</code> is triggered when the login form is submitted. When the login button is triggered, it sends a <code>Post</code> request to <code>/users/login</code> with the form data, which includes <code>{withCredentials: true}</code> to enable cookie handling. </p>
<p>If login is successful, it extracts the JWT token, user data, and Stream Chat token from the response and stores them in the <code>localStorage</code> so the user stays logged in across sessions. Then it redirects the user to the dashboard page using <code>navigate(‘/dashboard’)</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752704515845/55c6e74a-a8bc-462a-b988-67b0e8df40ac.png" alt="Frontend-Login" class="image--center mx-auto" width="859" height="583" loading="lazy"></p>
<h3 id="heading-set-up-the-frontend-route"><strong>Set Up the Frontend Route</strong></h3>
<p>Just as you set up the backend route, you have to do the same for the frontend.</p>
<p>Head to <code>App.jsx</code>. Before adding the route, make sure you have installed the <code>react-router-dom</code> package. If not, run this command in the frontend terminal: </p>
<p><code>npm install react-router-dom</code></p>
<p>Then, add the command to your <code>App.jsx</code> file:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { createBrowserRouter, RouterProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-router-dom'</span>;
<span class="hljs-keyword">import</span> HomeIndex <span class="hljs-keyword">from</span> <span class="hljs-string">'./pages/Home/HomeIndex'</span>;
<span class="hljs-keyword">import</span> Hero <span class="hljs-keyword">from</span> <span class="hljs-string">'./pages/Home/Hero'</span>;

<span class="hljs-comment">//Authentication Section</span>
<span class="hljs-keyword">import</span> NewUser <span class="hljs-keyword">from</span> <span class="hljs-string">'./pages/Auth/Join/NewUser'</span>;
<span class="hljs-keyword">import</span> Login <span class="hljs-keyword">from</span> <span class="hljs-string">'./pages/Auth/login/Login'</span>
<span class="hljs-keyword">import</span> VerifyAcct <span class="hljs-keyword">from</span> <span class="hljs-string">'./pages/Auth/login/VerifyAcct'</span>;

<span class="hljs-comment">// Dashboard</span>
<span class="hljs-keyword">import</span> Dashboard <span class="hljs-keyword">from</span> <span class="hljs-string">'./pages/Dashboard/Dashboard'</span>;
<span class="hljs-keyword">import</span> VideoStream <span class="hljs-keyword">from</span> <span class="hljs-string">'./components/VideoStream'</span>;

<span class="hljs-keyword">const</span> router = createBrowserRouter([
  {
    <span class="hljs-attr">path</span>: <span class="hljs-string">'/'</span>,
    <span class="hljs-attr">element</span>: <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">HomeIndex</span> /&gt;</span></span>,
    children: [
      { <span class="hljs-attr">index</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">element</span>: <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Hero</span> /&gt;</span></span> }
    ],
  },

  {
    <span class="hljs-attr">path</span>: <span class="hljs-string">'signup'</span>,
    <span class="hljs-attr">element</span>: <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">NewUser</span> /&gt;</span></span>,
    children: [
      { <span class="hljs-attr">index</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">element</span>: <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">NewUser</span> /&gt;</span></span> }
    ],
  },

  {
    <span class="hljs-attr">path</span>: <span class="hljs-string">'login'</span>,
    <span class="hljs-attr">element</span>: <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Login</span> /&gt;</span></span>,
    children: [
      {<span class="hljs-attr">index</span>:<span class="hljs-literal">true</span>, <span class="hljs-attr">element</span>:<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Login</span> /&gt;</span></span>}
    ]
  },

]);
{<span class="hljs-comment">// visit the frontend Github repository to see the remaining code for the OTP Verification</span>
<span class="hljs-attr">https</span>:<span class="hljs-comment">//github.com/Derekvibe/Telehealth_Frontend/blob/main/src/App.jsx}</span>

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'border border-red-700 w-full min-w-[100vw] min-h-[100vh]'</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">RouterProvider</span> <span class="hljs-attr">router</span>=<span class="hljs-string">{router}</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App;
</code></pre>
<h2 id="heading-stream-chat-and-video-integration"><strong>Stream Chat and Video Integration</strong></h2>
<p>Before proceeding to the dashboard, let’s integrate the Stream <a target="_blank" href="https://getstream.io/chat/">Chat</a> and <a target="_blank" href="https://getstream.io/video/">Video</a> functionality into the project.</p>
<p>First, <a target="_blank" href="https://getstream.io/try-for-free/">create a free Stream account</a>, start a new project in your dashboard, and get your <code>APP KEY</code> and <code>API_SECRET</code>.</p>
<pre><code class="lang-javascript">STREAM_API_KEY=your_app_key
STREAM_API_SECRET=your_api_secret
</code></pre>
<p>Watch the Stream <a target="_blank" href="https://youtu.be/kGKq4giL4ok?si=M_nkWAiq4IzGNYD_">Chat React Quick Start Guide</a> to see how you can set it up.</p>
<p>Next, store your Stream <code>APP KEY</code> and <code>API_SECRET</code> in your <code>.env</code>.</p>
<h3 id="heading-install-stream-packages-frontend"><strong>Install Stream Packages (Frontend)</strong></h3>
<p>Now, install the Stream Chat and Video packages in your terminal.</p>
<pre><code class="lang-javascript">npm install stream-chat stream-chat-react
npm install @stream-io/video-react-sdk
npm install @stream-io/stream-chat-css
</code></pre>
<h3 id="heading-stream-token-handler"><strong>Stream Token Handler</strong></h3>
<p>First, create a new file in your frontend Src directory and name it. In this example, it’s <code>StreamContext.jsx</code>. This file sets up a context to fetch and manage the Stream Chat token on login and includes logout functionality.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> React, { createContext, useContext, useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">"axios"</span>;

<span class="hljs-keyword">const</span> API_URL = <span class="hljs-keyword">import</span>.meta.env.VITE_API_URL || <span class="hljs-string">'https://telehealth-backend-2m1f.onrender.com/api/v1'</span>;

<span class="hljs-comment">// 1. Create the context</span>
<span class="hljs-keyword">const</span> StreamContext = createContext();

<span class="hljs-comment">// 2. Provider component</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> StreamProvider = <span class="hljs-function">(<span class="hljs-params">{ children }</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> [user, setUser] = useState(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [token, setToken] = useState(<span class="hljs-literal">null</span>);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> fetchToken = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> axios.get(<span class="hljs-string">`<span class="hljs-subst">${API_URL}</span>/stream/get-token`</span>, {
          <span class="hljs-attr">withCredentials</span>: <span class="hljs-literal">true</span>,
        });

        <span class="hljs-keyword">if</span> (res.data?.user &amp;&amp; res.data?.token) {
          setUser(res.data.user);
          setToken(res.data.token);
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Stream user/token:"</span>, res.data);
        } <span class="hljs-keyword">else</span> {
          <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Token or user missing in response:"</span>, res.data);
        }
      } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error fetching Stream token:"</span>, error);
      }
    };

    fetchToken();
  }, []);

  <span class="hljs-comment">//Log out Functionality</span>
  <span class="hljs-keyword">const</span> logout = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> axios.post(<span class="hljs-string">`<span class="hljs-subst">${API_URL}</span>/users/logout`</span>, {},
        {
          <span class="hljs-attr">withCredentials</span>: <span class="hljs-literal">true</span>
        });

      <span class="hljs-comment">// Clear localStorage</span>
      <span class="hljs-built_in">localStorage</span>.removeItem(<span class="hljs-string">'authToken'</span>);
      <span class="hljs-built_in">localStorage</span>.removeItem(<span class="hljs-string">'user'</span>);
      <span class="hljs-built_in">localStorage</span>.removeItem(<span class="hljs-string">'streamToken'</span>);

      <span class="hljs-comment">// Clear context</span>
      setUser(<span class="hljs-literal">null</span>);
      setToken(<span class="hljs-literal">null</span>);
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Logout failed"</span>, error);
    }
  };

  <span class="hljs-comment">// Expose Logout with capital L</span>
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">StreamContext.Provider</span> <span class="hljs-attr">value</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">user</span>, <span class="hljs-attr">token</span>, <span class="hljs-attr">Logout:logout</span> }}&gt;</span>
      {children}
    <span class="hljs-tag">&lt;/<span class="hljs-name">StreamContext.Provider</span>&gt;</span></span>
  );
};

<span class="hljs-comment">// 3. Custom hook for easy access</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> useStream = <span class="hljs-function">() =&gt;</span> useContext(StreamContext);
</code></pre>
<p>The code above creates a StreamContext using React’s Context API. In the <code>useEffect</code> section, it makes a <code>GET</code> request to <code>/stream/get-token</code> to fetch the authenticated user and their Stream token. Then it stores them in <code>user</code> and <code>token</code> states. It also provides the user/token through the context so that any component that needs it can make use of it.</p>
<p>Finally, it adds a <code>Logout</code> method that hits the logout endpoint and clears all stored auth data from <code>localStorage</code>.</p>
<p>Next, open your <code>main.jsx</code> and wrap your entire application with the <code>StreamProvider</code> so all child components can access the Stream context.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// main.jsx</span>
<span class="hljs-keyword">import</span> { createRoot } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-dom/client'</span>;
<span class="hljs-keyword">import</span> { StrictMode } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> App <span class="hljs-keyword">from</span> <span class="hljs-string">'./App'</span>;
<span class="hljs-keyword">import</span> { StreamProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">'./components/StreamContext'</span>;

createRoot(<span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'root'</span>)).render(
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">StrictMode</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">StreamProvider</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">App</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">StreamProvider</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">StrictMode</span>&gt;</span></span>
);
</code></pre>
<h3 id="heading-set-up-stream-api"><strong>Set Up Stream API</strong></h3>
<p>After successfully creating the streamContent, the next step is to set up the Stream API. This will be the endpoint from which the user ID and user Stream token can be generated and fetched during login.</p>
<p>To set it up, navigate to your backend directory by running <code>cd Backend</code> in your terminal. Then install the Stream package using the command:</p>
<pre><code class="lang-javascript">npm install getstream
npm install stream-chat stream-chat-react
</code></pre>
<p>Open your <code>.env</code> file and add your Stream <code>API KEY</code> and <code>API_SECRET</code>:</p>
<pre><code class="lang-javascript">STREAM_API_KEY=your_app_key
STREAM_API_SECRET=your_api_secret
</code></pre>
<p>Next, open your <code>authController.js</code> and create your Stream Chat logic:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//Initialize the Stream Client</span>
<span class="hljs-keyword">const</span> {StreamChat} = <span class="hljs-built_in">require</span>(<span class="hljs-string">"stream-chat"</span>);
<span class="hljs-keyword">const</span> streamClient = StreamChat.getInstance(
  process.env.STREAM_API_KEY,
  process.env.STREAM_API_SECRET
);

<span class="hljs-comment">// Modifies the `createSendToken to include `streamToken`</span>
<span class="hljs-keyword">const</span> createSendToken = <span class="hljs-function">(<span class="hljs-params">user, statusCode, res, message</span>) =&gt;</span> {
……
<span class="hljs-keyword">const</span> streamToken = streamClient.createToken(user._id.toString());

  <span class="hljs-comment">//structure of the cookie response when sent to the user</span>
  res.status(statusCode).json({
    <span class="hljs-attr">status</span>: <span class="hljs-string">"success"</span>,
    message,
    token,
    streamToken,
    <span class="hljs-attr">data</span>: {
      <span class="hljs-attr">user</span>: {
        <span class="hljs-attr">id</span>: user._id.toString(),
        <span class="hljs-attr">name</span>: user.username,
      },
    },
  });
};

<span class="hljs-comment">//login functionality</span>
<span class="hljs-built_in">exports</span>.login = catchAsync(<span class="hljs-keyword">async</span> (req, res, next) =&gt; {
 {…………………..}

<span class="hljs-comment">// Generate Stream token</span>
  <span class="hljs-keyword">await</span> streamClient.upsertUser({
    <span class="hljs-attr">id</span>: user._id.toString(),
    <span class="hljs-attr">name</span>: user.username,
  });
  <span class="hljs-keyword">const</span> streamToken = streamClient.createToken(user._id.toString());

user.password = <span class="hljs-literal">undefined</span>;

  res.status(<span class="hljs-number">200</span>).json({
    <span class="hljs-attr">status</span>: <span class="hljs-string">"success"</span>,
    <span class="hljs-attr">message</span>: <span class="hljs-string">"Login successful"</span>,
    token,
    <span class="hljs-attr">user</span>: {
      <span class="hljs-attr">id</span>: user._id.toString(),
      <span class="hljs-attr">name</span>: user.username,
    },
    streamToken,
  });
</code></pre>
<h3 id="heading-streamroutes-endpoint"><code>streamRoutes</code> <strong>Endpoint</strong></h3>
<p>Next, create an endpoint from which the Stream token can be called. To do this, go to your routes folder and create a new file called <code>streamRoutes.js</code>. In <code>streamRoutes.js</code>, add the command:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express"</span>);
<span class="hljs-keyword">const</span> { StreamChat } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"stream-chat"</span>);

<span class="hljs-keyword">const</span> protect = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../middlewares/protect"</span>);

<span class="hljs-keyword">const</span> router = express.Router();

<span class="hljs-keyword">const</span> apiKey = process.env.STREAM_API_KEY;
<span class="hljs-keyword">const</span> apiSecret = process.env.STREAM_API_SECRET;

<span class="hljs-keyword">if</span> (!apiKey || !apiSecret) {
  <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(
    <span class="hljs-string">"Missing Stream credentials. Check your environment variables."</span>
  );
}

<span class="hljs-keyword">const</span> streamClient = StreamChat.getInstance(apiKey, apiSecret);

router.get(<span class="hljs-string">"/get-token"</span>, protect, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> { id, username } = req.user || {};
    <span class="hljs-built_in">console</span>.log(req.user.id, <span class="hljs-string">"User"</span>);
    <span class="hljs-comment">// TRY LOGGING THE ID AND NAME FROM YOUR REQUEST FIRST</span>

    <span class="hljs-keyword">if</span> (!id || !username) {
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ <span class="hljs-attr">error</span>: <span class="hljs-string">"Invalid user data"</span> });
    }

    <span class="hljs-comment">// const userId = _id.toString();</span>
    <span class="hljs-keyword">const</span> user = { id, username };

    <span class="hljs-comment">// Ensure user exists in Stream backend</span>
    <span class="hljs-keyword">await</span> streamClient.upsertUser(user);

    <span class="hljs-comment">// Add user to my_general_chat channel</span>
    <span class="hljs-keyword">const</span> channel = streamClient.channel(<span class="hljs-string">"messaging"</span>, <span class="hljs-string">"my_general_chat"</span>);
    <span class="hljs-keyword">await</span> channel.addMembers([id]);


    <span class="hljs-comment">// Generate token</span>
    <span class="hljs-keyword">const</span> token = streamClient.createToken(id);
    res.status(<span class="hljs-number">200</span>).json({ token, user });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Stream token generation error:"</span>, error);
    res.status(<span class="hljs-number">500</span>).json({ <span class="hljs-attr">error</span>: <span class="hljs-string">"Failed to generate Stream token"</span> });
  }
});

<span class="hljs-comment">/**
 * @route   POST /api/stream/token
 * @desc    Generate a Stream token for any userId from request body (no auth)
 * @access  Public
 */</span>
router.post(<span class="hljs-string">"/token"</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> { userId, name } = req.body;

    <span class="hljs-keyword">if</span> (!userId) {
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ <span class="hljs-attr">error</span>: <span class="hljs-string">"userId is required"</span> });
    }

    <span class="hljs-keyword">const</span> userName = name || <span class="hljs-string">"Anonymous"</span>;
    <span class="hljs-keyword">const</span> user = { <span class="hljs-attr">id</span>: userId, <span class="hljs-attr">name</span>: userName };

    <span class="hljs-keyword">await</span> streamClient.upsertUser(user);

    <span class="hljs-comment">// Add user to my_general_chat channel</span>
    <span class="hljs-keyword">const</span> channel = streamClient.channel(<span class="hljs-string">"messaging"</span>, <span class="hljs-string">"my_general_chat"</span>);
    <span class="hljs-keyword">await</span> channel.addMembers([userId]);


    <span class="hljs-keyword">const</span> token = streamClient.createToken(userId);

    res.status(<span class="hljs-number">200</span>).json({
      token,
      <span class="hljs-attr">user</span>: {
        <span class="hljs-attr">id</span>: userId,
        <span class="hljs-attr">name</span>: name,
        <span class="hljs-attr">role</span>: <span class="hljs-string">"admin"</span>,
        <span class="hljs-attr">image</span>: <span class="hljs-string">`https://getstream.io/random_png/?name=<span class="hljs-subst">${name}</span>`</span>,
      },
    });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Public token generation error:"</span>, error);
    res.status(<span class="hljs-number">500</span>).json({ <span class="hljs-attr">error</span>: <span class="hljs-string">"Failed to generate token"</span> });
  }
});

<span class="hljs-built_in">module</span>.exports = router;
</code></pre>
<h2 id="heading-user-logout-endpoint"><strong>User Logout Endpoint</strong></h2>
<p>Go to your <code>authController.js</code> and create a functionality that handles logging out the user:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">exports</span>.logout = catchAsync(<span class="hljs-keyword">async</span> (req, res, next) =&gt; {
  res.cookie(<span class="hljs-string">"token"</span>, <span class="hljs-string">"loggedout"</span>, {
    <span class="hljs-attr">expires</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(<span class="hljs-number">0</span>),
    <span class="hljs-attr">httpOnly</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">secure</span>: process.env.NODE_ENV === <span class="hljs-string">"production"</span>,
  });

  res.status(<span class="hljs-number">200</span>).json({
    <span class="hljs-attr">status</span>: <span class="hljs-string">"success"</span>,
    <span class="hljs-attr">message</span>: <span class="hljs-string">"Logged out successfully"</span>,
  });
});
</code></pre>
<p>Then register your logout route to your <code>userRouters.js</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express"</span>);
<span class="hljs-keyword">const</span> {logout}= <span class="hljs-built_in">require</span>(<span class="hljs-string">"../controller/authController"</span>);
<span class="hljs-keyword">const</span> isAuthenticated = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../middlewares/isAuthenticated"</span>);


router.post(<span class="hljs-string">"/logout"</span>, isAuthenticated, logout);

<span class="hljs-built_in">module</span>.exports = router;
</code></pre>
<h2 id="heading-chat-and-video-function-frontend">Chat and Video Function (Frontend)</h2>
<p>After setting up your backend Stream API, the last task is setting up chat and video in your frontend application.</p>
<h3 id="heading-dashboardjsx"><code>Dashboard.jsx</code></h3>
<p>Create a new file <code>Dashboard.jsx</code> in your frontend directory. This is where you will set up your Stream and video function.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> React, { useState, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">"axios"</span>;
<span class="hljs-keyword">import</span> {
  Chat,
  Channel,
  ChannelHeader,
  MessageInput,
  MessageList,
  Thread,
  Window,
  useCreateChatClient,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"stream-chat-react"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"stream-chat-react/dist/css/v2/index.css"</span>;
<span class="hljs-keyword">import</span> { useStream } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../components/StreamContext"</span>;
<span class="hljs-keyword">import</span> VideoStream <span class="hljs-keyword">from</span> <span class="hljs-string">"../../components/VideoStream"</span>;
<span class="hljs-keyword">import</span> { useNavigate } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-router-dom"</span>;



<span class="hljs-keyword">const</span> apiKey = <span class="hljs-keyword">import</span>.meta.env.VITE_STREAM_API_KEY;

<span class="hljs-keyword">const</span> API_URL = <span class="hljs-keyword">import</span>.meta.env.VITE_API_URL || <span class="hljs-string">'https://telehealth-backend-2m1f.onrender.com/api/v1'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [channel, setChannel] = useState(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [clientReady, setClientReady] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> navigate = useNavigate();

  <span class="hljs-comment">// const ChatComponent = () =&gt; {</span>
    <span class="hljs-keyword">const</span> { user, token, Logout } = useStream();

    <span class="hljs-comment">// Always call the hook</span>
    <span class="hljs-keyword">const</span> chatClient = useCreateChatClient({
      apiKey,
      <span class="hljs-attr">tokenOrProvider</span>: token,
      <span class="hljs-attr">userData</span>: user?.id ? { <span class="hljs-attr">id</span>: user.id } : <span class="hljs-literal">undefined</span>,
    });

  <span class="hljs-comment">// Debug: See when user/token is ready</span>
  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Stream user:"</span>, user);
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Stream token:"</span>, token);
  }, [user, token]);

    <span class="hljs-comment">// Connect user to Stream</span>
    useEffect(<span class="hljs-function">() =&gt;</span> {
      <span class="hljs-keyword">const</span> connectUser = <span class="hljs-keyword">async</span> () =&gt; {
        <span class="hljs-keyword">if</span> (!chatClient || !user || !token || !user?.id) {
          <span class="hljs-built_in">console</span>.warn(<span class="hljs-string">"Missing chat setup data:"</span>, { chatClient, token, user });
          <span class="hljs-keyword">return</span>;
        }


        <span class="hljs-keyword">try</span> {
          <span class="hljs-keyword">await</span> chatClient.connectUser(
            {
              <span class="hljs-attr">id</span>: user.id,
              <span class="hljs-attr">name</span>: user.name || <span class="hljs-string">"Anonymous"</span>,
              <span class="hljs-attr">image</span>:
                user.image ||
                <span class="hljs-string">`https://getstream.io/random_png/?name=<span class="hljs-subst">${user.name || <span class="hljs-string">"user"</span>}</span>`</span>,
            },
            token
          );

          <span class="hljs-keyword">const</span> newChannel = chatClient.channel(<span class="hljs-string">"messaging"</span>, <span class="hljs-string">"my_general_chat"</span>, {
            <span class="hljs-attr">name</span>: <span class="hljs-string">"General Chat"</span>,
            <span class="hljs-attr">members</span>: [user.id],
          });

          <span class="hljs-keyword">await</span> newChannel.watch();
          setChannel(newChannel);
          setClientReady(<span class="hljs-literal">true</span>);
        } <span class="hljs-keyword">catch</span> (err) {
          <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error connecting user:"</span>, err);
        }
      };

      connectUser();
    }, [chatClient, user, token]);

    <span class="hljs-keyword">const</span> handleVideoCallClick = <span class="hljs-function">() =&gt;</span> {
      navigate(<span class="hljs-string">"/videoCall"</span>);
  };

  <span class="hljs-keyword">const</span> handleLogout = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">await</span> Logout();
    navigate(<span class="hljs-string">"/login"</span>);
  }

  <span class="hljs-keyword">if</span> (!user || !token) {
    <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-red-600"</span>&gt;</span>User or token not ready.<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>;
  }

  <span class="hljs-keyword">if</span> (!clientReady || !channel) <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>Loading chat...<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>;


  <span class="hljs-keyword">return</span> (
{ checkout the github repo}
            &lt;ChannelHeader /&gt;
            <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">MessageList</span> /&gt;</span></span>
            <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">MessageInput</span> /&gt;</span></span>
          &lt;/Window&gt;
          <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Thread</span> /&gt;</span></span>
        &lt;/Channel&gt;
      &lt;/Chat&gt;


      &lt;/div&gt;

    );
  }

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App;
</code></pre>
<h3 id="heading-video-setup"><strong>Video Setup</strong></h3>
<p>You’ll now set up the video function for your frontend. To do so, create a new file <code>VideoStream.jsx</code> and add the command:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> React, { useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { StreamVideoClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@stream-io/video-client"</span>;
<span class="hljs-keyword">import</span> { StreamVideo, StreamCall } <span class="hljs-keyword">from</span> <span class="hljs-string">"@stream-io/video-react-sdk"</span>;
<span class="hljs-keyword">import</span> { useNavigate } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-router-dom"</span>;


<span class="hljs-keyword">import</span> { useStream } <span class="hljs-keyword">from</span> <span class="hljs-string">"./StreamContext"</span>;
<span class="hljs-keyword">import</span> { MyUILayout } <span class="hljs-keyword">from</span> <span class="hljs-string">"./MyUILayout"</span>;


<span class="hljs-keyword">const</span> apiKey = <span class="hljs-keyword">import</span>.meta.env.VITE_STREAM_API_KEY;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">VideoStream</span>(<span class="hljs-params"></span>) </span>{

  <span class="hljs-keyword">const</span> [client, setClient] = useState(<span class="hljs-literal">null</span>);
    <span class="hljs-keyword">const</span> [call, setCall] = useState(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> { user, token } = useStream();
  <span class="hljs-keyword">const</span> navigate = useNavigate();

  useEffect(<span class="hljs-function">() =&gt;</span> {

    <span class="hljs-keyword">let</span> clientInstance;
    <span class="hljs-keyword">let</span> callInstance;


    <span class="hljs-keyword">const</span> setup = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">if</span> (!apiKey || !user || !token) <span class="hljs-keyword">return</span>;

      clientInstance = <span class="hljs-keyword">new</span> StreamVideoClient({ apiKey, user, token });

      callInstance = clientInstance.call(<span class="hljs-string">"default"</span>, user.id); <span class="hljs-comment">// Use user.id as callId</span>


      <span class="hljs-keyword">await</span> callInstance.join({ <span class="hljs-attr">create</span>: <span class="hljs-literal">true</span> });

      setClient(clientInstance);
      setCall(callInstance);
    };

    setup();

    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> {
      <span class="hljs-keyword">if</span> (callInstance) callInstance.leave();
      <span class="hljs-keyword">if</span> (clientInstance) clientInstance.disconnectUser();

    };
  }, [user, token]);

  <span class="hljs-keyword">const</span> handleLeaveCall = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (call) <span class="hljs-keyword">await</span> call.leave();
    <span class="hljs-keyword">if</span> (client) <span class="hljs-keyword">await</span> client.disconnectUser();

    setCall(<span class="hljs-literal">null</span>);
    setClient(<span class="hljs-literal">null</span>);

    navigate(<span class="hljs-string">"/dashboard"</span>); <span class="hljs-comment">// or any other route</span>
  };

  <span class="hljs-keyword">if</span> (!apiKey) <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>Missing Stream API Key<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>;

  <span class="hljs-keyword">if</span> (!client || !call)
    <span class="hljs-keyword">return</span> (
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex items-center justify-center h-screen text-xl font-semibold"</span>&gt;</span>
    Connecting to the video call...
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
    );

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"relative h-screen w-full p-2 sm:p-4 bg-gray-50"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">StreamVideo</span> <span class="hljs-attr">client</span>=<span class="hljs-string">{client}</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">StreamCall</span> <span class="hljs-attr">call</span>=<span class="hljs-string">{call}</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">MyUILayout</span> /&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">StreamCall</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">StreamVideo</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
        <span class="hljs-attr">onClick</span>=<span class="hljs-string">{handleLeaveCall}</span>
        <span class="hljs-attr">className</span>=<span class="hljs-string">"absolute top-2 right-2 sm:top-4 sm:right-4 bg-red-600 text-white text-sm sm:text-base px-3 sm:px-4 py-1.5 sm:py-2 rounded-lg shadow hover:bg-red-700 transition"</span>
      &gt;</span>
        Leave Call
      <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
    );
  }

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> VideoStream;
</code></pre>
<pre><code class="lang-javascript"><span class="hljs-comment">//MYUILayout.jsx</span>
<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> {
  useCall,
  useCallStateHooks,
  CallingState,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@stream-io/video-react-sdk'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MyUILayout</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> call = useCall();
  <span class="hljs-keyword">const</span> { useCallCallingState, useParticipantCount } = useCallStateHooks();
  <span class="hljs-keyword">const</span> callingState = useCallCallingState();
  <span class="hljs-keyword">const</span> participantCount = useParticipantCount();

  <span class="hljs-keyword">if</span> (callingState !== CallingState.JOINED) {
    <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>Joining call...<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>;
  }

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">padding:</span> '<span class="hljs-attr">1rem</span>', <span class="hljs-attr">fontSize:</span> '<span class="hljs-attr">1.2rem</span>' }}&gt;</span>
      ✅ Call "<span class="hljs-tag">&lt;<span class="hljs-name">strong</span>&gt;</span>{call?.id}<span class="hljs-tag">&lt;/<span class="hljs-name">strong</span>&gt;</span>" has <span class="hljs-tag">&lt;<span class="hljs-name">strong</span>&gt;</span>{participantCount}<span class="hljs-tag">&lt;/<span class="hljs-name">strong</span>&gt;</span> participants.
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}
</code></pre>
<h2 id="heading-project-demo">Project Demo</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752705861841/85b6d6b3-0f5e-402f-b8b5-8ab51d820403.gif" alt="Telehealth Final Project Demo" class="image--center mx-auto" width="1398" height="630" loading="lazy"></p>
<p>Congratulations! You have successfully integrated Stream’s chat and video function into your application.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>And that’s a wrap! </p>
<p>You’ve <a target="_blank" href="https://getstream.io/blog/telemedicine-app-development/">built a telehealth app</a> with secure video, real-time chat, and user authentication – all powered by Stream’s Chat and Video SDKs. </p>
<p>This foundation gives you the flexibility to expand further with features like appointment scheduling, patient history, or HIPPA-compliant file sharing. </p>
<p>You can find the <a target="_blank" href="https://github.com/Derekvibe/Telehealth_Backend">frontend</a> and <a target="_blank" href="https://github.com/Derekvibe/Telehealth_Frontend">backend</a> applications on GitHub. The frontend app is hosted using the Vercel hosting service, and the backend is hosted on Render.</p>
<p>Check out the <a target="_blank" href="https://telehealth-frontend.vercel.app/">repository of the application</a>.</p>
<p>Happy coding! 🚀</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
