<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: James Mateer</title>
    <description>The latest articles on DEV Community by James Mateer (@jaeger974).</description>
    <link>https://dev.to/jaeger974</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3907675%2F722930ca-a9ee-40a3-9b3f-17eefc3ce869.png</url>
      <title>DEV Community: James Mateer</title>
      <link>https://dev.to/jaeger974</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jaeger974"/>
    <language>en</language>
    <item>
      <title>“Simple” live chat almost sank this release</title>
      <dc:creator>James Mateer</dc:creator>
      <pubDate>Thu, 18 Jun 2026 13:55:31 +0000</pubDate>
      <link>https://dev.to/jaeger974/simple-live-chat-almost-sank-this-release-2pn7</link>
      <guid>https://dev.to/jaeger974/simple-live-chat-almost-sank-this-release-2pn7</guid>
      <description>&lt;p&gt;The feature that’s giving me the most trouble right now isn’t some fancy AI or 3D animation. It’s a “basic” live chat system glued to geolocation-based matching and a rating flow that looks trivial in the UI. Under the hood, tying all of this together has been one of the most technical things I’ve shipped so far.&lt;/p&gt;

&lt;p&gt;This post is a deep dive into how the app is structured, the decisions that aged well (and the ones that didn’t), and the surprising complexity behind “just show nearby users and let them chat.”&lt;/p&gt;

&lt;p&gt;High-level architecture&lt;br&gt;
At a high level, the app is:&lt;/p&gt;

&lt;p&gt;Frontend: React + TypeScript SPA, talking to a JSON API over HTTPS.&lt;/p&gt;

&lt;p&gt;Backend: Node.js (Express) API server with WebSocket support for live chat.&lt;/p&gt;

&lt;p&gt;Data: PostgreSQL for relational data (users, sessions, ratings), Redis for ephemeral presence / active sessions.&lt;/p&gt;

&lt;p&gt;The flow:&lt;/p&gt;

&lt;p&gt;User signs in → frontend stores a short-lived access token.&lt;/p&gt;

&lt;p&gt;Frontend sends location (coarse, not precise) to the backend.&lt;/p&gt;

&lt;p&gt;Backend matches user with nearby users based on a distance query.&lt;/p&gt;

&lt;p&gt;When a match is confirmed, the backend creates a chat room and establishes a WebSocket channel for both sides.&lt;/p&gt;

&lt;p&gt;After the session, users submit a rating, which feeds into a simple reputation system.&lt;/p&gt;

&lt;p&gt;Nothing exotic, but the devil is in the details of those three features: geolocation matching, live chat, and ratings.&lt;/p&gt;

&lt;p&gt;Geolocation matching: more edge cases than expected&lt;br&gt;
The goal: show users other users “nearby” and match them into sessions. Sounds like a single SQL query, right?&lt;/p&gt;

&lt;p&gt;In practice, it involved:&lt;/p&gt;

&lt;p&gt;Normalizing and storing locations as latitude/longitude.&lt;/p&gt;

&lt;p&gt;Using Postgres with the earthdistance / cube extensions (or a geospatial index) to query by distance.&lt;/p&gt;

&lt;p&gt;Handling users with no location / denied permission gracefully.&lt;/p&gt;

&lt;p&gt;A simplified version of the core matching query looks like this (roughly):&lt;/p&gt;

&lt;p&gt;ts&lt;br&gt;
// Pseudo-DAO: find users within radius (km)&lt;br&gt;
async function findNearbyUsers(userId: string, radiusKm: number) {&lt;br&gt;
  const userLocation = await db.query&amp;lt;{&lt;br&gt;
    lat: number;&lt;br&gt;
    lng: number;&lt;br&gt;
  }&amp;gt;(&lt;br&gt;
    &lt;code&gt;&lt;br&gt;
    SELECT lat, lng&lt;br&gt;
    FROM user_locations&lt;br&gt;
    WHERE user_id = $1&lt;br&gt;
&lt;/code&gt;,&lt;br&gt;
    [userId]&lt;br&gt;
  );&lt;/p&gt;

&lt;p&gt;if (!userLocation.rowCount) {&lt;br&gt;
    return [];&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;const { lat, lng } = userLocation.rows[0];&lt;/p&gt;

&lt;p&gt;const result = await db.query(&lt;br&gt;
    &lt;code&gt;&lt;br&gt;
    SELECT u.id, u.name, ul.lat, ul.lng&lt;br&gt;
    FROM users u&lt;br&gt;
    JOIN user_locations ul ON ul.user_id = u.id&lt;br&gt;
    WHERE u.id != $1&lt;br&gt;
      AND earth_distance(&lt;br&gt;
        ll_to_earth($2, $3),&lt;br&gt;
        ll_to_earth(ul.lat, ul.lng)&lt;br&gt;
      ) &amp;lt;= $4 * 1000&lt;br&gt;
    ORDER BY earth_distance(&lt;br&gt;
      ll_to_earth($2, $3),&lt;br&gt;
      ll_to_earth(ul.lat, ul.lng)&lt;br&gt;
    ) ASC&lt;br&gt;
    LIMIT 20&lt;br&gt;
&lt;/code&gt;,&lt;br&gt;
    [userId, lat, lng, radiusKm]&lt;br&gt;
  );&lt;/p&gt;

&lt;p&gt;return result.rows;&lt;br&gt;
}&lt;br&gt;
The hard parts weren’t just the math; they were:&lt;/p&gt;

&lt;p&gt;Deciding how to bucket “nearby” to avoid constantly recalculating for every move.&lt;/p&gt;

&lt;p&gt;Handling stale locations when someone moves but doesn’t reopen the app.&lt;/p&gt;

&lt;p&gt;Designing fallbacks when location permissions are denied.&lt;/p&gt;

&lt;p&gt;If starting over, there would probably be a clearer separation between “location collection” and “matching,” maybe even separate services or queues for heavier recalculations.&lt;/p&gt;

&lt;p&gt;Live chat: WebSockets meet reality&lt;br&gt;
Live chat looked straightforward on paper: WebSocket server, rooms, message broadcast. In reality, the tricky part was handling disconnects, reconnections, and keeping the UI in sync with backend state.&lt;/p&gt;

&lt;p&gt;The stack:&lt;/p&gt;

&lt;p&gt;WebSocket server running alongside the Node.js API.&lt;/p&gt;

&lt;p&gt;Redis pub/sub to broadcast messages across multiple server instances.&lt;/p&gt;

&lt;p&gt;A simple protocol: JOIN_ROOM, MESSAGE, LEAVE_ROOM, TYPING, etc.&lt;/p&gt;

&lt;p&gt;Here’s a simplified version of the server-side message handler that caused the most headaches:&lt;/p&gt;

&lt;p&gt;ts&lt;br&gt;
// Simplified WebSocket message handler&lt;br&gt;
type IncomingMessage =&lt;br&gt;
  | { type: 'JOIN_ROOM'; roomId: string }&lt;br&gt;
  | { type: 'LEAVE_ROOM'; roomId: string }&lt;br&gt;
  | { type: 'MESSAGE'; roomId: string; text: string };&lt;/p&gt;

&lt;p&gt;function createWebSocketHandler(ws: WebSocket, userId: string) {&lt;br&gt;
  const currentRooms = new Set();&lt;/p&gt;

&lt;p&gt;ws.on('message', async (raw) =&amp;gt; {&lt;br&gt;
    let msg: IncomingMessage;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;try {
  msg = JSON.parse(raw.toString());
} catch {
  // ignore invalid messages
  return;
}

switch (msg.type) {
  case 'JOIN_ROOM': {
    currentRooms.add(msg.roomId);
    await subscribeUserToRoom(userId, msg.roomId);
    break;
  }

  case 'LEAVE_ROOM': {
    currentRooms.delete(msg.roomId);
    await unsubscribeUserFromRoom(userId, msg.roomId);
    break;
  }

  case 'MESSAGE': {
    if (!currentRooms.has(msg.roomId)) {
      return; // ignore messages sent to rooms user hasn't joined
    }

    const saved = await saveMessage({
      roomId: msg.roomId,
      senderId: userId,
      text: msg.text,
    });

    await broadcastToRoom(msg.roomId, {
      type: 'MESSAGE',
      payload: {
        id: saved.id,
        senderId: userId,
        text: saved.text,
        createdAt: saved.createdAt,
      },
    });

    break;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;});&lt;/p&gt;

&lt;p&gt;ws.on('close', () =&amp;gt; {&lt;br&gt;
    // Cleanup all rooms on disconnect&lt;br&gt;
    for (const roomId of currentRooms) {&lt;br&gt;
      void unsubscribeUserFromRoom(userId, roomId);&lt;br&gt;
    }&lt;br&gt;
    currentRooms.clear();&lt;br&gt;
  });&lt;br&gt;
}&lt;br&gt;
What made this hard:&lt;/p&gt;

&lt;p&gt;Ensuring messages aren’t processed for rooms the user “left” mid-flight.&lt;/p&gt;

&lt;p&gt;Cleaning up subscriptions on unexpected disconnects.&lt;/p&gt;

&lt;p&gt;Handling duplicate joins / reconnects without creating ghost sessions.&lt;/p&gt;

&lt;p&gt;If rebuilding, there would likely be a more explicit state machine for connection and room membership, instead of ad-hoc flags and sets.&lt;/p&gt;

&lt;p&gt;Rating system: deceptively simple&lt;br&gt;
The rating system is “just a 1–5 star rating after each session,” but it quickly touches:&lt;/p&gt;

&lt;p&gt;Data modeling (who rates whom, per session).&lt;/p&gt;

&lt;p&gt;Preventing duplicate ratings for the same session.&lt;/p&gt;

&lt;p&gt;Aggregating ratings efficiently for profile display.&lt;/p&gt;

&lt;p&gt;The current approach:&lt;/p&gt;

&lt;p&gt;Each session has an id.&lt;/p&gt;

&lt;p&gt;Each rating is tied to session_id, rater_id, and ratee_id.&lt;/p&gt;

&lt;p&gt;A unique constraint on (session_id, rater_id) to prevent duplicates.&lt;/p&gt;

&lt;p&gt;A materialized view / cached aggregate to show average rating and count on profiles.&lt;/p&gt;

&lt;p&gt;The part that took time was realizing the need for proper constraints and avoiding over-calculating aggregates on every profile view.&lt;/p&gt;

&lt;p&gt;What would be done differently&lt;br&gt;
If starting this project again:&lt;/p&gt;

&lt;p&gt;Split “matching” and “chat” into separate modules (or services) with clear boundaries.&lt;/p&gt;

&lt;p&gt;Invest in more explicit modelling of connection state for WebSockets.&lt;/p&gt;

&lt;p&gt;Add more observability (logs, metrics, tracing) earlier, especially around chat and matching.&lt;/p&gt;

&lt;p&gt;Prototype the rating system as an event stream first, then derive aggregates from events.&lt;/p&gt;

&lt;p&gt;This has been the most technical article written so far on this project, but it matches how the work actually feels: lots of little decisions that add up to either a clean system or a brittle one.&lt;/p&gt;

&lt;p&gt;Your turn (and the repo)&lt;/p&gt;

&lt;p&gt;If you’ve built something similar—geolocation, matching, or live chat—there’s a lot that can be learned from your experience.&lt;/p&gt;

&lt;p&gt;What would you do differently in this architecture, or what’s one pattern you’ve used for live chat / matching that worked especially well?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>javascript</category>
      <category>career</category>
    </item>
    <item>
      <title>AI wrote my bug. I shipped it anyway. Here’s what I learned.</title>
      <dc:creator>James Mateer</dc:creator>
      <pubDate>Wed, 20 May 2026 11:08:43 +0000</pubDate>
      <link>https://dev.to/jaeger974/ai-wrote-my-bug-i-shipped-it-anyway-heres-what-i-learned-54ca</link>
      <guid>https://dev.to/jaeger974/ai-wrote-my-bug-i-shipped-it-anyway-heres-what-i-learned-54ca</guid>
      <description>&lt;p&gt;Last month, I let AI “help” me refactor a small but performance-critical bit of frontend code. It confidently gave me a clean, modern solution. Types checked, tests were green, and the code looked nicer than what I’d written before. I merged it.&lt;/p&gt;

&lt;p&gt;Two days later, a user reported that a key interaction was randomly failing. Turned out the bug was subtle, intermittent, and 100% introduced by that AI-generated snippet. The worst part: the code looked so reasonable that my brain just rubber-stamped it.&lt;/p&gt;

&lt;p&gt;This is how AI is in my workflow now: incredibly useful, occasionally dangerous, and something I treat more like a junior teammate than a trusted source of truth.&lt;/p&gt;

&lt;p&gt;Tools in my day-to-day workflow&lt;br&gt;
Here’s what I actually use as a JavaScript/web dev working on real projects:&lt;/p&gt;

&lt;p&gt;GitHub Copilot for inline suggestions, writing boilerplate React components, and quick refactors.&lt;/p&gt;

&lt;p&gt;Claude for “bigger brain” tasks: explaining legacy code, generating first-draft docs, and exploring alternative designs.&lt;/p&gt;

&lt;p&gt;Browser devtools + tests as the “reality check” layer when AI feels too confident.&lt;/p&gt;

&lt;p&gt;AI’s job in my workflow is to speed up the “typing” and help me explore ideas, not to replace my thinking.&lt;/p&gt;

&lt;p&gt;When AI really helped: reducing noisy event handlers&lt;br&gt;
I had a React component that attached a scroll listener and did some heavy work on every event. It wasn’t catastrophic, but it caused jank on low-end devices. I asked Copilot to suggest a lightweight throttling solution.&lt;/p&gt;

&lt;p&gt;AI-generated suggestion (good enough start)&lt;br&gt;
js&lt;br&gt;
// AI suggestion: basic throttle for scroll handler&lt;br&gt;
function throttle(fn, delay) {&lt;br&gt;
  let lastCall = 0;&lt;/p&gt;

&lt;p&gt;return function (...args) {&lt;br&gt;
    const now = Date.now();&lt;br&gt;
    if (now - lastCall &amp;gt;= delay) {&lt;br&gt;
      lastCall = now;&lt;br&gt;
      fn.apply(this, args);&lt;br&gt;
    }&lt;br&gt;
  };&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;function handleScroll() {&lt;br&gt;
  // expensive calculation&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;window.addEventListener('scroll', throttle(handleScroll, 100));&lt;br&gt;
This was actually pretty decent for my use case. It avoided calling handleScroll on every scroll event and eliminated most of the jank.&lt;/p&gt;

&lt;p&gt;I still made adjustments:&lt;/p&gt;

&lt;p&gt;Extracted the throttle into a shared utility.&lt;/p&gt;

&lt;p&gt;Added proper typing and tests.&lt;/p&gt;

&lt;p&gt;Verified behavior in the browser with real scrolling patterns.&lt;/p&gt;

&lt;p&gt;Final version after review&lt;br&gt;
ts&lt;br&gt;
// utils/throttle.ts&lt;br&gt;
export function throttle void&amp;gt;(&lt;br&gt;
  fn: T,&lt;br&gt;
  delay: number&lt;br&gt;
) {&lt;br&gt;
  let lastCall = 0;&lt;br&gt;
  let timeoutId: number | null = null;&lt;/p&gt;

&lt;p&gt;return function (this: unknown, ...args: Parameters) {&lt;br&gt;
    const now = Date.now();&lt;br&gt;
    const remaining = delay - (now - lastCall);&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (remaining &amp;lt;= 0) {
  lastCall = now;
  if (timeoutId !== null) {
    window.clearTimeout(timeoutId);
    timeoutId = null;
  }
  fn.apply(this, args);
} else if (timeoutId === null) {
  timeoutId = window.setTimeout(() =&amp;gt; {
    lastCall = Date.now();
    timeoutId = null;
    fn.apply(this, args);
  }, remaining);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;};&lt;br&gt;
}&lt;br&gt;
AI got me 60–70% of the way. I added the rest: better handling of calls at the edges, reuse, and type safety.&lt;/p&gt;

&lt;p&gt;This is AI at its best in my workflow: drafting something reasonable that I then refine.&lt;/p&gt;

&lt;p&gt;When AI misled me: a subtle async bug&lt;br&gt;
Now the painful story.&lt;/p&gt;

&lt;p&gt;I had some code that needed to ensure a DOM element existed before measuring it. I asked Copilot for a small helper that “waits for an element to exist then resolves with it or times out.” Copilot produced something like this:&lt;/p&gt;

&lt;p&gt;AI suggestion (looked correct, wasn’t)&lt;br&gt;
js&lt;br&gt;
function waitForElement(selector, timeout = 3000) {&lt;br&gt;
  return new Promise((resolve, reject) =&amp;gt; {&lt;br&gt;
    const element = document.querySelector(selector);&lt;br&gt;
    if (element) {&lt;br&gt;
      resolve(element);&lt;br&gt;
      return;&lt;br&gt;
    }&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const observer = new MutationObserver(() =&amp;gt; {
  const el = document.querySelector(selector);
  if (el) {
    observer.disconnect();
    resolve(el);
  }
});

observer.observe(document.body, { childList: true, subtree: true });

setTimeout(() =&amp;gt; {
  observer.disconnect();
  reject(new Error('Element not found'));
}, timeout);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;});&lt;br&gt;
}&lt;br&gt;
At a glance, it looked fine: observe DOM mutations, resolve when the element appears, reject on timeout. I skimmed it, tests passed, and I shipped it.&lt;/p&gt;

&lt;p&gt;The bug? Under certain conditions, the timeout fired after the element was found and resolved. In rare race conditions, the promise would resolve and then later be rejected, causing unpredictable error handling elsewhere.&lt;/p&gt;

&lt;p&gt;Corrected version with proper cleanup&lt;br&gt;
js&lt;br&gt;
function waitForElement(selector, timeout = 3000) {&lt;br&gt;
  return new Promise((resolve, reject) =&amp;gt; {&lt;br&gt;
    const existing = document.querySelector(selector);&lt;br&gt;
    if (existing) {&lt;br&gt;
      resolve(existing);&lt;br&gt;
      return;&lt;br&gt;
    }&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let settled = false;
const observer = new MutationObserver(() =&amp;gt; {
  const el = document.querySelector(selector);
  if (el &amp;amp;&amp;amp; !settled) {
    settled = true;
    observer.disconnect();
    window.clearTimeout(timeoutId);
    resolve(el);
  }
});

observer.observe(document.body, { childList: true, subtree: true });

const timeoutId = window.setTimeout(() =&amp;gt; {
  if (!settled) {
    settled = true;
    observer.disconnect();
    reject(new Error(`Element not found: ${selector}`));
  }
}, timeout);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;});&lt;br&gt;
}&lt;br&gt;
Key differences:&lt;/p&gt;

&lt;p&gt;Introduced a settled flag.&lt;/p&gt;

&lt;p&gt;Cleared the timeout when resolving.&lt;/p&gt;

&lt;p&gt;Guarded both paths so the promise only settles once.&lt;/p&gt;

&lt;p&gt;This is exactly the kind of subtle bug AI is prone to: the logic is “plausible” and looks idiomatic, but it hasn’t been truly thought through.&lt;/p&gt;

&lt;p&gt;My framework for trusting AI (or not)&lt;br&gt;
Here’s how AI fits into my decision-making:&lt;/p&gt;

&lt;p&gt;Use AI for:&lt;/p&gt;

&lt;p&gt;Boilerplate and repetitive patterns.&lt;/p&gt;

&lt;p&gt;Utility functions that I heavily review.&lt;/p&gt;

&lt;p&gt;Alternative approaches I might not think of on my own.&lt;/p&gt;

&lt;p&gt;Writing tests and docs drafts that I refine.&lt;/p&gt;

&lt;p&gt;Be skeptical when:&lt;/p&gt;

&lt;p&gt;The code touches async, concurrency, or subtle browser behavior.&lt;/p&gt;

&lt;p&gt;It involves security, authentication, or data validation.&lt;/p&gt;

&lt;p&gt;The code “just works” and I don’t fully understand why.&lt;/p&gt;

&lt;p&gt;Non-negotiables before merging AI code:&lt;/p&gt;

&lt;p&gt;Read it line by line and explain it in plain language.&lt;/p&gt;

&lt;p&gt;Add tests that would have caught the bug if the code were wrong.&lt;/p&gt;

&lt;p&gt;Run it in a realistic environment, not just unit tests.&lt;/p&gt;

&lt;p&gt;The mindset shift: AI is a junior engineer with infinite stamina but no real understanding. If it writes something you don’t fully get, that’s your bug waiting to happen.&lt;/p&gt;

&lt;p&gt;Your turn&lt;br&gt;
AI is now part of most dev workflows, for better or worse. For me, it’s worth the risk—but only when paired with a strong review habit and healthy skepticism.&lt;/p&gt;

&lt;p&gt;How are you using AI in your workflow right now, and what’s one time it either saved you hours or nearly burned you—what happened?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>The Scientific Method Made Me Better at Debugging Than I Expected</title>
      <dc:creator>James Mateer</dc:creator>
      <pubDate>Fri, 08 May 2026 13:21:00 +0000</pubDate>
      <link>https://dev.to/jaeger974/the-scientific-method-made-me-better-at-debugging-than-i-expected-3min</link>
      <guid>https://dev.to/jaeger974/the-scientific-method-made-me-better-at-debugging-than-i-expected-3min</guid>
      <description>&lt;p&gt;One of the first major bugs I hit as a junior developer looked simple at first.&lt;/p&gt;

&lt;p&gt;My React frontend refused to talk to my Express backend. The terminal kept throwing errors, the browser showed failed requests, and after a few hours I was convinced I had fundamentally misunderstood how APIs worked.&lt;/p&gt;

&lt;p&gt;At first, I did what I think a lot of beginners do: randomly changed things until something worked.&lt;/p&gt;

&lt;p&gt;Bad idea.&lt;/p&gt;

&lt;p&gt;Eventually I stopped, took a breath, and approached it the same way I used to approach problems in scientific environments: isolate variables and identify root causes.&lt;/p&gt;

&lt;p&gt;I started using a simple “5 Whys” approach.&lt;/p&gt;

&lt;p&gt;Problem: Frontend couldn’t connect to backend.&lt;/p&gt;

&lt;p&gt;Why?&lt;br&gt;
→ The request was failing.&lt;/p&gt;

&lt;p&gt;Why?&lt;br&gt;
→ The API endpoint returned a CORS error.&lt;/p&gt;

&lt;p&gt;Why?&lt;br&gt;
→ Express wasn’t configured to allow requests from the Vite frontend.&lt;/p&gt;

&lt;p&gt;Why?&lt;br&gt;
→ I never added CORS middleware.&lt;/p&gt;

&lt;p&gt;Why?&lt;br&gt;
→ I didn’t fully understand how frontend/backend communication actually worked yet.&lt;/p&gt;

&lt;p&gt;That single debugging session changed how I think about development.&lt;/p&gt;

&lt;p&gt;Instead of treating bugs like random disasters, I started treating them like experiments.&lt;/p&gt;

&lt;p&gt;import express from "express";&lt;br&gt;
import cors from "cors";&lt;/p&gt;

&lt;p&gt;const app = express();&lt;/p&gt;

&lt;p&gt;app.use(cors());&lt;/p&gt;

&lt;p&gt;app.listen(3000, () =&amp;gt; {&lt;br&gt;
  console.log("Server connected on port 3000");&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;The fix itself was tiny.&lt;/p&gt;

&lt;p&gt;The lesson was not.&lt;/p&gt;

&lt;p&gt;What surprised me most about software development is that professional developers don’t seem to know everything either. Before I started coding, I honestly thought experienced engineers just remembered entire frameworks and instantly knew solutions.&lt;/p&gt;

&lt;p&gt;What I’ve discovered instead is that good developers:&lt;/p&gt;

&lt;p&gt;Read documentation constantly&lt;br&gt;
Break problems into smaller problems&lt;br&gt;
Search intelligently&lt;br&gt;
Debug methodically&lt;br&gt;
Document what they learn&lt;/p&gt;

&lt;p&gt;That last one surprised me most.&lt;/p&gt;

&lt;p&gt;I used to think documentation was just corporate busywork. Now I understand it’s one of the most practical skills in development. Half the battle of solving future problems is leaving breadcrumbs for yourself.&lt;/p&gt;

&lt;p&gt;I’ve started keeping notes on:&lt;/p&gt;

&lt;p&gt;recurring errors&lt;br&gt;
useful terminal commands&lt;br&gt;
ESLint fixes&lt;br&gt;
Git commands I always forget&lt;br&gt;
project setup issues&lt;br&gt;
debugging patterns&lt;/p&gt;

&lt;p&gt;Something as simple as this has saved me hours:&lt;/p&gt;

&lt;p&gt;git status&lt;br&gt;
git add .&lt;br&gt;
git commit -m "fix: resolved auth middleware issue"&lt;br&gt;
git push origin main&lt;/p&gt;

&lt;p&gt;Another thing that surprised me was how emotionally challenging development can be.&lt;/p&gt;

&lt;p&gt;There were days where I felt genuinely underqualified. I’d spend hours on something that turned out to be a missing bracket or incorrect import path.&lt;/p&gt;

&lt;p&gt;But I’m starting to realise that debugging is not proof that you’re failing.&lt;/p&gt;

&lt;p&gt;Debugging is the work.&lt;/p&gt;

&lt;p&gt;Every bug forces you to understand the system better than you did before.&lt;/p&gt;

&lt;p&gt;One of the most encouraging moments I’ve had so far was seeing this message after hours of troubleshooting:&lt;/p&gt;

&lt;p&gt;Server connected on port 3000&lt;/p&gt;

&lt;p&gt;Tiny win. Massive confidence boost.&lt;/p&gt;

&lt;p&gt;I’m still early in my journey, but I already think my previous experience outside tech gave me an advantage: learning how to think systematically under pressure.&lt;/p&gt;

&lt;p&gt;Turns out that skill transfers surprisingly well into software development.&lt;/p&gt;

&lt;p&gt;If you're career changing into dev, what's the biggest thing your old career taught you?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>beginners</category>
      <category>career</category>
    </item>
  </channel>
</rss>
