Back
Data StructuresJavaScriptArticle · May 28, 2024 · 12 min

jonathanjuliani

Meeting Graph Algorithms with NodeJS and Javascript

BFS and DFS on graphs in JavaScript: visit order, queue vs stack, and visual examples. Data structures in a Nutshell episode.

Graph with shortest-path style traversal (BFS/DFS)

A graph from ep. 7 is inert without traversal. This episode implements BFS and DFS — foundation for shortest paths (unweighted), cycles, and topological order.

What you'll learn:

  • BFS with a queue and visited set
  • DFS recursive and iterative
  • when BFS finds shortest path (no edge weights)
  • cycle detection with DFS

Prerequisites: graphs (ep. 7).


BFS and DFS: traverse without infinite loops

Back in Part 7 we built a Graph structure with nodes and edges. Having the data structure is great, but without algorithms to navigate it, we're just storing a map without being able to use it. That's what this post is about: Breadth-First Search (BFS) and Depth-First Search (DFS)—the two fundamental algorithms for exploring a graph.

To keep things concrete, we'll use this graph for all our examples:

Undirected sample graph with nodes A through G

scene: en/data-structures-in-a-nutshell/graph-sample

Interactive Excalidraw view is not available in this build.

Control or Command + scroll to zoom. Click and drag to pan.
graph LR
  A --- B
  A --- C
  B --- D
  B --- E
  C --- F
  C --- G

Starting from node A, BFS and DFS will each visit all nodes—but in a completely different order.

Hands-on

First The Theory

BFS — Breadth-First Search

BFS uses a queue. It visits all neighbours of the current node before moving deeper. Think of it as expanding a wave: first every node one hop away, then every node two hops away, and so on.

Same graph with BFS visit order labels from A

scene: en/data-structures-in-a-nutshell/graph-bfs-order

Interactive Excalidraw view is not available in this build.

Control or Command + scroll to zoom. Click and drag to pan.
graph LR
  A["A (1st)"] --- B["B (2nd)"]
  A --- C["C (3rd)"]
  B --- D["D (4th)"]
  B --- E["E (5th)"]
  C --- F["F (6th)"]
  C --- G["G (7th)"]

BFS visit order from A: A → B → C → D → E → F → G

DFS — Depth-First Search

DFS uses a stack (or recursion). It dives as deep as possible along one path before backtracking. Think of it as exploring a maze: go all the way down one corridor, then come back and try the next one.

Same graph with DFS visit order labels from A

scene: en/data-structures-in-a-nutshell/graph-dfs-order

Interactive Excalidraw view is not available in this build.

Control or Command + scroll to zoom. Click and drag to pan.
graph LR
  A["A (1st)"] --- B["B (2nd)"]
  A --- C["C (5th)"]
  B --- D["D (3rd)"]
  B --- E["E (4th)"]
  C --- F["F (6th)"]
  C --- G["G (7th)"]

DFS visit order from A (following insertion order): A → B → D → E → C → F → G

Now The Practice

We'll reuse the Graph from Part 7. Paste the full file to start:

code
class Graph {
  constructor() {
    this.numberOfNodes = 0
    this.adjacentList = {}
  }
 
  add(node) {
    this.adjacentList[node] = []
    this.numberOfNodes++
  }
 
  addEdge(node1, node2) {
    this.adjacentList[node1].push(node2)
    this.adjacentList[node2].push(node1)
  }
}

Now let's add the BFS method to the Graph class:

code
  bfs(start) {
    const visited = new Set()
    const queue = [start]
    const order = []
 
    visited.add(start)
 
    while (queue.length) {
      const node = queue.shift()
      order.push(node)
 
      for (const neighbour of this.adjacentList[node]) {
        if (!visited.has(neighbour)) {
          visited.add(neighbour)
          queue.push(neighbour)
        }
      }
    }
 
    return order
  }

Breaking it down:

  • We start with a Set to track visited nodes (so we don't loop forever in cyclic graphs) and a queue seeded with the start node.
  • On each iteration we pull the front of the queue (shift()), record it, and enqueue all its unvisited neighbours.
  • Because we process the queue front-to-back, we naturally visit nodes level by level.

Now let's add DFS:

code
  dfs(start) {
    const visited = new Set()
    const order = []
 
    const explore = (node) => {
      visited.add(node)
      order.push(node)
      for (const neighbour of this.adjacentList[node]) {
        if (!visited.has(neighbour)) {
          explore(neighbour)
        }
      }
    }
 
    explore(start)
    return order
  }

Breaking it down:

  • We use a recursive helper explore that marks a node visited, records it, then immediately recurses into each unvisited neighbour.
  • Because we go deep into the first neighbour before considering siblings, we get the depth-first order.

Let's test both. After the class, add:

code
const g = new Graph()
g.add('A')
g.add('B')
g.add('C')
g.add('D')
g.add('E')
g.add('F')
g.add('G')
 
g.addEdge('A', 'B')
g.addEdge('A', 'C')
g.addEdge('B', 'D')
g.addEdge('B', 'E')
g.addEdge('C', 'F')
g.addEdge('C', 'G')
 
console.log('BFS from A:', g.bfs('A'))
console.log('DFS from A:', g.dfs('A'))

Run it:

code
node graph-algorithms.js

Expected output:

code
BFS from A: [ 'A', 'B', 'C', 'D', 'E', 'F', 'G' ]
DFS from A: [ 'A', 'B', 'D', 'E', 'C', 'F', 'G' ]

AWESOME!

Both algorithms visited all 7 nodes, but in completely different orders. BFS went wide (all direct neighbours of A first), while DFS went deep (followed the B branch all the way down before touching C).

Can I do iterative DFS instead of recursive?

Absolutely! Replace the explore function with a stack:

code
  dfsIterative(start) {
    const visited = new Set()
    const stack = [start]
    const order = []
 
    while (stack.length) {
      const node = stack.pop()
      if (visited.has(node)) continue
      visited.add(node)
      order.push(node)
      for (const neighbour of this.adjacentList[node]) {
        if (!visited.has(neighbour)) stack.push(neighbour)
      }
    }
 
    return order
  }

The iterative version uses an explicit stack instead of the call stack—safer for very large or deep graphs where recursion could hit the stack limit.


Edge cases: visited set, shift, stack depth

Without a visited Set, any cycle loops forever. Mark nodes when you enqueue/push, not only when you process.

Deep recursive DFS can exceed Node's stack — use an explicit stack for large graphs.

BFS on weighted graphs does not guarantee cheapest path — use Dijkstra with a priority queue.


TL;DR — BFS vs DFS

structuretypical use
BFSqueueshortest steps, levels
DFSstackcycles, topo sort

Next steps


Before you go

BFS or DFS in production? Comment if the sample graph has a bug.

Subscribe to the Engineering Ledger

Get architecture and performance notes in your inbox. Same list as the timed prompt—subscribe here anytime.

No spam. No third-party APIs. Just me sending updates.

The Engineering Ledger

Bi-weekly transmissions on architecture, performance, and practical engineering. Subscribe from any article—no spam.

Get in touch

For professional opportunities and direct contact, find me on LinkedIn.

Connect on LinkedInOpportunities, partnerships, and direct messages