<?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" version="2.0"><channel><title><![CDATA[Saumya Agrawal]]></title><description><![CDATA[The blogs are my way to showcase my learnings ]]></description><link>https://blog.saumyagrawal.in</link><image><url>https://cdn.hashnode.com/uploads/logos/678b775e773554ab7117f20a/195f761d-2130-4997-9213-bc3efd66aa8c.png</url><title>Saumya Agrawal</title><link>https://blog.saumyagrawal.in</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 08 Jun 2026 20:08:57 GMT</lastBuildDate><atom:link href="https://blog.saumyagrawal.in/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[One Person, One Vote? The Technical Struggle of Web Poll Integrity]]></title><description><![CDATA[If you've ever built a poll/survey, you've hit this wall: how do you stop one person from voting fifty times? It sounds simple but it isn't. Every approach trades off between friction and accuracy, an]]></description><link>https://blog.saumyagrawal.in/one-person-one-vote-the-technical-struggle-of-web-poll-integrity</link><guid isPermaLink="true">https://blog.saumyagrawal.in/one-person-one-vote-the-technical-struggle-of-web-poll-integrity</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Saumya]]></dc:creator><pubDate>Sat, 16 May 2026 12:25:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/0ff9c434-67f6-4587-90ff-7229255aab4b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you've ever built a poll/survey, you've hit this wall: how do you stop one person from voting fifty times? It sounds simple but it isn't. Every approach trades off between friction and accuracy, and the "right" answer depends on what you're building.</p>
<p>Here's what I've learned.</p>
<h2>Picking our poison: The trade-offs of tracking</h2>
<h3>IP-based fingerprinting</h3>
<p>Hash the voter's IP to restrict votes to one per IP.</p>
<p>Except not really. A university campus shares one public IP. An entire office behind a corporate NAT looks like a single user. You just locked out hundreds of legitimate voters. On the flip side, anyone with a VPN rotates their IP in seconds and votes again.</p>
<p>It works well enough for casual polls where the stakes are low. It falls apart the moment someone actually wants to game it.</p>
<table>
<thead>
<tr>
<th>Pros</th>
<th>Cons</th>
</tr>
</thead>
<tbody><tr>
<td>Zero client-side code</td>
<td>Shared NAT blocks legit voters</td>
</tr>
<tr>
<td>No cookies to clear</td>
<td>VPN defeats it instantly</td>
</tr>
<tr>
<td>Works in any environment</td>
<td>Entire households = one vote</td>
</tr>
</tbody></table>
<h3>Browser fingerprinting</h3>
<p>Libraries like FingerprintJS build a hash from your screen resolution, installed fonts, GPU renderer, and timezone. Because this fingerprint relies entirely on your browser's hardware profile, it requires zero cookies and easily survives incognito mode.</p>
<p>Sounds clever, and it is. But two people on identical company laptops with the same OS image produce the same fingerprint. Browser updates change the hash. And privacy-focused browsers actively fight fingerprinting by randomising these signals. You're building on sand that shifts with every Chrome update.</p>
<table>
<thead>
<tr>
<th>Pros</th>
<th>Cons</th>
</tr>
</thead>
<tbody><tr>
<td>Survives incognito mode</td>
<td>Identical devices = same hash</td>
</tr>
<tr>
<td>No login required</td>
<td>Browser updates break it</td>
</tr>
<tr>
<td>Hard for casual users to dodge</td>
<td>Privacy browsers randomise signals</td>
</tr>
<tr>
<td></td>
<td>Adds a client-side dependency</td>
</tr>
</tbody></table>
<h3>Cookie / localStorage tracking</h3>
<p>The simplest approach is to drop a cookie or localStorage flag after voting. Check it before allowing another vote. But it is the easiest to defeat.</p>
<p>Clearing cookies or opening an incognito window bypasses it instantly. Even non-technical users know this trick.</p>
<p>That said: most people won't bother. If your poll is "Where should we eat this Friday," cookies are fine.</p>
<table>
<thead>
<tr>
<th>Pros</th>
<th>Cons</th>
</tr>
</thead>
<tbody><tr>
<td>Trivial to implement</td>
<td>Clear cookies = vote again</td>
</tr>
<tr>
<td>No server-side storage needed</td>
<td>Incognito bypasses it</td>
</tr>
<tr>
<td>Zero friction for voters</td>
<td>Provides no real security</td>
</tr>
</tbody></table>
<h3>Account-based authentication</h3>
<p>Mandatory login to tie one vote to one account. This is the gold standard for accuracy, if someone has to authenticate with Google or GitHub, creating duplicate votes means creating duplicate accounts across those providers. That's real friction for an attacker.</p>
<p>The cost is real friction for everyone else too. Requiring login for a casual poll kills participation. Half your audience bounces at the login screen. The ones who stay are a self-selected group, which skews your results in a different way.</p>
<table>
<thead>
<tr>
<th>Pros</th>
<th>Cons</th>
</tr>
</thead>
<tbody><tr>
<td>Strongest duplicate prevention</td>
<td>Kills participation on casual polls</td>
</tr>
<tr>
<td>Can't bypass from Postman</td>
<td>Self-selection bias in respondents</td>
</tr>
<tr>
<td>Audit trail for every vote</td>
<td>Requires OAuth setup / auth infra</td>
</tr>
</tbody></table>
<h3>CAPTCHA (Cloudflare Turnstile, reCAPTCHA, hCaptcha)</h3>
<p>CAPTCHAs don't prevent duplicate votes directly, rather they prevent bots. Cloudflare Turnstile is the nicest version of this, it proves a human is present, not that the human hasn't voted before.</p>
<p>You'd pair CAPTCHA with another method. CAPTCHA + IP hashing stops casual scripting via Postman or curl, the CAPTCHA token requires a browser environment, so raw API calls can't fake it. But a determined human can still solve the CAPTCHA repeatedly from different IPs.</p>
<p>Worth adding if bot spam is a realistic threat. Overkill for a team lunch poll.</p>
<table>
<thead>
<tr>
<th>Pros</th>
<th>Cons</th>
</tr>
</thead>
<tbody><tr>
<td>Blocks bots and scripts</td>
<td>Doesn't prevent human duplicates</td>
</tr>
<tr>
<td>Turnstile is invisible to most</td>
<td>Adds external dependency</td>
</tr>
<tr>
<td>Stops Postman/curl abuse</td>
<td>Must pair with another method</td>
</tr>
</tbody></table>
<h3>Rate limiting</h3>
<p>It helps to throttle submissions per IP, like allowing five votes per minute. This doesn't prevent duplicates, but it slows down brute-force attempts. It's a supporting measure, not a standalone solution.</p>
<p>The nice thing: it costs almost nothing to implement and catches the laziest attacks. The bad thing: it only catches the laziest attacks.</p>
<table>
<thead>
<tr>
<th>Pros</th>
<th>Cons</th>
</tr>
</thead>
<tbody><tr>
<td>Near-zero implementation cost</td>
<td>Only slows, doesn't prevent</td>
</tr>
<tr>
<td>Catches lazy scripted attacks</td>
<td>Useless against patient humans</td>
</tr>
<tr>
<td>Good as a supporting layer</td>
<td>Not a standalone solution</td>
</tr>
</tbody></table>
<h3>Device attestation (Apple DeviceCheck, Android SafetyNet)</h3>
<p>Mobile platforms offer APIs that let you mark a device as "already voted" at the hardware level. The user can't clear it. Factory reset doesn't help.</p>
<p>Powerful, but platform-locked, and web polls can't use it. And it ties your system to Apple's and Google's infrastructure, which is a dependency you might not want.</p>
<table>
<thead>
<tr>
<th>Pros</th>
<th>Cons</th>
</tr>
</thead>
<tbody><tr>
<td>Hardware-level, can't be faked</td>
<td>Mobile only, no web support</td>
</tr>
<tr>
<td>Survives factory reset</td>
<td>Platform-locked (Apple / Google)</td>
</tr>
<tr>
<td>Strongest anonymous prevention</td>
<td>Adds infra dependency</td>
</tr>
</tbody></table>
<h2>How they compare</h2>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/57897ff4-4bd6-4bd4-9648-5f3d0cd26b9c.png" alt="" style="display:block;margin:0 auto" />

<h2>The Postman test</h2>
<p>Here's the question nobody asks until it's too late: what happens when someone opens Postman, grabs your <code>POST /api/poll/:id/respond</code> endpoint, and starts firing requests?</p>
<p>Attacker with Postman / Curl / Python Script:</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/bec54864-608f-48b9-b006-3deb0c43266b.png" alt="" style="display:block;margin:0 auto" />

<p>How each method holds up:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>Postman spam?</th>
<th>Why</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Cookies</strong></td>
<td>Wide open</td>
<td>Postman doesn't send or store cookies by default. Every request looks like a fresh visitor.</td>
</tr>
<tr>
<td><strong>IP hash</strong></td>
<td>1 vote goes through</td>
<td>Every request from the same machine has the same IP. First one lands, rest get rejected by the unique index.</td>
</tr>
<tr>
<td><strong>IP + UA hash</strong></td>
<td>1 vote per UA string</td>
<td>Attacker can fake the <code>User-Agent</code> header. Each unique UA string = one vote. A loop with 100 random UAs = 100 votes.</td>
</tr>
<tr>
<td><strong>Browser fingerprint</strong></td>
<td>Wide open</td>
<td>Fingerprinting requires a real browser with a DOM. Postman sends raw HTTP, there's no canvas, no WebGL, no font list. The fingerprint never gets generated. No fingerprint = no dedup.</td>
</tr>
<tr>
<td><strong>CAPTCHA</strong></td>
<td>Blocked</td>
<td>Turnstile/reCAPTCHA tokens require a browser challenge, and Postman can't solve them. The request gets rejected before it even reaches your vote logic.</td>
</tr>
<tr>
<td><strong>Auth (OAuth)</strong></td>
<td>Blocked</td>
<td>Needs a valid session cookie from a real OAuth flow. Postman can't fake a Google login. No session = 401.</td>
</tr>
<tr>
<td><strong>Rate limiting</strong></td>
<td>Slowed</td>
<td>5 requests get through per minute. The other 995 get 429'd. Doesn't prevent the 5 that land, just buys time.</td>
</tr>
<tr>
<td><strong>Device attestation</strong></td>
<td>Blocked</td>
<td>Requires a signed token from Apple/Google hardware APIs. Can't be generated from Postman.</td>
</tr>
</tbody></table>
<p>The uncomfortable truth about IP + UA hashing: it stops a lazy Postman test (same IP, same default UA = same fingerprint, duplicate rejected). But anyone who thinks to set a custom <code>User-Agent</code> header which is one line in Postman gets around it. The <code>User-Agent</code> is a client-supplied header. You're trusting the attacker to identify themselves honestly.</p>
<p>That's why CAPTCHA and auth are the only methods that truly block programmatic abuse. Everything else is a filter for laziness, not intent.</p>
<p>For Versus, I accepted this. The rate limiter catches the spray-and-pray scripts (5 req/min per IP), and the fingerprint index catches the ones that don't bother faking headers. A determined attacker with rotating IPs and random UAs could still stuff votes, but at that point, they're working harder than most people work at their actual jobs. For a poll about where to eat Friday, that's a risk I'll take.</p>
<h2>What I actually used in Versus</h2>
<p>I built a poll platform called Versus for a hackathon, and I needed something that worked for two very different modes: anonymous polls (anyone can vote) and authenticated polls (login required).</p>
<p>For <strong>authenticated polls</strong>, it's straightforward. Require OAuth login via Google or GitHub. Store <code>respondent</code> (user ID) on each response. A compound unique index on <code>{ poll, respondent }</code> with a <code>partialFilterExpression</code> makes the database reject duplicates at the storage layer. One account with one vote is enforced by MongoDB. Can't be bypassed from Postman because you'd need a valid session cookie.</p>
<p>For <strong>anonymous polls</strong>, I went with IP + User-Agent hashing. When a vote comes in, I compute <code>SHA-256(IP + User-Agent)</code> and store it as a <code>fingerprint</code> on the response. Another compound unique index on <code>{ poll, fingerprint }</code> rejects duplicates at the database level. The hash is one-way, and I never store raw IPs or user agents.</p>
<p>Why IP + UA instead of just IP? I had a realisation that my phone and laptop on the same WiFi share a public IP. Pure IP hashing meant my entire household got one vote per poll. Adding the User-Agent string means different devices (phone Safari vs. laptop Chrome) produce different hashes, even on the same network. The same browser on the same device still can't double-vote, but at least my family can each have an opinion.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/03aa3e6d-db28-4f3b-9f39-8b5bf836ed8a.png" alt="" style="display:block;margin:0 auto" />

<p>I chose not to add CAPTCHA or browser fingerprinting just to keep it simple and flexible. The main threat is someone idly hitting refresh, not a coordinated bot attack. IP+UA hashing catches that. And the cost of false negatives (one person switching browsers to vote twice) is low.</p>
<p>If I needed higher assurance, I'd add Cloudflare Turnstile as a bot gate and push users toward authenticated mode.</p>
<h2>How the rest of the industry does it</h2>
<p>While building Versus, I studied what existing platforms actually do. The interesting thing: their anti-duplicate strategy almost always mirrors their business model.</p>
<p><strong>Casual poll platforms</strong> optimise for participation. <strong>Survey tools</strong> optimise for response quality. <strong>Social platforms</strong> lean on account reputation. <strong>Enterprise tools</strong> optimise for auditability. And actual election systems treat accuracy as non-negotiable, friction be damned.</p>
<h3>Casual polls (StrawPoll, Mentimeter, Poll Everywhere)</h3>
<p>StrawPoll implements Cookies, IP tracking, optional CAPTCHA, rate limiting. They openly acknowledge that VPNs bypass their limits, and duplicate prevention is "best effort." Their goal is stopping casual spam.</p>
<p>Mentimeter and Poll Everywhere optimise for the "join instantly with a code" experience. Session limits, per-device restrictions, throttling, abuse detection. But they accept that a determined user can vote multiple times, because their use case is audience interaction during a live presentation. Locking down the poll harder than the room it's running in doesn't make sense.</p>
<h3>Survey platforms (SurveyMonkey, Google Forms, Typeform)</h3>
<p>SurveyMonkey's strongest mechanism is single-use survey links. Each respondent gets a unique URL (<code>survey.com/r/xyz123token</code>), and the token is invalidated after submission. For anonymous surveys, they fall back to cookies, IP restrictions, and session tracking. For controlled surveys, they have email invitations, unique links, authenticated respondents. The unique-link approach is extremely common in enterprise surveys.</p>
<p>Google Forms gives creators a toggle: "Limit to 1 response," which requires a Google account. Without it, there's essentially no strong duplicate prevention, they rely on cookies and Google's internal abuse detection. The hidden advantage Google has is that they already know enormous amounts about device reputation and traffic patterns. Small apps can't replicate that.</p>
<p>Typeform is interesting because they care more about response quality than strict uniqueness. They'll detect suspicious patterns and flag low-quality responses rather than hard-blocking. Cookies, hidden fields, optional auth, reCAPTCHA, and behavioural fraud systems for enterprise tiers.</p>
<h3>Enterprise (Qualtrics)</h3>
<p>This is where things get serious. Qualtrics supports SSO, signed links, panel management, fraud scoring, behavioural analytics, bot detection, geo analysis, and digital fingerprinting. They also do things like "speeding" detection (completed too fast to be human) and "straight-lining" detection (selecting the same option repeatedly). At this level, fraud analysis matters more than prevention, because sophisticated survey fraud is impossible to fully prevent.</p>
<h3>Social media (X, Reddit)</h3>
<p>X Polls has one vote per authenticated account. Internally, they likely layer account trust, age, phone verification, and spam reputation. But the visible enforcement is just "log in."</p>
<p>Reddit Polls: also account-based, but Reddit has an enormous hidden advantage, which is account reputation history. A 7-year-old active account is far more trustworthy than <code>u/free_crypto_airdrop_19382</code>. They can weight trust internally in ways that new platforms can't.</p>
<h3>Actual voting systems (ElectionBuddy, Simply Voting)</h3>
<p>Student government, union elections, shareholder voting use unique voter rolls, single-use cryptographic tokens, email verification, identity verification, audit logs, and cryptographic receipts. Some support anonymous-but-verifiable voting. The philosophy flips completely: accuracy over participation friction.</p>
<h2>What the industry is converging toward</h2>
<p>Pure IP blocking is considered weak now. CGNAT, mobile carriers, VPNs, IPv6 privacy rotation, an IP address is a weak signal, not an identity. Browser fingerprinting is weakening too. Safari ITP, Firefox anti-fingerprinting, Brave randomisation, Chrome's privacy sandbox direction. fingerprinting is now one signal among many, not the core system.</p>
<p>Most modern platforms are converging toward a layered model:</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/27568626-24c1-425b-b466-fa6615d28422.png" alt="" style="display:block;margin:0 auto" />

<p>Versus sits squarely in the casual-poll tier and that's intentional. Low-friction anonymous mode, stronger authenticated mode, database-level uniqueness, rate limiting, and a pragmatic threat model. The biggest industry-standard addition I'm currently missing is probably a lightweight bot/risk layer like Cloudflare Turnstile, plus optional trust scoring over time.</p>
<p>But that's a problem I am looking forward to solve.</p>
<hr />
<p><em>Versus is live</em> <a href="https://versus.saumyagrawal.in/"><em>here</em></a></p>
<p><em>This was a small project for hackathon, but I would be making some updates in near future. I welcome all the ideas, features and bug reports on</em> <a href="https://github.com/idreamfyrei/versus"><em>GitHub</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Express.js: I Stopped Fighting Node's HTTP Module and Everything Got Easier]]></title><description><![CDATA[I built my first Node.js server using the built-in http module. It worked. It also made me want to close my laptop and go outside. Not because the code was hard, but because every tiny thing required ]]></description><link>https://blog.saumyagrawal.in/express-js-i-stopped-fighting-node-s-http-module-and-everything-got-easier</link><guid isPermaLink="true">https://blog.saumyagrawal.in/express-js-i-stopped-fighting-node-s-http-module-and-everything-got-easier</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Saumya]]></dc:creator><pubDate>Fri, 08 May 2026 16:17:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/c3242e9e-43a4-4d6a-8478-6da125c6ed25.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I built my first Node.js server using the built-in <code>http</code> module. It worked. It also made me want to close my laptop and go outside. Not because the code was hard, but because every tiny thing required writing the same boilerplate over and over. Want to check if the request is a GET to <code>/users</code>? You write an <code>if</code> statement. Want to parse the body of a POST request? You manually collect chunks of data from a stream. Want to send JSON back? You set headers by hand and call <code>res.end()</code> with a stringified object.</p>
<p>Here is what a basic server looks like with just Node:</p>
<pre><code class="language-js">const http = require('http');

const server = http.createServer((req, res) =&gt; {
  if (req.method === 'GET' &amp;&amp; req.url === '/hello') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ message: 'hi there' }));
  } else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('not found');
  }
});

server.listen(3000, () =&gt; {
  console.log('running on port 3000');
});
</code></pre>
<p>Two routes and the code is already getting annoying. Imagine adding fifteen more. Imagine adding POST body parsing to each one. Imagine doing error handling across all of them. I tried. I got about four routes deep before the <code>if/else</code> chain became unreadable.</p>
<p>Express exists because nobody wants to write that.</p>
<hr />
<h2>What Express actually is</h2>
<p>Express is a library that sits on top of Node's <code>http</code> module. It does not replace it. Under the hood, Express still creates an <code>http.Server</code>. What it gives you is a cleaner way to define routes, read request data, and send responses without manually wiring everything together yourself.</p>
<p>Install it and set up a project:</p>
<pre><code class="language-bash">mkdir my-express-app &amp;&amp; cd my-express-app
npm init -y
npm install express
</code></pre>
<p>Now the same server from above, rewritten:</p>
<pre><code class="language-js">const express = require('express');
const app = express();

app.get('/hello', (req, res) =&gt; {
  res.json({ message: 'hi there' });
});

app.listen(3000, () =&gt; {
  console.log('running on port 3000');
});
</code></pre>
<p>No <code>writeHead</code>, or <code>JSON.stringify</code>. No checking <code>req.method</code> and <code>req.url</code> manually. One line defines the route, the method, and the handler. <code>res.json()</code> sets the content type and stringifies the object for you.</p>
<p>That difference gets wider the more routes you add.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/6cf063b3-9ac0-4d85-a8e3-b3b58ebc7061.png" alt="" style="display:block;margin:0 auto" />

<p>The request comes in. Express checks its list of registered routes. If it finds a match for both the HTTP method and the URL path, it runs the handler function you gave it. If nothing matches, Express sends a 404 by default. You do not have to write that yourself.</p>
<hr />
<h2>Handling GET requests</h2>
<p>GET requests are how clients ask for data. When you type a URL in your browser, that is a GET request. When a frontend calls <code>fetch('/api/users')</code>, that is a GET request.</p>
<pre><code class="language-js">const express = require('express');
const app = express();

const users = [
  { id: 1, name: 'Priya', email: 'priya@example.com' },
  { id: 2, name: 'Ravi', email: 'ravi@example.com' },
  { id: 3, name: 'Meera', email: 'meera@example.com' },
];

// get all users
app.get('/users', (req, res) =&gt; {
  res.json(users);
});

app.listen(3000, () =&gt; console.log('running on 3000'));
</code></pre>
<p>Hit <code>http://localhost:3000/users</code> in your browser and you get:</p>
<pre><code class="language-json">[
  { "id": 1, "name": "Priya", "email": "priya@example.com" },
  { "id": 2, "name": "Ravi", "email": "ravi@example.com" },
  { "id": 3, "name": "Meera", "email": "meera@example.com" }
]
</code></pre>
<p>Now say you want a single user by their ID. Express lets you put a colon in the URL path to create a route parameter:</p>
<pre><code class="language-js">// get one user by id
app.get('/users/:id', (req, res) =&gt; {
  const id = parseInt(req.params.id);
  const user = users.find(u =&gt; u.id === id);

  if (!user) {
    return res.status(404).json({ error: 'user not found' });
  }

  res.json(user);
});
</code></pre>
<p><code>req.params.id</code> pulls the value from the URL. A request to <code>/users/2</code> gives you <code>req.params.id</code> as the string <code>"2"</code>. You parse it to a number, look it up, and either return the user or a 404. That <code>:id</code> syntax is called a route parameter, and you can have multiple in one path: <code>/users/:userId/orders/:orderId</code> would give you both <code>req.params.userId</code> and <code>req.params.orderId</code>.</p>
<p>One thing that tripped me up early: route parameters always come back as strings. Even if the URL says <code>/users/2</code>, <code>req.params.id</code> is <code>"2"</code>, not <code>2</code>. Forgetting to parse it leads to comparison bugs that are annoying to track down because <code>"2" === 2</code> is <code>false</code> in JavaScript.</p>
<hr />
<h2>Handling POST requests</h2>
<p>POST requests send data to the server. A signup form, a new order, a comment being submitted. The data lives in the request body, and Express does not parse it automatically. You need to tell it how.</p>
<pre><code class="language-js">app.use(express.json());
</code></pre>
<p>That one line is middleware. It tells Express to parse incoming JSON bodies and attach the result to <code>req.body</code>. Without it, <code>req.body</code> is <code>undefined</code> and you will stare at your code wondering why nothing works. I spent 20 minutes on this the first time. Add this line near the top of your file, before your route definitions.</p>
<pre><code class="language-js">const express = require('express');
const app = express();

app.use(express.json()); // parse JSON bodies

const users = [
  { id: 1, name: 'Priya', email: 'priya@example.com' },
  { id: 2, name: 'Ravi', email: 'ravi@example.com' },
];

let nextId = 3;

app.post('/users', (req, res) =&gt; {
  const { name, email } = req.body;

  if (!name || !email) {
    return res.status(400).json({ error: 'name and email are required' });
  }

  const newUser = { id: nextId++, name, email };
  users.push(newUser);

  res.status(201).json(newUser);
});

app.listen(3000, () =&gt; console.log('running on 3000'));
</code></pre>
<p>Test it with curl from your terminal:</p>
<pre><code class="language-bash">curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Aditya", "email": "aditya@example.com"}'
</code></pre>
<p>Response:</p>
<pre><code class="language-json">{ "id": 3, "name": "Aditya", "email": "aditya@example.com" }
</code></pre>
<p>The <code>201</code> status code means "created." You could send <code>200</code> and it would work fine, but <code>201</code> tells the client specifically that a new resource was created. Using the right status codes is a habit worth building early. <code>200</code> means OK. <code>201</code> means created. <code>400</code> means the client sent bad data. <code>404</code> means not found. <code>500</code> means the server broke. Most real APIs use these consistently, and your code becomes easier to debug when the status code already tells you what category of problem you are looking at.</p>
<p>Here is what the full flow looks like when a POST request comes in:</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/0806d2c5-0752-4ea3-b6ac-830181d1e0cd.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Sending different types of responses</h2>
<p><code>res.json()</code> is the one you will use most, but Express has a few other response methods worth knowing.</p>
<pre><code class="language-js">// send plain text
app.get('/health', (req, res) =&gt; {
  res.send('OK');
});

// send JSON with a status code
app.get('/status', (req, res) =&gt; {
  res.status(200).json({ status: 'running', uptime: process.uptime() });
});

// send just a status code with no body
app.delete('/users/:id', (req, res) =&gt; {
  // ... delete logic here
  res.sendStatus(204); // 204 means "no content" - deleted successfully, nothing to return
});

// redirect to another URL
app.get('/old-page', (req, res) =&gt; {
  res.redirect('/new-page');
});
</code></pre>
<p><code>res.send()</code> is smart about what you give it. Pass a string, it sets the content type to <code>text/html</code>. Pass an object, it behaves like <code>res.json()</code>. Pass a Buffer, it sets the type to <code>application/octet-stream</code>. I would recommend being explicit and using <code>res.json()</code> for objects and <code>res.send()</code> for strings, rather than relying on the auto-detection. It makes your intent obvious when someone else reads the code later.</p>
<p>One gotcha: do not call <code>res.json()</code> or <code>res.send()</code> twice in the same handler. Express will throw an error that says "Cannot set headers after they are sent to the client." This usually happens when you forget a <code>return</code> statement before sending an error response:</p>
<pre><code class="language-js">// broken - sends two responses
app.get('/users/:id', (req, res) =&gt; {
  const user = users.find(u =&gt; u.id === parseInt(req.params.id));

  if (!user) {
    res.status(404).json({ error: 'not found' });
    // missing return! code keeps running
  }

  res.json(user); // crashes here because a response was already sent
});

// fixed
app.get('/users/:id', (req, res) =&gt; {
  const user = users.find(u =&gt; u.id === parseInt(req.params.id));

  if (!user) {
    return res.status(404).json({ error: 'not found' }); // return stops execution
  }

  res.json(user);
});
</code></pre>
<p>That <code>return</code> before <code>res.status(404)</code> is easy to forget and hard to debug if you do not know what the error message means.</p>
<hr />
<h2>Putting it all together</h2>
<p>Here is a complete working server with GET and POST routes, input validation, proper status codes, and the JSON parsing middleware. You can copy this into a file called <code>server.js</code> and run it with <code>node server.js</code>:</p>
<pre><code class="language-js">const express = require('express');
const app = express();
const PORT = 3000;

app.use(express.json());

const users = [
  { id: 1, name: 'Priya', email: 'priya@example.com' },
  { id: 2, name: 'Ravi', email: 'ravi@example.com' },
];

let nextId = 3;

// get all users
app.get('/users', (req, res) =&gt; {
  res.json(users);
});

// get single user
app.get('/users/:id', (req, res) =&gt; {
  const user = users.find(u =&gt; u.id === parseInt(req.params.id));
  if (!user) return res.status(404).json({ error: 'user not found' });
  res.json(user);
});

// create a user
app.post('/users', (req, res) =&gt; {
  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(400).json({ error: 'name and email are required' });
  }
  const newUser = { id: nextId++, name, email };
  users.push(newUser);
  res.status(201).json(newUser);
});

// health check
app.get('/health', (req, res) =&gt; {
  res.send('OK');
});

app.listen(PORT, () =&gt; {
  console.log(`server running on http://localhost:${PORT}`);
});
</code></pre>
<p>Test it with these curl commands:</p>
<pre><code class="language-bash"># get all users
curl http://localhost:3000/users

# get one user
curl http://localhost:3000/users/1

# create a user
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Aditya", "email": "aditya@example.com"}'

# try creating without required fields
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Aditya"}'
</code></pre>
<hr />
<h2>A few habits worth picking up now</h2>
<p>Use <code>nodemon</code> during development. It restarts your server automatically when you save a file. Without it you will be pressing Ctrl+C and typing <code>node server.js</code> hundreds of times a day.</p>
<pre><code class="language-bash">npm install --save-dev nodemon
</code></pre>
<p>Add a script to your <code>package.json</code>:</p>
<pre><code class="language-json">{
  "scripts": {
    "dev": "nodemon server.js"
  }
}
</code></pre>
<p>Then run <code>npm run dev</code> instead of <code>node server.js</code>.</p>
<p>Keep your route handlers short. If a handler is doing validation, database queries, and sending emails all in one function, it gets painful to maintain fast. As your app grows, pull logic into separate functions or files. You do not need to do this on day one, but keep it in mind as the file gets longer.</p>
<p>Always validate incoming data. Never trust <code>req.body</code> to contain what you expect. Check for missing fields. Check types when it matters. A missing <code>return</code> after sending an error response will crash your server, and bad input from a client should never be able to do that.</p>
<hr />
<h2>What comes next</h2>
<p>This covers the basics: setting up Express, handling GET and POST, sending responses, and avoiding the common early mistakes. The next piece you will run into is middleware, which is how Express handles things like authentication, logging, and error handling across routes without duplicating code everywhere. That will be its own post.</p>
<p>For now, build something small. A to-do list API. A bookmarks server. Something where you have two or three routes that create and read data. The concepts stick faster when you are solving a problem you actually care about, even a tiny one.</p>
<hr />
<h2>References</h2>
<ul>
<li><p><a href="https://expressjs.com/">Express.js official docs</a></p>
</li>
<li><p><a href="https://expressjs.com/en/guide/routing.html">Express routing guide</a></p>
</li>
<li><p><a href="https://expressjs.com/en/4x/api.html">Express req and res API reference</a></p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status">MDN: HTTP status codes</a></p>
</li>
<li><p><a href="https://www.npmjs.com/package/nodemon">nodemon on npm</a></p>
</li>
<li><p><a href="https://nodejs.org/api/http.html">Node.js http module docs</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Where Do Uploaded Files Actually Go? A Question I Should Have Asked Sooner.]]></title><description><![CDATA[I built a profile picture upload feature for a side project. It worked. Users picked a file, hit submit, and the server saved it. I was proud of myself for about three hours until I deployed to a free]]></description><link>https://blog.saumyagrawal.in/where-do-uploaded-files-actually-go-a-question-i-should-have-asked-sooner</link><guid isPermaLink="true">https://blog.saumyagrawal.in/where-do-uploaded-files-actually-go-a-question-i-should-have-asked-sooner</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Saumya]]></dc:creator><pubDate>Fri, 08 May 2026 07:44:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/b90a71da-2162-48a7-be1a-28c83b198766.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I built a profile picture upload feature for a side project. It worked. Users picked a file, hit submit, and the server saved it. I was proud of myself for about three hours until I deployed to a free hosting tier and every single uploaded image vanished after the server restarted. Gone. No error, no warning, just empty folders and broken <code>&lt;img&gt;</code> tags.</p>
<p>The problem was obvious once someone pointed it out: I was saving files to the server's local filesystem, and the hosting platform wiped the disk on every deploy. I had never actually thought about where uploaded files end up, or why that matters.</p>
<hr />
<h2>The two places files can live</h2>
<p>When a user uploads a file to your Express app, it has to go somewhere. You have two real options: save it on the same machine running your server, or send it to an external storage service.</p>
<p>Local storage means writing the file to a folder on your server's disk. The file sits right there next to your code. You can open it in your terminal, check its size, delete it manually. It is simple and fast for development.</p>
<p>External storage means sending the file to a service like AWS S3, Google Cloud Storage, or Cloudinary. The file lives on their servers, not yours. You get back a URL. Your server never holds the file long term.</p>
<pre><code class="language-plaintext">Local storage:
  User uploads photo -&gt; Express saves to /uploads/photo.jpg -&gt; served from your disk

External storage:
  User uploads photo -&gt; Express sends to S3 -&gt; S3 returns a URL -&gt; you save that URL in your database
</code></pre>
<p>For learning and local development, saving to disk is fine. For anything you deploy, external storage is almost always the right call. Your server's disk can fill up, get wiped on redeploy, or just stop existing if the hosting provider recycles your instance. S3 does not have these problems.</p>
<p>I will focus on local storage for the rest of this post because that is what you need to understand first. External storage is a topic for later, and it only clicks once you understand what local storage actually does and where it falls short.</p>
<hr />
<h2>Where files land when you use Multer</h2>
<p>Multer is the standard library for handling file uploads in Express. You have probably seen it in tutorials already. When a user submits a form with a file, Multer grabs that file from the incoming request and writes it to a destination you specify.</p>
<pre><code class="language-js">const express = require('express');
const multer = require('multer');
const path = require('path');

const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, 'uploads/');
  },
  filename: function (req, file, cb) {
    const uniqueName = Date.now() + '-' + Math.round(Math.random() * 1e9);
    cb(null, uniqueName + path.extname(file.originalname));
  },
});

const upload = multer({ storage });

const app = express();

app.post('/upload', upload.single('avatar'), (req, res) =&gt; {
  console.log(req.file);
  // {
  //   fieldname: 'avatar',
  //   originalname: 'selfie.png',
  //   filename: '1717012345678-482947123.png',
  //   path: 'uploads/1717012345678-482947123.png',
  //   size: 204800
  // }
  res.json({ message: 'uploaded', file: req.file.filename });
});
</code></pre>
<p>The <code>destination</code> function tells Multer which folder to write into. The <code>filename</code> function controls what the file gets named on disk. I am generating a unique name with a timestamp and random number because if two users both upload <code>photo.jpg</code>, the second one would overwrite the first. That is a bug you will not notice until a user complains that their profile picture is someone else's face.</p>
<p>The folder structure after a few uploads looks like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/b2926ac5-9b2e-4569-b424-12e144f4dd49.png" alt="" style="display:block;margin:0 auto" />

<p>That <code>uploads/</code> folder does not create itself. You need to make it before running the server, or Multer throws an error. A quick way to handle this:</p>
<pre><code class="language-js">const fs = require('fs');

const uploadDir = './uploads';
if (!fs.existsSync(uploadDir)) {
  fs.mkdirSync(uploadDir, { recursive: true });
}
</code></pre>
<p>Put that near the top of your server file, before Multer initializes.</p>
<hr />
<h2>Serving uploaded files so the browser can actually see them</h2>
<p>Saving a file to <code>uploads/</code> is only half the job. If a user uploads a profile picture, they need to see it in the browser. Right now, if you try to visit <code>http://localhost:3000/uploads/1717012345678-482947123.png</code>, Express returns a 404. The file is on disk but Express does not know you want to serve it.</p>
<p>Express does not serve files from your project folders by default. This is a good thing. You do not want someone guessing a URL and downloading your <code>.env</code> file or your <code>server.js</code>. Express only serves what you explicitly tell it to serve.</p>
<p>The way you tell it is <code>express.static()</code>:</p>
<pre><code class="language-js">app.use('/uploads', express.static('uploads'));
</code></pre>
<p>That single line says: when someone requests a URL starting with <code>/uploads</code>, look for a matching file in the <code>uploads/</code> folder on disk and send it back. Now <code>http://localhost:3000/uploads/1717012345678-482947123.png</code> returns the actual image.</p>
<p>The flow looks like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/84e2ae3b-94b5-4865-850f-60c9a04f31c7.png" alt="" style="display:block;margin:0 auto" />

<p>You can use any URL prefix you want. If you use <code>app.use('/files', express.static('uploads'))</code>, the browser URL becomes <code>/files/1717012345678-482947123.png</code> even though the folder on disk is still called <code>uploads</code>. The URL prefix and the folder name do not have to match, though keeping them the same is less confusing.</p>
<p><img src="align=%22center%22" alt="" /></p>
<p>A common pattern is serving the uploaded file URL from your API and storing it in your database:</p>
<pre><code class="language-js">app.post('/upload', upload.single('avatar'), async (req, res) =&gt; {
  const fileUrl = `/uploads/${req.file.filename}`;

  await User.findByIdAndUpdate(req.user._id, { avatar: fileUrl });

  res.json({ avatar: fileUrl });
});
</code></pre>
<p>On the frontend, you render it as:</p>
<pre><code class="language-html">&lt;img src="/uploads/1717012345678-482947123.png" alt="User avatar" /&gt;
</code></pre>
<p>That is the entire chain from upload to display.</p>
<hr />
<h2>The security stuff you should not skip</h2>
<p>File uploads are one of the most common attack vectors in web applications. Skipping these checks is how people end up with malware on their servers or their entire uploads folder exposed.</p>
<p><strong>Check the file type.</strong> Multer has a <code>fileFilter</code> option that lets you reject files before they are saved. Do not trust the file extension alone. A file named <code>cute-cat.jpg</code> can contain executable code if someone renamed it. Check the MIME type too:</p>
<pre><code class="language-js">const upload = multer({
  storage,
  fileFilter: function (req, file, cb) {
    const allowed = ['image/jpeg', 'image/png', 'image/webp'];
    if (allowed.includes(file.mimetype)) {
      cb(null, true);
    } else {
      cb(new Error('Only JPEG, PNG, and WebP images are allowed'));
    }
  },
  limits: {
    fileSize: 5 * 1024 * 1024, // 5 MB
  },
});
</code></pre>
<p>The <code>limits</code> option caps file size. Without it, someone can upload a 2GB file and fill your disk. Your server stops responding, all users are affected, and you are debugging at midnight wondering why <code>apt update</code> fails because the disk is full.</p>
<p><strong>Never use the original filename.</strong> I showed the random name approach earlier, and there is a real reason for it beyond avoiding collisions. If you save files with the original name, a user can upload a file called <code>../../server.js</code> and potentially overwrite your actual server code. This is called a path traversal attack. The generated filename with <code>Date.now()</code> and a random number removes this risk entirely because the name has no directory separators and no meaningful path components.</p>
<p><strong>Do not serve your project root as static.</strong> I have seen beginners write <code>app.use(express.static('.'))</code> to serve "everything" and wonder why their <code>.env</code> file is accessible from a browser. Only point <code>express.static()</code> at a dedicated upload folder that contains nothing sensitive.</p>
<p><strong>Add authentication where it matters.</strong> If uploads should only be visible to the user who uploaded them, do not serve them with <code>express.static()</code> at all. Instead, write a route that checks permissions before sending the file:</p>
<pre><code class="language-js">app.get('/my-files/:filename', authMiddleware, async (req, res) =&gt; {
  const file = await File.findOne({
    filename: req.params.filename,
    owner: req.user._id,
  });

  if (!file) return res.status(404).json({ error: 'Not found' });

  res.sendFile(path.resolve('uploads', file.filename));
});
</code></pre>
<p>This way, a user cannot access another user's files by guessing the filename. The route checks ownership first.</p>
<hr />
<h2>A quick note on what happens when you deploy</h2>
<p>When you push code to services like Railway, Render, or Heroku, the filesystem is ephemeral. Files saved to <code>uploads/</code> survive until the next deploy or the next server restart, and then they are gone. This is not a bug. The platform provisions a fresh container each time.</p>
<p>That is the point where local storage stops being enough and external storage like S3 starts making sense. You store the file somewhere permanent, save the returned URL in your database, and your server never worries about disk space or lost files again. I will cover that setup in a later post. For now, knowing that this limitation exists saves you the confusion I had when my uploaded images all disappeared on deploy.</p>
<hr />
<h2>References</h2>
<ul>
<li><p><a href="https://www.npmjs.com/package/multer">Multer documentation on npm</a></p>
</li>
<li><p><a href="https://expressjs.com/en/starter/static-files.html">Express static files documentation</a></p>
</li>
<li><p><a href="https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html">OWASP file upload cheat sheet</a></p>
</li>
<li><p><a href="https://expressjs.com/en/api.html#res.sendFile">Express res.sendFile documentation</a></p>
</li>
<li><p><a href="https://docs.aws.amazon.com/s3/">AWS S3 documentation</a> (for when you are ready for external storage)</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Express Middleware: The Stuff That Runs Before Your Route Does Anything]]></title><description><![CDATA[I spent a full afternoon staring at a bug where every single route in my Express app returned undefined for req.body. The POST request was fine. The JSON was valid. The route handler was correct. I wa]]></description><link>https://blog.saumyagrawal.in/express-middleware-the-stuff-that-runs-before-your-route-does-anything</link><guid isPermaLink="true">https://blog.saumyagrawal.in/express-middleware-the-stuff-that-runs-before-your-route-does-anything</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Saumya]]></dc:creator><pubDate>Fri, 08 May 2026 02:03:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/8e930c7d-8edd-4b59-bcd7-1ebcbb81461a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I spent a full afternoon staring at a bug where every single route in my Express app returned <code>undefined</code> for <code>req.body</code>. The POST request was fine. The JSON was valid. The route handler was correct. I was losing it. Then someone on Discord said "did you add <code>express.json()</code>?" and I realized I had no idea what that line actually did. I had been copy-pasting it from tutorials without understanding it.</p>
<p>That line is middleware. And once I understood what middleware is, a lot of Express stopped feeling like magic and started making sense.</p>
<hr />
<h2>What middleware actually is</h2>
<p>When a request hits your Express server, it does not go straight to the route handler. It passes through a series of functions first. Each of those functions can look at the request, modify it, send a response, or pass control to the next function in line. Those functions are middleware.</p>
<p>Think of it like an airport. You do not walk off the plane and straight into the city. You go through passport control, then customs, then baggage claim. Each checkpoint can let you through, send you back, or redirect you somewhere else. Your route handler is the exit door. Middleware is everything between landing and getting outside.</p>
<p>A middleware function in Express looks like this:</p>
<pre><code class="language-js">function myMiddleware(req, res, next) {
  // do something with req or res
  next(); // pass control to the next middleware or route
}
</code></pre>
<p>Three parameters. <code>req</code> is the incoming request object. <code>res</code> is the response object. <code>next</code> is a function that, when called, moves execution to the next middleware in the chain. If you do not call <code>next()</code> and you do not send a response, the request just hangs. The client waits forever. No error, no timeout message from Express, nothing. It just sits there. This will trip you up at least once.</p>
<hr />
<h2>Where middleware sits in the request lifecycle</h2>
<p>Every HTTP request that reaches your Express app follows the same path. It enters the app, passes through middleware functions in the order they were registered, hits a matching route handler (if one exists), and a response goes back to the client.</p>
<pre><code class="language-plaintext">Client sends request
       |
       v
  Express app receives it
       |
       v
  Middleware 1 (e.g., parse JSON body)
       |
       v
  Middleware 2 (e.g., log the request)
       |
       v
  Middleware 3 (e.g., check authentication)
       |
       v
  Route handler (your actual logic)
       |
       v
  Response sent back to client
</code></pre>
<p>The order matters. If your authentication middleware is registered after your route handler, the route runs without any auth check. Express processes middleware in the exact order you call <code>app.use()</code> or <code>app.get()</code> in your code. Move a line up or down and the behavior changes. This is not a design quirk. It is how the whole system works.</p>
<hr />
<h2>A minimal example to see it moving</h2>
<p>Before getting into categories and theory, here is a working Express app with two middleware functions and one route. Run it locally and watch the console.</p>
<pre><code class="language-js">const express = require('express');
const app = express();

// Middleware 1: log every request
app.use((req, res, next) =&gt; {
  console.log(`\({req.method} \){req.url}`);
  next();
});

// Middleware 2: add a timestamp to the request object
app.use((req, res, next) =&gt; {
  req.requestTime = new Date().toISOString();
  next();
});

// Route handler
app.get('/', (req, res) =&gt; {
  res.json({ message: 'hello', time: req.requestTime });
});

app.listen(3000, () =&gt; console.log('running on 3000'));
</code></pre>
<p>Hit <code>http://localhost:3000/</code> in your browser or with curl. The console prints something like:</p>
<pre><code class="language-plaintext">GET /
</code></pre>
<p>And the response body looks like:</p>
<pre><code class="language-json">{
  "message": "hello",
  "time": "2025-01-15T10:23:45.123Z"
}
</code></pre>
<p>Middleware 1 logged the method and URL. Middleware 2 attached a timestamp to <code>req</code>. The route handler read that timestamp and sent it back. Each function did one thing, then called <code>next()</code> to let the next one run. That is the entire pattern.</p>
<hr />
<h2>Types of middleware</h2>
<p>Express middleware falls into a few categories based on where and how you register it.</p>
<h3>Application-level middleware</h3>
<p>This is middleware registered on the app object using <code>app.use()</code> or <code>app.METHOD()</code>. It runs for every matching request, or for every request if no path is specified.</p>
<pre><code class="language-js">// runs for ALL requests to any route
app.use((req, res, next) =&gt; {
  console.log('this runs on every request');
  next();
});

// runs only for requests to /api/users
app.use('/api/users', (req, res, next) =&gt; {
  console.log('this only runs for /api/users routes');
  next();
});
</code></pre>
<p>The first version with no path argument is the most common. You see it for things like body parsing, CORS headers, and logging. The second version with a path lets you scope middleware to a specific section of your API without affecting other routes.</p>
<h3>Router-level middleware</h3>
<p>Express has a <code>Router</code> class that works like a mini-app. You can attach middleware to a router instead of the main app, and the middleware only applies to routes defined on that router.</p>
<pre><code class="language-js">const express = require('express');
const router = express.Router();

// this middleware only applies to routes on this router
router.use((req, res, next) =&gt; {
  console.log('admin router middleware');
  next();
});

router.get('/dashboard', (req, res) =&gt; {
  res.json({ page: 'admin dashboard' });
});

router.get('/users', (req, res) =&gt; {
  res.json({ page: 'manage users' });
});

// mount the router at /admin
app.use('/admin', router);
</code></pre>
<p>Now the middleware only runs when someone hits <code>/admin/dashboard</code> or <code>/admin/users</code>. A request to <code>/api/products</code> never touches it. This is how larger Express apps stay organized. You group related routes into routers, attach the middleware those routes need, and mount the router at a path. The main app file stays clean.</p>
<p>In a real project the file structure usually looks something like this:</p>
<pre><code class="language-plaintext">routes/
  admin.js      &lt;-- router with admin-only middleware
  auth.js       &lt;-- router for login/signup
  orders.js     &lt;-- router for order CRUD
server.js       &lt;-- mounts all routers with app.use()
</code></pre>
<h3>Built-in middleware</h3>
<p>Express ships with three built-in middleware functions. You have probably used at least one without knowing it was middleware.</p>
<p><code>express.json()</code> parses incoming JSON request bodies. Without it, <code>req.body</code> is <code>undefined</code> on POST and PUT requests. This is the one that got me at the start.</p>
<pre><code class="language-js">app.use(express.json());

app.post('/users', (req, res) =&gt; {
  console.log(req.body); // { name: "Priya", email: "priya@example.com" }
  res.json({ received: true });
});
</code></pre>
<p>Without that <code>express.json()</code> line:</p>
<pre><code class="language-js">app.post('/users', (req, res) =&gt; {
  console.log(req.body); // undefined
  res.json({ received: true });
});
</code></pre>
<p>No error. No warning. Just <code>undefined</code>. You find out when your database insert fails because you passed <code>undefined</code> as the document.</p>
<p><code>express.urlencoded({ extended: true })</code> does the same thing for form submissions. HTML forms send data in a format called <code>application/x-www-form-urlencoded</code>, and this middleware parses that into <code>req.body</code>.</p>
<pre><code class="language-js">app.use(express.urlencoded({ extended: true }));
</code></pre>
<p>The <code>extended: true</code> option uses the <code>qs</code> library for parsing, which supports nested objects in form data. With <code>extended: false</code> it uses the built-in <code>querystring</code> module, which only handles flat key-value pairs. For most apps, <code>extended: true</code> is what you want.</p>
<p><code>express.static()</code> serves files from a directory. CSS, images, JavaScript files for the browser. You point it at a folder and Express serves everything in it.</p>
<pre><code class="language-js">app.use(express.static('public'));
</code></pre>
<p>A file at <code>public/style.css</code> becomes accessible at <code>http://localhost:3000/style.css</code>. No route handler needed.</p>
<hr />
<h2>Execution order and why it matters more than you think</h2>
<p>I mentioned earlier that order matters. Here is a concrete example of what goes wrong when you get it backwards.</p>
<pre><code class="language-js">const express = require('express');
const app = express();

// Route defined BEFORE the JSON parser
app.post('/users', (req, res) =&gt; {
  console.log(req.body);  // undefined
  res.json({ user: req.body });
});

// JSON parser registered AFTER the route
app.use(express.json());

app.listen(3000);
</code></pre>
<p>Send a POST request with a JSON body to <code>/users</code>. <code>req.body</code> is <code>undefined</code>. The JSON parser exists in your code, but Express already matched the route and ran the handler before reaching the parser. By the time <code>express.json()</code> is registered, the request is already done.</p>
<p>Fix:</p>
<pre><code class="language-js">const express = require('express');
const app = express();

// Parser FIRST
app.use(express.json());

// Route SECOND
app.post('/users', (req, res) =&gt; {
  console.log(req.body);  // { name: "Priya" }
  res.json({ user: req.body });
});

app.listen(3000);
</code></pre>
<p>The general rule: put <code>app.use()</code> middleware at the top of your file, before any route definitions. Body parsers, CORS, logging, authentication checks. All of it goes above your routes. There are exceptions (error handlers go at the bottom, which I will get to), but this ordering keeps things predictable.</p>
<p>When you have multiple middleware, Express runs them in sequence:</p>
<pre><code class="language-js">app.use(middlewareA);
app.use(middlewareB);
app.use(middlewareC);

app.get('/test', handler);
</code></pre>
<p>A GET request to <code>/test</code> runs <code>middlewareA</code>, then <code>middlewareB</code>, then <code>middlewareC</code>, then <code>handler</code>. Each one has to call <code>next()</code> for the next one to run. If <code>middlewareB</code> does not call <code>next()</code>, <code>middlewareC</code> and <code>handler</code> never execute.</p>
<hr />
<h2>The <code>next()</code> function</h2>
<p><code>next()</code> is what connects the chain. Without it, the pipeline stops. There are three things you can do with it.</p>
<p><strong>Call it with no arguments</strong> to pass control to the next middleware:</p>
<pre><code class="language-js">app.use((req, res, next) =&gt; {
  console.log('step 1');
  next(); // move to step 2
});
</code></pre>
<p><strong>Do not call it and send a response instead</strong>, which ends the cycle:</p>
<pre><code class="language-js">app.use((req, res, next) =&gt; {
  if (!req.headers.authorization) {
    return res.status(401).json({ error: 'No token provided' });
    // next() is never called. The chain stops here.
  }
  next();
});
</code></pre>
<p><strong>Call it with an error</strong> to skip to the error-handling middleware:</p>
<pre><code class="language-js">app.use((req, res, next) =&gt; {
  try {
    // some operation that might throw
    const data = JSON.parse(req.headers['x-custom-data']);
    req.customData = data;
    next();
  } catch (err) {
    next(err); // jumps to the error handler
  }
});
</code></pre>
<p>Calling <code>next(err)</code> with an argument skips every regular middleware and route handler and goes straight to the first error-handling middleware. I will show what that looks like in a moment.</p>
<p>A common mistake: calling <code>next()</code> and then continuing to execute code after it.</p>
<pre><code class="language-js">app.use((req, res, next) =&gt; {
  if (!req.headers.authorization) {
    res.status(401).json({ error: 'unauthorized' });
    // forgot to return here
  }
  next(); // this still runs even after sending the 401
});
</code></pre>
<p>This causes the "Cannot set headers after they are sent to the client" error. The 401 response was already sent, then <code>next()</code> called the route handler which tried to send another response. The fix is either using <code>return</code> before <code>next()</code> in the else case, or adding <code>return</code> before the <code>res.status()</code> call:</p>
<pre><code class="language-js">app.use((req, res, next) =&gt; {
  if (!req.headers.authorization) {
    return res.status(401).json({ error: 'unauthorized' });
  }
  next();
});
</code></pre>
<p>That <code>return</code> prevents any code below it from running after the response is sent.</p>
<hr />
<h2>Real-world examples</h2>
<h3>Request logging</h3>
<p>In development you want to see every request that hits your server. What method, what URL, how long it took. A logging middleware handles this without touching any route.</p>
<pre><code class="language-js">app.use((req, res, next) =&gt; {
  const start = Date.now();

  // this runs after the response is sent
  res.on('finish', () =&gt; {
    const duration = Date.now() - start;
    console.log(`\({req.method} \){req.url} \({res.statusCode} \){duration}ms`);
  });

  next();
});
</code></pre>
<p>Output when you hit a few routes:</p>
<pre><code class="language-plaintext">GET / 200 3ms
GET /api/users 200 12ms
POST /api/users 201 8ms
GET /api/users/999 404 2ms
</code></pre>
<p>The <code>res.on('finish')</code> listener fires after Express sends the response, so you get the actual status code and timing. Without that listener, you would have to log before the response was sent and you would not know the status code yet.</p>
<p>In production, most people use <a href="https://www.npmjs.com/package/morgan">morgan</a> instead of writing their own logger. It gives you configurable log formats and can write to files:</p>
<pre><code class="language-js">const morgan = require('morgan');
app.use(morgan('dev'));
</code></pre>
<pre><code class="language-plaintext">GET / 200 2.540 ms
POST /api/users 201 15.230 ms
</code></pre>
<p>But writing your own first, like the example above, teaches you what morgan is doing under the hood.</p>
<h3>Authentication</h3>
<p>A common pattern in APIs: certain routes require the user to be logged in. Instead of checking authentication inside every route handler, you write a middleware that does it once.</p>
<pre><code class="language-js">const jwt = require('jsonwebtoken');

function authenticate(req, res, next) {
  const header = req.headers.authorization;

  if (!header || !header.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }

  const token = header.split(' ')[1];

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded; // attach user info to the request
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
}
</code></pre>
<p>You can apply it to specific routes:</p>
<pre><code class="language-js">// public routes, no auth needed
app.post('/api/login', loginHandler);
app.post('/api/signup', signupHandler);

// protected routes
app.get('/api/profile', authenticate, (req, res) =&gt; {
  // req.user exists here because authenticate put it there
  res.json({ user: req.user });
});

app.get('/api/orders', authenticate, (req, res) =&gt; {
  // same, req.user is available
  res.json({ orders: getUserOrders(req.user.id) });
});
</code></pre>
<p>Or apply it to an entire router:</p>
<pre><code class="language-js">const protectedRouter = express.Router();
protectedRouter.use(authenticate); // every route on this router requires auth

protectedRouter.get('/profile', profileHandler);
protectedRouter.get('/orders', ordersHandler);
protectedRouter.put('/settings', settingsHandler);

app.use('/api', protectedRouter);
</code></pre>
<p>The second approach is cleaner when you have many protected routes. You do not have to remember to add <code>authenticate</code> to each new route. If a route is on the protected router, it is protected.</p>
<p>A note about storing secrets: <code>process.env.JWT_SECRET</code> comes from a <code>.env</code> file loaded with <code>dotenv</code>. Do not hardcode the secret in your source code. A hardcoded secret that gets committed to Git is a leaked secret.</p>
<h3>Request validation</h3>
<p>You want to make sure the data coming into your API makes sense before your route handler tries to use it. Checking inside the handler works, but it gets repetitive fast. A validation middleware keeps the checking separate from the business logic.</p>
<p>Here is a simple version without any library:</p>
<pre><code class="language-js">function validateUser(req, res, next) {
  const { name, email } = req.body;
  const errors = [];

  if (!name || typeof name !== 'string' || name.trim().length &lt; 2) {
    errors.push('Name is required and must be at least 2 characters');
  }

  if (!email || !/^\S+@\S+\.\S+$/.test(email)) {
    errors.push('A valid email is required');
  }

  if (errors.length &gt; 0) {
    return res.status(400).json({ errors });
  }

  next();
}

app.post('/api/users', validateUser, (req, res) =&gt; {
  // if we reach here, name and email are valid
  const user = createUser(req.body);
  res.status(201).json(user);
});
</code></pre>
<p>Send a bad request:</p>
<pre><code class="language-bash">curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name": "", "email": "not-an-email"}'
</code></pre>
<pre><code class="language-json">{
  "errors": [
    "Name is required and must be at least 2 characters",
    "A valid email is required"
  ]
}
</code></pre>
<p>The route handler never runs. The validation middleware caught the bad input and sent back a 400 before the request got any further.</p>
<p>For production apps, most teams use a validation library like <a href="https://www.npmjs.com/package/joi">Joi</a> or <a href="https://www.npmjs.com/package/zod">Zod</a>. They give you a schema-based approach that handles edge cases you do not want to think about manually:</p>
<pre><code class="language-js">const Joi = require('joi');

const userSchema = Joi.object({
  name: Joi.string().trim().min(2).max(60).required(),
  email: Joi.string().email().required(),
  age: Joi.number().integer().min(16).max(120),
});

function validate(schema) {
  return (req, res, next) =&gt; {
    const { error } = schema.validate(req.body, { abortEarly: false });
    if (error) {
      const messages = error.details.map(d =&gt; d.message);
      return res.status(400).json({ errors: messages });
    }
    next();
  };
}

app.post('/api/users', validate(userSchema), (req, res) =&gt; {
  const user = createUser(req.body);
  res.status(201).json(user);
});
</code></pre>
<p>The <code>validate</code> function is a middleware factory. It takes a schema and returns a middleware function. This pattern lets you reuse the same validation logic across different routes with different schemas. You write <code>validate(userSchema)</code> for user routes and <code>validate(orderSchema)</code> for order routes, and the actual validation logic stays in one place.</p>
<hr />
<h2>Error-handling middleware</h2>
<p>Regular middleware has three parameters: <code>req</code>, <code>res</code>, <code>next</code>. Error-handling middleware has four: <code>err</code>, <code>req</code>, <code>res</code>, <code>next</code>. That extra first parameter is how Express knows it is an error handler.</p>
<pre><code class="language-js">// this goes AFTER all your routes
app.use((err, req, res, next) =&gt; {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: err.message || 'Something went wrong',
  });
});
</code></pre>
<p>When any middleware or route calls <code>next(err)</code> with an argument, Express skips everything until it finds a middleware with four parameters. That is your error handler.</p>
<pre><code class="language-js">app.get('/api/users/:id', async (req, res, next) =&gt; {
  try {
    const user = await User.findById(req.params.id);
    if (!user) {
      const err = new Error('User not found');
      err.status = 404;
      return next(err); // goes to the error handler
    }
    res.json(user);
  } catch (err) {
    next(err); // database error, goes to the error handler
  }
});
</code></pre>
<p>Without the error handler, unhandled errors crash your server or produce an ugly HTML error page that leaks your stack trace to the client. Neither is what you want.</p>
<p>The error handler must be registered after all other <code>app.use()</code> and route definitions. If you put it at the top, it will never catch anything because Express matches error handlers in order, and errors thrown after the handler's position in the code will not reach it.</p>
<p>A pattern I picked up from a more experienced developer: create custom error classes so your error handler can make smarter decisions.</p>
<pre><code class="language-js">class AppError extends Error {
  constructor(message, status) {
    super(message);
    this.status = status;
  }
}

// in a route
if (!user) {
  throw new AppError('User not found', 404);
}

// in the error handler
app.use((err, req, res, next) =&gt; {
  if (err instanceof AppError) {
    return res.status(err.status).json({ error: err.message });
  }
  // unexpected error, log it, send generic message
  console.error(err);
  res.status(500).json({ error: 'Internal server error' });
});
</code></pre>
<p>Known errors get their proper status code and message. Unknown errors get logged for debugging but the client only sees a generic message. You do not want to send a raw database error or a stack trace to whoever is calling your API.</p>
<hr />
<h2>Putting it all together</h2>
<p>Here is a stripped-down Express app with middleware in the right order. This is the skeleton most Express projects start from.</p>
<pre><code class="language-js">const express = require('express');
const morgan = require('morgan');
const app = express();

// 1. Built-in middleware (body parsing)
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 2. Third-party middleware (logging)
app.use(morgan('dev'));

// 3. Custom middleware (add request timestamp)
app.use((req, res, next) =&gt; {
  req.requestTime = new Date().toISOString();
  next();
});

// 4. Routes
app.get('/', (req, res) =&gt; {
  res.json({ status: 'running', time: req.requestTime });
});

app.use('/api/users', require('./routes/users'));
app.use('/api/orders', require('./routes/orders'));

// 5. 404 handler (no route matched)
app.use((req, res) =&gt; {
  res.status(404).json({ error: 'Route not found' });
});

// 6. Error handler (must be last, must have 4 parameters)
app.use((err, req, res, next) =&gt; {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: err.message || 'Internal server error',
  });
});

app.listen(3000, () =&gt; console.log('running on 3000'));
</code></pre>
<p>The order follows a pattern: parsing goes first so <code>req.body</code> is available everywhere, logging goes next so every request gets recorded, custom middleware goes after that, then routes, then the 404 catch-all for unmatched routes, and the error handler goes dead last.</p>
<p>The 404 handler is a regular middleware with no path argument. Since it is registered after all routes, Express only reaches it when no route matched the incoming request. It does not have four parameters, so it is not an error handler. It just sends a 404 response.</p>
<hr />
<h2>Common mistakes I made (so you do not have to)</h2>
<p><strong>Forgetting</strong> <code>next()</code>: The request hangs and the client eventually times out. No error in the console. If a route is mysteriously not responding, check if a middleware above it forgot to call <code>next()</code>.</p>
<p><strong>Calling</strong> <code>next()</code> <strong>after sending a response</strong>: Causes the "headers already sent" error. Use <code>return</code> before <code>res.send()</code> or <code>res.json()</code> in conditional blocks.</p>
<p><strong>Putting middleware after routes</strong>: The middleware never runs for those routes because Express already matched and handled the request.</p>
<p><strong>Not using</strong> <code>express.json()</code>: <code>req.body</code> is <code>undefined</code> and you blame your frontend, your database, your HTTP client, and possibly the universe before realizing you forgot one line.</p>
<p><strong>Writing async middleware without try/catch</strong>: Unhandled promise rejections in Express 4 do not trigger the error handler. They crash the process or get silently swallowed. Wrap async middleware in try/catch and pass errors to <code>next(err)</code>. Express 5 fixes this, but most projects are still on 4.</p>
<pre><code class="language-js">// Express 4: you need this wrapper
app.get('/api/data', async (req, res, next) =&gt; {
  try {
    const data = await fetchSomething();
    res.json(data);
  } catch (err) {
    next(err);
  }
});
</code></pre>
<hr />
<h2>What to read next</h2>
<p>Middleware is the pattern that holds an Express app together. Once you understand the request pipeline and the role of <code>next()</code>, most Express features make more sense. The concepts you will run into next build directly on this: route grouping with <code>express.Router()</code>, authentication strategies with <a href="http://www.passportjs.org/">Passport.js</a>, more sophisticated validation with <a href="https://zod.dev/">Zod</a> or <a href="https://joi.dev/">Joi</a>, rate limiting with <a href="https://www.npmjs.com/package/express-rate-limit">express-rate-limit</a>, and CORS handling with the <a href="https://www.npmjs.com/package/cors">cors</a> package. All of them are middleware. They follow the same <code>(req, res, next)</code> pattern you just learned.</p>
<p>If something in your Express app is not working and you cannot figure out why, add a <code>console.log</code> to each middleware and check the order. Nine times out of ten, the answer is either a missing <code>next()</code>, a misplaced <code>app.use()</code>, or middleware running in the wrong sequence.</p>
<hr />
<h2>References</h2>
<ul>
<li><p><a href="https://expressjs.com/en/guide/using-middleware.html">Express documentation: Using middleware</a></p>
</li>
<li><p><a href="https://expressjs.com/en/guide/writing-middleware.html">Express documentation: Writing middleware</a></p>
</li>
<li><p><a href="https://expressjs.com/en/guide/error-handling.html">Express documentation: Error handling</a></p>
</li>
<li><p><a href="https://expressjs.com/en/api.html#express.json">Express documentation: express.json()</a></p>
</li>
<li><p><a href="https://expressjs.com/en/api.html#router">Express documentation: express.Router()</a></p>
</li>
<li><p><a href="https://www.npmjs.com/package/morgan">morgan: HTTP request logger middleware</a></p>
</li>
<li><p><a href="https://www.npmjs.com/package/joi">Joi: schema-based validation</a></p>
</li>
<li><p><a href="https://zod.dev/">Zod: TypeScript-first schema validation</a></p>
</li>
<li><p><a href="https://www.npmjs.com/package/jsonwebtoken">jsonwebtoken: JWT implementation for Node.js</a></p>
</li>
<li><p><a href="https://www.npmjs.com/package/express-rate-limit">express-rate-limit: rate limiting middleware</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Node.js: How JavaScript Escaped the Browser and Why That Matters]]></title><description><![CDATA[For years, JavaScript lived inside the browser and nowhere else. You could make buttons change color, validate a form before it was submitted, maybe animate a dropdown menu. That was it. If you wanted]]></description><link>https://blog.saumyagrawal.in/node-js-how-javascript-escaped-the-browser-and-why-that-matters</link><guid isPermaLink="true">https://blog.saumyagrawal.in/node-js-how-javascript-escaped-the-browser-and-why-that-matters</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Saumya]]></dc:creator><pubDate>Thu, 07 May 2026 19:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/7c19fe1e-03eb-424a-b60c-b98ad17aacfc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>For years, JavaScript lived inside the browser and nowhere else. You could make buttons change color, validate a form before it was submitted, maybe animate a dropdown menu. That was it. If you wanted to build the actual backend of a website, the part that talks to databases and handles logins and serves pages, you had to learn a completely different language. PHP, Java, Python, Ruby. Pick one, learn it alongside JavaScript, and maintain two mental models for one project.</p>
<p>I remember the first time someone told me I could write server-side code in JavaScript. My reaction was something like "wait, that does not sound right." JavaScript was the language that ran inside Chrome. How would it read files from a hard drive? How would it listen for HTTP requests? It did not have access to any of that. The browser would never let it.</p>
<p>They were talking about Node.js.</p>
<hr />
<h2>Why JavaScript could not run outside the browser</h2>
<p>This confused me until I understood what JavaScript actually is versus what runs it.</p>
<p>JavaScript the language is just a specification. It defines syntax, data types, how loops work, how functions behave. It does not say anything about the DOM, or <code>window</code>, or <code>document.getElementById</code>. Those are APIs that the browser provides. The browser says "here, JavaScript, I will give you access to this object called <code>document</code> and you can use it to manipulate the page." JavaScript itself has no idea what a webpage is.</p>
<p>The thing that executes JavaScript code is called a runtime. In a browser, the runtime is the browser itself, specifically a JavaScript engine embedded inside it. Chrome uses an engine called V8. Firefox uses SpiderMonkey. Safari uses JavaScriptCore. Each one reads your JavaScript, compiles it into machine code, and runs it. But they also provide browser-specific APIs like <code>fetch</code>, <code>localStorage</code>, and <code>setTimeout</code> that let your code interact with the browser environment.</p>
<p>So JavaScript was not really trapped in the browser. The <em>engines</em> that ran it were trapped in the browser. The language itself was perfectly general. Someone just had to take one of those engines out of the browser and give it a different set of APIs.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/844cc90e-986d-4257-bc67-fce6742e8f6f.png" alt="" style="display:block;margin:0 auto" />

<p>That someone was Ryan Dahl, and the thing he built in 2009 was Node.js.</p>
<hr />
<h2>What Node.js actually is</h2>
<p>Node.js took Chrome's V8 engine, pulled it out of the browser, and wrapped it with APIs for things that servers need. File system access. Network sockets. HTTP request handling. Process management. Child processes. Streams. Buffers for raw binary data. All the things the browser would never let JavaScript touch.</p>
<p>So when you write JavaScript in Node.js, the core language is identical. Variables, functions, arrays, objects, promises, async/await, all the same. What changes is what is available around the language. There is no <code>document</code>. There is no <code>window</code>. There is no DOM. Instead you get <code>fs</code> for reading and writing files, <code>http</code> for building web servers, <code>path</code> for working with file paths, and <code>process</code> for interacting with the operating system.</p>
<pre><code class="language-js">// This runs in Node.js, not in a browser
const fs = require('fs');
const http = require('http');

// read a file from the hard drive
const data = fs.readFileSync('config.json', 'utf-8');
console.log(data);

// start an HTTP server that listens on port 3000
const server = http.createServer((req, res) =&gt; {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello from Node.js');
});

server.listen(3000, () =&gt; {
  console.log('Server running at http://localhost:3000');
});
</code></pre>
<p>Try running <code>fs.readFileSync</code> in a browser console. You get an error. The browser has no idea what <code>fs</code> is. Try running <code>document.querySelector</code> in Node.js. Same thing, other direction. Same language, different environment.</p>
<pre><code class="language-plaintext">// browser console
&gt; fs.readFileSync('test.txt')
  Uncaught ReferenceError: fs is not defined

// node repl
&gt; document.querySelector('div')
  ReferenceError: document is not defined
</code></pre>
<hr />
<h2>V8 under the hood</h2>
<p>You do not need to know how V8 works internally to use Node.js. But knowing the basics helps when people throw around terms like "JIT compilation" or "single-threaded" and expect you to nod along.</p>
<p>V8 compiles JavaScript to machine code. Older JavaScript engines used to interpret code line by line, which was slow. V8 compiles it first, which makes execution much faster. It also uses something called Just-In-Time (JIT) compilation, where it watches which parts of your code run frequently and optimizes those sections further while the program is already running.</p>
<p>The part that matters practically: V8 is fast. Not fast for a scripting language, just fast. This is a big reason Node.js took off. Before V8, running JavaScript outside the browser would have been painfully slow for server workloads. V8 made it competitive with languages that had been doing backend work for decades.</p>
<hr />
<h2>The event loop and why Node.js handles requests differently</h2>
<p>This is the concept that made Node.js click for me, and also the one that confused me the longest.</p>
<p>Traditional backend runtimes like PHP or Java (with thread-per-request models) handle each incoming request by assigning it a thread. A thread is like a worker. One request comes in, one worker picks it up, does everything the request needs (database query, file read, external API call), and sends the response. If 1000 requests come in at once, you need close to 1000 threads. Each thread uses memory. Each one sits around waiting when it hits a database query that takes 200ms to come back. A lot of resources spent on waiting.</p>
<p>Node.js uses a single thread with an event loop. One worker handles all incoming requests. When that worker hits something slow, like a database query, it does not wait. It says "let me know when the result comes back" and moves on to handle the next request. When the database responds, a callback fires and the worker picks up where it left off.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/72b1008a-1a0d-4dd1-ba30-3444e5f65397.png" alt="" style="display:block;margin:0 auto" />

<pre><code class="language-js">// request comes in -&gt; wait for database -&gt; wait -&gt; wait -&gt; respond

// Node does this:
// request comes in -&gt; ask database -&gt; move to next request
// database responds -&gt; pick up first request -&gt; respond

const http = require('http');

const server = http.createServer((req, res) =&gt; {
  // simulate a database call that takes 2 seconds
  setTimeout(() =&gt; {
    res.end('Data from database');
  }, 2000);
  // Node does NOT freeze here for 2 seconds
  // it goes and handles other requests while waiting
});

server.listen(3000);
</code></pre>
<p>The tradeoff is real though. If you write code that does heavy computation directly on that single thread, like crunching numbers in a loop for 5 seconds, nothing else runs during that time. Every other request waits. Node.js is excellent at I/O-heavy work (database queries, API calls, file reads) where the program spends most of its time waiting for external things. It is not the best choice for CPU-heavy work like image processing or complex math, unless you offload that to worker threads or separate services.</p>
<hr />
<h2>How Node.js compares to PHP and Java</h2>
<p>When I was deciding what to learn for backend, I kept reading comparisons that were more tribal allegiance than actual explanation. Here is what actually differs.</p>
<p>PHP traditionally runs one process per request. A request comes in, PHP starts up, runs the script, sends the response, and the process dies. The next request starts fresh. This is simple and means one request cannot easily corrupt another, but it also means there is overhead in starting up for every single request. Newer PHP (with tools like Swoole or FrankenPHP) can keep processes alive, but the traditional model is request-and-die.</p>
<p>Java with something like Spring Boot runs a multi-threaded server. Each request gets a thread from a thread pool. This handles concurrency well but threads have memory overhead, and context switching between many threads has a cost. Java is fast and battle-tested for large enterprise systems, but it takes more boilerplate code to get a simple server running.</p>
<p>Node.js sits in a different spot. One thread, non-blocking I/O, the event loop. Less memory per connection than a thread-per-request model. The cost is that your code has to be written in an asynchronous style, and CPU-heavy tasks need special handling.</p>
<p>The reason a lot of JavaScript developers picked Node.js over learning PHP or Java is straightforward: they already knew the language. One language for the frontend and the backend. One set of syntax rules to remember. Shared code between browser and server when it made sense. Shared tooling (npm, ESLint, Prettier). For small teams and startups building web apps, that was a real productivity win.</p>
<hr />
<h2>Where Node.js gets used in practice</h2>
<p>Node.js runs the backend for a lot of companies you have probably used. Netflix moved parts of their stack to Node.js and reported faster startup times. LinkedIn rebuilt their mobile backend in Node.js and reduced their server count. PayPal rewrote parts of their Java backend in Node.js and saw pages render faster.</p>
<p>But the practical everyday uses are more grounded than big company case studies. These are the places where Node.js shows up most often.</p>
<p>REST APIs and GraphQL servers. If you are building a web or mobile app that needs a backend to handle data, Node.js with Express or Fastify is one of the most common setups. Lightweight, quick to build, handles many concurrent connections well.</p>
<p>Real-time applications. Chat apps, live notifications, collaborative editors, multiplayer game servers. The event loop and WebSocket support make Node.js a natural fit for anything where the server needs to push data to clients without them asking for it.</p>
<p>Command-line tools. npm itself is written in Node.js. So are tools like ESLint, Prettier, Webpack, and Vite. If you have installed anything with <code>npm install -g</code>, you have been running Node.js programs on your machine.</p>
<p>Build tooling and automation. Task runners, bundlers, dev servers, testing frameworks. The JavaScript ecosystem's entire build pipeline runs on Node.js.</p>
<p>Server-side rendering. Frameworks like Next.js run on Node.js to render React pages on the server before sending them to the browser. This is how a lot of modern websites ship fast initial page loads.</p>
<pre><code class="language-js">// A simple REST API endpoint using Express
const express = require('express');
const app = express();

app.use(express.json());

const users = [];

app.get('/users', (req, res) =&gt; {
  res.json(users);
});

app.post('/users', (req, res) =&gt; {
  const user = { id: users.length + 1, name: req.body.name };
  users.push(user);
  res.status(201).json(user);
});

app.listen(3000, () =&gt; {
  console.log('API running on http://localhost:3000');
});
</code></pre>
<p>That is a working API. Fourteen lines of actual logic. In Java with Spring Boot, the equivalent setup involves annotations, dependency injection configuration, a controller class, and a build file. Both approaches are valid, but one of them gets you to a working prototype in two minutes.</p>
<hr />
<h2>What to take away from this</h2>
<p>Node.js is V8 plus system-level APIs, packaged as a runtime that lets JavaScript run outside the browser. The language is the same. The environment is different. The event loop makes it handle concurrent I/O well, but CPU-heavy work needs a different approach.</p>
<p>You do not need to understand V8 internals or event loop phases to start building things. Write a small Express server. Hit it with a few requests. Read files from disk. Connect to a database. The concepts land faster when you have running code in front of you than when you are reading about them.</p>
<p>In the next post we will set up a Node.js project from scratch and dig into how <code>require</code>, <code>module.exports</code>, and the module system actually work. That is where the real building starts.</p>
<hr />
<h2>References</h2>
<ul>
<li><p><a href="https://nodejs.org/en/docs">Node.js official documentation</a> - start with the "Getting Started" guides</p>
</li>
<li><p><a href="https://v8.dev/">V8 JavaScript engine</a> - Chrome's JS engine that powers Node.js</p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/JavaScript_technologies_overview">MDN: JavaScript runtime</a> - good explanation of engines vs runtimes</p>
</li>
<li><p><a href="https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick">Node.js event loop explained</a> - the official guide to how the event loop works</p>
</li>
<li><p><a href="https://expressjs.com/">Express.js</a> - the most popular Node.js web framework for beginners</p>
</li>
<li><p><a href="https://www.youtube.com/watch?v=ENrzD9HAZK4">Fireship: Node.js in 100 Seconds</a> - quick visual overview, worth watching before going further</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Node.js Runs on One Thread. Here Is How It Handles Thousands of Users Anyway.]]></title><description><![CDATA[The first time someone told me Node.js is single-threaded, I thought they were messing with me. How does a server that runs on one thread handle thousands of people hitting it at the same time? That s]]></description><link>https://blog.saumyagrawal.in/node-js-runs-on-one-thread-here-is-how-it-handles-thousands-of-users-anyway</link><guid isPermaLink="true">https://blog.saumyagrawal.in/node-js-runs-on-one-thread-here-is-how-it-handles-thousands-of-users-anyway</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Saumya]]></dc:creator><pubDate>Thu, 07 May 2026 15:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/9cb595dc-e0a9-419f-86c9-10216bd0ecf8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The first time someone told me Node.js is single-threaded, I thought they were messing with me. How does a server that runs on one thread handle thousands of people hitting it at the same time? That sounds like putting one cashier in a store during Black Friday and expecting everything to go fine.</p>
<p>I had this wrong mental model for weeks. I kept picturing a single thread like a single person doing everything, start to finish, one task at a time. That is not what happens. And once I understood what actually happens, a lot of things about Node clicked that had confused me before.</p>
<p>This post is about that. How Node.js works under the hood, why one thread is not as limiting as it sounds, and where it actually falls apart.</p>
<hr />
<h2>Threads and processes, without the textbook definition</h2>
<p>A process is a running program. When you open VS Code, that is a process. When you start a Node server with <code>node server.js</code>, that is a process too. Each process gets its own chunk of memory from the operating system and runs independently.</p>
<p>A thread lives inside a process. It is a single sequence of instructions being executed. A process can have many threads running at the same time, sharing the same memory. That is how languages like Java handle multiple users: spin up a new thread for each incoming request, let them all run in parallel.</p>
<p>Node.js does not do that. Your JavaScript code runs on one thread. One. If you write a function that takes 3 seconds to finish, nothing else runs during those 3 seconds. No other request gets processed, no other callback fires. The whole server just sits there waiting.</p>
<p>That sounds terrible. But Node gets away with it, and the reason is the event loop.</p>
<hr />
<h2>The chef analogy</h2>
<p>Think of a restaurant kitchen with one chef. This chef cannot clone themselves. There is only one.</p>
<p>A regular multi-threaded server is like hiring a new chef for every order. Ten orders come in, ten chefs are cooking simultaneously. Works fine until you have 10,000 orders and need 10,000 chefs and your kitchen is on fire.</p>
<p>Node's approach is different. The one chef takes an order, puts the pasta on the stove, and immediately moves to the next order. They do not stand at the stove watching water boil. They chop vegetables for order two, put that in the oven, then check if the pasta is done. When the oven timer goes off, they plate that dish. One chef, many dishes in progress, none of them blocking the others.</p>
<p>The chef is your single JavaScript thread. The stove, the oven, the timers are the operating system and the thread pool handling the slow stuff in the background. The thing connecting it all, the system that tells the chef "the pasta is ready" or "the oven timer went off," that is the event loop.</p>
<pre><code class="language-plaintext">One chef, many dishes in progress:

  Order 1: pasta on stove       [waiting... ready!] --&gt; plate it
  Order 2: veggies chopping --&gt; oven [waiting....... ready!] --&gt; plate it
  Order 3: salad prep --&gt; done immediately --&gt; plate it
  Order 4: soup heating        [waiting.... ready!] --&gt; plate it

  The chef moves between tasks. They never just stand and wait.
</code></pre>
<hr />
<h2>What the event loop actually does</h2>
<p>When your Node server receives a request, your JavaScript code starts running. If that code does something synchronous like math, string manipulation, or building a JSON response, it runs right there on the main thread and finishes.</p>
<p>But most server work is not like that. Reading a file from disk, querying a database, making an HTTP call to another API. These are I/O operations, and they are slow compared to CPU work. Disk reads take milliseconds. Network calls take tens or hundreds of milliseconds. If the main thread sat around waiting for each of these, the server would be useless.</p>
<p>So Node does not wait. When your code says "read this file," Node hands that job to the operating system. The OS does the actual reading on its own, separate from your JavaScript thread. Node registers a callback function, basically a note saying "when the file is done, run this function." Then it moves on immediately to handle the next thing.</p>
<p>The event loop is the mechanism that checks whether any of those background tasks have finished. It runs in a continuous cycle. Each cycle, it looks at a queue of completed tasks, picks up the associated callbacks, and runs them one at a time on the main thread.</p>
<pre><code class="language-js">const fs = require('fs');

console.log('before reading file');

fs.readFile('/some/big/file.txt', 'utf8', (err, data) =&gt; {
  // this callback runs later, when the file is done being read
  console.log('file has been read');
});

console.log('after calling readFile');
</code></pre>
<p>Output:</p>
<pre><code class="language-plaintext">before reading file
after calling readFile
file has been read
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/854cd366-25d2-4a74-a838-741fd2971192.png" alt="" style="display:block;margin:0 auto" />

<p>The <code>readFile</code> call returns immediately. It does not block. The callback runs later, after the event loop picks it up from the completed-tasks queue. Between calling <code>readFile</code> and the callback firing, Node is free to handle other requests.</p>
<hr />
<h2>The background workers</h2>
<p>"But someone has to actually read the file." Yes. Node is not doing magic. The work still happens, just not on your JavaScript thread.</p>
<p>Node.js is built on top of a C library called libuv. libuv maintains a thread pool, which defaults to 4 threads. When you call <code>fs.readFile</code>, libuv assigns the actual disk read to one of those pool threads. When that thread finishes, it places the result in a queue. The event loop picks it up on the next cycle and runs your callback.</p>
<p>For network I/O, it is even more efficient. libuv uses operating system features like epoll on Linux or kqueue on macOS. These let a single thread monitor thousands of network sockets without dedicating a thread to each one. That is why Node handles network-heavy workloads particularly well.</p>
<pre><code class="language-plaintext">Your code (main thread)          libuv (background)
--------------------------       ---------------------------
fs.readFile('data.json')  ---&gt;   Worker thread 1: reading file
db.query('SELECT ...')    ---&gt;   OS network I/O: waiting for DB
fetch('https://api...')   ---&gt;   OS network I/O: waiting for response
                                 
  Main thread is free to         Each finishes independently
  handle new requests             and queues the result
                                 
  Event loop picks up results &lt;--- callback queue
  and runs your callbacks
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/f32ec1d6-b0c6-4b3e-ab9a-1d7321763f0b.png" alt="" style="display:block;margin:0 auto" />

<p>You can increase the thread pool size if your app does a lot of file system work:</p>
<pre><code class="language-js">// set this before any I/O happens, usually at the very top of your entry file
process.env.UV_THREADPOOL_SIZE = 8;
</code></pre>
<p>The default of 4 is fine for most apps. If you are doing heavy file processing or DNS lookups (which also use the pool), bumping it up can help.</p>
<hr />
<h2>Handling multiple clients</h2>
<p>Here is a concrete example. Say your Node server gets three requests at roughly the same time. Each one queries a database, which takes about 50ms.</p>
<pre><code class="language-js">const express = require('express');
const app = express();

app.get('/user/:id', async (req, res) =&gt; {
  const user = await db.findUser(req.params.id);  // ~50ms, non-blocking
  res.json(user);
});

app.listen(3000);
</code></pre>
<p>What happens:</p>
<ol>
<li><p>Request A arrives. Node starts running the handler, hits <code>await db.findUser()</code>, sends the query to the database over the network, and moves on. The main thread is free.</p>
</li>
<li><p>Request B arrives a few milliseconds later. Same thing. Query sent, main thread free.</p>
</li>
<li><p>Request C arrives. Same deal.</p>
</li>
<li><p>The database responds to request A's query. The event loop picks up the callback, Node runs <code>res.json(user)</code> and sends the response.</p>
</li>
<li><p>Request B's query finishes. Same process.</p>
</li>
<li><p>Request C's query finishes.</p>
</li>
</ol>
<p>All three requests were "in progress" at the same time, even though only one thread ran JavaScript. The actual waiting happened elsewhere. The main thread only did the quick parts: parsing the request, building the response, sending it out.</p>
<p>This is concurrency without parallelism. Multiple things are in progress at the same time, but only one piece of JavaScript runs at any given instant. The parallelism happens in the background, in libuv's thread pool and the OS kernel's network handling.</p>
<hr />
<h2>Where this breaks down</h2>
<p>The event loop works beautifully for I/O. It falls apart when you put heavy computation on the main thread.</p>
<pre><code class="language-js">app.get('/heavy', (req, res) =&gt; {
  // this blocks the entire server
  let sum = 0;
  for (let i = 0; i &lt; 1_000_000_000; i++) {
    sum += i;
  }
  res.json({ sum });
});
</code></pre>
<p>While that loop runs, every other request to your server sits in a queue, waiting. No callbacks fire. No responses go out. The event loop cannot cycle because your code is hogging the thread. If that loop takes 2 seconds, every user experiences a 2-second delay, not just the one who hit <code>/heavy</code>.</p>
<p>This is the tradeoff. Node gives you a fast, lightweight concurrency model for I/O-bound work. In exchange, you have to keep the main thread clear of heavy computation.</p>
<p>When you do need CPU-intensive work, Node gives you <code>worker_threads</code>:</p>
<pre><code class="language-js">const { Worker } = require('worker_threads');

app.get('/heavy', (req, res) =&gt; {
  const worker = new Worker('./heavy-calc.js');
  
  worker.on('message', (result) =&gt; {
    res.json({ sum: result });
  });
  
  worker.on('error', (err) =&gt; {
    res.status(500).json({ error: err.message });
  });
});
</code></pre>
<pre><code class="language-js">// heavy-calc.js
const { parentPort } = require('worker_threads');

let sum = 0;
for (let i = 0; i &lt; 1_000_000_000; i++) {
  sum += i;
}

parentPort.postMessage(sum);
</code></pre>
<p>The heavy calculation runs on a separate thread. The main thread stays free. The worker sends the result back when it is done. Your other routes keep responding normally while the calculation runs.</p>
<hr />
<h2>Why Node scales well for the right workload</h2>
<p>Traditional multi-threaded servers allocate a thread per connection. Each thread consumes memory (typically around 1-2MB for the stack alone). Ten thousand concurrent connections means ten thousand threads and gigabytes of memory just for thread stacks, before your application code does anything.</p>
<p>Node uses one thread for JavaScript and lets the OS handle the waiting. A single Node process can hold tens of thousands of concurrent connections because most of those connections are just sitting in an OS-level queue, waiting for data. The memory cost per connection is tiny.</p>
<p>This is why Node became popular for real-time applications, chat servers, API gateways, streaming services. These are all I/O-heavy, CPU-light workloads where thousands of connections are open but most of them are idle at any given moment.</p>
<p>It is not the right tool for everything. Image processing, video encoding, machine learning inference, anything that pins the CPU for extended periods is a poor fit for Node's main thread. You can work around it with worker threads or by calling out to separate services, but at that point you are fighting the architecture instead of working with it.</p>
<hr />
<h2>A quick summary of the mental model</h2>
<p>Your JavaScript runs on one thread. When it hits an I/O operation (file read, database query, network call), it hands that off to the system and keeps going. The event loop continuously checks whether any of those operations finished. When one finishes, the event loop runs the associated callback on the main thread.</p>
<p>The golden rule: never block the event loop. Keep the main thread doing quick work. Push slow I/O to the system. Push heavy computation to worker threads. If you follow that, one thread handles more concurrent users than you would expect.</p>
<pre><code class="language-js">// good: non-blocking I/O
const data = await fs.promises.readFile('data.json', 'utf8');

// bad: blocking the event loop
const data = fs.readFileSync('data.json', 'utf8');
</code></pre>
<p>The <code>readFileSync</code> version freezes your server until the file is read. The <code>await</code> version lets the event loop keep cycling. In a script you run once, <code>readFileSync</code> is fine. In a server handling requests, it is not.</p>
<hr />
<h2>References</h2>
<ul>
<li><p><a href="https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick">Node.js docs: The event loop</a> - the official explanation, worth reading after this post</p>
</li>
<li><p><a href="https://nodejs.org/api/worker_threads.html">Node.js docs: Worker threads</a> - for CPU-bound work</p>
</li>
<li><p><a href="https://docs.libuv.org/">libuv documentation</a> - the C library underneath Node that handles the thread pool and OS-level I/O</p>
</li>
<li><p><a href="https://www.youtube.com/watch?v=8aGhZQkoFbQ">Philip Roberts: What the heck is the event loop anyway?</a> - the classic JSConf talk, still the best visual explanation out there</p>
</li>
<li><p><a href="https://nodejs.org/en/learn/asynchronous-work/dont-block-the-event-loop">Node.js docs: Don't block the event loop</a> - practical advice on keeping your server responsive</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[URL Parameters and Query Strings: The Two Ways Your URL Talks to Your Server]]></title><description><![CDATA[I spent an embarrassing amount of time early on not understanding why sometimes data showed up in my URL after a slash and sometimes after a question mark. I would look at URLs like /users/42 and /use]]></description><link>https://blog.saumyagrawal.in/url-parameters-and-query-strings-the-two-ways-your-url-talks-to-your-server</link><guid isPermaLink="true">https://blog.saumyagrawal.in/url-parameters-and-query-strings-the-two-ways-your-url-talks-to-your-server</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Saumya]]></dc:creator><pubDate>Thu, 07 May 2026 13:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/b7f189fc-fd71-4a59-85e3-eced45e26f93.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I spent an embarrassing amount of time early on not understanding why sometimes data showed up in my URL after a slash and sometimes after a question mark. I would look at URLs like <code>/users/42</code> and <code>/users?name=saumya</code> and think they were doing the same thing. They are not. They solve different problems, and once the distinction clicked, half of my routing confusion disappeared overnight.</p>
<p>This post covers what URL parameters and query strings are, how they differ, and how you actually use them in Express. If you have been copy-pasting route code without fully understanding the <code>:id</code> part or the <code>req.query</code> part, this should clear that up.</p>
<hr />
<h2>A URL has parts, and they have names</h2>
<p>Before anything else, look at a URL and know what each piece is called. Take this one:</p>
<pre><code class="language-plaintext">https://api.foodapp.com/restaurants/5f2b/menu?category=biryani&amp;sort=price
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/68695178-8f81-4866-9e53-b751457d9f68.png" alt="" style="display:block;margin:0 auto" />

<p>The path is <code>/restaurants/5f2b/menu</code>. The <code>5f2b</code> in there is a URL parameter, a variable piece embedded in the path itself. Everything after the <code>?</code> is the query string. <code>category=biryani</code> and <code>sort=price</code> are query parameters, key-value pairs separated by <code>&amp;</code>.</p>
<p>Two different mechanisms. Same URL. Different jobs.</p>
<hr />
<h2>URL parameters: the "which one" part</h2>
<p>URL parameters identify a specific resource. When you write <code>/users/42</code>, you are saying "I want user number 42." Not a list of users. Not a filtered set. One specific user.</p>
<p>In Express, you define a URL parameter by putting a colon before the name in your route:</p>
<pre><code class="language-js">// :id is the parameter
app.get('/users/:id', (req, res) =&gt; {
  console.log(req.params);
  // { id: '42' }

  console.log(req.params.id);
  // '42'
});
</code></pre>
<p>When someone hits <code>GET /users/42</code>, Express matches the route and puts <code>42</code> into <code>req.params.id</code>. The name after the colon is what you use to access it. Call it <code>:id</code>, <code>:userId</code>, <code>:slug</code>, whatever makes sense for your route.</p>
<p>You can have multiple parameters in one route. A food delivery app might need both a restaurant and a menu item:</p>
<pre><code class="language-js">app.get('/restaurants/:restaurantId/menu/:itemId', (req, res) =&gt; {
  console.log(req.params);
  // { restaurantId: '5f2b', itemId: 'a1c3' }
  
  const restaurant = await Restaurant.findById(req.params.restaurantId);
  const item = restaurant.menu.id(req.params.itemId);
  
  res.json(item);
});
</code></pre>
<p>The URL <code>/restaurants/5f2b/menu/a1c3</code> fills both params. Each <code>:name</code> in the route definition maps to the matching segment in the actual URL, left to right.</p>
<p>One thing to watch: <code>req.params.id</code> is always a string. Even if the URL is <code>/users/42</code>, you get the string <code>'42'</code>, not the number <code>42</code>. If you are passing it to a database query that expects a number, you need to convert it yourself. MongoDB ObjectIds work fine as strings, but if your IDs are numeric you will want <code>parseInt(req.params.id)</code> or <code>Number(req.params.id)</code>.</p>
<hr />
<h2>Query strings: the "how do you want it" part</h2>
<p>Query strings modify a request. They filter, sort, paginate, or adjust what comes back, but they do not change which resource you are looking at. The URL <code>/users</code> means "all users." The URL <code>/users?role=rider&amp;sort=name</code> still means "all users," just filtered to riders and sorted by name.</p>
<p>Express parses query strings automatically. Everything after the <code>?</code> ends up in <code>req.query</code>:</p>
<pre><code class="language-js">// URL: /users?role=rider&amp;sort=name&amp;page=2
app.get('/users', (req, res) =&gt; {
  console.log(req.query);
  // { role: 'rider', sort: 'name', page: '2' }

  console.log(req.query.role);
  // 'rider'

  console.log(req.query.page);
  // '2' (still a string)
});
</code></pre>
<p>Same as params, everything in <code>req.query</code> is a string. <code>page</code> comes back as <code>'2'</code>, not <code>2</code>. Parse it before doing math with it.</p>
<p>Query parameters are optional by nature. If someone hits <code>/users</code> without any query string, <code>req.query</code> is just an empty object <code>{}</code>. Your code needs to handle that. Default values are a good habit:</p>
<pre><code class="language-js">app.get('/users', async (req, res) =&gt; {
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const sort = req.query.sort || 'createdAt';
  const role = req.query.role; // undefined if not provided

  const filter = {};
  if (role) filter.role = role;

  const users = await User.find(filter)
    .sort(sort)
    .skip((page - 1) * limit)
    .limit(limit);

  res.json(users);
});
</code></pre>
<p>This route works whether you call it as <code>/users</code>, <code>/users?page=3</code>, or <code>/users?role=admin&amp;sort=name&amp;page=2&amp;limit=5</code>. The defaults cover whatever the caller did not specify.</p>
<hr />
<h2>The difference, shown side by side</h2>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/335eaf23-51ab-4542-9b6b-07b0e383ae6d.png" alt="" style="display:block;margin:0 auto" />

<p>The short version: params say "which thing," query strings say "how you want it."</p>
<p>If you are fetching one specific user, that is a param: <code>/users/42</code>. If you are searching for users who match some criteria, those criteria go in the query string: <code>/users?city=mumbai&amp;role=rider</code>. If you are getting a specific order for a specific user, both user and order are params: <code>/users/42/orders/99</code>. If you want that order's invoice in PDF format instead of JSON, that is a query: <code>/users/42/orders/99?format=pdf</code>.</p>
<p>I used to overthink this. The test I use now is simple: would the route still make sense without this piece of data? If removing it makes the URL meaningless (like <code>/users/</code> with no ID when you want one user), it is a param. If removing it just gives you a broader or default response, it is a query.</p>
<hr />
<h2>Using both at the same time</h2>
<p>Most real routes use params and query strings together. Here is a route that fetches orders for a specific user, with optional filtering:</p>
<pre><code class="language-js">// GET /users/:userId/orders?status=delivered&amp;page=1
app.get('/users/:userId/orders', async (req, res) =&gt; {
  const { userId } = req.params;
  const { status, page = 1, limit = 10 } = req.query;

  const filter = { user: userId };
  if (status) filter.status = status;

  const orders = await Order.find(filter)
    .sort({ createdAt: -1 })
    .skip((parseInt(page) - 1) * parseInt(limit))
    .limit(parseInt(limit))
    .populate('restaurant', 'name');

  res.json(orders);
});
</code></pre>
<p>The param <code>:userId</code> tells us whose orders we are looking at. The query strings <code>status</code>, <code>page</code>, and <code>limit</code> let the caller control the response shape. Remove the query strings and you still get a valid response: all orders for that user, first page, default limit. Remove the param and the route does not match at all.</p>
<hr />
<h2>A few things that tripped me up</h2>
<p><strong>Params are positional.</strong> Express matches them left to right in the URL path. If you have two routes like <code>app.get('/users/:id')</code> and <code>app.get('/users/active')</code>, the order you define them matters. Express checks routes in the order they are registered. If <code>:id</code> comes first, a request to <code>/users/active</code> matches it with <code>req.params.id</code> equal to <code>'active'</code>. Put the specific route before the parameterized one:</p>
<pre><code class="language-js">// specific route first
app.get('/users/active', (req, res) =&gt; {
  // handles /users/active
});

// parameterized route second
app.get('/users/:id', (req, res) =&gt; {
  // handles /users/42, /users/abc, etc.
});
</code></pre>
<p><strong>Query strings do not affect route matching.</strong> Express matches routes based on the path only. <code>/users</code> and <code>/users?role=admin</code> hit the same route handler. You never define query parameters in your route string.</p>
<p><strong>Empty query values are valid.</strong> A URL like <code>/users?name=</code> gives you <code>req.query.name</code> as an empty string <code>''</code>, which is truthy in a conditional check... actually no, empty strings are falsy in JavaScript. So <code>if (req.query.name)</code> fails on both <code>undefined</code> and <code>''</code>. If you need to distinguish between "not provided" and "provided but empty," check for <code>undefined</code> explicitly:</p>
<pre><code class="language-js">if (req.query.name !== undefined) {
  // they sent the parameter, even if it is empty
}
</code></pre>
<hr />
<h2>When to use which</h2>
<p>Params for identification: <code>/products/:productId</code>, <code>/orders/:orderId</code>, <code>/users/:userId/profile</code>.</p>
<p>Query strings for everything optional: search terms, filters, sort order, pagination, format preferences, feature flags.</p>
<p>If you catch yourself putting optional data in the path, move it to the query string. If you catch yourself using query strings to identify a specific resource, move it to a param. The URL should read like a sentence. <code>/restaurants/5f2b/menu</code> reads as "the menu for restaurant 5f2b." That makes sense. <code>/restaurants?id=5f2b&amp;show=menu</code> technically works but reads like a database query leaked into your URL.</p>
<p>REST API conventions reinforce this pattern. You will see it everywhere once you start reading API documentation for services like Stripe, GitHub, or Twilio. The resource goes in the path. The options go after the question mark.</p>
<hr />
<h2>References</h2>
<ul>
<li><p><a href="https://expressjs.com/en/guide/routing.html">Express routing guide</a></p>
</li>
<li><p><a href="https://expressjs.com/en/api.html#req.params">Express req.params docs</a></p>
</li>
<li><p><a href="https://expressjs.com/en/api.html#req.query">Express req.query docs</a></p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_is_a_URL">MDN: What is a URL</a></p>
</li>
<li><p><a href="https://restfulapi.net/resource-naming/">REST API design guidelines on URL structure</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Node.js Under the Hood: Why It Is Fast and When It Is Not]]></title><description><![CDATA[I kept hearing people say Node.js is fast. In interviews, in blog posts, in random Discord threads at 2am. But nobody explained what "fast" actually meant. Fast compared to what? Fast at what? I had t]]></description><link>https://blog.saumyagrawal.in/node-js-under-the-hood-why-it-is-fast-and-when-it-is-not</link><guid isPermaLink="true">https://blog.saumyagrawal.in/node-js-under-the-hood-why-it-is-fast-and-when-it-is-not</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Saumya]]></dc:creator><pubDate>Thu, 07 May 2026 10:46:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/bc6eae4d-a93a-43b2-85e5-187f44e53c9c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I kept hearing people say Node.js is fast. In interviews, in blog posts, in random Discord threads at 2am. But nobody explained what "fast" actually meant. Fast compared to what? Fast at what? I had this vague sense that it had something to do with being "non-blocking" and "event-driven," two phrases I nodded along to without understanding for longer than I want to admit.</p>
<p>So I went and figured it out. The mental model that finally made it click for me was a restaurant, which I will get to in a second. But first, the thing that confused me most.</p>
<hr />
<h2>Node.js runs on one thread. That sounds terrible.</h2>
<p>Most backend technologies like Java or Python spin up a new thread for every incoming request. Thread gets a request, does the work, sends the response, dies. Simple. If you have 1,000 requests coming in at once, you have 1,000 threads running. Each thread takes up memory and CPU time. At some point the server runs out of threads and starts queuing, or worse, crashing.</p>
<p>Node.js does not do this. It runs your JavaScript code on a single thread. One. That is it. When I first read that, my reaction was: how does that not immediately fall over?</p>
<p>The answer is in what Node does with that single thread. It never lets it sit around waiting.</p>
<hr />
<h2>The restaurant analogy</h2>
<p>Think of a traditional multi-threaded server as a restaurant where every table gets a dedicated waiter. The waiter takes the order, walks it to the kitchen, stands at the kitchen window waiting for the food, then walks it back to the table. While the waiter is standing at the kitchen window doing nothing, they cannot help anyone else. If ten tables need service at the same time, you need ten waiters standing around staring at kitchen windows.</p>
<p>Node.js is the restaurant with one waiter who is extremely good at their job. The waiter takes the order from table one, hands the ticket to the kitchen, and immediately walks to table two. Takes that order, hands it to the kitchen, checks on table three. When the kitchen rings the bell saying table one's food is ready, the waiter picks it up and delivers it. The waiter never stands at the kitchen window. They are always moving, always taking the next task that is ready.</p>
<p>The kitchen in this analogy is the operating system doing I/O: reading files from disk, making network requests, querying databases. That work takes time, but your JavaScript thread does not need to be the one waiting for it. The OS handles it in the background, and when the result comes back, Node puts a callback in a queue. The thread picks it up when it is free.</p>
<p>That is non-blocking I/O. Your code says "go read this file" and moves on. It does not pause. It does not wait. It handles the next thing.</p>
<pre><code class="language-js">const fs = require('fs');

// non-blocking: Node hands this to the OS and moves on
fs.readFile('/some/big/file.txt', 'utf8', (err, data) =&gt; {
  // this runs later, when the file is actually read
  console.log('file contents loaded');
});

// this runs immediately, before the file is done reading
console.log('I did not wait for the file');
</code></pre>
<p>Output:</p>
<pre><code class="language-plaintext">I did not wait for the file
file contents loaded
</code></pre>
<p>The second <code>console.log</code> runs first because <code>readFile</code> does not block execution. The callback fires later, when the OS finishes reading the file. If you have used <code>fetch</code> in the browser, you already understand this pattern. Same idea, server side.</p>
<hr />
<h2>Blocking vs non-blocking: what the difference looks like in code</h2>
<p>Here is the blocking version of that same file read:</p>
<pre><code class="language-js">const fs = require('fs');

// blocking: the thread stops here until the file is fully read
const data = fs.readFileSync('/some/big/file.txt', 'utf8');
console.log('file contents loaded');

// this cannot run until the file read is completely done
console.log('now I can do other things');
</code></pre>
<p>Output:</p>
<pre><code class="language-plaintext">file contents loaded
now I can do other things
</code></pre>
<p>If that file takes 500ms to read, the entire server is frozen for 500ms. No other request gets handled. Nobody gets a response. The thread is parked at the kitchen window.</p>
<p>In a real app with real traffic, that 500ms freeze means every user currently waiting for a response is also waiting for your file read to finish. This is why Node's standard library has async versions of almost every function. The <code>Sync</code> versions exist for startup scripts and CLI tools where blocking is fine because there is no one else waiting.</p>
<hr />
<h2>The event loop</h2>
<p>The event loop is the mechanism that makes all of this work. It is a loop that runs continuously, checking: is there a callback ready to execute? If yes, run it. If no, wait for one.</p>
<pre><code class="language-plaintext">   ┌───────────────────────────┐
┌─&gt;│         timers            │  setTimeout, setInterval callbacks
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │  I/O callbacks deferred to next loop
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │  internal use only
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │         poll              │  retrieve new I/O events
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │         check             │  setImmediate callbacks
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │    close callbacks        │  socket.on('close', ...)
│  └─────────────┬─────────────┘
└─────────────────┘
</code></pre>
<p>You do not need to memorize these phases. What matters is the mental model: your code runs, hands off slow work to the OS, and the event loop picks up the results when they are ready. Everything async in Node, from reading files to database queries to HTTP requests, goes through this cycle.</p>
<p>Here is a quick way to see the order for yourself:</p>
<pre><code class="language-js">console.log('1: synchronous, runs first');

setTimeout(() =&gt; {
  console.log('4: timer callback, runs in the timers phase');
}, 0);

setImmediate(() =&gt; {
  console.log('5: setImmediate, runs in the check phase');
});

process.nextTick(() =&gt; {
  console.log('2: nextTick, runs before any phase');
});

Promise.resolve().then(() =&gt; {
  console.log('3: promise, runs after nextTick but before timers');
});
</code></pre>
<p>Output:</p>
<pre><code class="language-plaintext">1: synchronous, runs first
2: nextTick, runs before any phase
3: promise, runs after nextTick but before timers
4: timer callback, runs in the timers phase
5: setImmediate, runs in the check phase
</code></pre>
<p><code>process.nextTick</code> runs before the event loop continues to the next phase. Promises run right after that. <code>setTimeout</code> and <code>setImmediate</code> come later. This ordering matters when you are debugging race conditions or figuring out why a callback fires before another. Run this snippet, stare at the output, modify it. That teaches the order faster than reading about it.</p>
<hr />
<h2>Concurrency is not parallelism</h2>
<p>This is where people get confused. Node handles many requests concurrently. It does not handle them in parallel. The difference matters.</p>
<p>Concurrency means managing multiple things at once. Parallelism means doing multiple things at the same time. Node is concurrent. It juggles thousands of requests by never waiting for any single one to finish. But all your JavaScript runs on one thread, so two pieces of JS code never execute at the exact same moment.</p>
<p>For I/O-heavy work (reading files, calling APIs, querying databases), this is completely fine. The thread spends almost no time on each request because it hands off the slow part and moves on. A single Node process can handle tens of thousands of concurrent connections this way.</p>
<p>For CPU-heavy work (image processing, video encoding, heavy math), this falls apart. If your code runs a loop that takes 3 seconds of pure computation, nothing else happens during those 3 seconds. The event loop is blocked. Every request in the queue sits there waiting.</p>
<pre><code class="language-js">// this blocks the event loop for ~3 seconds
// no request gets handled during this time
function heavyComputation() {
  let sum = 0;
  for (let i = 0; i &lt; 3_000_000_000; i++) {
    sum += i;
  }
  return sum;
}

// while heavyComputation runs, the server is frozen
app.get('/slow', (req, res) =&gt; {
  const result = heavyComputation();
  res.json({ result });
});

// this route also stops responding while /slow is computing
app.get('/fast', (req, res) =&gt; {
  res.json({ message: 'hello' });
});
</code></pre>
<p>Hit <code>/slow</code> once and try hitting <code>/fast</code> in another tab. It hangs until the computation finishes. One bad route takes down the entire server.</p>
<p>If you need to do CPU-heavy work in Node, the answer is <code>worker_threads</code>. They run JavaScript in a separate thread so the main event loop stays free:</p>
<pre><code class="language-js">const { Worker } = require('worker_threads');

app.get('/slow', (req, res) =&gt; {
  const worker = new Worker('./heavy-computation.js');
  worker.on('message', (result) =&gt; {
    res.json({ result });
  });
  worker.on('error', (err) =&gt; {
    res.status(500).json({ error: err.message });
  });
});
</code></pre>
<p>The computation happens off the main thread. The event loop keeps handling other requests. Your <code>/fast</code> route stays responsive.</p>
<hr />
<h2>Where Node is a good fit and where it is not</h2>
<p>Node works well for anything that is mostly I/O with a thin layer of logic on top. REST APIs, GraphQL servers, real-time apps with WebSockets, chat applications, streaming services, proxy servers. The pattern is the same: receive a request, ask some other service or database for data, send the response. The thread barely does any work itself. It is a traffic controller.</p>
<p>Node is a poor fit for raw computation. If your app resizes images on every request, transcodes video, runs machine learning inference, or does heavy data crunching, a single-threaded runtime is working against you. You can use worker threads, but at that point you might be better off with Go, Rust, or Python with proper multiprocessing depending on the workload.</p>
<p>The honest answer is that most web applications are I/O-bound. Most of the time your server is waiting: waiting for the database, waiting for an external API, waiting for a file to be read. Node was built exactly for that waiting problem.</p>
<hr />
<h2>Who actually uses Node.js in production</h2>
<p>Netflix runs parts of their backend on Node. They switched from Java for some services and saw startup times drop significantly. Their UI layer runs on Node because server-side rendering with React was simpler there than in a Java stack.</p>
<p>PayPal rewrote their account overview page from Java to Node. Their team reported being able to build the Node version with fewer people in less time, and the resulting app handled more requests per second with lower average response times.</p>
<p>LinkedIn moved their mobile backend from Ruby on Rails to Node. They went from running 30 servers to 3, partly because Node handled the same concurrent load with fewer resources.</p>
<p>Uber's matching system, the part that pairs riders with drivers, runs on Node. The system needs to handle massive numbers of concurrent connections with low latency, which is exactly the kind of I/O-bound real-time workload Node handles well.</p>
<p>These are not small experiments. They are production systems handling millions of users. The pattern across all of them is the same: lots of concurrent connections, mostly I/O, relatively light computation per request.</p>
<hr />
<h2>Wrapping up</h2>
<p>Node is fast because it never lets its single thread sit idle. Instead of dedicating a thread to each request and letting it wait around, Node hands off the slow work and moves on to the next thing. The event loop picks up the results when they are ready. That model lets a single process handle thousands of concurrent connections without the memory overhead of thousands of threads.</p>
<p>The tradeoff is CPU-bound work. Block the event loop and everything stops. Know when you are doing I/O (use async, let Node do its thing) and when you are doing computation (use worker threads or pick a different tool).</p>
<p>If you are building APIs, real-time features, or anything that talks to databases and external services, Node is a strong choice. Understand the event loop, keep it free, and it will handle more traffic than you expect from a single process.</p>
<hr />
<h2>References</h2>
<ul>
<li><p><a href="https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick">Node.js docs: The event loop</a> - the official explanation with all the phases</p>
</li>
<li><p><a href="https://nodejs.org/api/worker_threads.html">Node.js docs: Worker threads</a> - how to offload CPU work</p>
</li>
<li><p><a href="https://nodejs.org/api/fs.html">Node.js docs: File system</a> - async vs sync file operations</p>
</li>
<li><p><a href="https://www.youtube.com/watch?v=8aGhZQkoFbQ">Philip Roberts: What the heck is the event loop anyway?</a> - the best visual explanation of the event loop, 26 minutes that will save you hours</p>
</li>
<li><p><a href="https://netflixtechblog.com/making-netflix-com-faster-f95d15f2e972">Node.js at Netflix</a> - their experience migrating from Java</p>
</li>
<li><p><a href="https://medium.com/paypal-tech/node-js-at-paypal-4e2d1d08ce4f">PayPal engineering blog on Node.js</a> - performance comparisons with their Java stack</p>
</li>
<li><p><a href="http://docs.libuv.org/">libuv documentation</a> - the C library that handles Node's async I/O under the hood</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[File Uploads in Express: Why They Do Not Just Work Out of the Box]]></title><description><![CDATA[I had a form with a file input. I had an Express server with a POST route. I hit submit, opened req.body in my route handler, and got... nothing. The text fields were there. The file was not. I spent ]]></description><link>https://blog.saumyagrawal.in/file-uploads-in-express-why-they-do-not-just-work-out-of-the-box</link><guid isPermaLink="true">https://blog.saumyagrawal.in/file-uploads-in-express-why-they-do-not-just-work-out-of-the-box</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[Express]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Saumya]]></dc:creator><pubDate>Wed, 06 May 2026 16:05:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/69b655a2-d4a5-43e1-8c4d-6b54c48cce26.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I had a form with a file input. I had an Express server with a POST route. I hit submit, opened <code>req.body</code> in my route handler, and got... nothing. The text fields were there. The file was not. I spent twenty minutes convinced my form was broken before someone in a Discord server said "Express does not parse files by default."</p>
<p>That was the moment I realized file uploads are a different kind of HTTP request, and your server needs help handling them.</p>
<hr />
<h2>Why your server cannot read files from a normal form</h2>
<p>When you submit a regular HTML form with text fields, the browser encodes the data as <code>application/x-www-form-urlencoded</code>. That is the default. It looks like a query string: <code>name=Priya&amp;email=priya@example.com</code>. Express's built-in <code>express.json()</code> and <code>express.urlencoded()</code> middleware can parse this without any trouble.</p>
<p>Files do not work that way. A file is binary data. It could be an image, a PDF, a video. You cannot flatten that into a query string. So the browser switches to a different encoding called <code>multipart/form-data</code>. The request body gets split into multiple parts, each separated by a randomly generated boundary string. One part might be a text field, another part is the raw file bytes with metadata like the original filename and MIME type.</p>
<p>Express has no built-in parser for multipart data. It sees the request come in, does not recognize the encoding, and skips it. That is why <code>req.body</code> was empty. The data was there in the request. Express just did not know how to read it.</p>
<pre><code class="language-html">&lt;!-- this form sends multipart/form-data --&gt;
&lt;form action="/upload" method="POST" enctype="multipart/form-data"&gt;
  &lt;input type="text" name="caption" /&gt;
  &lt;input type="file" name="photo" /&gt;
  &lt;button type="submit"&gt;Upload&lt;/button&gt;
&lt;/form&gt;
</code></pre>
<p>Without <code>enctype="multipart/form-data"</code>, the browser sends the filename as a string but not the actual file contents. I made this mistake twice before it stuck.</p>
<hr />
<h2>What Multer does</h2>
<p>Multer is a Node.js middleware built specifically for handling <code>multipart/form-data</code>. It parses the incoming request, extracts the file(s), saves them wherever you tell it to, and attaches file metadata to <code>req.file</code> or <code>req.files</code> so your route handler can use it.</p>
<p>You install it like any other npm package:</p>
<pre><code class="language-bash">npm install multer
</code></pre>
<p>Multer only processes multipart forms. If someone sends a regular JSON request to an endpoint that uses Multer, it ignores the request and passes it along unchanged. It does not interfere with your other routes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/040c87ac-5fa1-4e01-b35c-2e0c68da327c.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Handling a single file upload</h2>
<p>The simplest case: one file input, one upload. Multer gives you a function called <code>upload.single('fieldname')</code> that handles this. The field name must match the <code>name</code> attribute on your HTML file input.</p>
<pre><code class="language-js">const express = require('express');
const multer = require('multer');

const app = express();
const upload = multer({ dest: 'uploads/' });

app.post('/upload', upload.single('photo'), (req, res) =&gt; {
  console.log(req.file);
  console.log(req.body); // text fields still work
  res.json({ file: req.file });
});

app.listen(3000);
</code></pre>
<p>When you submit the form, Multer saves the file to the <code>uploads/</code> folder and populates <code>req.file</code> with an object like this:</p>
<pre><code class="language-js">{
  fieldname: 'photo',
  originalname: 'vacation.jpg',
  encoding: '7bit',
  mimetype: 'image/jpeg',
  destination: 'uploads/',
  filename: 'a1b2c3d4e5f6',       // random name, no extension
  path: 'uploads/a1b2c3d4e5f6',
  size: 245920                     // bytes
}
</code></pre>
<p>Notice the filename. Multer strips the original name and extension, replacing it with a random hex string. This is a security measure. If Multer kept the original filename, someone could upload a file called <code>../../etc/passwd</code> or <code>malicious.html</code> and potentially cause problems depending on how you serve it. The random name prevents path traversal and naming conflicts.</p>
<p>The <code>uploads/</code> directory needs to exist before you start the server. Multer does not create it for you. If the folder is missing, the upload fails with an error. Add this near the top of your server file:</p>
<pre><code class="language-js">const fs = require('fs');
const path = require('path');

const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) {
  fs.mkdirSync(uploadDir);
}
</code></pre>
<hr />
<h2>Handling multiple file uploads</h2>
<p>Two scenarios here. First: multiple files from the same input field. Second: multiple file inputs with different names.</p>
<p>For the same field, use <code>upload.array()</code>:</p>
<pre><code class="language-js">// accepts up to 5 files from a single input named "photos"
app.post('/gallery', upload.array('photos', 5), (req, res) =&gt; {
  console.log(req.files); // array of file objects
  console.log(req.files.length); // how many were uploaded
  res.json({ count: req.files.length });
});
</code></pre>
<pre><code class="language-html">&lt;input type="file" name="photos" multiple /&gt;
</code></pre>
<p>The second argument to <code>upload.array()</code> is the maximum number of files. If someone tries to send 10 files when you set the limit to 5, Multer rejects the entire request.</p>
<p>For different fields, use <code>upload.fields()</code>:</p>
<pre><code class="language-js">app.post('/profile', upload.fields([
  { name: 'avatar', maxCount: 1 },
  { name: 'resume', maxCount: 1 },
]), (req, res) =&gt; {
  console.log(req.files['avatar'][0]); // single file object
  console.log(req.files['resume'][0]); // single file object
  res.json({ message: 'got both files' });
});
</code></pre>
<p>With <code>upload.fields()</code>, <code>req.files</code> is an object where each key is the field name and each value is an array of file objects for that field. Even if <code>maxCount</code> is 1, you still get an array, so you access the file at index <code>[0]</code>.</p>
<hr />
<h2>Controlling where and how files get saved</h2>
<p>The <code>dest: 'uploads/'</code> shorthand works, but you get random filenames with no extensions. For anything beyond a quick prototype, you want more control. Multer's <code>diskStorage</code> engine lets you set the destination folder and the filename separately.</p>
<pre><code class="language-js">const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, 'uploads/');
  },
  filename: function (req, file, cb) {
    // keep original extension, prefix with timestamp to avoid collisions
    const uniqueName = Date.now() + '-' + file.originalname;
    cb(cb, uniqueName);
  },
});

const upload = multer({ storage: storage });
</code></pre>
<p>Wait, there is a bug in that code. I left it in on purpose because I made this exact mistake when I first wrote a storage config. The <code>filename</code> callback should be <code>cb(null, uniqueName)</code>, not <code>cb(cb, uniqueName)</code>. The first argument is an error (or <code>null</code> if no error). Passing the callback function itself as the error is a silent failure that produces a confusing error message about callbacks not being a function. Here is the corrected version:</p>
<pre><code class="language-js">const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, 'uploads/');
  },
  filename: function (req, file, cb) {
    const uniqueName = Date.now() + '-' + file.originalname;
    cb(null, uniqueName);
  },
});

const upload = multer({ storage: storage });
</code></pre>
<p>Now a file called <code>vacation.jpg</code> gets saved as something like <code>1717843200000-vacation.jpg</code>. You keep the extension, avoid naming collisions, and can still identify the original file.</p>
<p>You should also filter what types of files you accept. Without a filter, someone can upload anything, executables included. Add a <code>fileFilter</code> function:</p>
<pre><code class="language-js">const upload = multer({
  storage: storage,
  limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB max
  fileFilter: function (req, file, cb) {
    const allowed = ['image/jpeg', 'image/png', 'image/webp'];
    if (allowed.includes(file.mimetype)) {
      cb(null, true);
    } else {
      cb(new Error('Only JPEG, PNG, and WebP images are allowed'));
    }
  },
});
</code></pre>
<p>The <code>limits</code> option caps file size. The <code>fileFilter</code> checks the MIME type. If the file fails either check, Multer throws an error before writing anything to disk.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/c0f4b470-6035-4d9a-87cd-46544c26be4f.png" alt="" style="display:block;margin:0 auto" />

<p>Handle Multer errors in your route so the client gets a readable response instead of a stack trace:</p>
<pre><code class="language-js">app.post('/upload', (req, res) =&gt; {
  upload.single('photo')(req, res, function (err) {
    if (err instanceof multer.MulterError) {
      // Multer-specific error (file too large, too many files, etc.)
      return res.status(400).json({ error: err.message });
    }
    if (err) {
      // your custom error from fileFilter
      return res.status(400).json({ error: err.message });
    }
    // no error, file is in req.file
    res.json({ file: req.file });
  });
});
</code></pre>
<hr />
<h2>Serving uploaded files back to users</h2>
<p>Files sitting in an <code>uploads/</code> folder are useless unless users can access them. The simplest way is to serve the folder as static files:</p>
<pre><code class="language-js">app.use('/uploads', express.static('uploads'));
</code></pre>
<p>Now a file saved as <code>uploads/1717843200000-vacation.jpg</code> is accessible at <code>http://localhost:3000/uploads/1717843200000-vacation.jpg</code>. You can use this URL in an <code>&lt;img&gt;</code> tag or send it back in an API response.</p>
<pre><code class="language-js">app.post('/upload', upload.single('photo'), (req, res) =&gt; {
  const fileUrl = `\({req.protocol}://\){req.get('host')}/uploads/${req.file.filename}`;
  res.json({
    message: 'uploaded',
    url: fileUrl,
  });
});
</code></pre>
<p>A word about production. Serving files directly from your Node server works for learning and small projects. Once you have real users and real traffic, you move uploaded files to a dedicated storage service like AWS S3 or Google Cloud Storage, and serve them through a CDN. The Node server should not be responsible for serving static files at scale. But that is a separate topic, and when you are starting out, <code>express.static</code> is the right tool.</p>
<hr />
<h2>A quick tip about .gitignore</h2>
<p>Add your uploads folder to <code>.gitignore</code>. You do not want user-uploaded files committed to your repository. It bloats the repo, and if any of those files contain personal data, you have a privacy problem baked into your version history.</p>
<pre><code class="language-plaintext"># .gitignore
uploads/
</code></pre>
<p>Keep the folder structure in your repo by adding an empty <code>.gitkeep</code> file inside <code>uploads/</code> so Git tracks the directory without tracking its contents.</p>
<hr />
<h2>The full working example</h2>
<p>Here is everything together in one file. In a real project you would split this into separate modules, but for learning it helps to see it all in one place.</p>
<pre><code class="language-js">const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');

const app = express();

// make sure uploads/ exists
const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) {
  fs.mkdirSync(uploadDir);
}

// storage config
const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, 'uploads/');
  },
  filename: function (req, file, cb) {
    cb(null, Date.now() + '-' + file.originalname);
  },
});

// upload instance with limits and filter
const upload = multer({
  storage: storage,
  limits: { fileSize: 5 * 1024 * 1024 },
  fileFilter: function (req, file, cb) {
    const allowed = ['image/jpeg', 'image/png', 'image/webp'];
    if (allowed.includes(file.mimetype)) {
      cb(null, true);
    } else {
      cb(new Error('Only JPEG, PNG, and WebP images are allowed'));
    }
  },
});

// serve uploaded files
app.use('/uploads', express.static('uploads'));

// single file upload
app.post('/upload', upload.single('photo'), (req, res) =&gt; {
  const fileUrl = `\({req.protocol}://\){req.get('host')}/uploads/${req.file.filename}`;
  res.json({ url: fileUrl });
});

// multiple files from same field
app.post('/gallery', upload.array('photos', 5), (req, res) =&gt; {
  const urls = req.files.map(f =&gt;
    `\({req.protocol}://\){req.get('host')}/uploads/${f.filename}`
  );
  res.json({ urls });
});

app.listen(3000, () =&gt; console.log('running on port 3000'));
</code></pre>
<hr />
<h2>What to take away from this</h2>
<p>Express does not handle file uploads on its own. The browser sends files using <code>multipart/form-data</code>, which is a different encoding than what Express parses by default. Multer fills that gap.</p>
<p>For a single file, use <code>upload.single()</code>. For multiple files from one input, use <code>upload.array()</code>. For files from different inputs, use <code>upload.fields()</code>. Always configure a <code>fileFilter</code> and a <code>fileSize</code> limit, even in development, because building that habit early saves you from accepting unexpected files later.</p>
<p>Start with <code>express.static</code> for serving uploads. Move to cloud storage when your project outgrows a single server. And keep your uploads out of Git.</p>
<hr />
<h2>References</h2>
<ul>
<li><p><a href="https://www.npmjs.com/package/multer">Multer documentation on npm</a></p>
</li>
<li><p><a href="https://github.com/expressjs/multer">Multer GitHub repository</a></p>
</li>
<li><p><a href="https://expressjs.com/en/starter/static-files.html">Express static files documentation</a></p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/POST">MDN: multipart/form-data</a></p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects">MDN: Using FormData objects</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[JWT Authentication: I Locked Myself Out of My Own API and Learned How Tokens Work]]></title><description><![CDATA[I built my first Express API and felt good about it. Routes worked, data came back, everything looked right. Then a friend asked "so how do you stop random people from deleting stuff?" and I realized ]]></description><link>https://blog.saumyagrawal.in/jwt-authentication-i-locked-myself-out-of-my-own-api-and-learned-how-tokens-work</link><guid isPermaLink="true">https://blog.saumyagrawal.in/jwt-authentication-i-locked-myself-out-of-my-own-api-and-learned-how-tokens-work</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[JWT]]></category><dc:creator><![CDATA[Saumya]]></dc:creator><pubDate>Wed, 06 May 2026 15:57:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/4ee98976-5a22-4081-ae5e-ecd3618855ea.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I built my first Express API and felt good about it. Routes worked, data came back, everything looked right. Then a friend asked "so how do you stop random people from deleting stuff?" and I realized the entire thing was wide open. Anyone with the URL could hit any endpoint. There was no login, no identity check, nothing. It was a public buffet where anyone could walk into the kitchen.</p>
<p>That is how I ended up learning about authentication. Not from a textbook or a course outline, but from the realization that my API had no front door.</p>
<hr />
<h2>What authentication actually means</h2>
<p>Authentication is the server asking "who are you?" before doing anything. When you log into Instagram, you type your email and password. Instagram checks those against what it has stored. If they match, you are in. If they do not, you are not. That is authentication.</p>
<p>There is a related concept called authorization, which is "okay I know who you are, but are you allowed to do this?" A regular user can view posts. An admin can delete them. Authentication comes first. Authorization comes after. This post is about the first one.</p>
<p>The reason authentication matters for APIs specifically is that APIs do not have a login screen sitting in front of them. A browser shows you a form. An API just accepts HTTP requests from anywhere. Without authentication, every endpoint is open to anyone who knows the URL. Your delete route, your admin panel, your user data. All of it.</p>
<hr />
<h2>The old way: sessions</h2>
<p>Before tokens became popular, most web apps used sessions. The flow worked like this: you log in, the server creates a session object and stores it somewhere (usually in memory or a database), and it sends you back a session ID as a cookie. Every time your browser makes a request, the cookie goes along automatically, and the server looks up the session to figure out who you are.</p>
<p>This works fine until you have multiple servers. If server A created the session and your next request hits server B, server B has no idea who you are because the session data lives on server A. You can fix this with a shared session store like Redis, but now you have another piece of infrastructure to manage.</p>
<p>Sessions are stateful. The server has to remember something about you between requests. That is the part that gets complicated at scale.</p>
<hr />
<h2>The stateless idea</h2>
<p>JWTs take a different approach. Instead of the server remembering who you are, the server gives you a token after login that contains your identity information. On every request, you send the token back. The server reads the token, verifies it has not been tampered with, and knows who you are. No lookup needed. No session store. No shared state between servers.</p>
<p>The server does not remember anything. It just reads what is in the token and checks the signature. Stateless.</p>
<p>This is the core idea behind token-based authentication, and it is why JWTs became the default for APIs, single-page applications, and mobile apps. The server does less work per request, and scaling horizontally is simpler because any server can verify any token independently.</p>
<hr />
<h2>What a JWT actually looks like</h2>
<p>JWT stands for JSON Web Token. When you see one, it looks like three chunks of gibberish separated by dots:</p>
<pre><code class="language-plaintext">eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI2NWFiYzEyMyIsInJvbGUiOiJjdXN0b21lciIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxNzAwMDg2NDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
</code></pre>
<p>Three parts. Each separated by a period. Each part is base64-encoded, which means it is not encrypted. Anyone can decode it. I will say that again because it surprised me: JWTs are not encrypted by default. They are encoded. There is a big difference, and I will get to why that matters.</p>
<hr />
<h2>The three parts</h2>
<h3>Header</h3>
<p>The first chunk is the header. Decode it and you get:</p>
<pre><code class="language-json">{
  "alg": "HS256",
  "typ": "JWT"
}
</code></pre>
<p>Two fields. <code>alg</code> is the algorithm used to create the signature (more on that in a second). <code>typ</code> says it is a JWT. That is it. The header is boring. You will almost never think about it.</p>
<h3>Payload</h3>
<p>The second chunk is the payload. This is where the actual information lives:</p>
<pre><code class="language-json">{
  "userId": "65abc123",
  "role": "customer",
  "iat": 1700000000,
  "exp": 1700086400
}
</code></pre>
<p><code>userId</code> and <code>role</code> are claims you put there yourself when creating the token. <code>iat</code> is "issued at," a Unix timestamp of when the token was created. <code>exp</code> is the expiration time. After that timestamp, the token is invalid.</p>
<p>You can put whatever you want in the payload. The user's name, their email, their permissions. But remember what I said about encoding versus encryption. Anyone who gets this token can decode the payload and read it. Do not put passwords, credit card numbers, or anything sensitive in here. Think of it like writing something on a postcard. Anyone handling it can read it. The signature just proves nobody changed the text.</p>
<h3>Signature</h3>
<p>The third chunk is the signature, and this is the part that actually matters for security. The server takes the header, the payload, and a secret key that only the server knows, and runs them through the algorithm specified in the header:</p>
<pre><code class="language-plaintext">HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  your-secret-key
)
</code></pre>
<p>The output is the signature. When the server receives a token later, it repeats this exact calculation using the header and payload from the token plus its own secret key. If the result matches the signature in the token, the data has not been tampered with. If someone changed the <code>role</code> from <code>customer</code> to <code>admin</code> in the payload, the signature would not match, and the server would reject the token.</p>
<p>The secret key is the entire security model. If someone gets your secret key, they can forge valid tokens for any user. Keep it in an environment variable, never commit it to Git, and make it long and random.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/7c5b8e7b-8f0a-4e97-9ae7-cf752a2cce4c.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>The login flow</h2>
<p>Here is what happens when a user logs in and then accesses a protected route. I am going to walk through every step because seeing the full picture once makes the individual pieces make more sense.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/d1ba3565-7ed1-4e09-8f09-ff3446cb3cfd.png" alt="" style="display:block;margin:0 auto" />

<p>Step by step:</p>
<ol>
<li><p>The client sends the email and password to a login endpoint.</p>
</li>
<li><p>The server looks up the user in the database and compares the password using bcrypt (never compare plain text, always hash).</p>
</li>
<li><p>If the password matches, the server creates a JWT containing the user's ID, role, and an expiration time.</p>
</li>
<li><p>The server sends the token back to the client.</p>
</li>
<li><p>The client stores the token (usually in localStorage or memory, depending on the app).</p>
</li>
<li><p>On every subsequent request, the client includes the token in the <code>Authorization</code> header.</p>
</li>
<li><p>The server reads the token, verifies the signature, checks expiration, and either processes the request or rejects it.</p>
</li>
</ol>
<p>No session stored anywhere. No database lookup on every request to check who is logged in. The token itself carries the identity.</p>
<hr />
<h2>Building it</h2>
<p>Enough theory. Let me show you the actual code, starting from an empty Express app with a MongoDB connection (covered in Part 5 of this series).</p>
<h3>Install what you need</h3>
<pre><code class="language-bash">npm install express mongoose dotenv bcryptjs jsonwebtoken
</code></pre>
<p><code>bcryptjs</code> is for hashing passwords. <code>jsonwebtoken</code> is the library that creates and verifies JWTs.</p>
<h3>The User model</h3>
<p>This is a trimmed version of the model from the Mongoose post. Password hashing happens in a pre-save hook so you never accidentally store a plain text password.</p>
<pre><code class="language-js">// models/User.js
const { Schema, model } = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new Schema({
  name: {
    type: String,
    required: [true, 'Name is required'],
    trim: true,
  },
  email: {
    type: String,
    required: [true, 'Email is required'],
    unique: true,
    lowercase: true,
    trim: true,
  },
  password: {
    type: String,
    required: [true, 'Password is required'],
    minlength: [8, 'Password must be at least 8 characters'],
    select: false, // never return password in queries by default
  },
  role: {
    type: String,
    enum: ['customer', 'admin'],
    default: 'customer',
  },
}, { timestamps: true });

// hash password before saving
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  this.password = await bcrypt.hash(this.password, 12);
  next();
});

// compare password for login
userSchema.methods.comparePassword = async function(candidate) {
  return bcrypt.compare(candidate, this.password);
};

module.exports = model('User', userSchema);
</code></pre>
<p>The <code>select: false</code> on the password field means Mongoose will not include the password in query results unless you explicitly ask for it with <code>.select('+password')</code>. This prevents you from accidentally sending password hashes to the client in a response.</p>
<h3>Generating a token</h3>
<p>Create a small utility function that wraps the <code>jsonwebtoken</code> library. This keeps your route handlers clean and gives you one place to change token settings later.</p>
<pre><code class="language-js">// utils/generateToken.js
const jwt = require('jsonwebtoken');

function generateToken(user) {
  return jwt.sign(
    {
      userId: user._id,
      role: user.role,
    },
    process.env.JWT_SECRET,
    {
      expiresIn: '24h',
    }
  );
}

module.exports = generateToken;
</code></pre>
<p><code>jwt.sign()</code> takes three arguments: the payload (what data to put in the token), the secret key, and an options object. <code>expiresIn</code> accepts human-readable strings like <code>'24h'</code>, <code>'7d'</code>, or <code>'30m'</code>. The library converts this into an <code>exp</code> claim in the payload automatically.</p>
<p>Your <code>.env</code> file needs a JWT_SECRET:</p>
<pre><code class="language-plaintext">MONGO_URI=mongodb://localhost:27017/myapp
JWT_SECRET=a-long-random-string-that-nobody-can-guess-not-this-one-though
</code></pre>
<p>Generate a proper random secret in your terminal:</p>
<pre><code class="language-bash">node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
</code></pre>
<p>Use the output of that command. Not "mysecret", not "jwt123", not "password". A short or guessable secret defeats the entire purpose of signing tokens.</p>
<h3>The signup route</h3>
<pre><code class="language-js">// routes/auth.js
const express = require('express');
const router = express.Router();
const User = require('../models/User');
const generateToken = require('../utils/generateToken');

router.post('/signup', async (req, res) =&gt; {
  try {
    const { name, email, password } = req.body;

    // check if user already exists
    const existing = await User.findOne({ email });
    if (existing) {
      return res.status(409).json({ error: 'Email already registered' });
    }

    const user = await User.create({ name, email, password });
    const token = generateToken(user);

    res.status(201).json({
      token,
      user: {
        id: user._id,
        name: user.name,
        email: user.email,
        role: user.role,
      },
    });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});
</code></pre>
<p>Notice the response does not include <code>user.password</code>. Because of <code>select: false</code> in the schema, the password hash is not on the user object returned by <code>User.create()</code>. But even if it were, the response explicitly picks which fields to send. Be deliberate about what goes over the wire.</p>
<h3>The login route</h3>
<pre><code class="language-js">router.post('/login', async (req, res) =&gt; {
  try {
    const { email, password } = req.body;

    // .select('+password') overrides select: false for this query
    const user = await User.findOne({ email }).select('+password');
    if (!user) {
      return res.status(401).json({ error: 'Invalid email or password' });
    }

    const isMatch = await user.comparePassword(password);
    if (!isMatch) {
      return res.status(401).json({ error: 'Invalid email or password' });
    }

    const token = generateToken(user);

    res.json({
      token,
      user: {
        id: user._id,
        name: user.name,
        email: user.email,
        role: user.role,
      },
    });
  } catch (err) {
    res.status(500).json({ error: 'Login failed' });
  }
});

module.exports = router;
</code></pre>
<p>One thing to notice: the error message is the same for "email not found" and "wrong password." This is intentional. If you say "email not found," an attacker knows that email is not in your system. If you say "wrong password," they know the email is valid and can focus on brute-forcing the password. Same generic message for both prevents that information leak.</p>
<hr />
<h2>Sending the token with requests</h2>
<p>After login, the client gets a token back. On every subsequent request to a protected endpoint, the client needs to send that token. The standard way is through the <code>Authorization</code> header using the <code>Bearer</code> scheme:</p>
<pre><code class="language-plaintext">Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
</code></pre>
<p>If you are testing with <code>curl</code>:</p>
<pre><code class="language-bash">curl http://localhost:3000/api/orders \
  -H "Authorization: Bearer YOUR_TOKEN_HERE"
</code></pre>
<p>If you are building a frontend with fetch:</p>
<pre><code class="language-js">const token = localStorage.getItem('token');

const response = await fetch('/api/orders', {
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
});

const data = await response.json();
</code></pre>
<p>If you are using axios, you can set a default header so you do not have to add it to every request:</p>
<pre><code class="language-js">const axios = require('axios');

// set once after login
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;

// now every request includes the token automatically
const res = await axios.get('/api/orders');
</code></pre>
<p>The word <code>Bearer</code> before the token is a convention defined in <a href="https://www.rfc-editor.org/rfc/rfc6750">RFC 6750</a>. It tells the server "I am authenticating by bearing (carrying) this token." You will see this in almost every API that uses JWTs.</p>
<hr />
<h2>Protecting routes with middleware</h2>
<p>This is the part where the authentication actually happens on the server side. You write a middleware function that runs before your route handler. It checks for the token, verifies it, and either lets the request through or rejects it.</p>
<pre><code class="language-js">// middleware/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');

async function authenticate(req, res, next) {
  // 1. get the token from the header
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }

  const token = authHeader.split(' ')[1];

  try {
    // 2. verify the token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);

    // 3. check if the user still exists
    const user = await User.findById(decoded.userId);
    if (!user) {
      return res.status(401).json({ error: 'User no longer exists' });
    }

    // 4. attach user to the request object
    req.user = user;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
}

module.exports = authenticate;
</code></pre>
<p>Walking through this line by line:</p>
<p>First, it checks if the <code>Authorization</code> header exists and starts with <code>Bearer</code> . If not, the request is rejected immediately. No token, no access.</p>
<p>Then it pulls the token out by splitting on the space and taking the second part (index 1). <code>"Bearer eyJhb..."</code> becomes <code>"eyJhb..."</code>.</p>
<p><code>jwt.verify()</code> does two things at once. It checks the signature to make sure the token was not tampered with, and it checks if the token has expired. If either check fails, it throws an error.</p>
<p>The <code>User.findById(decoded.userId)</code> step is optional but I recommend it. The token might be valid but the user could have been deleted from the database since the token was issued. Without this check, a deleted user's token would still work until it expires.</p>
<p>Finally, it attaches the user to <code>req.user</code>. This means any route handler that runs after the middleware can access <code>req.user</code> without doing another database lookup.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/8240056e-6451-4f9f-8097-4c2090dfe7c0.png" alt="" style="display:block;margin:0 auto" />

<h3>Using the middleware</h3>
<p>Apply it to any route that needs protection:</p>
<pre><code class="language-js">// routes/orders.js
const express = require('express');
const router = express.Router();
const authenticate = require('../middleware/auth');

// public route, no auth needed
router.get('/menu', async (req, res) =&gt; {
  // anyone can see the menu
  const items = await MenuItem.find();
  res.json(items);
});

// protected route
router.get('/orders', authenticate, async (req, res) =&gt; {
  // req.user is available because authenticate middleware set it
  const orders = await Order.find({ user: req.user._id });
  res.json(orders);
});

// protected route
router.post('/orders', authenticate, async (req, res) =&gt; {
  const order = await Order.create({
    user: req.user._id,
    items: req.body.items,
    totalAmount: req.body.totalAmount,
  });
  res.status(201).json(order);
});
</code></pre>
<p>The pattern is simple: add <code>authenticate</code> as a second argument before the route handler. It runs first. If the token is valid, <code>next()</code> passes control to the route handler and <code>req.user</code> is populated. If not, the middleware sends a 401 response and the route handler never executes.</p>
<hr />
<h2>Role-based access control</h2>
<p>Once you have authentication working, adding authorization is a small step. You already have <code>req.user.role</code> available. Write another middleware that checks it:</p>
<pre><code class="language-js">// middleware/authorize.js
function authorize(...allowedRoles) {
  return (req, res, next) =&gt; {
    if (!req.user) {
      return res.status(401).json({ error: 'Not authenticated' });
    }

    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Not authorized' });
    }

    next();
  };
}

module.exports = authorize;
</code></pre>
<p>Use it after <code>authenticate</code>:</p>
<pre><code class="language-js">const authenticate = require('../middleware/auth');
const authorize = require('../middleware/authorize');

// only admins can delete menu items
router.delete(
  '/menu/:id',
  authenticate,
  authorize('admin'),
  async (req, res) =&gt; {
    await MenuItem.findByIdAndDelete(req.params.id);
    res.json({ message: 'Deleted' });
  }
);

// both admins and riders can update order status
router.patch(
  '/orders/:id/status',
  authenticate,
  authorize('admin', 'rider'),
  async (req, res) =&gt; {
    const order = await Order.findByIdAndUpdate(
      req.params.id,
      { status: req.body.status },
      { new: true }
    );
    res.json(order);
  }
);
</code></pre>
<p>The 401 versus 403 distinction matters. 401 means "I do not know who you are" (not authenticated). 403 means "I know who you are, but you are not allowed to do this" (not authorized). Mixing them up makes debugging harder for the person consuming your API.</p>
<hr />
<h2>Token expiration and why it matters</h2>
<p>I set <code>expiresIn: '24h'</code> in the token generation function. That means after 24 hours, the token stops working and the user has to log in again. This seems annoying, and it is, but it exists for a good reason.</p>
<p>JWTs cannot be invalidated individually. Once a token is issued, it is valid until it expires. There is no "log this token out" button on the server because the server does not track tokens. If someone steals a token, the attacker has access until that token expires. A short expiration limits the damage window.</p>
<p>The tradeoff is user experience. A 15-minute token means users log in constantly. A 7-day token means a stolen token is useful for a week. Most APIs pick something in the middle, like 1 to 24 hours, and use refresh tokens for longer sessions. Refresh tokens are a deeper topic that I will cover in a separate post, but the basic idea is: the short-lived access token handles regular requests, and a longer-lived refresh token lets the client get a new access token without the user re-entering their password.</p>
<p>For now, 24 hours is a reasonable default while you are learning. Adjust based on what your app needs.</p>
<hr />
<h2>Common mistakes I made (and how to avoid them)</h2>
<h3>Storing tokens in localStorage without thinking about it</h3>
<p><code>localStorage</code> is the easy option, and for many apps it is fine. But <code>localStorage</code> is accessible to any JavaScript running on your page. If your site has an XSS vulnerability (a script injection), an attacker can read the token from <code>localStorage</code> and send it to their own server.</p>
<p>The more secure option for browser apps is an httpOnly cookie, which JavaScript cannot access at all. The browser sends it automatically with every request. The tradeoff is that cookies require CORS configuration and are vulnerable to CSRF attacks instead, which you mitigate with CSRF tokens. There is no perfect answer. For a learning project, <code>localStorage</code> is fine. For a production app with real users, research httpOnly cookies.</p>
<h3>Using a weak secret</h3>
<p>I used <code>"secret"</code> as my JWT_SECRET during development. If an attacker knows or guesses your secret, they can create valid tokens for any user. Use the <code>crypto.randomBytes</code> command I showed earlier. Make it at least 64 characters.</p>
<h3>Putting sensitive data in the payload</h3>
<p>I put a user's email in the payload once, which was fine. Then I put their phone number in there, which was less fine. Then I considered putting their address in there and realized I was heading in a bad direction. The payload is readable by anyone. Only put identifiers and roles in there, not personal information.</p>
<h3>Not handling expired tokens on the frontend</h3>
<p>When a token expires, the server returns 401. If your frontend does not handle this, the user sees a broken page with no explanation. Add a response interceptor (if using axios) or a wrapper around fetch that checks for 401 responses and redirects to the login page:</p>
<pre><code class="language-js">axios.interceptors.response.use(
  (response) =&gt; response,
  (error) =&gt; {
    if (error.response &amp;&amp; error.response.status === 401) {
      localStorage.removeItem('token');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);
</code></pre>
<hr />
<h2>Testing the whole flow</h2>
<p>Here is how to test everything end to end using curl. You can also use Postman or Thunder Client in VS Code, but curl works anywhere.</p>
<pre><code class="language-bash"># 1. Sign up
curl -X POST http://localhost:3000/api/auth/signup \
  -H "Content-Type: application/json" \
  -d '{"name": "Priya", "email": "priya@example.com", "password": "mypassword123"}'

# response:
# {
#   "token": "eyJhbGciOiJIUzI1NiIs...",
#   "user": { "id": "...", "name": "Priya", "email": "priya@example.com", "role": "customer" }
# }

# 2. Copy the token from the response and use it:
TOKEN="eyJhbGciOiJIUzI1NiIs..."

# 3. Access a protected route
curl http://localhost:3000/api/orders \
  -H "Authorization: Bearer $TOKEN"

# 4. Try without a token
curl http://localhost:3000/api/orders
# { "error": "No token provided" }

# 5. Try with a garbage token
curl http://localhost:3000/api/orders \
  -H "Authorization: Bearer this.is.fake"
# { "error": "Invalid token" }
</code></pre>
<hr />
<h2>The full project structure</h2>
<pre><code class="language-plaintext">project/
  .env
  server.js
  db.js
  models/
    User.js
  routes/
    auth.js
    orders.js
  middleware/
    auth.js
    authorize.js
  utils/
    generateToken.js
</code></pre>
<pre><code class="language-js">// server.js
require('dotenv').config();
const express = require('express');
const connectDB = require('./db');
const authRoutes = require('./routes/auth');
const orderRoutes = require('./routes/orders');

const app = express();
app.use(express.json());

connectDB();

app.use('/api/auth', authRoutes);
app.use('/api', orderRoutes);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () =&gt; {
  console.log(`server running on port ${PORT}`);
});
</code></pre>
<pre><code class="language-js">// db.js
const mongoose = require('mongoose');

async function connectDB() {
  try {
    await mongoose.connect(process.env.MONGO_URI);
    console.log('connected to MongoDB');
  } catch (err) {
    console.error('connection failed:', err.message);
    process.exit(1);
  }
}

module.exports = connectDB;
</code></pre>
<hr />
<h2>What I wish someone had told me</h2>
<p>JWTs are not a complete solution. They handle the "prove who you are on each request" part well, but they do not handle logout (you cannot invalidate a token without maintaining state), they do not handle "this user changed their password so all their existing tokens should stop working" (same problem), and they do not handle refresh flows without extra work. For a learning project or an API where short token lifetimes are acceptable, JWTs are great. For a complex app with real security requirements, you will eventually layer more on top.</p>
<p>The thing that actually made authentication click for me was building it once from scratch, messing it up, locking myself out, and then understanding each piece by fixing it. The login route is just comparing a password and creating a signed string. The middleware is just reading that string back and checking the signature. The protected route is just checking if the middleware said "this person is real" before running. None of it is magic. It is just strings and if statements and a bit of cryptography you do not have to understand deeply to use correctly.</p>
<p>Start with the code in this post, get it running, then try breaking it. Change the secret key after creating a token and see what happens. Remove the <code>exp</code> field and think about what that means. Send a request without the <code>Bearer</code> prefix. The errors you get will teach you more than reading about it ever could.</p>
<hr />
<h2>References</h2>
<ul>
<li><p><a href="https://www.npmjs.com/package/jsonwebtoken">jsonwebtoken on npm</a> - the library used throughout this post</p>
</li>
<li><p><a href="https://jwt.io/">jwt.io</a> - paste a token here to decode and inspect it visually, also has a debugger</p>
</li>
<li><p><a href="https://www.npmjs.com/package/bcryptjs">bcryptjs on npm</a> - password hashing library</p>
</li>
<li><p><a href="https://www.rfc-editor.org/rfc/rfc7519">RFC 7519: JSON Web Token</a> - the official JWT specification, dense but complete</p>
</li>
<li><p><a href="https://www.rfc-editor.org/rfc/rfc6750">RFC 6750: Bearer Token Usage</a> - why the Authorization header uses the word "Bearer"</p>
</li>
<li><p><a href="https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html">OWASP JWT Cheat Sheet</a> - security best practices for JWTs in production</p>
</li>
<li><p><a href="https://mongoosejs.com/docs/">Mongoose documentation</a> - for the User model and pre-save hooks</p>
</li>
<li><p><a href="https://expressjs.com/en/guide/using-middleware.html">Express middleware guide</a> - how middleware works in Express</p>
</li>
</ul>
<hr />
]]></content:encoded></item><item><title><![CDATA[I Installed Node.js and Accidentally Became a Backend Developer]]></title><description><![CDATA[I had been writing JavaScript in the browser for a while. Console logs, DOM manipulation, making buttons change color. It felt productive until someone asked me to build something that saved data. "Ju]]></description><link>https://blog.saumyagrawal.in/i-installed-node-js-and-accidentally-became-a-backend-developer</link><guid isPermaLink="true">https://blog.saumyagrawal.in/i-installed-node-js-and-accidentally-became-a-backend-developer</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Saumya]]></dc:creator><pubDate>Wed, 06 May 2026 00:25:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/0f041b49-9cb9-4fa5-9dca-a52c2a9d6d2f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I had been writing JavaScript in the browser for a while. Console logs, DOM manipulation, making buttons change color. It felt productive until someone asked me to build something that saved data. "Just use Node," they said. I did not know what Node was. I thought JavaScript only ran inside Chrome.</p>
<p>Turns out JavaScript escaped the browser in 2009. A developer named Ryan Dahl took Chrome's V8 engine, the thing that actually reads and runs your JavaScript code, and wrapped it in a program that could run on your operating system directly. No browser needed. That program is Node.js. You write JavaScript in a file, you run that file with the <code>node</code> command, and your operating system executes it the same way it would run a Python script or a C program. The language is the same. The environment is completely different.</p>
<p>The browser gives you <code>document</code>, <code>window</code>, <code>alert()</code>. Node gives you the filesystem, network access, the ability to start an HTTP server. Same syntax, different world underneath.</p>
<p>I want to walk through everything it takes to go from "I have never touched Node" to "I just wrote a working web server." No frameworks, no npm packages, just Node itself and what it ships with.</p>
<hr />
<h2>Installing Node.js</h2>
<p>Go to <a href="https://nodejs.org">nodejs.org</a>. You will see two versions. One says LTS (Long Term Support), the other says Current. Pick LTS. Current has newer features but LTS is what companies actually use in production, and it gets security patches for longer. You are not going to need bleeding-edge features right now.</p>
<p>The installer works on Windows, macOS, and Linux. Download it, run it, click through the defaults. On Linux you can also install through your package manager, but the version in <code>apt</code> or <code>yum</code> is often outdated. The official installer or a version manager is a better bet.</p>
<p>If you are on macOS or Linux and want to manage multiple Node versions later, look into <a href="https://github.com/nvm-sh/nvm">nvm</a>. It lets you install and switch between Node versions with a single command. On Windows, <a href="https://github.com/coreybutler/nvm-windows">nvm-windows</a> does the same thing. You do not need this today, but knowing it exists saves future headaches.</p>
<pre><code class="language-bash"># Install via nvm (macOS/Linux) if you want this route
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
nvm install --lts
nvm use --lts
</code></pre>
<p>Or just use the installer from the website. Both work.</p>
<hr />
<h2>Checking that it actually installed</h2>
<p>Open a terminal. On Windows that is PowerShell or Command Prompt. On macOS it is Terminal. On Linux, whatever terminal emulator you have.</p>
<pre><code class="language-bash">node -v
</code></pre>
<pre><code class="language-plaintext">v22.15.0
</code></pre>
<p>If you see a version number, Node is installed. If you see "command not found," the installer either failed or your terminal does not know where Node lives (a PATH issue, which I covered in the Linux post if you want the full story on what PATH actually is).</p>
<p>Check npm too:</p>
<pre><code class="language-bash">npm -v
</code></pre>
<pre><code class="language-plaintext">10.9.2
</code></pre>
<p>npm is Node's package manager. It comes bundled with Node automatically. You will use it later to install libraries, but for this post we are not touching it. Everything here uses only what Node ships with out of the box.</p>
<p>Your version numbers will probably be different from mine. That is fine. As long as both commands return a number and not an error, you are good.</p>
<hr />
<h2>The REPL: JavaScript without a file</h2>
<p>Before writing any files, there is something worth trying. Type <code>node</code> in your terminal with no arguments:</p>
<pre><code class="language-bash">node
</code></pre>
<pre><code class="language-plaintext">Welcome to Node.js v22.15.0.
Type ".help" for more information.
&gt;
</code></pre>
<p>That <code>&gt;</code> prompt is the REPL. REPL stands for Read, Eval, Print, Loop. It reads what you type, evaluates it as JavaScript, prints the result, and loops back waiting for more input. It is an interactive JavaScript sandbox right in your terminal.</p>
<pre><code class="language-js">&gt; 2 + 2
4
&gt; "hello".toUpperCase()
'HELLO'
&gt; const x = 10
undefined
&gt; x * 3
30
&gt; [1, 2, 3].map(n =&gt; n * 2)
[ 2, 4, 6 ]
</code></pre>
<p>Every line executes immediately. No file, no save, no refresh. The <code>undefined</code> you see after <code>const x = 10</code> is just the REPL telling you that the assignment expression itself did not return a value. It is not an error.</p>
<p>The REPL is useful for quick experiments. You want to check how a string method works, or test whether your regex matches something, or try out an array operation before putting it in your code. Faster than opening a browser console, and it runs the exact same V8 engine.</p>
<p>A few REPL commands worth knowing:</p>
<pre><code class="language-plaintext">.help     shows available commands
.exit     quits the REPL (Ctrl+C twice also works)
.editor   opens multi-line mode so you can paste or type a function
</code></pre>
<p>Try <code>.editor</code> mode:</p>
<pre><code class="language-js">&gt; .editor
// Entering editor mode (Ctrl+D to finish, Ctrl+C to cancel)
function greet(name) {
  return `hey ${name}, welcome to Node`;
}
// press Ctrl+D
&gt; greet("Saumya")
'hey Saumya, welcome to Node'
</code></pre>
<p>The REPL is a scratchpad. Use it when you want to test one thing quickly. For anything longer than a few lines, write a file.</p>
<p>To exit: type <code>.exit</code> or hit Ctrl+C twice.</p>
<hr />
<h2>Your first JavaScript file</h2>
<p>Create a folder somewhere. Call it whatever you want. I will use <code>node-basics</code>.</p>
<pre><code class="language-bash">mkdir node-basics
cd node-basics
</code></pre>
<p>Create a file called <code>hello.js</code>. You can use any text editor. VS Code, Sublime, Vim, Nano, Notepad, it does not matter. The file just needs to end in <code>.js</code>.</p>
<pre><code class="language-js">// hello.js
const name = "Saumya";
const hour = new Date().getHours();

let greeting;

if (hour &lt; 12) {
  greeting = "Good morning";
} else if (hour &lt; 17) {
  greeting = "Good afternoon";
} else {
  greeting = "Good evening";
}

console.log(`\({greeting}, \){name}.`);
console.log(`It is currently ${hour}:00 hours.`);
console.log("This is running outside the browser.");
</code></pre>
<p>Nothing fancy. Variables, a conditional, template literals, <code>console.log</code>. The same JavaScript you already know. The only difference is where it runs.</p>
<hr />
<h2>Running it</h2>
<pre><code class="language-bash">node hello.js
</code></pre>
<pre><code class="language-plaintext">Good afternoon, Saumya.
It is currently 14:00 hours.
This is running outside the browser.
</code></pre>
<p>That is it. Node read your file, executed the JavaScript, printed the output to your terminal. No browser opened. No HTML page. Your operating system ran JavaScript the same way it runs any other program.</p>
<p>What just happened under the hood is worth understanding, even briefly.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/bdc62a81-b48a-44de-8068-8317e27f0e93.png" alt="" style="display:block;margin:0 auto" />

<p>When you type <code>node hello.js</code>, the Node.js runtime starts up. It hands your code to V8, which compiles the JavaScript into machine code that your CPU can actually execute. V8 does not interpret JavaScript line by line like some languages do. It compiles it. That is why Node is fast enough to run web servers handling thousands of requests.</p>
<p>The output goes to stdout, which is what your terminal is reading. <code>console.log</code> in Node writes to stdout. In the browser, <code>console.log</code> writes to the browser's developer console. Same function name, different destination.</p>
<p>Try adding an error to the file on purpose:</p>
<pre><code class="language-js">// error-test.js
console.log("this will print");
undefinedVariable.doSomething();
console.log("this will NOT print");
</code></pre>
<pre><code class="language-bash">node error-test.js
</code></pre>
<pre><code class="language-plaintext">this will print
/home/you/node-basics/error-test.js:2
undefinedVariable.doSomething();
^

ReferenceError: undefinedVariable is not defined
    at Object.&lt;anonymous&gt; (/home/you/node-basics/error-test.js:2:1)
</code></pre>
<p>Node gives you the file name, the line number, a caret pointing at the exact spot, and the error type. Read these. They tell you what went wrong and where. The first line printed because Node executes sequentially until it hits an unhandled error, then it stops. The third line never ran.</p>
<hr />
<h2>A few things Node has that the browser does not</h2>
<p>Before we build the server, I want to show a couple of things that make Node different from browser JavaScript. You cannot access these in Chrome's console.</p>
<pre><code class="language-js">// node-extras.js

// __dirname: the absolute path to the folder this file is in
console.log("This file lives in:", __dirname);

// __filename: the absolute path to this file itself
console.log("This file is:", __filename);

// process: information about the running Node process
console.log("Node version:", process.version);
console.log("Operating system:", process.platform);
console.log("Current directory:", process.cwd());

// process.argv: command line arguments passed to the script
console.log("Arguments:", process.argv);
</code></pre>
<pre><code class="language-bash">node node-extras.js hello world
</code></pre>
<pre><code class="language-plaintext">This file lives in: /home/you/node-basics
This file is: /home/you/node-basics/node-extras.js
Node version: v22.15.0
Operating system: linux
Current directory: /home/you/node-basics
Arguments: [ '/usr/bin/node', '/home/you/node-basics/node-extras.js', 'hello', 'world' ]
</code></pre>
<p><code>process.argv</code> is an array. The first element is always the path to the Node binary. The second is the path to your script. Everything after that is whatever you typed after the filename. This is how CLI tools read your input.</p>
<p>A quick practical use:</p>
<pre><code class="language-js">// greet-cli.js
const name = process.argv[2] || "stranger";
console.log(`Hello, ${name}.`);
</code></pre>
<pre><code class="language-bash">node greet-cli.js Ravi
</code></pre>
<pre><code class="language-plaintext">Hello, Ravi.
</code></pre>
<pre><code class="language-bash">node greet-cli.js
</code></pre>
<pre><code class="language-plaintext">Hello, stranger.
</code></pre>
<p>You just built a tiny CLI tool. Nothing installed, no libraries, just <code>process.argv</code> and a fallback.</p>
<hr />
<h2>Writing a Hello World server</h2>
<p>This is the part where Node stops feeling like "JavaScript that runs in the terminal" and starts feeling like something genuinely different. We are going to write an HTTP server from scratch. No Express, no framework, just the <code>http</code> module that ships with Node.</p>
<pre><code class="language-js">// server.js
const http = require("http");

const server = http.createServer((req, res) =&gt; {
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Hello from Node.js\n");
});

server.listen(3000, () =&gt; {
  console.log("Server running at http://localhost:3000");
});
</code></pre>
<pre><code class="language-bash">node server.js
</code></pre>
<pre><code class="language-plaintext">Server running at http://localhost:3000
</code></pre>
<p>Open your browser and go to <code>http://localhost:3000</code>. You will see "Hello from Node.js" on the page. Your terminal is still running. The server is sitting there, waiting for requests.</p>
<p>Let me break down what each part does.</p>
<p><code>require("http")</code> loads Node's built-in HTTP module. This module knows how to speak HTTP, the protocol your browser uses to talk to websites. You did not install this. It comes with Node.</p>
<p><code>http.createServer()</code> creates a server object. The function you pass it runs every single time someone makes a request to your server. That function gets two arguments: <code>req</code> (the incoming request) and <code>res</code> (the response you send back).</p>
<p><code>res.writeHead(200, { "Content-Type": "text/plain" })</code> sets the HTTP status code to 200 (which means "OK, everything is fine") and tells the browser the response body is plain text.</p>
<p><code>res.end("Hello from Node.js\n")</code> sends the actual response body and closes the connection. If you forget <code>res.end()</code>, the browser will hang forever waiting for the response to finish.</p>
<p><code>server.listen(3000)</code> tells the server to start listening on port 3000. A port is just a number that identifies which program should receive incoming network traffic. Port 80 is the default for HTTP, port 443 for HTTPS. We use 3000 because it is unlikely to conflict with anything else running on your machine.</p>
<p>The callback in <code>listen()</code> runs once the server is ready. That is where the console.log fires.</p>
<p>Here is what happens when your browser visits <code>localhost:3000</code>:</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/a138fa25-9b75-49b8-843e-c1a7b4901bb4.png" alt="" style="display:block;margin:0 auto" />

<p>To stop the server, go back to your terminal and press Ctrl+C. The process exits and the port is freed.</p>
<hr />
<h2>Making the server do more than one thing</h2>
<p>A server that returns the same text for every URL is not very useful. Let's handle different routes:</p>
<pre><code class="language-js">// server-routes.js
const http = require("http");

const server = http.createServer((req, res) =&gt; {
  // req.url contains the path the browser requested
  // req.method contains GET, POST, etc.

  if (req.url === "/" &amp;&amp; req.method === "GET") {
    res.writeHead(200, { "Content-Type": "text/html" });
    res.end("&lt;h1&gt;Home page&lt;/h1&gt;&lt;p&gt;Try visiting /about or /time&lt;/p&gt;");

  } else if (req.url === "/about" &amp;&amp; req.method === "GET") {
    res.writeHead(200, { "Content-Type": "text/html" });
    res.end("&lt;h1&gt;About&lt;/h1&gt;&lt;p&gt;This is a Node.js server with no framework.&lt;/p&gt;");

  } else if (req.url === "/time" &amp;&amp; req.method === "GET") {
    const now = new Date().toLocaleTimeString();
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ time: now }));

  } else {
    res.writeHead(404, { "Content-Type": "text/plain" });
    res.end("404 - not found\n");
  }
});

server.listen(3000, () =&gt; {
  console.log("Server running at http://localhost:3000");
});
</code></pre>
<pre><code class="language-bash">node server-routes.js
</code></pre>
<p>Now try these in your browser:</p>
<ul>
<li><p><code>http://localhost:3000/</code> returns an HTML page</p>
</li>
<li><p><code>http://localhost:3000/about</code> returns a different HTML page</p>
</li>
<li><p><code>http://localhost:3000/time</code> returns JSON with the current time</p>
</li>
<li><p><code>http://localhost:3000/anything-else</code> returns a 404</p>
</li>
</ul>
<p>Notice the <code>/time</code> route sets <code>Content-Type</code> to <code>application/json</code> and uses <code>JSON.stringify</code> to turn a JavaScript object into a JSON string. This is how APIs work. The browser (or any HTTP client) gets back structured data it can parse and use.</p>
<p>The <code>req.url</code> and <code>req.method</code> check is the most primitive form of routing. In a real app you would use Express or Fastify to handle this, because writing <code>if/else</code> blocks for every URL gets old fast. But seeing it done manually first is how you understand what those frameworks actually do underneath. They are reading <code>req.url</code> and <code>req.method</code> and matching patterns, just with nicer syntax.</p>
<hr />
<h2>A habit worth building right now</h2>
<p>Every time you modify your server code, you have to stop the server (Ctrl+C) and restart it (<code>node server.js</code>). This gets annoying within about ten minutes.</p>
<p>Node 22 has a built-in watch mode:</p>
<pre><code class="language-bash">node --watch server.js
</code></pre>
<p>Now when you save changes to <code>server.js</code>, Node automatically restarts the server. No extra tools needed. If you are on an older Node version, <code>nodemon</code> is the classic npm package that does the same thing, but if your Node supports <code>--watch</code>, use that first.</p>
<hr />
<h2>What is actually different from browser JavaScript</h2>
<p>By this point you have written and run JavaScript outside the browser. The syntax is identical, but the environment is not. Here is what changed:</p>
<p>The browser gives you <code>window</code>, <code>document</code>, <code>localStorage</code>, <code>fetch</code> (in modern browsers), <code>alert()</code>, and the entire DOM. None of these exist in Node. If you try <code>document.getElementById</code> in a Node script, you get a ReferenceError. There is no document. There is no HTML page. There is no DOM.</p>
<p>Node gives you <code>process</code>, <code>require()</code> (or <code>import</code> with ES modules), <code>__dirname</code>, <code>__filename</code>, access to the filesystem through the <code>fs</code> module, the ability to create servers with <code>http</code>, read and write files, run child processes, interact with your operating system. None of these exist in the browser. If you try <code>require("fs")</code> in Chrome's console, it will not work.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/0738c178-8934-47f5-aa54-3f21bd4b1f89.png" alt="" style="display:block;margin:0 auto" />

<p>The language is one thing. The environment is another. People mix these up constantly when starting out. If a tutorial shows <code>document.querySelector</code> it is browser JavaScript. If it shows <code>require("http")</code> it is Node JavaScript. The JavaScript itself is identical. What changes is the stuff around it.</p>
<hr />
<h2>Common mistakes when you are starting out</h2>
<p>I am putting these here because I made all of them.</p>
<p><strong>Running</strong> <code>node</code> <strong>with no filename and wondering why your file did not execute.</strong> Typing <code>node</code> alone opens the REPL. Typing <code>node server.js</code> runs the file. They do different things.</p>
<p><strong>Editing the file and expecting the running server to pick up changes.</strong> Node reads your file once when it starts. If you change the file, the running process does not know. Restart it, or use <code>--watch</code>.</p>
<p><strong>Forgetting</strong> <code>res.end()</code> <strong>in your server.</strong> The browser will just keep spinning. The connection stays open, waiting for more data that never comes. Every response needs to be closed with <code>res.end()</code>.</p>
<p><strong>Using</strong> <code>require</code> <strong>for a file and forgetting the path prefix.</strong> <code>require("http")</code> loads a built-in module. <code>require("myfile")</code> tries to find a package called "myfile" in node_modules. <code>require("./myfile")</code> loads a file relative to the current file. The <code>./</code> matters.</p>
<p><strong>Port already in use.</strong> If you see <code>EADDRINUSE</code> when starting your server, another process is already using that port. Either that process is still running from last time (check with <code>lsof -i :3000</code> on macOS/Linux or <code>netstat -ano | findstr :3000</code> on Windows), or pick a different port number.</p>
<hr />
<h2>Where to go from here</h2>
<p>At this point you can install Node, run JavaScript files, use the REPL for quick tests, and write a basic HTTP server. That is enough to start building things.</p>
<p>The next post in this series covers npm and packages: what <code>package.json</code> is, how to install and use libraries, and the difference between dependencies and devDependencies. After that we will get into Express, which takes the manual routing we did here and makes it not terrible.</p>
<p>For now, the best thing you can do is experiment. Write scripts that read command-line arguments and do something with them. Add more routes to your server. Try returning different content types. Break things and read the error messages.</p>
<hr />
<h2>References</h2>
<ul>
<li><p><a href="https://nodejs.org">Node.js official site</a> - download and installation</p>
</li>
<li><p><a href="https://nodejs.org/docs/latest/api/http.html">Node.js docs: HTTP module</a> - the module we used to build the server</p>
</li>
<li><p><a href="https://nodejs.org/docs/latest/api/process.html">Node.js docs: Process</a> - everything about the <code>process</code> object</p>
</li>
<li><p><a href="https://github.com/nvm-sh/nvm">nvm (Node Version Manager)</a> - manage multiple Node versions on macOS/Linux</p>
</li>
<li><p><a href="https://github.com/coreybutler/nvm-windows">nvm-windows</a> - same thing for Windows</p>
</li>
<li><p><a href="https://v8.dev/">V8 JavaScript engine</a> - the engine inside both Chrome and Node</p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript">MDN: JavaScript reference</a> - when you need to look up any JS syntax</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[I Tried to Read Three Files at Once and My Code Became a Staircase]]></title><description><![CDATA[I had a simple task. Read three files, combine their contents, write the result to a new file. In any other context this is maybe ten minutes of work. In Node.js, this was the afternoon I learned that]]></description><link>https://blog.saumyagrawal.in/i-tried-to-read-three-files-at-once-and-my-code-became-a-staircase</link><guid isPermaLink="true">https://blog.saumyagrawal.in/i-tried-to-read-three-files-at-once-and-my-code-became-a-staircase</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Saumya]]></dc:creator><pubDate>Wed, 06 May 2026 00:14:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/57bd45fe-b9d3-4f4f-8e89-909221a6c6e4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I had a simple task. Read three files, combine their contents, write the result to a new file. In any other context this is maybe ten minutes of work. In Node.js, this was the afternoon I learned that JavaScript does not wait for you.</p>
<p>If you are coming from Python or Java or really anything where code runs line by line and waits for each thing to finish before moving on, Node.js will confuse you. It confused me. I wrote what looked like perfectly reasonable code, and the output came back empty. The file existed. The path was correct. The data just was not there yet when my code tried to use it.</p>
<p>This post is about why that happens, what callbacks are, why they get ugly fast, and how promises fix the mess.</p>
<hr />
<h2>Why Node.js does not wait</h2>
<p>Most programming languages are synchronous by default. You say "read this file" and the program stops, waits for the disk to return the data, then moves to the next line. Simple. Predictable.</p>
<p>Node.js does not do that. When you tell Node to read a file, it sends that request to the operating system and immediately moves to the next line of code. It does not wait. The file might take 5 milliseconds or 500 milliseconds to read, and Node does not care. It has other things to do.</p>
<p>This is called non-blocking I/O, and the reason Node works this way is that it runs on a single thread. One thread. If it stopped and waited for every file read, every database query, every HTTP request, it could only do one thing at a time. A web server handling 1000 users would grind to a halt because user 2 is waiting for user 1's database query to finish.</p>
<p>So Node keeps moving. It fires off the I/O operation, continues executing whatever comes next, and when the result comes back, it runs a function you gave it earlier. That function is a callback.</p>
<p>Here is what it looks like when you do not account for this:</p>
<pre><code class="language-js">const fs = require('fs');

let content;
fs.readFile('greeting.txt', 'utf8', (err, data) =&gt; {
  content = data;
});

console.log(content);
</code></pre>
<pre><code class="language-plaintext">undefined
</code></pre>
<p>The <code>console.log</code> runs before <code>readFile</code> finishes. By the time the callback fires and assigns <code>data</code> to <code>content</code>, the log is long gone. The variable was <code>undefined</code> at the moment you printed it. The data showed up a few milliseconds later, but nobody was listening anymore.</p>
<p>This is the first wall every beginner hits with Node. Your code runs out of order.</p>
<hr />
<h2>Callbacks: the original solution</h2>
<p>A callback is a function you hand to another function and say "call this when you are done." Instead of waiting for a result, you describe what should happen with the result whenever it arrives.</p>
<pre><code class="language-js">const fs = require('fs');

fs.readFile('greeting.txt', 'utf8', (err, data) =&gt; {
  if (err) {
    console.error('Failed to read file:', err.message);
    return;
  }
  console.log(data);
});

console.log('This prints first');
</code></pre>
<pre><code class="language-plaintext">This prints first
Hello from greeting.txt!
</code></pre>
<p>The third argument to <code>readFile</code> is the callback. Node reads the file in the background, and when it finishes, it calls your function with two arguments: an error (if something went wrong) and the data (if it worked). This pattern of <code>(err, data)</code> as the callback signature is called "error-first callbacks" and it is everywhere in older Node code.</p>
<p>The <code>if (err)</code> check at the top is not optional. If you skip it and the file does not exist, <code>data</code> is <code>undefined</code> and your code breaks in a confusing way downstream instead of failing clearly right where the problem is.</p>
<p>Let me walk through the flow of what actually happens at runtime:</p>
<pre><code class="language-plaintext">1. Node sees fs.readFile() — sends read request to OS, moves on
2. Node sees console.log('This prints first') — prints it
3. Node has nothing left to do — enters the event loop, waits
4. OS finishes reading the file — puts callback in the queue
5. Event loop picks up the callback — runs your function
6. console.log(data) prints the file contents
</code></pre>
<p>The event loop is the mechanism that checks "are there any callbacks waiting to run?" in a continuous cycle. When your main code finishes executing, the event loop takes over and processes whatever I/O results have come back.</p>
<hr />
<h2>The staircase problem</h2>
<p>One callback is fine. Two nested callbacks are tolerable. Three gets uncomfortable. The task I mentioned at the start, reading three files and combining them, looks like this with callbacks:</p>
<pre><code class="language-js">const fs = require('fs');

fs.readFile('header.txt', 'utf8', (err1, header) =&gt; {
  if (err1) {
    console.error('header failed:', err1.message);
    return;
  }
  fs.readFile('body.txt', 'utf8', (err2, body) =&gt; {
    if (err2) {
      console.error('body failed:', err2.message);
      return;
    }
    fs.readFile('footer.txt', 'utf8', (err3, footer) =&gt; {
      if (err3) {
        console.error('footer failed:', err3.message);
        return;
      }
      const combined = header + '\n' + body + '\n' + footer;
      fs.writeFile('page.txt', combined, (err4) =&gt; {
        if (err4) {
          console.error('write failed:', err4.message);
          return;
        }
        console.log('page.txt written');
      });
    });
  });
});
</code></pre>
<p>Look at the shape of that code. Every step indents further right. Four levels deep for something that is, conceptually, four sequential steps. This is what people call callback hell, and the name is earned. The logic is correct but the structure makes it genuinely hard to follow, hard to modify, and hard to handle errors consistently.</p>
<p>Try adding a fifth step in the middle of that. Try moving the order of operations around. Try figuring out which closing brace belongs to which callback. I spent a while counting braces and parentheses before accepting that there had to be a better way.</p>
<p>The error handling is especially painful. Each callback checks for its own error separately. There is no single place to catch failures. If you forget the error check in one callback, that error silently vanishes and the next step runs with <code>undefined</code> data.</p>
<hr />
<h2>Promises: the better way</h2>
<p>A Promise is an object that represents a value you do not have yet but will have later. It has three states: pending (still waiting), fulfilled (got the value), or rejected (something went wrong).</p>
<pre><code class="language-plaintext">Promise states:

  PENDING ——&gt; FULFILLED (resolved with a value)
      |
      +——&gt; REJECTED (failed with an error)

Once settled (fulfilled or rejected), a Promise never changes state again.
</code></pre>
<p>Node's <code>fs</code> module has a promise-based version built in. You get it from <code>fs/promises</code>:</p>
<pre><code class="language-js">const fs = require('fs/promises');

fs.readFile('greeting.txt', 'utf8')
  .then(data =&gt; {
    console.log(data);
  })
  .catch(err =&gt; {
    console.error('Failed:', err.message);
  });
</code></pre>
<p>No callback argument. <code>readFile</code> returns a Promise. You call <code>.then()</code> on it to say what happens when it succeeds, and <code>.catch()</code> to say what happens when it fails. The error handling is in one place instead of scattered inside every callback.</p>
<p>Now here is the same three-file task with promises:</p>
<pre><code class="language-js">const fs = require('fs/promises');

let headerText, bodyText;

fs.readFile('header.txt', 'utf8')
  .then(header =&gt; {
    headerText = header;
    return fs.readFile('body.txt', 'utf8');
  })
  .then(body =&gt; {
    bodyText = body;
    return fs.readFile('footer.txt', 'utf8');
  })
  .then(footer =&gt; {
    const combined = headerText + '\n' + bodyText + '\n' + footer;
    return fs.writeFile('page.txt', combined);
  })
  .then(() =&gt; {
    console.log('page.txt written');
  })
  .catch(err =&gt; {
    console.error('Something failed:', err.message);
  });
</code></pre>
<p>No staircase. The code reads top to bottom. Each <code>.then()</code> returns a new promise, so they chain instead of nest. And the <code>.catch()</code> at the bottom handles errors from any step in the chain. If <code>body.txt</code> fails to read, the chain skips straight to <code>.catch()</code> and you get one clear error message.</p>
<p>This is the real win. One <code>.catch()</code> at the end replaces four separate <code>if (err)</code> checks.</p>
<p>But we can do better. If the three files do not depend on each other, there is no reason to read them one after another. <code>Promise.all</code> runs them at the same time:</p>
<pre><code class="language-js">const fs = require('fs/promises');

Promise.all([
  fs.readFile('header.txt', 'utf8'),
  fs.readFile('body.txt', 'utf8'),
  fs.readFile('footer.txt', 'utf8'),
])
  .then(([header, body, footer]) =&gt; {
    return fs.writeFile('page.txt', header + '\n' + body + '\n' + footer);
  })
  .then(() =&gt; console.log('page.txt written'))
  .catch(err =&gt; console.error('Failed:', err.message));
</code></pre>
<p>Three file reads happening in parallel, results collected into an array in the same order you passed them. If any one fails, the whole thing goes to <code>.catch()</code>. This is both shorter and faster than the sequential version.</p>
<hr />
<h2>Making your own promises</h2>
<p>Sometimes you are working with older libraries that only support callbacks. You can wrap them in a promise yourself:</p>
<pre><code class="language-js">function readFilePromise(path) {
  return new Promise((resolve, reject) =&gt; {
    const fs = require('fs');
    fs.readFile(path, 'utf8', (err, data) =&gt; {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

readFilePromise('greeting.txt')
  .then(data =&gt; console.log(data))
  .catch(err =&gt; console.error(err.message));
</code></pre>
<p>The <code>new Promise()</code> constructor takes a function with two arguments: <code>resolve</code> (call this with the value when it works) and <code>reject</code> (call this with the error when it fails). Once you call either one, the promise settles and <code>.then()</code> or <code>.catch()</code> fires accordingly.</p>
<p>Node also has <code>util.promisify</code> which does this wrapping for you on any function that follows the error-first callback pattern:</p>
<pre><code class="language-js">const { promisify } = require('util');
const fs = require('fs');

const readFile = promisify(fs.readFile);

readFile('greeting.txt', 'utf8')
  .then(data =&gt; console.log(data))
  .catch(err =&gt; console.error(err.message));
</code></pre>
<p>One line to convert any callback-based function into a promise-based one. This is how a lot of codebases transitioned from callbacks to promises without rewriting everything.</p>
<hr />
<h2>A few things that tripped me up</h2>
<p>Forgetting to return inside <code>.then()</code>. If you start an async operation inside a <code>.then()</code> block but do not <code>return</code> it, the chain does not wait for it. The next <code>.then()</code> fires immediately with <code>undefined</code>. This is a quiet bug that does not throw errors, it just produces wrong results.</p>
<pre><code class="language-js">// broken — missing return
.then(header =&gt; {
  fs.readFile('body.txt', 'utf8'); // floats away, nobody waits for it
})
.then(body =&gt; {
  console.log(body); // undefined
})
</code></pre>
<pre><code class="language-js">// fixed
.then(header =&gt; {
  return fs.readFile('body.txt', 'utf8');
})
.then(body =&gt; {
  console.log(body); // actual file contents
})
</code></pre>
<p>The other thing: <code>.catch()</code> only catches errors from promises above it in the chain. If you put <code>.catch()</code> in the middle and then add more <code>.then()</code> calls after it, the chain continues after the catch. Sometimes that is what you want. Usually it is not, and having <code>.catch()</code> at the very end is the safer default.</p>
<hr />
<h2>What to actually use in 2025</h2>
<p>If you are writing new code, use <code>async/await</code>. It is built on top of promises and reads like synchronous code. But you need to understand promises first because <code>async/await</code> is just syntax over them. When you see a confusing <code>await</code> error or need to run things in parallel with <code>Promise.all</code>, the promise knowledge is what lets you debug it.</p>
<p>For reference, here is the same three-file task with <code>async/await</code> as a preview:</p>
<pre><code class="language-js">const fs = require('fs/promises');

async function buildPage() {
  try {
    const [header, body, footer] = await Promise.all([
      fs.readFile('header.txt', 'utf8'),
      fs.readFile('body.txt', 'utf8'),
      fs.readFile('footer.txt', 'utf8'),
    ]);
    await fs.writeFile('page.txt', header + '\n' + body + '\n' + footer);
    console.log('page.txt written');
  } catch (err) {
    console.error('Failed:', err.message);
  }
}

buildPage();
</code></pre>
<p>That reads like normal code. No <code>.then()</code> chains, no nesting, <code>try/catch</code> instead of <code>.catch()</code>. But under the hood, every <code>await</code> is a promise. The next post will break that apart properly.</p>
<hr />
<h2>References</h2>
<ul>
<li><p><a href="https://nodejs.org/api/fs.html#fspromisesreadfilepath-options">Node.js docs: fs/promises</a></p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">MDN: Promise</a></p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises">MDN: Using promises</a></p>
</li>
<li><p><a href="https://nodejs.org/api/util.html#utilpromisifyoriginal">Node.js docs: util.promisify</a></p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all">MDN: Promise.all</a></p>
</li>
<li><p><a href="https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick">Node.js event loop explained</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[REST APIs: What Your Frontend Is Actually Saying to Your Backend]]></title><description><![CDATA[I spent an embarrassing amount of time building my first full-stack app before I understood what was happening between the browser and the server. I had a React frontend. I had an Express backend. The]]></description><link>https://blog.saumyagrawal.in/rest-apis-what-your-frontend-is-actually-saying-to-your-backend</link><guid isPermaLink="true">https://blog.saumyagrawal.in/rest-apis-what-your-frontend-is-actually-saying-to-your-backend</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[REST API]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Saumya]]></dc:creator><pubDate>Tue, 05 May 2026 14:09:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/c1d21662-3444-406b-a39e-5d8f3c91588a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I spent an embarrassing amount of time building my first full-stack app before I understood what was happening between the browser and the server. I had a React frontend. I had an Express backend. They talked to each other. But if you had asked me <em>how</em> they talked, or what the rules were, I would have mumbled something about fetch and JSON and changed the subject.</p>
<p>Then someone explained REST to me in about ten minutes, and suddenly half the confusing stuff I had been copying from tutorials made sense. So that is what this post is.</p>
<hr />
<h2>APIs are just agreements</h2>
<p>When your frontend needs data, it sends a request to your backend. The backend sends a response. That exchange is an API call. API stands for Application Programming Interface, which sounds complicated but the idea is boring: two programs agreed on how to talk to each other.</p>
<p>Think of it like ordering food. You do not walk into the kitchen and start cooking. You talk to the waiter. You say what you want in a format they understand (the menu), they bring back what you ordered. The waiter is the API. The kitchen is the server. You are the client.</p>
<p>REST is one specific set of rules for how that conversation should work. It stands for Representational State Transfer. The name is not helpful. What matters is the pattern: you use standard HTTP methods (GET, POST, PUT, DELETE) to perform actions on resources, and the server responds with data and a status code telling you what happened.</p>
<p>Most APIs you will use as a beginner follow REST conventions. Once you learn the pattern, every new API you encounter feels familiar.</p>
<hr />
<h2>Resources: the nouns of your API</h2>
<p>In REST, everything revolves around resources. A resource is any piece of data your application manages. Users, posts, comments, products, orders. Each one is a resource.</p>
<p>The way you identify a resource is through its URL (or more formally, its URI). And the naming convention is simple: use nouns, not verbs. Plural nouns.</p>
<pre><code class="language-plaintext">/users          (the whole collection of users)
/users/42       (one specific user, ID 42)
/posts          (all posts)
/posts/7        (post number 7)
</code></pre>
<p>You do not put actions in the URL. No <code>/getUser</code>, no <code>/deletePost</code>, no <code>/createNewComment</code>. The HTTP method (GET, POST, PUT, DELETE) already tells the server what action to perform. The URL just says <em>what thing</em> you are acting on.</p>
<p>This is the part that clicked for me late. I kept naming routes like <code>/api/getUserById</code> and <code>/api/createUser</code> because that felt natural. It works, but it falls apart once your app has 30 endpoints and none of them follow a pattern. REST gives you the pattern.</p>
<hr />
<h2>HTTP methods: the verbs</h2>
<p>There are four methods you will use constantly. Each one maps to a CRUD operation (Create, Read, Update, Delete), which is the set of basic actions almost every application needs.</p>
<table>
<thead>
<tr>
<th>HTTP Method</th>
<th>CRUD Operation</th>
<th>What it does</th>
<th>Example</th>
</tr>
</thead>
<tbody><tr>
<td>GET</td>
<td>Read</td>
<td>Fetch data, change nothing</td>
<td>GET /users</td>
</tr>
<tr>
<td>POST</td>
<td>Create</td>
<td>Send new data to the server</td>
<td>POST /users</td>
</tr>
<tr>
<td>PUT</td>
<td>Update</td>
<td>Replace existing data</td>
<td>PUT /users/42</td>
</tr>
<tr>
<td>DELETE</td>
<td>Delete</td>
<td>Remove data</td>
<td>DELETE /users/42</td>
</tr>
</tbody></table>
<p>That is the whole mapping. Four methods, four operations. Let me walk through each one with actual Express code, because seeing the request and response together is what made this real for me.</p>
<h3>GET: give me the data</h3>
<p>GET requests fetch data. They should never change anything on the server. If a GET request modifies data, something is wrong with your design. This is called being "safe" in REST terminology.</p>
<pre><code class="language-js">// GET /users - fetch all users
app.get('/users', async (req, res) =&gt; {
  const users = await User.find();
  res.status(200).json(users);
});

// GET /users/42 - fetch one user
app.get('/users/:id', async (req, res) =&gt; {
  const user = await User.findById(req.params.id);
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.status(200).json(user);
});
</code></pre>
<p>The response for a list of users looks like:</p>
<pre><code class="language-json">[
  { "_id": "abc123", "name": "Priya", "email": "priya@example.com" },
  { "_id": "def456", "name": "Ravi", "email": "ravi@example.com" }
]
</code></pre>
<p>Notice the route <code>/users/:id</code>. The colon means <code>id</code> is a parameter. When someone hits <code>/users/42</code>, Express puts <code>"42"</code> into <code>req.params.id</code>. You use that to look up one specific resource.</p>
<h3>POST: create something new</h3>
<p>POST sends data to the server to create a new resource. The data goes in the request body, not the URL.</p>
<pre><code class="language-js">// POST /users - create a new user
app.post('/users', async (req, res) =&gt; {
  try {
    const user = await User.create(req.body);
    res.status(201).json(user);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});
</code></pre>
<p>The request body (what the client sends) would look like:</p>
<pre><code class="language-json">{
  "name": "Meera",
  "email": "meera@example.com",
  "age": 24
}
</code></pre>
<p>And the response comes back with the created user, including the <code>_id</code> that MongoDB generated:</p>
<pre><code class="language-json">{
  "_id": "ghi789",
  "name": "Meera",
  "email": "meera@example.com",
  "age": 24,
  "createdAt": "2025-01-15T10:30:00Z"
}
</code></pre>
<p>Status 201 means "created." Not 200. This is a small detail that a lot of beginners skip, and it matters because status codes tell the client exactly what happened without parsing the response body.</p>
<h3>PUT: replace something that exists</h3>
<p>PUT updates an existing resource. You send the full updated version, and the server replaces what was there before.</p>
<pre><code class="language-js">// PUT /users/42 - update user 42
app.put('/users/:id', async (req, res) =&gt; {
  const user = await User.findByIdAndUpdate(
    req.params.id,
    req.body,
    { new: true, runValidators: true }
  );
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.status(200).json(user);
});
</code></pre>
<p><code>{ new: true }</code> tells Mongoose to return the document <em>after</em> the update, not before. Without it you get back the old version, which is confusing the first time it happens.</p>
<p>There is also PATCH, which updates only specific fields instead of replacing the whole document. In practice, many APIs use PUT and PATCH interchangeably, which is technically wrong but extremely common. When you are starting out, PUT is fine. You can worry about the distinction later.</p>
<h3>DELETE: remove something</h3>
<p>DELETE removes a resource. The response usually confirms what was deleted or just returns an empty success.</p>
<pre><code class="language-js">// DELETE /users/42 - delete user 42
app.delete('/users/:id', async (req, res) =&gt; {
  const user = await User.findByIdAndDelete(req.params.id);
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.status(200).json({ message: 'User deleted' });
});
</code></pre>
<p>Some APIs return 204 (No Content) with an empty body on successful deletion. Either approach is fine as long as you pick one and stick with it across your whole API.</p>
<hr />
<h2>Status codes: what the numbers mean</h2>
<p>You have already seen a few of these. Status codes are three-digit numbers the server sends back to tell the client how things went. You do not need to memorize all of them. The groupings are what matter.</p>
<table>
<thead>
<tr>
<th>Range</th>
<th>Category</th>
<th>Meaning</th>
</tr>
</thead>
<tbody><tr>
<td>2xx</td>
<td>Success</td>
<td>The request worked</td>
</tr>
<tr>
<td>4xx</td>
<td>Client error</td>
<td>The request had a problem (your fault)</td>
</tr>
<tr>
<td>5xx</td>
<td>Server error</td>
<td>The server broke while handling it (our fault)</td>
</tr>
</tbody></table>
<p>The ones you will use in almost every project:</p>
<p><strong>200</strong> means OK. The request succeeded. Used for GET, PUT, and DELETE responses.</p>
<p><strong>201</strong> means Created. A new resource was made. Used after a successful POST.</p>
<p><strong>400</strong> means Bad Request. The client sent something invalid, like missing required fields or a malformed email.</p>
<p><strong>401</strong> means Unauthorized. The client is not logged in or the auth token is missing.</p>
<p><strong>403</strong> means Forbidden. The client is logged in but does not have permission for this action.</p>
<p><strong>404</strong> means Not Found. The resource does not exist at that URL.</p>
<p><strong>500</strong> means Internal Server Error. Something crashed on the server. The client did nothing wrong.</p>
<p>A quick way to think about 401 vs 403: 401 is "who are you?" and 403 is "I know who you are, and no."</p>
<hr />
<h2>Route design: putting it together</h2>
<p>Here is the full set of routes for a users resource, laid out the way a REST API expects them:</p>
<pre><code class="language-plaintext">GET    /api/users          Fetch all users
GET    /api/users/:id      Fetch one user by ID
POST   /api/users          Create a new user
PUT    /api/users/:id      Update a user by ID
DELETE /api/users/:id      Delete a user by ID
</code></pre>
<p>Five routes. One resource. Consistent naming. If you add a posts resource later, the pattern is identical:</p>
<pre><code class="language-plaintext">GET    /api/posts
GET    /api/posts/:id
POST   /api/posts
PUT    /api/posts/:id
DELETE /api/posts/:id
</code></pre>
<p>You can nest resources too. If posts belong to users:</p>
<pre><code class="language-plaintext">GET    /api/users/:userId/posts       All posts by user
POST   /api/users/:userId/posts       Create a post for a user
</code></pre>
<p>Keep nesting to one level deep. <code>/api/users/:userId/posts/:postId/comments/:commentId</code> is technically valid but miserable to work with. If you find yourself going three levels deep, flatten it out.</p>
<hr />
<h2>A complete working example</h2>
<p>This is all five user routes in one file, using Express and Mongoose, with proper status codes and error handling. You can drop this into a project and it works.</p>
<pre><code class="language-js">const express = require('express');
const router = express.Router();
const User = require('../models/User');

// GET /api/users
router.get('/', async (req, res) =&gt; {
  try {
    const users = await User.find().select('name email role');
    res.status(200).json(users);
  } catch (err) {
    res.status(500).json({ error: 'Failed to fetch users' });
  }
});

// GET /api/users/:id
router.get('/:id', async (req, res) =&gt; {
  try {
    const user = await User.findById(req.params.id);
    if (!user) return res.status(404).json({ error: 'User not found' });
    res.status(200).json(user);
  } catch (err) {
    if (err.name === 'CastError') {
      return res.status(400).json({ error: 'Invalid user ID format' });
    }
    res.status(500).json({ error: 'Failed to fetch user' });
  }
});

// POST /api/users
router.post('/', async (req, res) =&gt; {
  try {
    const user = await User.create(req.body);
    res.status(201).json(user);
  } catch (err) {
    if (err.name === 'ValidationError') {
      const messages = Object.values(err.errors).map(e =&gt; e.message);
      return res.status(400).json({ error: messages.join(', ') });
    }
    if (err.code === 11000) {
      return res.status(409).json({ error: 'Email already exists' });
    }
    res.status(500).json({ error: 'Failed to create user' });
  }
});

// PUT /api/users/:id
router.put('/:id', async (req, res) =&gt; {
  try {
    const user = await User.findByIdAndUpdate(
      req.params.id,
      req.body,
      { new: true, runValidators: true }
    );
    if (!user) return res.status(404).json({ error: 'User not found' });
    res.status(200).json(user);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// DELETE /api/users/:id
router.delete('/:id', async (req, res) =&gt; {
  try {
    const user = await User.findByIdAndDelete(req.params.id);
    if (!user) return res.status(404).json({ error: 'User not found' });
    res.status(200).json({ message: 'User deleted' });
  } catch (err) {
    res.status(500).json({ error: 'Failed to delete user' });
  }
});

module.exports = router;
</code></pre>
<p>In your main <code>server.js</code>, mount it like this:</p>
<pre><code class="language-js">const userRoutes = require('./routes/users');
app.use('/api/users', userRoutes);
</code></pre>
<p>Now every route inside the file is prefixed with <code>/api/users</code> automatically. The <code>router.get('/')</code> becomes <code>GET /api/users</code>, and <code>router.get('/:id')</code> becomes <code>GET /api/users/:id</code>. Clean.</p>
<hr />
<h2>Things I wish someone told me earlier</h2>
<p>Keep your route files separate from your logic. The route handler should be short. If your <code>POST /users</code> handler is 60 lines long with validation, database calls, email sending, and logging all mixed together, pull the logic into a controller or service function. The route file should read like a table of contents.</p>
<p>Use <code>express.json()</code> middleware. Without it, <code>req.body</code> is undefined on POST and PUT requests because Express does not parse JSON by default.</p>
<pre><code class="language-js">app.use(express.json());
</code></pre>
<p>I forgot this once and spent 40 minutes wondering why my request body was always empty. The server was fine. The database was fine. The client was sending data. Express just was not reading it.</p>
<p>Test your API with a tool like Postman, Insomnia, or even curl from the terminal. Testing through the browser only works for GET requests. You need a tool that lets you set the HTTP method, add headers, and include a JSON body.</p>
<pre><code class="language-bash"># quick test from the terminal
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Priya", "email": "priya@example.com"}'
</code></pre>
<hr />
<h2>References</h2>
<ul>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods">MDN Web Docs: HTTP request methods</a></p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status">MDN Web Docs: HTTP response status codes</a></p>
</li>
<li><p><a href="https://expressjs.com/en/guide/routing.html">Express.js routing guide</a></p>
</li>
<li><p><a href="https://stackoverflow.blog/2020/03/02/best-practices-for-rest-api-design/">REST API design best practices (Stack Overflow blog)</a></p>
</li>
<li><p><a href="https://www.postman.com/">Postman: API testing tool</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[My Server Was Freezing and I Did Not Know Why. Then I Learned What Blocking Means.]]></title><description><![CDATA[I had a Node.js server running. A simple Express app, maybe four routes. One of those routes read a JSON file from disk and sent it back as a response. Worked fine when I tested it alone. Then I opene]]></description><link>https://blog.saumyagrawal.in/my-server-was-freezing-and-i-did-not-know-why-then-i-learned-what-blocking-means</link><guid isPermaLink="true">https://blog.saumyagrawal.in/my-server-was-freezing-and-i-did-not-know-why-then-i-learned-what-blocking-means</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Saumya]]></dc:creator><pubDate>Tue, 05 May 2026 13:13:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/cfe33a5a-09d5-441e-a87d-dd9a263094a0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I had a Node.js server running. A simple Express app, maybe four routes. One of those routes read a JSON file from disk and sent it back as a response. Worked fine when I tested it alone. Then I opened two browser tabs and hit the route at the same time. The second tab waited. Not for a network reason, not because my machine was slow. It waited because the first request had not finished reading the file yet, and my server was doing absolutely nothing else in the meantime.</p>
<p>I had written blocking code without knowing what that meant.</p>
<p>This took me an embarrassingly long time to understand, partly because every explanation I found started with the event loop and callback queues and microtask priorities. I did not need any of that yet. I needed to understand one thing: when your code blocks, your entire server stops.</p>
<hr />
<h2>What blocking code actually looks like</h2>
<p>Blocking means the program stops on one line and refuses to move to the next line until that operation finishes. The CPU sits there, waiting. Nothing else runs.</p>
<p>Here is the Node.js version of shooting yourself in the foot:</p>
<pre><code class="language-js">const fs = require('fs');

console.log('before reading file');

const data = fs.readFileSync('./bigfile.json', 'utf-8');

console.log('after reading file');
console.log('doing other stuff');
</code></pre>
<p>Output:</p>
<pre><code class="language-plaintext">before reading file
// ... long pause while the file loads ...
after reading file
doing other stuff
</code></pre>
<p><code>readFileSync</code>. That <code>Sync</code> at the end is the giveaway. It means synchronous. The program reaches that line, stops everything, waits for the entire file to load into memory, and only then moves on. If that file is 500MB, every line of code below it waits for 500MB to finish loading. If another user hits your server during that wait, they get nothing. The server is busy staring at a hard drive.</p>
<p>For a script you run once on your own machine, this is fine. For a server handling multiple users, it is a problem.</p>
<hr />
<h2>The restaurant analogy</h2>
<p>Think about a restaurant with one waiter. A customer orders a steak. The waiter walks to the kitchen, hands in the order, and then stands there watching the chef cook. Does not take any other orders, does not refill anyone's water, does not greet new customers walking in. Just stands at the kitchen window, arms crossed, waiting for the steak.</p>
<p>That is blocking.</p>
<p>Now picture the same waiter, same restaurant. Customer orders a steak. The waiter hands the order to the kitchen and immediately walks back to the floor. Takes another order. Refills a drink. Seats a new table. When the kitchen rings the bell to say the steak is ready, the waiter picks it up and delivers it.</p>
<p>That is non-blocking.</p>
<p>The kitchen still takes the same amount of time to cook. The steak is not faster. But the waiter is not frozen in place while waiting for it, so every other customer in the restaurant gets served in the meantime.</p>
<p>Node.js is that single waiter. It has one thread. If you make it stand around waiting for a file to load or a database to respond, nobody else gets served.</p>
<hr />
<h2>What non-blocking code looks like</h2>
<p>The non-blocking version of the same file read uses a callback:</p>
<pre><code class="language-js">const fs = require('fs');

console.log('before reading file');

fs.readFile('./bigfile.json', 'utf-8', (err, data) =&gt; {
  if (err) {
    console.error('read failed:', err.message);
    return;
  }
  console.log('file loaded, length:', data.length);
});

console.log('not waiting around');
console.log('doing other stuff');
</code></pre>
<p>Output:</p>
<pre><code class="language-plaintext">before reading file
not waiting around
doing other stuff
file loaded, length: 4893722
</code></pre>
<p>Look at the order. "not waiting around" prints before the file finishes loading. The program did not stop. It told the operating system "read this file and call me back when you are done," then immediately moved to the next line. When the file finished loading, the callback function ran.</p>
<p>The file still took the same amount of time to read. But the rest of the program did not freeze while it happened.</p>
<hr />
<h2>Why this matters for servers</h2>
<p>Here is where it gets real. Put blocking code inside a route handler and watch what happens to every other request.</p>
<pre><code class="language-js">const express = require('express');
const fs = require('fs');
const app = express();

// bad: blocking route
app.get('/data', (req, res) =&gt; {
  const data = fs.readFileSync('./bigfile.json', 'utf-8');
  res.json(JSON.parse(data));
});

// this route works fine on its own
app.get('/health', (req, res) =&gt; {
  res.json({ status: 'ok' });
});

app.listen(3000);
</code></pre>
<p>If someone hits <code>/data</code> and the file takes 3 seconds to read, every request to <code>/health</code> during those 3 seconds also waits 3 seconds. The health check has nothing to do with the file. It should return instantly. But Node.js has one thread, and that thread is stuck on <code>readFileSync</code>. Everybody waits.</p>
<p>The fix:</p>
<pre><code class="language-js">// good: non-blocking route
app.get('/data', async (req, res) =&gt; {
  try {
    const data = await fs.promises.readFile('./bigfile.json', 'utf-8');
    res.json(JSON.parse(data));
  } catch (err) {
    res.status(500).json({ error: 'could not read file' });
  }
});
</code></pre>
<p>Same result. Same file. But now while the file loads, Node.js goes back to handling other requests. The <code>/health</code> route responds instantly regardless of what <code>/data</code> is doing.</p>
<hr />
<h2>The timeline, visually</h2>
<p>Here is what happens with blocking code when two requests come in:</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/3f48af2e-269f-42a6-99df-43d932bfcdf9.png" alt="" style="display:block;margin:0 auto" />

<p>User2 asked for a health check. A response that takes 0 milliseconds to generate. They waited 3 seconds because the server was stuck reading a file for someone else.</p>
<p>Now the non-blocking version:</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/40722cda-581a-4ebc-bc51-85469a60fae9.png" alt="" style="display:block;margin:0 auto" />

<p>User2 gets their response immediately. User1 still waits 3 seconds for the file, because the file is still big. But they are no longer holding the entire server hostage.</p>
<hr />
<h2>Callbacks, promises, async/await</h2>
<p>Node.js has gone through three eras of writing non-blocking code. All three still work. You will see all three in other people's codebases.</p>
<p>Callbacks came first. You pass a function that runs when the async work finishes:</p>
<pre><code class="language-js">fs.readFile('./config.json', 'utf-8', (err, data) =&gt; {
  if (err) {
    console.error(err);
    return;
  }
  console.log(data);
});
</code></pre>
<p>The problem with callbacks shows up when you need to do three async things in sequence. Read a file, then use its contents to query a database, then use that result to send an email. Each step nests inside the previous callback. The code drifts to the right and becomes genuinely hard to follow. People call this callback hell, and the name is earned.</p>
<pre><code class="language-js">// callback hell
fs.readFile('./config.json', 'utf-8', (err, config) =&gt; {
  if (err) return handleError(err);
  db.query(config.query, (err, rows) =&gt; {
    if (err) return handleError(err);
    sendEmail(rows[0].email, 'hello', (err, result) =&gt; {
      if (err) return handleError(err);
      console.log('done');
    });
  });
});
</code></pre>
<p>Promises fixed the nesting:</p>
<pre><code class="language-js">fs.promises.readFile('./config.json', 'utf-8')
  .then(config =&gt; db.query(JSON.parse(config).query))
  .then(rows =&gt; sendEmail(rows[0].email, 'hello'))
  .then(result =&gt; console.log('done'))
  .catch(err =&gt; handleError(err));
</code></pre>
<p>Flat. Each <code>.then()</code> returns a new promise. Errors fall through to <code>.catch()</code>. Easier to read, easier to reason about.</p>
<p>Async/await made it look like regular code:</p>
<pre><code class="language-js">async function handleRequest() {
  try {
    const raw = await fs.promises.readFile('./config.json', 'utf-8');
    const config = JSON.parse(raw);
    const rows = await db.query(config.query);
    await sendEmail(rows[0].email, 'hello');
    console.log('done');
  } catch (err) {
    handleError(err);
  }
}
</code></pre>
<p>This is still non-blocking. The <code>await</code> keyword does not freeze the server the way <code>readFileSync</code> does. When execution hits <code>await</code>, Node.js pauses that specific function and goes to handle other work. When the awaited operation finishes, it comes back and picks up where it left off. Other requests keep flowing.</p>
<p>If you are starting fresh, use async/await. It reads like sequential code but behaves like non-blocking code. The callbacks and promise chains are worth recognizing because you will run into them in older projects, but for new code, async/await is the standard.</p>
<hr />
<h2>A real-world example: database calls</h2>
<p>File reading is one thing, but the same blocking problem applies to anything that takes time. Database queries are the most common culprit in real applications.</p>
<pre><code class="language-js">// imagine a route that fetches user data and their orders
app.get('/user/:id/summary', async (req, res) =&gt; {
  try {
    const user = await User.findById(req.params.id);
    const orders = await Order.find({ user: req.params.id }).limit(10);

    res.json({
      name: user.name,
      email: user.email,
      recentOrders: orders,
    });
  } catch (err) {
    res.status(500).json({ error: 'something broke' });
  }
});
</code></pre>
<p>Both <code>findById</code> and <code>find</code> are async. While MongoDB processes the query, Node.js handles other requests. No one is stuck waiting.</p>
<p>But notice something: the user query finishes before the orders query starts. They do not depend on each other. You could run them at the same time:</p>
<pre><code class="language-js">app.get('/user/:id/summary', async (req, res) =&gt; {
  try {
    const [user, orders] = await Promise.all([
      User.findById(req.params.id),
      Order.find({ user: req.params.id }).limit(10),
    ]);

    res.json({
      name: user.name,
      email: user.email,
      recentOrders: orders,
    });
  } catch (err) {
    res.status(500).json({ error: 'something broke' });
  }
});
</code></pre>
<p><code>Promise.all</code> fires both queries at the same time and waits for both to finish. If the user query takes 50ms and the orders query takes 80ms, the total wait is 80ms instead of 130ms. For two queries the difference is small. For five or six independent async operations, it adds up fast.</p>
<hr />
<h2>Common mistakes and how to avoid them</h2>
<p>I am putting these here because I made every single one of them.</p>
<p><strong>Using Sync methods in a server.</strong> Any function ending in <code>Sync</code> blocks the entire process. <code>readFileSync</code>, <code>writeFileSync</code>, <code>execSync</code>. These are fine in a CLI script or a build tool. They have no place in a server handling requests. If you see <code>Sync</code> in server code, replace it with the async version.</p>
<p><strong>Forgetting try/catch around await.</strong> An unhandled promise rejection used to just log a warning. In newer versions of Node.js it crashes the process. Wrap your awaits in try/catch or use an error-handling middleware in Express.</p>
<p><strong>Running independent async operations in sequence when they could run in parallel.</strong> If operation B does not depend on the result of operation A, use <code>Promise.all</code>. The code runs faster with no extra complexity.</p>
<p><strong>Not understanding that</strong> <code>await</code> <strong>pauses the function, not the server.</strong> I see beginners avoid <code>await</code> because they think it "blocks." It does not. It pauses that one function and frees the thread for everything else. That is the whole point. Use it.</p>
<hr />
<h2>What to read next</h2>
<p>If you want to go deeper on how Node.js actually handles all of this under the hood, here are solid resources.</p>
<ul>
<li><p><a href="https://nodejs.org/en/learn/asynchronous-work/overview-of-blocking-vs-non-blocking">Node.js docs: blocking vs non-blocking</a> gives the official explanation with examples</p>
</li>
<li><p><a href="https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick">Node.js docs: the event loop</a> covers what happens behind the scenes when you write async code</p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Async_JS/Promises">MDN: async/await</a> walks through promises and async/await with examples</p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises">MDN: using promises</a> if you want to understand the promise chain pattern</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Sessions, Cookies, and JWTs: How I Stopped Confusing Authentication With Identity]]></title><description><![CDATA[I spent an embarrassing amount of time thinking authentication was just "checking if the password is correct." It is not. Checking the password is one moment. Authentication is the ongoing question: h]]></description><link>https://blog.saumyagrawal.in/sessions-cookies-and-jwts-how-i-stopped-confusing-authentication-with-identity</link><guid isPermaLink="true">https://blog.saumyagrawal.in/sessions-cookies-and-jwts-how-i-stopped-confusing-authentication-with-identity</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[JWT]]></category><category><![CDATA[authentication]]></category><dc:creator><![CDATA[Saumya]]></dc:creator><pubDate>Mon, 04 May 2026 23:48:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/d9e2d64d-9504-4cc2-ab37-a3d9adfeca2e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I spent an embarrassing amount of time thinking authentication was just "checking if the password is correct." It is not. Checking the password is one moment. Authentication is the ongoing question: how does the server know who you are on the second request, the third, the fiftieth?</p>
<p>HTTP has no memory. Every request your browser sends is a stranger knocking on a door. The server does not know if this is the same person who logged in ten seconds ago or someone else entirely. The protocol was built in 1991 to serve documents, not to remember people. That design choice is the reason sessions, cookies, and tokens exist.</p>
<p>The real question is not "how do I check a password." It is: after I check the password once, how do I avoid asking again on every single request?</p>
<pre><code class="language-plaintext">Browser                              Server
  |                                    |
  |--- POST /login (email, pass) -----&gt;|
  |                                    | checks password... correct
  |&lt;--- 200 OK, here is your data -----|
  |                                    |
  |--- GET /dashboard ---------------&gt;|
  |                                    | who is this? no idea.
  |&lt;--- 401 Unauthorized --------------|
</code></pre>
<p>The login worked. The very next request fails because HTTP forgets.</p>
<p>Two families of solutions exist. One says the server remembers you. The other says the server gives you a signed note that proves who you are, and you carry it around. That is the session vs JWT split, and everything else is details.</p>
<hr />
<h2>Cookies: the envelope, not the letter</h2>
<p>People mix up cookies with sessions constantly. A cookie is just a transport mechanism. It is a small piece of text that a server sends to a browser, and the browser automatically sends it back on every future request to that server. The cookie does not know what it is carrying. It could hold a session ID, a JWT, or your preferred theme color.</p>
<pre><code class="language-js">res.cookie('sid', 'abc123', {
  httpOnly: true,   // JavaScript cannot read this cookie
  secure: true,     // only sent over HTTPS
  sameSite: 'lax',  // restricts cross-site sending
  maxAge: 3600000,  // 1 hour
});
</code></pre>
<p><code>httpOnly</code> is the flag that matters most. It hides the cookie from all JavaScript on the page, including malicious scripts injected through XSS attacks. The cookie only travels in HTTP headers, invisible to <code>document.cookie</code>.</p>
<p>Cookies are not an authentication method. They are an envelope. The question is what you put inside.</p>
<hr />
<h2>Sessions: the server remembers you</h2>
<p>Session auth works like a valet parking. You walk in, hand over your car keys, get a numbered ticket. The cR KEYS stays with them. When you show the ticket, they find your car. When you leave, you hand back the ticket and they forget about you.</p>
<p>Replace "car key" with "your user data" and "ticket" with "session ID."</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/3bd9157f-5711-4fd5-bf41-fcd098673a56.png" alt="" style="display:block;margin:0 auto" />

<p>In Express:</p>
<pre><code class="language-js">const session = require('express-session');

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 24 * 60 * 60 * 1000,
  },
}));

app.post('/login', async (req, res) =&gt; {
  const user = await User.findOne({ email: req.body.email }).select('+password');
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });

  const match = await user.comparePassword(req.body.password);
  if (!match) return res.status(401).json({ error: 'Invalid credentials' });

  req.session.userId = user._id;
  req.session.role = user.role;
  res.json({ name: user.name, email: user.email });
});

app.post('/logout', (req, res) =&gt; {
  req.session.destroy(() =&gt; {
    res.clearCookie('connect.sid');
    res.json({ message: 'Logged out' });
  });
});
</code></pre>
<p>The browser never sees or stores user data. It holds a meaningless session ID string. The server holds everything else. And because the server holds it, the server can delete it at any time. User changes their password? Destroy the session. Suspicious login? Destroy it. That instant revocation is the main thing sessions give you.</p>
<hr />
<h2>JWTs</h2>
<p>JWT (JSON Web Token) works differently. Instead of keeping your data and giving you a reference to it, the server writes your data into a token, signs it with a secret key, and hands it to you. On every request, you show the token. The server verifies the signature, reads the data out of the token, and never looks anything up.</p>
<p>Think of it as a stamped letter of introduction. Someone writes "This is Priya, she is a customer, valid until 3pm," stamps it with an official seal, and gives it to her. Anyone who recognizes the seal trusts the letter without calling anyone.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/38755f55-8604-4cfa-aee6-86257e7b39c0.png" alt="" style="display:block;margin:0 auto" />

<p>A JWT has three parts separated by dots: header, payload, signature.</p>
<pre><code class="language-js">// Payload (base64 decoded, readable by anyone)
{
  "userId": 42,
  "role": "customer",
  "exp": 1735086400   // expiry timestamp
}
</code></pre>
<p>The payload is not encrypted. Anyone can decode it. The signature does not hide the data. It prevents tampering. If someone changes <code>role</code> from <code>"customer"</code> to <code>"admin"</code>, the signature will not match and the server rejects the token. Do not put passwords or sensitive data in a JWT. It is signed, not sealed.</p>
<p>In Express:</p>
<pre><code class="language-js">import jwt from 'jsonwebtoken';

function generateToken(user) {
  return jwt.sign(
    { userId: user._id, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: '1h' }
  );
}

app.post('/login', async (req, res) =&gt; {
  const user = await User.findOne({ email: req.body.email }).select('+password');
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });

  const match = await user.comparePassword(req.body.password);
  if (!match) return res.status(401).json({ error: 'Invalid credentials' });

  res.json({ token: generateToken(user) });
});

function requireAuth(req, res, next) {
  const header = req.headers.authorization;
  if (!header || !header.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token' });
  }
  try {
    req.user = jwt.verify(header.split(' ')[1], process.env.JWT_SECRET);
    next();
  } catch {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
}
</code></pre>
<p>No session store. No database lookup for authentication. The server reads user data straight from the token.</p>
<p>The catch: once a JWT is issued, the server cannot take it back. There is no session to delete. The token lives until it expires. If you need to ban a user instantly, you either maintain a blacklist of revoked tokens (which re-introduces server-side state) or you accept a window where the token is still valid.</p>
<hr />
<h2>When to use which</h2>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/0b0a2591-b5ad-45c0-a771-4b576bfccea3.png" alt="" style="display:block;margin:0 auto" />

<table>
<thead>
<tr>
<th></th>
<th>Sessions</th>
<th>JWTs</th>
</tr>
</thead>
<tbody><tr>
<td>User data lives</td>
<td>On the server</td>
<td>Inside the token</td>
</tr>
<tr>
<td>Browser holds</td>
<td>Meaningless session ID</td>
<td>Readable payload + signature</td>
</tr>
<tr>
<td>Instant revocation</td>
<td>Yes, delete the session</td>
<td>No, valid until expiry</td>
</tr>
<tr>
<td>Works across services</td>
<td>Needs shared session store</td>
<td>Each service verifies independently</td>
</tr>
<tr>
<td>XSS risk</td>
<td>Lower (httpOnly cookie hides ID)</td>
<td>Higher if stored in localStorage</td>
</tr>
<tr>
<td>CSRF risk</td>
<td>Yes (cookies sent automatically)</td>
<td>No if using Authorization header</td>
</tr>
</tbody></table>
<p>Three questions I ask on every new project:</p>
<p>Is the client a browser talking to one backend? Sessions. Less code, instant revocation, <code>express-session</code> handles it.</p>
<p>Do multiple services need to verify identity independently? JWTs. No shared state between services.</p>
<p>Is instant revocation non-negotiable? Sessions. JWT blacklisting works but you lose the stateless advantage.</p>
<p>For the food delivery app in this series, sessions win. One backend, browser clients, and the ability to kill sessions immediately when a password changes. If I were building an API consumed by a mobile app and three microservices, JWTs.</p>
<hr />
<h2>Mistakes to avoid regardless of which you pick</h2>
<p>Use HTTPS in production. Without it, anyone on the same wifi can copy your cookie or token in transit. Free certificates from <a href="https://letsencrypt.org/">Let's Encrypt</a> have existed for years.</p>
<p>Hash passwords with bcrypt.</p>
<p>If using JWTs in a browser, put them in <code>httpOnly</code> cookies, not <code>localStorage</code>. Most tutorials show <code>localStorage</code> because it is fewer lines. It is also readable by any JavaScript on the page, including injected scripts.</p>
<p>Keep JWT payloads small. User ID and role. Not the full user profile.</p>
<p>Do not put secrets in your Git repo. Use <code>.env</code> files and add them to <code>.gitignore</code>.</p>
<hr />
<h2>References</h2>
<ul>
<li><p><a href="https://www.npmjs.com/package/express-session">express-session on npm</a></p>
</li>
<li><p><a href="https://www.npmjs.com/package/jsonwebtoken">jsonwebtoken on npm</a></p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies">MDN: HTTP cookies</a></p>
</li>
<li><p><a href="https://jwt.io/">JWT.io: decode and inspect tokens</a></p>
</li>
<li><p><a href="https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html">OWASP: Session Management Cheat Sheet</a></p>
</li>
<li><p><a href="https://letsencrypt.org/">Let's Encrypt: free HTTPS certificates</a></p>
</li>
</ul>
<hr />
<p><em>Next: middleware. What it actually is, how Express processes requests through a chain, and why the order of</em> <code>app.use()</code> <em>calls matters more than you think.</em></p>
]]></content:encoded></item><item><title><![CDATA[The Event Loop: Why Your Node.js Code Doesn't Just Run Top to Bottom]]></title><description><![CDATA[When I first started writing Node.js, I ran into something that genuinely confused me for a couple of days. I wrote this:
console.log("1. start");

setTimeout(() => {
  console.log("2. inside timeout"]]></description><link>https://blog.saumyagrawal.in/the-event-loop-why-your-node-js-code-doesn-t-just-run-top-to-bottom</link><guid isPermaLink="true">https://blog.saumyagrawal.in/the-event-loop-why-your-node-js-code-doesn-t-just-run-top-to-bottom</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[Event Loop]]></category><dc:creator><![CDATA[Saumya]]></dc:creator><pubDate>Mon, 04 May 2026 23:21:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/59175d6c-5640-409a-a530-75a3a24f22ed.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I first started writing Node.js, I ran into something that genuinely confused me for a couple of days. I wrote this:</p>
<pre><code class="language-javascript">console.log("1. start");

setTimeout(() =&gt; {
  console.log("2. inside timeout");
}, 0);

console.log("3. end");
</code></pre>
<p>I expected it to print <code>1</code>, then <code>2</code>, then <code>3</code>. The timeout was zero milliseconds. Zero. How could anything come before it?</p>
<p>Output:</p>
<pre><code class="language-plaintext">1. start
3. end
2. inside timeout
</code></pre>
<p>This confused me, I even checked if I had written the numbers wrong. The thing is, JavaScript is not broken here. This is exactly how it's supposed to work, and once you understand why, a huge chunk of async confusion just disappears.</p>
<hr />
<h2>One thing at a time for One thread</h2>
<p>JavaScript runs on a single thread. There's one worker, and it can only do one job at a time. There is no parallel execution. It moves to next after it completes the current task.</p>
<p>This creates an obvious problem. If your code makes a network request that takes 3 seconds, does the entire program freeze for 3 seconds? In a traditional blocking model, yes. Everything stops, waits, then continues. That's fine for scripts but terrible for a web server that has to handle hundreds of requests.</p>
<p>Node.js solves this without adding more threads. The event loop is how it does that.</p>
<p>Think of it like a restaurant with one chef. The chef can't cook two dishes at the same time but they're not just standing at the stove staring at the pasta either. While the pasta boils, they chop vegetables, prep the sauce, plate a different dish. When the timer goes off, they come back to the pasta. One person, many things in progress, nothing truly blocked.</p>
<p>The event loop is the system that makes this coordination possible.</p>
<hr />
<h2>What's actually in memory when your code runs</h2>
<p>Before getting into how the event loop works, it helps to know what's happening structurally when JavaScript executes your code.</p>
<p>There are two places that matter: the call stack and the task queue.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/6d4e21c8-3dd5-4c89-9566-d8d464ead3d1.png" alt="" style="display:block;margin:0 auto" />

<p>The call stack is where your code runs. When you call a function, it gets pushed onto the stack. When it returns, it pops off. JavaScript executes whatever is at the top of the stack.</p>
<p>The task queue is a waiting area. When an async operation finishes, like a <code>setTimeout</code> triggering or a file read completing, the callback gets placed in this queue.</p>
<p>The event loop's job is one loop that repeated forever or can say it's a while true loop which check if the call stack is empty. If it is, take the first task from the queue and push it onto the stack. That's the whole mechanism.</p>
<p>This is why the timeout in the example above printed last even though it was zero milliseconds. The <code>setTimeout</code> callback didn't run immediately. It went into the queue. The event loop didn't touch the queue until <code>console.log("3. end")</code> finished and the stack became empty.</p>
<hr />
<h2>Walking through the code, step by step</h2>
<p>Let's trace exactly what happens with this code:</p>
<pre><code class="language-js">console.log("1. start");

setTimeout(() =&gt; {
  console.log("2. inside timeout");
}, 0);

console.log("3. end");
</code></pre>
<p><strong>Step 1:</strong> <code>console.log("1. start")</code> gets pushed onto the call stack and runs. Prints <code>1. start</code>. Pops off.</p>
<p><strong>Step 2:</strong> <code>setTimeout(...)</code> gets pushed onto the call stack. Node hands the timer off to the browser/OS (this part happens outside the JS engine), and the callback is registered. <code>setTimeout</code> itself pops off immediately. The JS engine didn't wait at all.</p>
<p><strong>Step 3:</strong> <code>console.log("3. end")</code> pushes onto the stack, runs, prints <code>3. end</code>, pops off.</p>
<p><strong>Step 4:</strong> The call stack is now empty. The event loop checks the task queue. The 0ms timer already fired, so the callback is sitting there waiting. The event loop pushes it onto the now-empty stack.</p>
<p><strong>Step 5:</strong> <code>console.log("2. inside timeout")</code> runs. Prints <code>2. inside timeout</code>.</p>
<pre><code class="language-plaintext">Timeline:

[start]   stack: [log "1"]          -&gt; prints "1. start"
          stack: [setTimeout]       -&gt; hands off to Node internals, pops immediately
          stack: [log "3"]          -&gt; prints "3. end"
          stack: []                 -&gt; EVENT LOOP sees queue has the cb
          stack: [cb -&gt; log "2"]    -&gt; prints "2. inside timeout"
[end]
</code></pre>
<p>The timeout didn't delay by 0 milliseconds. It delayed until the stack was clear. Zero is the minimum wait, not a guarantee of immediacy.</p>
<hr />
<h2>Why Node.js specifically needs this</h2>
<p>Node was designed for servers. A server's entire job is handling requests, and requests involve waiting: reading from databases, fetching from APIs, reading files from disk. All of that is slow relative to CPU speed.</p>
<p>If Node ran synchronously, a single slow database query would freeze the server. Every other user would sit and wait. You'd need one thread per user, which is how older servers like Apache worked. That works until it doesn't, and it falls over somewhere in the hundreds or low thousands of concurrent connections because threads are expensive.</p>
<p>Node's event loop lets a single thread handle thousands of connections. While it's waiting on a database response for user A, it's processing user B's request. When the database responds for A, that callback goes in the queue and gets handled when the stack clears. No thread spawning, no context switching overhead.</p>
<p>This is why Node became popular for APIs and real-time apps because this model handles I/O-heavy workloads with very low resource usage.</p>
<hr />
<h2>Async operations: where do they actually go</h2>
<p>When you call <code>setTimeout</code>, <code>fs.readFile</code>, <code>fetch</code>, or pretty much anything async, you're handing work to something outside the JS engine. Node.js has a layer called <code>libuv</code> that handles these operations using the operating system's async capabilities and a thread pool for things that aren't natively async (like some file operations).</p>
<pre><code class="language-plaintext">Your JS Code
     |
     v
Node.js Runtime
     |
     ----&gt; V8 Engine (executes JS synchronously)
     |
     ----&gt; libuv (handles async I/O, timers, etc.)
                |
                ----&gt; OS networking (epoll/kqueue/IOCP)
                ----&gt; Thread pool (file I/O, crypto, etc.)
</code></pre>
<p>When an async operation completes in libuv, the callback gets handed to the event loop, which puts it in the appropriate queue. Then the event loop picks it up when the call stack is empty and runs it.</p>
<p>This is why you can write code like this and it works fine:</p>
<pre><code class="language-js">const fs = require('fs');

fs.readFile('bigfile.txt', 'utf8', (err, data) =&gt; {
  console.log('file done');
});

console.log('this runs while file is being read');
</code></pre>
<p>Output:</p>
<pre><code class="language-plaintext">this runs while file is being read
file done
</code></pre>
<p>The <code>readFile</code> call dispatched the work and returned immediately. Your next line ran. Somewhere later, the file read finished, the callback arrived in the queue, and the event loop ran it once the stack was free.</p>
<hr />
<h2>Timers vs I/O callbacks</h2>
<p>Timers (<code>setTimeout</code>, <code>setInterval</code>) and I/O callbacks don't go into the same queue. The event loop has multiple phases, and it visits them in order. Timers fire in one phase, I/O callbacks fire in another, and there are special microtask queues (for Promises) that run between phases.</p>
<p>The simplified picture:</p>
<pre><code class="language-plaintext">Event Loop Phases (simplified):

  [timers]         -&gt; setTimeout, setInterval callbacks
       |
  [I/O callbacks]  -&gt; file reads, network, etc.
       |
  [poll]           -&gt; wait for new I/O if nothing pending
       |
  [check]          -&gt; setImmediate callbacks
       |
  [close]          -&gt; cleanup callbacks
       |
  loop back to [timers]

Between EVERY phase:
  [microtask queue] -&gt; Promise .then(), queueMicrotask()
  (runs to completion before next phase starts)
</code></pre>
<p>The microtask queue (Promises) is the sneaky one. It runs to completion before the event loop moves to the next phase. So Promise callbacks have higher priority than <code>setTimeout</code> callbacks.</p>
<p>This is why:</p>
<pre><code class="language-js">console.log("start");

setTimeout(() =&gt; console.log("setTimeout"), 0);

Promise.resolve().then(() =&gt; console.log("promise"));

console.log("end");
</code></pre>
<p>Output:</p>
<pre><code class="language-plaintext">start
end
promise
setTimeout
</code></pre>
<p>The Promise resolved synchronously but its <code>.then()</code> callback went into the microtask queue. The <code>setTimeout</code> callback went into the timer queue. After <code>console.log("end")</code> cleared the stack, the event loop drained the microtask queue first, then moved to timers.</p>
<hr />
<h2>What "non-blocking" actually means in practice</h2>
<p>You'll see "non-blocking I/O" used to describe Node everywhere. When you do I/O (reading a file, making a network call), Node doesn't block the call stack waiting for it. It hands the work off and continues. The callback runs later.</p>
<p>The implication is that your JavaScript code is always running, never waiting. If something seems to be blocking, it's almost always CPU-heavy synchronous JavaScript rather than I/O. A function that does massive string manipulation or a tight loop will actually block Node because that work stays on the call stack.</p>
<pre><code class="language-js">// this blocks Node completely while running
function blockForMs(ms) {
  const end = Date.now() + ms;
  while (Date.now() &lt; end) {}
}

setTimeout(() =&gt; console.log("timer"), 100);

blockForMs(3000); // blocks for 3 seconds

// the timer callback fires at 3000ms+, not 100ms
</code></pre>
<p>The timer was set for 100ms but the <code>while</code> loop held the call stack hostage for 3 seconds. The event loop couldn't check the queue. This is called "blocking the event loop" and it's the thing Node developers actively try to avoid.</p>
<p>For CPU-heavy work, you'd use worker threads or break the work into smaller chunks. But for I/O, which is most of what servers do, the event loop handles it cleanly.</p>
<hr />
<h2>A slightly more realistic example</h2>
<p>Here's something closer to actual server code:</p>
<pre><code class="language-js">const fs = require('fs');

console.log("server starting...");

// imagine these are three incoming requests hitting at the same time
fs.readFile('user1.json', 'utf8', (err, data) =&gt; {
  console.log('user1 data ready');
});

fs.readFile('user2.json', 'utf8', (err, data) =&gt; {
  console.log('user2 data ready');
});

setTimeout(() =&gt; {
  console.log('cleanup timer fired');
}, 50);

console.log("all requests dispatched, waiting...");
</code></pre>
<p>Output (order of user1/user2 depends on which file read finishes first):</p>
<pre><code class="language-plaintext">server starting...
all requests dispatched, waiting...
user1 data ready
user2 data ready
cleanup timer fired
</code></pre>
<p>Three async operations dispatched in rapid succession. None of them blocked the others. The last <code>console.log</code> ran before any of them returned because the stack clears synchronous code first. Then the event loop started processing callbacks as they arrived.</p>
<hr />
<h2>Scalability: why this matters for real apps</h2>
<p>The event loop model is what lets Node handle high concurrency without throwing more hardware at the problem. A traditional server doing 1000 concurrent requests would have 1000 threads, each consuming memory and context switching time. Node handles those same 1000 requests in a single thread, with 1000 callbacks sitting in queues, processed one at a time as responses come in.</p>
<p>There are tradeoffs. CPU-intensive tasks hurt Node because they block the loop. Long synchronous operations hurt because they starve everything else in the queue. The model rewards I/O-heavy code that spends most of its time waiting, which is exactly what most APIs do.</p>
<p>Real apps at scale also use Node's cluster module or run multiple Node processes behind a load balancer to take advantage of multiple CPU cores. The event loop being single-threaded doesn't mean you're limited to one core. It means each Node process is single-threaded.</p>
<hr />
<p>The call stack runs your synchronous code. One thing at a time, top to bottom. When you call an async function, the actual work is handed off outside the JS engine, and a callback is registered. When that work completes, the callback goes into a queue. The event loop watches the call stack, and when it's empty, it pulls from the queue and runs the next callback.</p>
<p>Microtasks (Promises) run before timers. Timers run before I/O callbacks in some phases. But for day-to-day code, the thing that matters is: synchronous code runs first, then queued callbacks, and Promises resolve before <code>setTimeout</code>.</p>
<p>You don't need to memorise the phase order to write good async code. You need to understand that the event loop is a queue manager, that your synchronous code always runs before any callbacks, and that blocking the call stack with heavy synchronous work kills your app's responsiveness.</p>
<hr />
<h2>References</h2>
<ul>
<li><p><a href="https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick">Node.js docs: The Node.js Event Loop</a> : the official explanation, worth reading once you're comfortable with the basics</p>
</li>
<li><p><a href="https://www.youtube.com/watch?v=8aGhZQkoFbQ">Philip Roberts: What the heck is the event loop anyway?</a> : the talk that explained this to JS developers, still the best visual walkthrough</p>
</li>
<li><p><a href="https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/">Jake Archibald: Tasks, microtasks, queues and schedules</a> : the definitive deep dive on microtasks vs task queues</p>
</li>
<li><p><a href="https://libuv.org/">libuv documentation</a> : if you want to understand the layer below Node that actually does the async I/O</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[MongoDB is Great. Mongoose Makes it Better.]]></title><description><![CDATA[When I first started learning backend development, the database question came up almost immediately. Everyone has an opinion. SQL people will tell you it is the only correct way to store data. NoSQL p]]></description><link>https://blog.saumyagrawal.in/mongodb-is-great-mongoose-makes-it-better</link><guid isPermaLink="true">https://blog.saumyagrawal.in/mongodb-is-great-mongoose-makes-it-better</guid><category><![CDATA[MongoDB]]></category><category><![CDATA[NoSQL]]></category><category><![CDATA[Databases]]></category><category><![CDATA[ChaiCode]]></category><dc:creator><![CDATA[Saumya]]></dc:creator><pubDate>Fri, 24 Apr 2026 05:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/44da9ef3-53b1-444f-8d5b-84c8694333fe.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I first started learning backend development, the database question came up almost immediately. Everyone has an opinion. SQL people will tell you it is the only correct way to store data. NoSQL people will tell you SQL is legacy thinking. I got confused fast, and honestly I am glad to have started with MongoDB.</p>
<p>MongoDB is not always the right choice, but for a JavaScript developer learning backend for the first time, the mental model clicks in a way that SQL tables do not. Your data looks like objects. Your queries feel like JavaScript. You do not have to learn an entirely separate language just to get something saved and retrieved. You write code that looks like this:</p>
<pre><code class="language-js">const user = {
  name: "Abc",
  email: "abc@example.com",
  age: 28
}
</code></pre>
<p>And that is also, more or less, exactly what gets stored. There is no translation layer in your head where you have to think "okay, this object maps to these columns in this table with these foreign keys." It is just data, shaped the way you already think about data in JavaScript.</p>
<p>That familiarity is MongoDB's biggest advantage when you are starting out. It gets out of your way.</p>
<hr />
<h2>What is MongoDB</h2>
<p>MongoDB is a database, but it stores data differently from databases like MySQL or PostgreSQL. Those are relational databases that store data in tables with rows and columns, like a spreadsheet. MongoDB is a document database. It stores data as documents, which are essentially JSON objects, and groups them into collections instead of tables.</p>
<p>So instead of a <code>users</code> table where every row has the same columns, you have a <code>users</code> collection where each document can have a slightly different shape. One user might have a <code>phone</code> field, another might not. One might have a nested <code>address</code> object, another might not. MongoDB simply save the data into the fields as it receives.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/b5a645c8-2913-4817-a623-74c0c0a76440.png" alt="" style="display:block;margin:0 auto" />

<p>Each document automatically gets a unique <code>_id</code> field. MongoDB generates this as an <code>ObjectId</code>, unique across the entire database. <code>_id</code> is used to fetch specific documents.</p>
<p>The flexibility is real when your application is young and you are still figuring out what data you actually need. You can add a field to some documents without running a migration script. You can change your mind about the shape of your data without a painful database alteration. When you are building version one of something and requirements are shifting every week, this matters.</p>
<p>That said, the flexibility has a cost. Nothing stops two developers on your team from storing the same concept under different field names. Nothing stops your code from saving a string where a number was expected. MongoDB will happily store anything, and you find out something was wrong not when you save the data, but when you try to read it three weeks later and it comes back in an unexpected shape.</p>
<p>This problem gets solved by Mongoose.</p>
<hr />
<h2>Why you cannot just use MongoDB directly in your Node app</h2>
<p>Technically, you can. MongoDB has an official Node.js driver that lets you connect and run queries without any extra library. But the raw driver gives you back plain JavaScript objects with no type information. Your editor cannot autocomplete field names. If you make a typo, nothing warns you. If someone saves <code>{ eMail: "..." }</code> instead of <code>{ email: "..." }</code>, the database accepts it without complaint.</p>
<p>For a small personal project, that is probably fine. For anything with more than one developer, or anything you want to maintain six months from now, you start wanting structure.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/efea3602-c0f9-4b19-b975-3eb503ed3e81.png" alt="" style="display:block;margin:0 auto" />

<p>Mongoose is a library that sits between your Node.js code and MongoDB. It is often called an ODM (Object Document Mapper), which is the document database equivalent of an ORM. You define the shape of your data in code, and the library makes sure anything going to the database matches that shape. You get schemas, validation, methods on documents, hooks that run before or after operations, and a proper query API.</p>
<hr />
<h2>Setting up the connection</h2>
<p>Before any schema work, you connect. Mongoose wraps MongoDB's connection handling so you do it once and every model you define automatically uses that connection.</p>
<pre><code class="language-javascript">// db.js
import mongoose from 'mongoose';

async function connectDB() {
  try {
    await mongoose.connect(process.env.MONGO_URI);
    console.log('Connected to MongoDB');
  } catch (err) {
    console.error('MongoDB connection failed:', err);
    process.exit(1);
  }
}

export default connectDB;
</code></pre>
<pre><code class="language-javascript">// server.js
import 'dotenv/config';
import connectDB from './db.js';

async function startServer(){
    await connectDB();
}
</code></pre>
<p>The connection string points to your MongoDB instance and names the database. For a local setup it looks like <code>mongodb://localhost:27017/foodapp</code>. For MongoDB Atlas, it comes from the dashboard and works identically. Always store it in a <code>.env</code> file. Hardcoding it is fine while learning but you should not commit credentials to Git.</p>
<hr />
<h2>Writing a schema</h2>
<p>A Mongoose schema is where you tell your application what a document is supposed to look like. You create one with <code>new mongoose.Schema({})</code>, passing an object where each key is a field name and the value describes the expected type.</p>
<p>We will build a food delivery app through this post. The user schema starts simple:</p>
<pre><code class="language-javascript">// user/user.model.js
import { Schema, model } from 'mongoose';

const userSchema = new Schema({
  name:     String,
  email:    String,
  phone:    String,
  age:      Number,
  role:     String,
  isActive: Boolean,
});

export const User = model('User', userSchema);
</code></pre>
<p>The <code>model()</code> call is what turns a schema into something you can actually use. The name you pass, <code>'User'</code>, determines the MongoDB collection name. Mongoose lowercases and pluralizes it automatically, so documents land in a collection called <code>users</code>.</p>
<p>Using the model looks exactly like working with a class:</p>
<pre><code class="language-javascript">import { User } from './models/User.js';

async function createUser() {
  const user = new User({
    name:  'John Doe',
    email: 'john@example.com',
    phone: '9999999999',
    age:   25,
  });

  await user.save();
  console.log(user._id); // MongoDB auto-generates a unique ID
}

createUser();
</code></pre>
<p>This works, but the schema above does not enforce much. You can save a user with no email, an age of -500, or a role called <code>'supervillain'</code>. Mongoose lets it all through because you only told it the type, not the rules. For that, you need constraints.</p>
<hr />
<h2>Adding constraints</h2>
<p>Constraints are what make a schema actually useful. Instead of just passing a type, you pass an object with the type and whatever rules you want enforced.</p>
<pre><code class="language-javascript">const userSchema = new Schema({
  name: {
    type:      String,
    required:  [true, 'Name is required'],
    trim:      true,
    minlength: [2,  'Name must be at least 2 characters'],
    maxlength: [60, 'Name cannot exceed 60 characters'],
  },
  email: {
    type:      String,
    required:  [true, 'Email is required'],
    unique:    true,
    lowercase: true,
    trim:      true,
    match:     [/^\S+@\S+\.\S+$/, 'Please enter a valid email'],
  },
  phone: {
    type:  String,
    match: [/^[6-9]\d{9}$/, 'Enter a valid 10-digit mobile number'], // Regex for Indian phone no.
  },
  age: {
    type: Number,
    min:  [16, 'Must be at least 16 to order'],
    max:  [120, 'Please enter a valid age'],
  },
  role: {
    type:    String,
    enum:    ['customer', 'rider', 'admin'],
    default: 'customer',
  },
  isActive: {
    type:    Boolean,
    default: true,
  },
}, {
  timestamps: true,
});
</code></pre>
<p>The <code>timestamps: true</code> option automatically adds <code>createdAt</code> and <code>updatedAt</code> fields to every document and keeps <code>updatedAt</code> current on every save, without you having to remember to do it manually.</p>
<p>When validation fails, Mongoose throws a <code>ValidationError</code> before the document reaches MongoDB. The error includes your custom message, so you can send it directly back to the user:</p>
<pre><code class="language-javascript">try {
  const bad = new User({ name: 'A', email: 'notanemail' });
  await bad.save();
} catch (err) {
  console.log(err.errors.name.message);
  // "Name must be at least 2 characters"
  console.log(err.errors.email.message);
  // "Please enter a valid email"
}
</code></pre>
<p>One thing worth knowing about <code>unique: true</code>: it creates a database level index in MongoDB rather than running a Mongoose check. The error looks different, instead of a <code>ValidationError</code> you get an error with <code>err.code === 11000</code>.</p>
<p>Sometimes the built-in options are not enough. You can pass a custom validator function for any field:</p>
<pre><code class="language-javascript">deliveryPincode: {
  type: String,
  validate: {
    validator: function(v) {
      return /^\d{6}$/.test(v); 
    },
    message: (props) =&gt; `${props.value} is not a valid pincode`,
  },
},
</code></pre>
<hr />
<h2>Nested objects and references</h2>
<p>Data in real applications is never straightforward. A user has an address. An order has multiple items. Mongoose gives you two ways to handle this: embed the related data directly inside the document, or store a reference to another document by its <code>_id</code> and look it up separately.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/c8a6edd3-cb77-4781-9982-842cbd067ed4.png" alt="" style="display:block;margin:0 auto" />

<p>For an address, embedding makes sense. You would never want to fetch an address without the user it belongs to, and an address has no meaning outside of that context. The schema looks like a nested object:</p>
<pre><code class="language-js">address: {
  street:  { type: String, trim: true },
  city:    { type: String, trim: true },
  pincode: {
    type:  String,
    match: [/^\d{6}$/, 'Invalid pincode'],
  },
},
</code></pre>
<p>You read it by chaining dots: <code>user.address.city</code>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/c2d507ec-b85e-450e-99b4-2ff840ac47f8.png" alt="" style="display:block;margin:0 auto" />

<p>For an order that belongs to a user, a reference makes more sense. A user exists independently and appears in many places. You store the user's <code>_id</code> in the order:</p>
<pre><code class="language-js">// models/order.model.js
const orderSchema = new Schema({
  user: {
    type: mongoose.Schema.Types.ObjectId,
    ref:  'User',
    required: true,
  },
  restaurant: {
    type: mongoose.Schema.Types.ObjectId,
    ref:  'Restaurant',
    required: true,
  },
  items: [{
    name:     { type: String, required: true },
    quantity: { type: Number, min: 1, default: 1 },
    price:    { type: Number, required: true, min: 0 },
  }],
  totalAmount: { type: Number, required: true },
  status: {
    type:    String,
    enum:    ['placed', 'confirmed', 'out_for_delivery', 'delivered', 'cancelled'],
    default: 'placed',
  },
}, { timestamps: true });
</code></pre>
<p>The <code>ref: 'User'</code> tells Mongoose which model to look up when you call <code>.populate()</code>:</p>
<pre><code class="language-js">const order = await Order
  .findById(orderId)
  .populate('user', 'name email')
  .populate('restaurant', 'name address');

console.log(order.user.name); // Prints the name of the user of the particular id
</code></pre>
<p>Without <code>populate()</code>, <code>order.user</code> is just an ID string. With it, Mongoose runs the extra lookup and replaces the ID with the actual document. The rule for choosing is simpler than it sounds: if the data only makes sense alongside its parent and will always be read together with it, embed it. If the data exists on its own and gets used from multiple places, reference it.</p>
<hr />
<h2>Virtual fields</h2>
<p>A virtual is a field that does not get saved to the database. It is computed from other fields whenever you read the document. For the food delivery app, a <code>displayAddress</code> virtual combines the separate address fields into one readable string:</p>
<pre><code class="language-js">userSchema.virtual('displayAddress').get(function() {
  if (!this.address?.city) return 'No address saved';
  return `\({this.address.street}, \){this.address.city} - ${this.address.pincode}`;
});

const user = await User.findOne({ email: 'john@example.com' });
console.log(user.displayAddress);
// example:"Civil Lines, Delhi - 110054"
</code></pre>
<p>Virtuals do not appear in <code>JSON.stringify()</code> output by default, which means they will not show up in API responses unless you enable them. Add <code>toJSON: { virtuals: true }</code> to the schema options:</p>
<pre><code class="language-js">const userSchema = new Schema({ /* fields */ }, {
  timestamps: true,
  toJSON:   { virtuals: true },
  toObject: { virtuals: true },
});
</code></pre>
<p>Use regular functions, not arrow functions, for virtuals and hooks. Arrow functions do not have their own <code>this</code>, so <code>this.address</code> inside an arrow function refers to nothing useful. Mongoose passes the document as <code>this</code>, and a regular function is what receives it correctly.</p>
<hr />
<h2>Hooks</h2>
<p>A hook is a function that runs automatically before or after a database operation.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/ee0ee78f-79c2-439d-b7d3-23364910efd8.png" alt="" style="display:block;margin:0 auto" />

<p>The most important hook in almost every app is hashing a password before saving it. You should never store plain text passwords in a database:</p>
<pre><code class="language-js">import bcrypt from 'bcryptjs';

// Mongoose handles the promise through async; hence next() is not required
// if async absent, use next()
userSchema.pre('save', async function() {
  
  if (!this.isModified('password')) return;

  this.password = await bcrypt.hash(this.password, 12);
});
// 12 is Cost Factor; Number of iterations for hashing
</code></pre>
<p>The <code>isModified('password')</code> check is necessary. Without it, every time you update any field on a user, the hook would rehash an already hashed password. Checking <code>isModified</code> means the hook only runs when the password field actually changed.</p>
<p>Post hooks run after the operation completes:</p>
<pre><code class="language-js">// send welcome email to new users
userSchema.post('save', async function (doc) {
  if (doc._wasNew) {
    try {
      await sendWelcomeEmail(doc.email, doc.name);
    } catch (err) {
      console.error("Email failed to send:", err);
    }
  }
});
</code></pre>
<p>There is a gotcha with update operations. When you use <code>findByIdAndUpdate()</code>, the <code>pre('save')</code> hook does not fire. Mongoose treats updates differently from saves. If you want validation to run on updates too:</p>
<pre><code class="language-js">userSchema.pre('findOneAndUpdate', function() {
  this.setOptions({ runValidators: true });
});
</code></pre>
<hr />
<h2>Instance methods and static methods</h2>
<p>Instance methods run on individual documents. Static methods run on the model class itself.</p>
<p>The most common instance method is password comparison for login:</p>
<pre><code class="language-js">userSchema.methods.comparePassword = async function(plainTextPassword) {
  return bcrypt.compare(plainTextPassword, this.password);
};

// in your login route:
const user = await User.findOne({ email: req.body.email }).select('+password');
const match = await user.comparePassword(req.body.password);
if (!match) return res.status(401).json({ error: 'Wrong password' });
</code></pre>
<p>Use <code>.select('+password')</code> when you mark a field with <code>select: false</code> in the schema, Mongoose leaves it out of all query results by default. The <code>+password</code> syntax opts it back in for this one query where you actually need it.</p>
<p>Static methods are good for reusable query logic that belongs on the model itself:</p>
<pre><code class="language-js">userSchema.statics.findActiveRiders = async function(city) {
  return this.find({
    role: 'rider',
    isActive: true,
    'address.city': city,
  });
};

const riders = await User.findActiveRiders('Mumbai');
</code></pre>
<hr />
<h2>Querying</h2>
<p>Mongoose wraps MongoDB's query system in a clean API. These operations cover almost everything you will need day to day:</p>
<pre><code class="language-js">// all customers
const users = await User.find({ role: 'customer' });

// find one by email
const user = await User.findOne({ email: 'john@example.com' });

// find by MongoDB _id
const user = await User.findById(userId);

// update and get back the updated version
const updated = await User.findByIdAndUpdate(
  userId,
  { isActive: false },
  { new: true } // this returns the document after change
);

// delete
await User.findByIdAndDelete(userId);

// pick specific fields, sort, paginate
const page2 = await User
  .find({ role: 'customer' })
  .select('name email')
  .sort({ createdAt: -1 })
  .skip(10)
  .limit(10);
</code></pre>
<p><code>.select()</code> is worth understanding early. Controlling which fields come back is how you avoid accidentally sending password hashes to the frontend.</p>
<hr />
<h2>Indexes</h2>
<p>Indexes were one concept that took me a while to wrap my head around. Without indexes, MongoDB reads every document in a collection one by one to find a match. That is fine for a hundred documents. At a hundred thousand, a query that took 2ms starts taking seconds. The culprit is almost always a missing index.</p>
<p>An index is a separate, sorted data structure that MongoDB maintains alongside your collection. When you query a field that has an index, MongoDB looks up the value in that structure instead of scanning every document. The difference in speed can be several orders of magnitude.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/f9bda2b4-b3ca-4df1-bf27-b0a887dc8ece.png" alt="" style="display:block;margin:0 auto" />

<p>You add indexes inside the schema in Mongoose. <code>unique: true</code> on the email field already creates one automatically. For other fields, you either add <code>index: true</code> on the field definition or call <code>schema.index()</code> directly:</p>
<pre><code class="language-js">// option 1: inline on the field
email: { type: String, unique: true }, // index created automatically
role:  { type: String, index: true  }, // single-field index

// option 2: schema.index() for more control
userSchema.index({ role: 1, isActive: 1 });    // compound index
userSchema.index({ 'address.city': 1 });       // index on a nested field
userSchema.index({ createdAt: -1 });           // -1 = descending order
</code></pre>
<p>The <code>1</code> means ascending order, <code>-1</code> means descending. For most lookups the direction does not matter much, but for sorted queries it can. A compound index covers queries that filter by multiple fields together. If your app frequently fetches active riders in a specific city, an index on <code>{ role: 1, isActive: 1, 'address.city': 1 }</code> will serve that query far faster than three separate single-field indexes.</p>
<p>There is a tradeoff. Indexes speed up <code>find()</code> but slightly slow down <code>save()</code>, because MongoDB has to update every index whenever a document changes. For most applications this is completely fine. The read speedup far outweighs the write cost. But indexing every field by default is not a good habit. Add indexes for fields you actually filter or sort by. Also, if you're adding an index to a live database with millions of records, do it during off-peak hours. Building an index on a massive active collection can lock the collection or spike CPU usage, leading to a temporary slowdown for your users.</p>
<p>A special case worth knowing is the TTL (Time To Live) index. It automatically deletes documents after a certain amount of time. This is perfect for email verification tokens, password reset links, or session records that should expire on their own:</p>
<pre><code class="language-js">const tokenSchema = new Schema({
  userId:    { type: mongoose.Schema.Types.ObjectId, required: true },
  token:     { type: String, required: true },
  createdAt: { type: Date, default: Date.now },
});

// MongoDB automatically deletes these documents 1 hour after createdAt
tokenSchema.index({ createdAt: 1 }, { expireAfterSeconds: 3600 });
</code></pre>
<p>MongoDB handles the deletion automatically without a cleanup script.</p>
<hr />
<h2>Error handling</h2>
<p>MongoDB and Mongoose throw different types of errors, and you need to handle them differently. Bundling all errors into a single catch block and returning a 500 response is a pattern that makes debugging very frustrating.</p>
<p>There are three categories you will encounter regularly. The first is a <code>ValidationError</code> from Mongoose, which fires when a document fails schema constraint checks. The second is error code <code>11000</code> from MongoDB itself, which fires when a unique constraint is violated at the database level. The third is a <code>CastError</code>, which fires when you pass something to <code>findById()</code> that is not a valid MongoDB <code>ObjectId</code> format.</p>
<pre><code class="language-js">// a reusable error handler you can drop into any project
function handleMongooseError(err) {
  // validation failed before reaching the database
  if (err.name === 'ValidationError') {
    const messages = Object.values(err.errors).map(e =&gt; e.message);
    return { status: 400, message: messages.join(', ') };
  }

  // duplicate key: unique constraint violated
  if (err.code === 11000) {
    const field = Object.keys(err.keyValue)[0];
    return { status: 409, message: `${field} already exists` };
  }

  // invalid ObjectId format passed to findById or similar
  if (err.name === 'CastError') {
    return { status: 400, message: 'Invalid ID format' };
  }

  return { status: 500, message: 'An unexpected error occurred on the server.' };
}
</code></pre>
<p>If your route receives an <code>id</code> parameter from a URL and that parameter is not a valid ObjectId, calling <code>User.findById(id)</code> throws before even hitting the database. Without handling it, that becomes an unhandled exception. With it, you return a clean 400 response.</p>
<p>Using this in a route looks like:</p>
<pre><code class="language-js">app.post('/users', async (req, res) =&gt; {
  try {
    const user = await User.create(req.body);
    res.status(201).json(user);
  } catch (err) {
    const { status, message } = handleMongooseError(err);
    res.status(status).json({ error: message });
  }
});
</code></pre>
<hr />
<h2>Aggregation pipelines</h2>
<p>Once your data is in MongoDB and you understand basic querying, you will eventually need to answer questions like: what is the total revenue per restaurant this month, how many orders has each rider completed, what is the average order value by city. Basic <code>find()</code> queries cannot answer these. Aggregation pipelines can.</p>
<p>An aggregation pipeline is a sequence of stages that transform documents. Each stage takes documents in, does something to them, and passes the result to the next stage. Think of it like an assembly line for your data.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/47d4d0a1-41ee-463e-af21-d52d306c3d5f.png" alt="" style="display:block;margin:0 auto" />

<pre><code class="language-js">const revenueByRestaurant = await Order.aggregate([
  // stage 1: only delivered orders from this month
  {
    $match: {
      status:    'delivered',
      createdAt: { $gte: new Date('2024-12-01') },
    }
  },
  // stage 2: group by restaurant and sum totals
  {
    $group: {
      _id:          '$restaurant',
      totalRevenue: { \(sum: '\)totalAmount' },
      orderCount:   { $sum: 1 },
    }
  },
  // stage 3: sort by revenue, highest first
  {
    $sort: { totalRevenue: -1 }
  },
  // stage 4: join with restaurants to get the name
  {
    $lookup: {
      from:         'restaurants',
      localField:   '_id',
      foreignField: '_id',
      as:           'restaurantInfo',
    }
  },
  // stage 5: reshape the output
  {
    $project: {
      restaurantName: { \(arrayElemAt: ['\)restaurantInfo.name', 0] },
      totalRevenue:   1,
      orderCount:     1,
    }
  }
]);
</code></pre>
<p><code>\(match</code> is like a <code>find()</code> filter. Put it as early as possible in the pipeline so you are working with fewer documents in the stages that follow. <code>\)group</code> is how you aggregate: you specify a <code>_id</code> to group by and then use accumulator operators like <code>\(sum</code>, <code>\)avg</code>, <code>\(min</code>, and <code>\)max</code> to compute values. <code>\(lookup</code> is MongoDB's join equivalent. It pulls in documents from another collection and attaches them to the current documents. <code>\)project</code> controls which fields appear in the output, similar to <code>.select()</code> on a regular query.</p>
<p>These four stages cover the vast majority of reporting and analytics work you will do in a real application.</p>
<hr />
<h2>Transactions</h2>
<p>By default, a MongoDB write either succeeds or fails on its own. That is fine most of the time. But sometimes you have an operation that involves multiple writes that must all succeed together or all fail together. Charging a user for an order and creating the order record is the classic example. If the charge succeeds but the order creation fails, the user paid for something that does not exist. That is a bad place to be.</p>
<p>Mongoose exposes them through sessions:</p>
<pre><code class="language-js">const session = await mongoose.startSession();

try {
  session.startTransaction();

  // both of these run inside the same transaction
  const order = await Order.create([{
    user:        userId,
    restaurant:  restaurantId,
    items:       cartItems,
    totalAmount: total,
    status:      'placed',
  }], { session });

  await User.findByIdAndUpdate(
    userId,
    { $inc: { orderCount: 1 } },
    { session }
  );

  await session.commitTransaction();
  return order[0];

} catch (err) {
  // if anything fails, roll back both writes
  await session.abortTransaction();
  throw err;

} finally {
  session.endSession();
}
</code></pre>
<p>The key is passing <code>{ session }</code> to every operation inside the transaction. Without it, that operation runs outside the transaction and will not be rolled back if something fails. The <code>finally</code> block ensures the session is always closed, even when an error is thrown.</p>
<hr />
<h2>The complete schema</h2>
<p>Pulling it all together, here is the full User model with constraints, a nested address, a virtual, the password hook, the login method, and indexes:</p>
<pre><code class="language-js">// models/user.model.js
import { Schema, model } from 'mongoose';
import bcrypt from 'bcryptjs';

const userSchema = new Schema({
  name: {
    type:      String,
    required:  [true, 'Name is required'],
    trim:      true,
    minlength: [2,  'Name must be at least 2 characters'],
    maxlength: [60, 'Name cannot exceed 60 characters'],
  },
  email: {
    type:      String,
    required:  [true, 'Email is required'],
    unique:    true,
    lowercase: true,
    trim:      true,
    match:     [/^\S+@\S+\.\S+$/, 'Please enter a valid email'],
  },
  password: {
    type:      String,
    required:  [true, 'Password is required'],
    minlength: [8, 'Password must be at least 8 characters'],
    select:    false,
  },
  phone: {
    type:  String,
    match: [/^[6-9]\d{9}$/, 'Enter a valid 10-digit mobile number'],
  },
  address: {
    street:  { type: String, trim: true },
    city:    { type: String, trim: true },
    pincode: { type: String, match: [/^\d{6}$/, 'Invalid pincode'] },
  },
  role: {
    type:    String,
    enum:    ['customer', 'rider', 'admin'],
    default: 'customer',
  },
  isActive: { type: Boolean, default: true },
}, {
  timestamps: true,
  toJSON:   { virtuals: true },
  toObject: { virtuals: true },
});

// indexes
userSchema.index({ role: 1, isActive: 1 });
userSchema.index({ 'address.city': 1 });

// virtual
userSchema.virtual('displayAddress').get(function() {
  if (!this.address?.city) return 'No address saved';
  return `\({this.address.street}, \){this.address.city} - ${this.address.pincode}`;
});

// hooks
userSchema.pre('save', async function() {
  if (!this.isModified('password')) return;
  this.password = await bcrypt.hash(this.password, 12);
});

userSchema.pre('findOneAndUpdate', function() {
  this.setOptions({ runValidators: true });
});

// instance method
userSchema.methods.comparePassword = async function(candidate) {
  return bcrypt.compare(candidate, this.password);
};

export const User = model('User', userSchema);
</code></pre>
<hr />
<h2>How it all fits together</h2>
<p>It helps to see the full picture in one place. Your application code talks to Mongoose. Mongoose talks to MongoDB. The schema sits in the middle describing what is allowed, what gets validated, what runs automatically, and how queries behave.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/56b2effb-ec47-4c46-93d6-b23a072efca8.png" alt="" style="display:block;margin:0 auto" />

<p>Your route calls a method on the model. The model checks the schema constraints. Any matching pre-hooks run. The data goes to MongoDB. Post-hooks run. The result comes back as a typed Mongoose document with your virtual fields and methods attached.</p>
<hr />
<p>MongoDB is a genuinely good place to start. The document model matches how JavaScript developers already think about data, the queries are readable, and you can get something working quickly without a lot of infrastructure. Mongoose adds the structure that MongoDB lacks on its own, which is the right tradeoff for most Node.js projects.</p>
<hr />
<h2>References</h2>
<ul>
<li><p><a href="https://mongoosejs.com/docs/guide.html">Mongoose documentation: Schemas</a></p>
</li>
<li><p><a href="https://mongoosejs.com/docs/validation.html">Mongoose documentation: Validation</a></p>
</li>
<li><p><a href="https://mongoosejs.com/docs/middleware.html">Mongoose documentation: Middleware</a></p>
</li>
<li><p><a href="https://mongoosejs.com/docs/populate.html">Mongoose documentation: Populate</a></p>
</li>
<li><p><a href="https://mongoosejs.com/docs/guide.html#virtuals">Mongoose documentation: Virtuals</a></p>
</li>
<li><p><a href="https://mongoosejs.com/docs/guide.html#indexes">Mongoose documentation: Indexes</a></p>
</li>
<li><p><a href="https://mongoosejs.com/docs/transactions.html">Mongoose documentation: Transactions</a></p>
</li>
<li><p><a href="https://www.mongodb.com/docs/manual/reference/operator/aggregation/">MongoDB docs: Aggregation pipeline operators</a></p>
</li>
<li><p><a href="https://www.mongodb.com/docs/manual/indexes/">MongoDB docs: Indexes</a></p>
</li>
<li><p><a href="https://www.mongodb.com/atlas">MongoDB Atlas</a></p>
</li>
<li><p><a href="https://www.npmjs.com/package/bcryptjs">bcryptjs on npm: password hashing library</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[The "Everything is a File" Tour of Linux]]></title><description><![CDATA[I wanted to overcome my command line phobia and join the 4% club of human bots who don't panic when they see the terminal, typing away in their plain terminal. I wanted to feel like a hacker. Not the ]]></description><link>https://blog.saumyagrawal.in/the-everything-is-a-file-tour-of-linux</link><guid isPermaLink="true">https://blog.saumyagrawal.in/the-everything-is-a-file-tour-of-linux</guid><category><![CDATA[Linux]]></category><category><![CDATA[linux for beginners]]></category><category><![CDATA[ChaiCode]]></category><dc:creator><![CDATA[Saumya]]></dc:creator><pubDate>Tue, 21 Apr 2026 12:40:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/c0eeaa53-8510-4b7a-865c-1b31414b82c9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I wanted to overcome my command line phobia and join the 4% club of human bots who don't panic when they see the terminal, typing away in their plain terminal. I wanted to feel like a hacker. Not the movie kind with three keyboards and cascading green text, but the kind who actually understands what their computer is doing. So I googled "How to learn Linux" and landed on the most mundane, beige documentation website I have ever seen in my life. I dropped into forums next, expecting a more human approach. The first post I found had someone saying "just build your own kernel." I just wanted to learn things, not go on a spiritual journey.</p>
<p>People told me Linux had a learning curve. Except this curve was a cliff.</p>
<hr />
<p>If you want a 12-minute brush through Linux basics before diving into the weird stuff, <a href="https://www.youtube.com/watch?v=LKCVKw9CzFo">Fireship made a video for exactly that</a>.</p>
<p>Before we go into a rabbit hole, I will suggest to try all of this on a VPS, a VM, or a Docker container. Not your main machine. The reason is simple: some of what I will show you involves deliberately breaking things. Try to understand the command before copying, because imagine copying <code>rm -rf /</code>. But if you are HIM, nobody is stopping you. On a throwaway container? You type <code>exit</code>, spin up a fresh one, and try again. Freedom to destroy without consequences is genuinely how you learn Linux fastest.</p>
<hr />
<h2>Two tools that will save you more than any tutorial</h2>
<p>Before anything else: <code>man</code> and <code>tldr</code>. These are your cheat codes.</p>
<p><code>man</code> is the built-in Linux manual. Every command, every system file, every config format has one. When you wonder what a flag does or what a file is for, <code>man</code> has the answer written by the people who built the thing.</p>
<pre><code class="language-bash">man ls
man chmod
man 5 passwd      # the 5 means file format manual, not the command
man resolv.conf   # the config file itself has a man page
</code></pre>
<p>The problem with <code>man</code> is density. It was written by engineers for engineers. Enter <code>tldr</code>, a community-written alternative that gives you just the practical common cases.</p>
<pre><code class="language-bash">apt install tldr
tldr ls
tldr chmod
</code></pre>
<p><code>tldr</code> gives you five useful examples instead of fifty pages of specification. Use <code>tldr</code> when you want to quickly understand what something does. Use <code>man</code> when you need the full picture. If <code>man</code> is the textbook, <code>tldr</code> is the cheat sheet your classmate scribbled the night before the exam.</p>
<p>One more habit worth building early: when something fails, read the actual error message before googling. "Permission denied" means permissions. "No such file or directory" means the path is wrong or the file doesn't exist. "Command not found" means it is not installed or not in your PATH. Linux error messages are not trying to confuse you. They are telling you what happened.</p>
<hr />
<h2>What even is <code>/etc</code></h2>
<p><code>/etc</code> is where Linux keeps its brain. Configuration for almost everything the system does lives here in plain text files you can open with any editor. I spent the first hour just running <code>ls /etc</code> and opening random files, and breaking things up. That felt like finding an unlocked door in a building I thought was sealed.</p>
<h3>DNS redirection or how I broke Google with one line</h3>
<p>The one thing I tried in <code>/etc</code> was <code>/etc/hosts</code>. I wanted to reach my local dev server by a name instead of <code>127.0.0.1:3000</code>. Opening the file:</p>
<pre><code class="language-bash">nano /etc/hosts
</code></pre>
<pre><code class="language-plaintext">127.0.0.1   localhost
::1         localhost ip6-localhost
</code></pre>
<p>I added one line at the bottom and saved:</p>
<pre><code class="language-shell">127.0.0.1   myapp.local
</code></pre>
<p>It worked immediately. No restart, no daemon reload. The file is read fresh on every single lookup because this check happens before Linux even thinks about asking a DNS server anything.</p>
<p>Then I had an idea. What if I pointed <code>instagram.com</code> at localhost?</p>
<pre><code class="language-shell">nano /etc/hosts
# add: 127.0.0.1   instagram.com
</code></pre>
<pre><code class="language-shell">ping instagram.com
</code></pre>
<pre><code class="language-shell">PING instagram.com (127.0.0.1) 56 bytes of data.
</code></pre>
<p>This is poor man's parental control. All of Meta's infrastructure, the entire DNS system, bypassed by one line in a text file on your machine. The whole internet is the fallback. This file runs first.</p>
<p>Imagine making your friend slowly lose their mind on a shared Linux machine, redirect their most visited sites to <code>127.0.0.1</code> in <code>/etc/hosts</code>. They will restart their wifi three times, blame their ISP, factory reset their router, and spend an hour in a chat with customer support before anyone thinks to check a text file.</p>
<hr />
<h2><code>/etc/resolv.conf</code>: the file that keeps rewriting itself</h2>
<p>Once I understood <code>/etc/hosts</code>, I wanted to know what happened when a hostname was not found there. That fallback is <code>/etc/resolv.conf</code>, which holds the actual DNS server address the system asks.</p>
<pre><code class="language-shell">cat /etc/resolv.conf
</code></pre>
<pre><code class="language-shell"># This file is managed by man:systemd-resolved(8). Do not edit.
nameserver 127.0.0.53
</code></pre>
<p>I had edited this on my machine and my changes kept vanishing. Now I understood why. <code>systemd-resolved</code> is a local DNS middleman running at <code>127.0.0.53</code>. It manages this file and rewrites it whenever it restarts.</p>
<p>Let's break the internet:</p>
<pre><code class="language-shell">echo "nameserver 0.0.0.0" &gt; /etc/resolv.conf
ping google.com
</code></pre>
<pre><code class="language-shell">ping: google.com: Temporary failure in name resolution
</code></pre>
<p>The machine is still connected to the network. It just cannot resolve any names because it is asking a DNS server that does not exist. <code>apt update</code> fails, <code>curl</code> fails, package installs fail. The network is fine, only name resolution is dead. This is what a misconfigured DNS server looks like from the inside.</p>
<pre><code class="language-shell"># Fix it immediately
echo "nameserver 8.8.8.8" &gt; /etc/resolv.conf
</code></pre>
<p>The permanent fix without the file fighting back is editing <code>systemd-resolved</code>'s own config:</p>
<pre><code class="language-shell">nano /etc/systemd/resolved.conf
# under [Resolve]:
# DNS=1.1.1.1
# FallbackDNS=8.8.8.8
systemctl restart systemd-resolved
</code></pre>
<hr />
<h2><code>/etc/passwd</code> and <code>/etc/shadow</code>: the user accounts are just a text file</h2>
<p>I heard that everything in Linux is treated as a file, from hardware device to system configurations, just a file. This in itself was surprising to me, till I started exploring it through <code>/etc/passwd</code></p>
<pre><code class="language-shell">cat /etc/passwd
</code></pre>
<pre><code class="language-shell">root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
saumya:x:1001:1001::/home/saumya:/bin/bash

# the above is in the form of name:password:UID:GID:GECOS:directory:shell (root:x:0:0:root:/root:/bin/bash)
</code></pre>
<p>Seven colon-separated fields per line: username, password placeholder, user ID, group ID, comment, home directory, login shell. The <code>x</code> in the second field means the actual password hash is stored in <code>/etc/shadow</code>, which only root can read.</p>
<pre><code class="language-shell">sudo cat /etc/shadow | grep saumya
</code></pre>
<pre><code class="language-shell">saumya:\(6\)rounds=4096\(saltstring\)hashedpassword:19820:0:99999:7:::

# 9 fields separated by ':'
</code></pre>
<p>The 9 fields are username, encrypted password, last change, minimum age, maximum age, warning, activity, expiration and reserved.</p>
<p><code>\(6\)</code> means SHA-512. The <code>rounds=4096</code> deliberately makes hashing slow. Slow hashing means brute-force attacks take longer. Your password is protected partly by intentional computational expense.</p>
<p>Now the interesting part. Every account with <code>/usr/sbin/nologin</code> as its login shell cannot open an interactive session. Try to SSH in as <code>www-data</code> and the connection closes immediately. No password prompt, no shell, nothing. These accounts exist only so the processes running as them have a numeric user identity for permissions. They are not meant for humans to log into.</p>
<p>Now, i wanted to try breaking it and hence, changed my login shell to <code>/bin/false</code> in <code>/etc/passwd</code>.</p>
<pre><code class="language-shell">nano /etc/passwd

# find your line, change /bin/bash to /bin/false
</code></pre>
<p>When i tried to connect in a new terminal, the session closes the instant the shell starts because <code>/bin/false</code> exits immediately with a failure code. My account exists, password is correct, the SSH connection succeeds, but I am immediately disconnected. I have locked yourself out without deleting the account or changing the password.</p>
<p>I fixed the current session by changing <code>/bin/false</code> back to <code>/bin/bash</code>.</p>
<hr />
<h2><code>/etc/ssh/sshd_config</code>: watching bots try your front door</h2>
<p>On any internet-facing VPS, run this after it has been up for a day:</p>
<pre><code class="language-shell">grep "Failed password" /var/log/auth.log | wc -l
</code></pre>
<p>On a fresh VPS after few hours, that number is usually in the thousands. Automated bots scan every IP address on the internet looking for open port 22 and trying common username and password combinations. <code>root</code>, <code>admin</code>, <code>ubuntu</code>, <code>123456</code>. Constantly, from everywhere.</p>
<p>All SSH behavior is controlled by <code>/etc/ssh/sshd_config</code>:</p>
<pre><code class="language-shell">nano /etc/ssh/sshd_config
</code></pre>
<p>You would see a file with multiple lines with Port info, permits and authentications. Two lines that matter immediately:</p>
<pre><code class="language-shell">PasswordAuthentication no
PermitRootLogin no
</code></pre>
<p>Setting <code>PasswordAuthentication no</code> means SSH only accepts key-based authentication. The bots still knock but there is nothing to brute-force. No password means no attack surface. <code>PermitRootLogin no</code> means even with valid root credentials, nobody SSHs directly into root.</p>
<p>Want to watch the bots in real time:</p>
<pre><code class="language-shell">tail -f /var/log/auth.log
</code></pre>
<p>Run that for five minutes on a fresh VPS. The attempts come in constantly from IP addresses across dozens of countries. It is mildly unsettling and also clarifying. This is just what the internet is like all the time.</p>
<p>After disabling passwords:</p>
<pre><code class="language-shell">systemctl restart sshd
grep "Failed password" /var/log/auth.log | tail -5
# bots still try, still fail, but now they fail faster
</code></pre>
<p>Whenever the bots try a password, the server tells them 'NO', not even letting them trying the password to authenticate.</p>
<hr />
<h2><code>/etc/fstab</code>: the file that can stop your machine from booting</h2>
<p><code>/etc/fstab</code> is read at boot. Linux mounts everything listed here. USB drives, additional partitions, network shares. Get an entry wrong and the machine may not boot.</p>
<pre><code class="language-shell">cat /etc/fstab
</code></pre>
<pre><code class="language-shell"># in the order
# file system, mount point, type, options, dump and pass


UUID=abc123   /        ext4    defaults    0 1
UUID=def456   /boot    ext4    defaults    0 2
tmpfs         /tmp     tmpfs   defaults    0 0
</code></pre>
<p>UUID is used instead of the device name because the name might change but the UUID won't. This acts as the Aadhar Number of the device.</p>
<p>The experiment:</p>
<pre><code class="language-shell">nano /etc/fstab
# add at the bottom:
broken-entry-here
</code></pre>
<p>'broken-entry-here' is a string of text without the columns. When the mount command encounters it, it has no idea what the device is, and where it should be mounted.</p>
<p>Now test without rebooting:</p>
<pre><code class="language-shell">mount -a
</code></pre>
<p>This command is essentially the dry run of the boot process.</p>
<pre><code class="language-shell">mount: /: can't find broken-entry-here.
</code></pre>
<p>Error. Exactly what you would get on boot if you saved this and restarted. Fix it by removing or commenting out the broken line, then run <code>mount -a</code> again to confirm it passes.</p>
<p>The rule: always test with <code>mount -a</code> before rebooting when you touch <code>/etc/fstab</code>. Always. A bad fstab entry drops you into recovery mode or an emergency shell with no obvious explanation. Keep a mental note that this file exists and kills boots if you get it wrong.</p>
<hr />
<h2><code>/proc</code>: the folder that is not actually on disk</h2>
<p><code>/proc</code> is a virtual filesystem. There are no files here stored on disk anywhere. When you read something from <code>/proc</code>, the kernel generates the content on the spot from its own live internal data. It is a window into running system state, not stored data.</p>
<pre><code class="language-shell">cat /proc/meminfo
</code></pre>
<p>Run it twice, ten seconds apart. The numbers change. Because it is live.</p>
<pre><code class="language-shell">cat /proc/cpuinfo | grep "flags" | head -1
</code></pre>
<p>The flags like vmx, aes and sse which details the CPU capability the processor supports. <code>vmx</code> means hardware virtualization is available. If you try to run KVM and it refuses, check for this flag. <code>aes</code> means the CPU has built-in AES hardware acceleration. Disk encryption and TLS are genuinely faster because of this single flag. sse/avx means CPU has Streaming SIMD Extensions which can boost performance for heavy computations, and video encoding.</p>
<p>The per-process data is where things get interesting. Every running process gets its own directory:</p>
<pre><code class="language-shell"># Find something running
ps aux | grep nginx
# Say it's PID 1234

ls /proc/1234/
</code></pre>
<pre><code class="language-shell">cmdline  cwd  environ  exe  fd  maps  mem  net  status
</code></pre>
<p><code>fd/</code> lists every file descriptor the process has open right now. Log files, sockets, pipes, all of it as symlinks. <code>environ</code> has the exact environment variables the process launched with. <code>cmdline</code> shows the full command that started it. <code>status</code> has memory use, current state, which user it runs as.</p>
<pre><code class="language-shell">cat /proc/1234/cmdline | tr '\0' ' '
ls -la /proc/1234/fd/
</code></pre>
<p>The kernel stores command arguments separated by null bytes (<code>\0</code>) to avoid confusion with spaces inside arguments. <code>tr</code> swaps those null spaces to make it readable.</p>
<p>When you delete a file while a process holds it open, the data stays on disk. The file is gone from <code>ls</code> but the space is allocated until the process releases its file descriptor. In Linux, file is a link to a block of data, you are only removing the name. If the process has file descriptor open, data block will be kept alive. By redirecting to the FD in <code>/proc</code>, you're telling the kernel to wipe the actual data blocks while leaving the 'ghost' handle intact.<br />Find these with <code>lsof | grep deleted</code>. Fix without restarting the process:</p>
<pre><code class="language-shell">&gt; /proc/1234/fd/4
</code></pre>
<p>That truncates the held-open file to zero bytes through the process's own descriptor. Disk space reclaimed, process uninterrupted.</p>
<hr />
<h2>Routing: where packets actually go</h2>
<p>The routing table is what your machine consults before sending any packet. It decides which network interface and which gateway to use.</p>
<pre><code class="language-shell">ip route show
</code></pre>
<pre><code class="language-shell">default via 192.168.1.1 dev eth0 proto dhcp src 192.168.1.100 metric 100
# local lane
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.100
</code></pre>
<p>The <code>default</code> line handles everything. It says "for any destination I don't have a specific rule for, send it to 192.168.1.1." That is your router. Every internet-bound packet passes through that rule.</p>
<p>Delete it and watch what happens:</p>
<pre><code class="language-shell">ip route del default
ping google.com
</code></pre>
<pre><code class="language-shell">connect: Network is unreachable
</code></pre>
<p>The machine is still on the network. It just cannot reach anything outside the local subnet because there is no default route. This is one of the first things to check when a server cannot reach the internet. The default route may simply not exist, perhaps because a VPN misconfigured it, or because someone ran <code>ip route flush</code> without knowing what it did.</p>
<pre><code class="language-shell">ip route add default via 192.168.1.1   
# put it back

# Most useful debugging command for routing
ip route get 8.8.8.8
</code></pre>
<pre><code class="language-shell">8.8.8.8 via 192.168.1.1 dev eth0 src 192.168.1.100
</code></pre>
<p>One line is the complete answer about which interface and gateway any packet would use to reach any destination.</p>
<hr />
<h2><code>/dev</code>: the devices that are not devices</h2>
<p><code>/dev/null</code>, <code>/dev/zero</code>, <code>/dev/random</code> all live in the same directory as actual hardware. They have the same character device type marker. None of them are physical hardware.</p>
<p><code>/dev/null</code> is a black hole. Write anything to it, and it is gone silently. Read from it, you get immediate end-of-file. This is what <code>command &gt; /dev/null 2&gt;&amp;1</code> does: stdout goes to the black hole, <code>2&gt;&amp;1</code> sends stderr to wherever stdout is going (also the black hole).<br />The numbers represent 'streams.' <code>1</code> is standard output (normal text), and <code>2</code> is standard error. <code>2&gt;&amp;1</code> means, take all the errors and send them into the same pipe where the normal text is going. Since that pipe leads to <code>/dev/null</code>, the command becomes perfectly silent.</p>
<pre><code class="language-shell"># Silence all output but still check if it worked
some_noisy_command &gt; /dev/null 2&gt;&amp;1
echo $?     # 0 means success, anything else means it failed
</code></pre>
<p><code>/dev/zero</code> produces infinite zero bytes. Good for filling disks or creating blank test files:</p>
<pre><code class="language-shell">fallocate -l 500M bigfile   # faster for large files
# or the /dev/zero version:
dd if=/dev/zero of=bigfile bs=1M count=500

df -h    # watch disk fill up
rm bigfile
</code></pre>
<p><code>fallocate</code> is instant because it just tells the filesystem to reserve the space without actually writing anything to the disk. <code>dd if=/dev/zero</code> forces the CPU to actually churn out millions of zeros and write them one by one.</p>
<p><code>/dev/urandom</code> produces cryptographically random bytes collected from hardware entropy:</p>
<pre><code class="language-shell">cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 24
echo
# or the cleaner version:
openssl rand -base64 18
</code></pre>
<p>The thing that made this concrete for me: the same <code>read()</code> system call your code uses to open a text file reads random bytes from <code>/dev/urandom</code>. Same interface but completely different behavior underneath. That is the "everything is a file" idea became real.</p>
<hr />
<h2><code>/boot</code>: where the kernel lives</h2>
<pre><code class="language-shell">ls /boot
</code></pre>
<pre><code class="language-shell">vmlinuz-5.15.0-91-generic
initrd.img-5.15.0-91-generic
grub/
config-5.15.0-91-generic
</code></pre>
<p><code>vmlinuz</code> is the compressed Linux kernel (vm = virtual machine, z- compressed like .zip). The actual operating system, stored as a file. <code>initrd.img</code> is a temporary RAM filesystem that acts as a bridge, which loads before the real filesystem mounts at boot. <code>grub/</code> has the bootloader that lets you choose between operating systems.</p>
<pre><code class="language-shell">cat /boot/config-$(uname -r) | grep CONFIG_EXT4
</code></pre>
<pre><code class="language-shell">CONFIG_EXT4_FS=y
</code></pre>
<p>That file is the kernel build configuration. Every driver, every feature, listed. Most of it is cryptic without context but searching it teaches you what the kernel actually supports.</p>
<p>The only advice here: do not delete anything from <code>/boot</code>. Deleting <code>vmlinuz</code> means the computer cannot boot at all. Recovery requires a live USB. This is the one folder in the filesystem where curiosity should not turn into experiments.</p>
<blockquote>
<p>Consider <code>/etc</code> is the brain and <code>/proc</code> is the nervous system, <code>/boot</code> is the <strong>heart</strong> of Linux.</p>
</blockquote>
<hr />
<h2><code>/var/log</code>: the system's diary</h2>
<p><code>/var/log</code> is where the machine writes what happened. Everything.</p>
<pre><code class="language-shell">ls /var/log
</code></pre>
<pre><code class="language-shell">auth.log    syslog    kern.log    dpkg.log
nginx/      apt/      journal/
</code></pre>
<p><code>auth.log</code> records every login, failed or successful. <code>syslog</code> is the general system log. <code>kern.log</code> has kernel messages. <code>dpkg.log</code> records every package installed or removed and when.</p>
<p>Some logs are plain text files you can <code>cat</code> like <code>syslog</code>, while others live in the <code>journal/</code> directory stores logs in a binary format to make them searchable and faster to filter. The <code>journalctl</code> command is used to read these binary logs.</p>
<pre><code class="language-shell">tail -f /var/log/syslog           # live system events
journalctl -u nginx -f            # live logs for a specific service
journalctl -xe                    # recent journal with explanations
journalctl --since "1 hour ago"   # last hour of everything
</code></pre>
<p>When something breaks, look for the log file for the specific thing that broke. It almost always contains the exact error. If logs kept growing forever, they’d fill your entire disk. Linux uses a tool called <code>logrotate</code> that periodically takes the old log, compresses it in the <code>.gz</code> files, and eventually deletes it</p>
<hr />
<h2><code>/etc/systemd/system/</code> writing a service that survives crashes</h2>
<p>Systemd starts everything on boot and manages services. Unit files in <code>/etc/systemd/system/</code> tell it what to run.</p>
<p>I wanted a script running on boot that restarted if it crashed. Cron can run on boot with <code>@reboot</code> but has no crash recovery. A service file does:</p>
<pre><code class="language-shell">nano /etc/systemd/system/myapp.service
</code></pre>
<pre><code class="language-shell">[Unit]
Description=My app
After=network.target

[Service]
Type=simple
User=saumya
WorkingDirectory=/home/saumya/app
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
</code></pre>
<p><code>After=</code><a href="http://network.target"><code>network.target</code></a> line is a safety check that tells Linux to not start this app until the network is actually up.<br />By default, system services want to run as <code>root</code> and can be a security risk. If the app gets hacked, <code>User=</code> makes sure the attacker is trapped in the user account and doesn't get access to keys. <code>WorkingDirectory</code> ensures that if the code looks for a file in <code>./config</code>, it's looking in the right folder.</p>
<pre><code class="language-shell">systemctl daemon-reload
systemctl enable myapp
systemctl start myapp
systemctl status myapp
</code></pre>
<p><code>Restart=on-failure</code> is the important line. The process crashes, it comes back in 5 seconds because of <code>RestartSec=5</code> which can be adjusted. You stop it with <code>systemctl stop</code>. The distinction between a crash and an intentional stop is handled automatically.</p>
<p>Logs for this service:</p>
<pre><code class="language-shell">journalctl -u myapp -f
</code></pre>
<hr />
<h2><code>PATH</code> why commands vanish when you break one variable</h2>
<p><code>PATH</code> tells the shell where to look for executables. When you type <code>ls</code>, the shell searches every directory in PATH until it finds a file named <code>ls</code>. If PATH is wrong, every command fails.</p>
<pre><code class="language-shell">export PATH=""
ls
</code></pre>
<pre><code class="language-shell">bash: ls: command not found
</code></pre>
<p>Everything is gone. The binaries still exist on disk but the shell just lost its map. It can be fixed with:</p>
<pre><code class="language-shell">export PATH=/usr/bin:/bin:/usr/sbin:/sbin
ls    # back
</code></pre>
<p>Why this matters beyond the experiment: cron jobs run with a stripped PATH by default. If your script calls something in <code>/usr/local/bin</code> and cron's PATH does not include that directory, the cron job silently fails with "command not found" while the exact same script works fine in your terminal. Set PATH explicitly in your crontab:</p>
<pre><code class="language-shell">PATH=/usr/local/bin:/usr/bin:/bin
* * * * * /home/saumya/myscript.sh
</code></pre>
<p>If you’re ever unsure where a command is actually coming from, use <code>which</code>. Running <code>which ls</code> shows the exact path the shell found. It also helps to verify if the map is working.</p>
<hr />
<h2>Permissions</h2>
<p>Permissions in theory is <code>chmod</code>, <code>rwx</code>, octal numbers. In practice I kept getting them wrong until I broke something on purpose.</p>
<pre><code class="language-shell">echo '#!/bin/bash
echo "this ran"' &gt; myscript.sh
./myscript.sh
</code></pre>
<pre><code class="language-shell">bash: ./myscript.sh: Permission denied
</code></pre>
<p>The file exists and the content is correct, but newly created files have no execute permission. Check with:</p>
<pre><code class="language-shell">ls -la myscript.sh
# -rw-r--r-- 1 saumya saumya 30 myscript.sh
</code></pre>
<p>No <code>x</code> anywhere. Add it:</p>
<pre><code class="language-shell">chmod +x myscript.sh
./myscript.sh
# this ran
</code></pre>
<p>Now the more interesting break. Remove execute from <code>ls</code> itself:</p>
<pre><code class="language-shell">chmod -x /bin/ls
ls
</code></pre>
<pre><code class="language-shell">bash: /bin/ls: Permission denied
</code></pre>
<p><code>ls</code> still exists on disk. It just cannot be executed. From the shell's perspective it might as well not be there. Fix:</p>
<pre><code class="language-shell">chmod +x /bin/ls
</code></pre>
<p>The three positions: owner, group, others mean the same permissions behave differently depending on who runs the command. When your web server returns 403 errors and logs say "permission denied," this is why. The files exist, the server is running, but <code>www-data</code> does not have the permission to read them.</p>
<pre><code class="language-shell">chown -R www-data:www-data /var/www/html
chmod -R 755 /var/www/html
</code></pre>
<p>For a file: +x means you can run it; But for a directory: +x means you can view it using <code>cd</code>.<br />Linux works on <strong>Principle of Least Privilege(PoLP)</strong>, hence avoid the temptation to run <code>chmod 777</code>.</p>
<hr />
<h2>Environment variables</h2>
<p>A friend set a variable in <code>~/.bashrc</code>. It worked in his terminal. His systemd service couldn't see it which is followed by hours of debugging.</p>
<p>Every process inherits environment variables from its parent. The terminal is started by the login process, which sources <code>~/.bashrc</code>, which sets your variables. Systemd (PID 1) starts services. Systemd never read your <code>~/.bashrc</code>. The variables your terminal has, the service has never heard of.</p>
<p>Your terminal reads <code>~/.bashrc</code> and <code>/etc/profile</code> and has everything you configured. A cron job reads almost nothing. PATH is <code>/usr/bin:/bin</code>. Your tools in <code>/usr/local/bin</code> are invisible. A systemd service reads only what you explicitly declare in its unit file under <code>[Service]</code>.</p>
<p>For services, declare variables in the unit file:</p>
<pre><code class="language-shell">[Service]
Environment=NODE_ENV=production
EnvironmentFile=/etc/myapp/.env
</code></pre>
<p>For cron, set PATH at the top of the crontab or use absolute paths everywhere. For your own sessions, <code>~/.bashrc</code> works. The confusion only happens when you expect one environment to be visible in another. In Linux, explicit is better than implicit. If a service needs a variable, tell the service directly.</p>
<hr />
<h2>Terminal shortcuts worth memorizing now</h2>
<p><code>Ctrl+R</code> searches command history interactively. Start typing any part of a command you ran before. This is faster than retyping long paths.</p>
<p><code>cd -</code> goes to the last directory you were in. Jump between two locations without retyping either path.</p>
<p><code>!!</code> repeats the last command. The classic use is <code>sudo !!</code> when you forgot sudo.</p>
<p><code>Ctrl+A</code> and <code>Ctrl+E</code> jump to the start and end of the current line. <code>Ctrl+W</code> deletes the last word backwards. These three reduce terminal frustration noticeably.</p>
<p>When a config file edit breaks something and you want to know exactly what changed: <code>diff /etc/nginx/nginx.conf.backup /etc/nginx/nginx.conf</code>. Keep backups before editing important files. <code>cp nginx.conf nginx.conf.bak</code> takes two seconds.</p>
<p>I found two resources that can be helpful in the learning curve of Linux:</p>
<ul>
<li><p><a href="https://labex.io/linuxjourney">Linux Journey</a> : This has organized learning path that breaks down the complexities of the Linux operating system into digestible stages.</p>
</li>
<li><p><a href="https://overthewire.org/wargames/bandit/">Bandit</a> : It is wargame designed to teach the fundamentals of the Linux command line. Visually looks boring, but is really interesting to follow through.</p>
</li>
</ul>
<hr />
<h3>References</h3>
<ul>
<li><p><a href="https://www.youtube.com/watch?v=LKCVKw9CzFo">Fireship: Linux in 100 Seconds</a> : start here if you haven't</p>
</li>
<li><p><a href="https://man7.org/linux/man-pages/">Linux man pages</a> : every file and command has one</p>
</li>
<li><p><a href="https://tldr.sh/">tldr pages</a> : install with <code>apt install tldr</code>, use it for every new command</p>
</li>
<li><p><a href="https://wiki.archlinux.org/">Arch Wiki</a> : the best Linux documentation on the internet, works for all distros regardless of what you're running</p>
</li>
<li><p><a href="https://explainshell.com/">explainshell.com</a> : paste any command and it breaks down every flag visually</p>
</li>
<li><p><a href="https://www.kernel.org/doc/html/latest/filesystems/index.html">The filesystem docs</a> : official kernel documentation</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Error Handling]]></title><description><![CDATA[Every developer learns this eventually, usually at the worst possible time. You write something, it works on your machine, you ship it, and then a user does something you didn't predict, types letters]]></description><link>https://blog.saumyagrawal.in/error-handling</link><guid isPermaLink="true">https://blog.saumyagrawal.in/error-handling</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[ChaiCode]]></category><dc:creator><![CDATA[Saumya]]></dc:creator><pubDate>Thu, 26 Mar 2026 07:18:50 GMT</pubDate><content:encoded><![CDATA[<p>Every developer learns this eventually, usually at the worst possible time. You write something, it works on your machine, you ship it, and then a user does something you didn't predict, types letters into a number field, loses their internet connection mid-request, passes in an empty string where you expected an array. The code falls over.</p>
<p>The mistake isn't that the code broke. That's going to happen. The mistake is not planning for it. A user who hits an unhandled error sees nothing useful. You, the developer, also see nothing useful, no message, no context, no idea what went wrong. Compare that to code that catches the error, logs it properly, and shows the user something sensible. Same breakage, completely different experience for everyone involved.</p>
<p>Before getting into how to handle errors, it's worth knowing the types you'll actually run into. There are three that show up constantly.</p>
<p><strong>ReferenceError:</strong> You used a variable that doesn't exist. Usually a typo or a scoping problem.</p>
<p><strong>TypeError:</strong> You tried to call something that isn't a function, or access a property on <code>null</code> or <code>undefined</code>.</p>
<p><strong>SyntaxError:</strong> The code itself is malformed. A missing bracket, a bad JSON string. Usually caught before the script even runs.</p>
<p><strong>RangeError:</strong> A value is outside an acceptable range.</p>
<p><strong>NetworkError:</strong> Not a built-in JS type, but the most common runtime failure. The fetch failed. The API timed out. The user went offline.</p>
<h2><strong>Try and Catch</strong></h2>
<p>The fix for unhandled errors is a <code>try...catch</code> block. Put the code that might fail inside <code>try</code>. If it throws, JavaScript jumps straight to <code>catch</code> and hands you the error object. The rest of <code>try</code> doesn't run and nothing crashes.</p>
<pre><code class="language-javascript">try {
  // This will throw a ReferenceError
  const result = undeclaredVariable * 2;
  console.log(result); // never runs
} catch (err) {
  // err is the Error object JavaScript created
  console.log(err.name);    // "ReferenceError"
  console.log(err.message); // "undeclaredVariable is not defined"
}

// Code runs normally
console.log("we're still going");
</code></pre>
<p>The error object always has two properties you'll use: <code>name</code> tells you the type of error, and <code>message</code> tells you what went wrong. In modern environments there's also <code>stack</code>, which gives you the full call trace. That one is invaluable when you're debugging something three functions deep.</p>
<p>One thing that trips people up: <code>try...catch</code> only works on runtime errors. If your code has a syntax error, JavaScript never runs the block at all. And if you're working with async code, a plain <code>try...catch</code> won't catch promise rejections unless you're inside an <code>async</code> function. There's a separate section on that below.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/b943c6cc-f0b9-4d8b-b84c-c9368d4c140e.png" alt="" style="display:block;margin:0 auto" />

<h2><strong>The Finally Block</strong></h2>
<p>There's a third part to the pattern that a lot of people skip until they actually need it. <code>finally</code> runs no matter what. Whether the <code>try</code> succeeded, whether <code>catch</code> ran, whether someone threw again inside <code>catch</code>. It doesn't matter. <code>finally</code> runs.</p>
<p>The use case is cleanup. If you opened a database connection, you need to close it. If you set a loading spinner to visible, you need to hide it. These things should happen regardless of whether the operation worked. That's exactly what <code>finally</code> is for.</p>
<pre><code class="language-javascript">async function loadUserData(id) {
  showSpinner(); // loading starts

  try {
    const res  = await fetch(`/api/users/${id}`);
    const data = await res.json();
    renderProfile(data);
  } catch (err) {
    showErrorMessage("Couldn't load profile. Try again.");
    console.error(err);
  } finally {
    hideSpinner(); // always runs 
  }
}
</code></pre>
<p>Without <code>finally</code>, you'd have to call <code>hideSpinner()</code> in both the success path and the <code>catch</code> block. That's easy to forget, especially when the function grows. <code>finally</code> means you write that cleanup logic once and it always runs.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/8cc91f93-151f-41e0-a96a-2fecb4789658.png" alt="" style="display:block;margin:0 auto" />

<p>A good mental model: <code>try</code> is the attempt, <code>catch</code> is the fallback, <code>finally</code> is the cleanup.</p>
<h2><strong>Throwing Custom Errors</strong></h2>
<p>JavaScript lets you throw errors yourself with the <code>throw</code> keyword. You can technically throw anything, a string, a number, an object. In practice, always throw an <code>Error</code> object. That's because <code>Error</code> objects include a stack trace automatically, which is the thing you actually want when debugging.</p>
<pre><code class="language-javascript">function divide(a, b) {
  if (b === 0) {
    throw new Error("Division by zero is not allowed");
  }
  return a / b;
}

try {
  divide(10, 0);
} catch (err) {
  console.log(err.message); // "Division by zero is not allowed"
}
</code></pre>
<p>For bigger applications, you'll want to extend <code>Error</code> with custom classes. This way a <code>catch</code> block can check what kind of error it got and respond differently, show a 404 UI for not-found errors, show a login prompt for auth errors, and so on.</p>
<pre><code class="language-javascript">class NotFoundError extends Error { 
    constructor(resource) { 
        super(${resource} not found); 
        this.name = "NotFoundError"; 
        this.resource = resource; 
      } 
}
class AuthError extends Error { 
    constructor(msg) { 
        super(msg); 
        this.name = "AuthError";
    }
}
async function getPost(id) { 
    const res = await fetch(/api/posts/${id});

    if (res.status === 401) throw new AuthError("Session expired"); 
    if (res.status === 404) throw new NotFoundError("post");
    return res.json(); 
}

try { 
    await getPost(99); 
} catch (err) { 
    if (err instanceof AuthError) redirectToLogin(); 
    if (err instanceof NotFoundError) show404Page(); 
    else showGenericError(err.message); 
}
</code></pre>
<p>The <code>instanceof</code> check is what makes this pattern useful. One <code>catch</code> block, multiple kinds of failure, each handled differently. And if something unexpected comes through, the <code>else</code> branch catches it so nothing goes silently missing.</p>
<h2><strong>Why This Matters</strong></h2>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/2490cecc-5c50-4918-a358-916081994389.png" alt="" style="display:block;margin:0 auto" />

<ul>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch">MDN Web Docs : try...catch</a></p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error">MDN Web Docs : Error object</a></p>
</li>
<li><p><a href="https://javascript.info/try-catch">javascript.info : Error handling, "try...catch"</a></p>
</li>
<li><p><a href="https://javascript.info/custom-errors">javascript.info : Custom errors, extending Error</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Callback Functions]]></title><description><![CDATA[Before callbacks make sense, one thing has to click: in JavaScript, a function is a value. Like a number. Like a string. You can store it in a variable, pass it to another function, even return it fro]]></description><link>https://blog.saumyagrawal.in/callback-functions</link><guid isPermaLink="true">https://blog.saumyagrawal.in/callback-functions</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[ChaiCode]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[Saumya]]></dc:creator><pubDate>Wed, 25 Mar 2026 16:44:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/a93e2680-813f-4ccc-bd11-412ac673164d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Before callbacks make sense, one thing has to click: in JavaScript, a function is a value. Like a number. Like a string. You can store it in a variable, pass it to another function, even return it from one. Most people know this in theory and then forget it the moment they try to use it.</p>
<p>Here's what that looks like in practice:</p>
<pre><code class="language-javascript">// Storing a function in a variable 
const greet = function(name) {
  console.log("Hello, " + name);
};

// Passing that variable to another function
function run(fn) {
  fn("Aditya"); // calls whatever was passed in
}

run(greet); // prints: Hello, Aditya
</code></pre>
<p><code>greet</code> is just a value here. We handed it to <code>run</code>, and <code>run</code> called it. That's the whole mechanic. A callback is just a function passed to another function so that other function can call it later.</p>
<p>The word "callback" sounds formal, but the idea is simple: you're giving someone your phone number and saying "call me back when you're done." The function you pass is that phone number.</p>
<h2><strong>Passing Functions as Arguments</strong></h2>
<p>You've probably used callbacks already without noticing. <code>addEventListener</code>, <code>setTimeout</code>, <code>Array.forEach</code>, all of these take a function as an argument. That function is the callback.</p>
<pre><code class="language-javascript">// setTimeout takes a callback: runs it after 2 seconds
setTimeout(function() {
  console.log("Two seconds later");
}, 2000);

// forEach takes a callback: runs it once per item
["Manas", "Saumya", "Priya"].forEach(function(name) {
  console.log(name);
});

// addEventListener takes a callback: runs it on click
button.addEventListener("click", function() {
  console.log("Clicked");
});
</code></pre>
<p>Notice the pattern. You pass a function, something else holds onto it, and calls it at the right moment. You are not in charge of <em>when</em> it runs. That's kind of the point.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/e97ae593-2727-449f-bf5e-1433d09e12ca.png" alt="" style="display:block;margin:0 auto" />

<h2><strong>Why Async Even Needs Callbacks</strong></h2>
<p>JavaScript runs one thing at a time. There is no threading, no parallel execution of your code. One call stack, one line at a time. So when you need to wait for something, a network request, a file read, a timer, you have a problem: if you wait, everything else stops.</p>
<p>Think about a restaurant. The chef doesn't stand at the kitchen pass staring at your food until you come collect it. They put it on the counter, ring a bell, and move on to the next order. You come when you're ready. The kitchen keeps running.</p>
<p>Callbacks are that bell. You hand the runtime a function and say: when the thing is done, run this. Meanwhile, the rest of your code keeps going.</p>
<pre><code class="language-javascript">// SYNCHRONOUS: second log waits for the whole loop
console.log("start");
for (let i = 0; i &lt; 1000000000; i++) {} // browser hangs here
console.log("end");

// ASYNC WITH CALLBACK: second log runs immediately
console.log("start");
setTimeout(function() {
  console.log("I ran later"); // runs after 2s, non-blocking
}, 2000);
console.log("end"); // this prints before the callback
</code></pre>
<p>Running that second block, you'll see "start", then "end", then two seconds later "I ran later". The callback didn't block anything.</p>
<h2><strong>Common Places Callbacks Show Up</strong></h2>
<p>Callbacks are everywhere. Once you start seeing the pattern, you can't unsee it. Here are the places you'll hit them most as a beginner.</p>
<pre><code class="language-javascript">// array methods: forEach, filter, map
const scores = [42, 88, 61, 95, 37];

// forEach: run callback for each item
scores.forEach(function(score) {
  console.log(score);
});

// filter: keep items where callback returns true
const passing = scores.filter(function(score) {
  return score &gt;= 60;
}); // [88, 61, 95]

// map: transform each item, return new array
const grades = scores.map(function(score) {
  return score &gt;= 60 ? "Pass" : "Fail";
}); // ["Fail", "Pass", "Pass", "Pass", "Fail"]
</code></pre>
<pre><code class="language-javascript">// Event Listeners
const btn = document.getElementById("submit");

// callback runs every time the button is clicked
btn.addEventListener("click", function(event) {
  event.preventDefault();
  console.log("Form submitted");
});
</code></pre>
<pre><code class="language-javascript">// fetch API
// fetch uses .then()  which takes a callback
fetch("/api/users/1")
  .then(function(response) {
    return response.json(); // callback 1: parse response
  })
  .then(function(data) {
    console.log(data.name); // callback 2: use the data
  });
</code></pre>
<p>Arrow functions are common shorthand for callbacks: <code>scores.filter(s =&gt; s &gt;= 60)</code> does the same thing as the <code>function</code> version above. Same idea, fewer keystrokes.</p>
<h2><strong>Writing Your Own Function That Takes a Callback</strong></h2>
<p>It helps to write one yourself, even if it's small. Once you've built a function that accepts and calls a callback, the whole model locks in.</p>
<pre><code class="language-javascript">// greetUser takes a name and a callback
function greetUser(name, callback) {
  console.log("Fetching user data...");
  callback(name); // call whatever was passed in
}

// Two different behaviours, same greetUser function
greetUser("Saumya", function(name) {
  console.log("Hello, " + name + "!"); // "Hello, Saumya!"
});

greetUser("Manas", function(name) {
  console.log("Welcome back, " + name); // "Welcome back, Manas"
});
</code></pre>
<p><code>greetUser</code> doesn't care what the callback does. It just calls it with the name. The caller decides the behaviour. That flexibility is exactly why callbacks are so useful — you write the structure once, plug in different logic as needed.</p>
<p>A real-world version of this is <code>Array.sort</code>. The sort algorithm is built into JavaScript, but you pass a callback that defines how to compare two items. You bring the comparison logic; sort brings the algorithm.</p>
<pre><code class="language-javascript">// sort with custom comparator callback
const users = [
  { name: "Manas",  age: 28 },
  { name: "Saumya", age: 24 },
  { name: "Priya",  age: 31 },
];

// sort needs to know HOW to compare — you provide that
users.sort(function(a, b) {
  return a.age - b.age; // youngest first
});
</code></pre>
<h2><strong>The Nesting Problem</strong></h2>
<p>Here's where callbacks get genuinely painful. Suppose you need to do three async things in sequence: fetch a user, then fetch their orders, then fetch the details of the first order. Each step depends on the previous one. With callbacks, you end up putting them inside each other.</p>
<pre><code class="language-javascript">getUser(1, function(user) {

  getOrders(user.id, function(orders) {

    getOrderDetails(orders[0].id, function(details) {

      console.log(details); // finally, the thing we wanted

    });
  });
});
</code></pre>
<p>Three levels. Now add error handling at each step. Add a fourth request. The indentation becomes a visual pyramid, and the logic is buried inside it. This has a name: callback hell.</p>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/61b2e9ce-8cc0-44d1-8ccb-c43a42177a18.png" alt="" style="display:block;margin:0 auto" />

<p>The nesting itself is not wrong, it works. The problem is readability. Errors are hard to catch at the right level. Adding a step means nesting deeper. Debugging means tracing through a maze of closing braces. It gets old fast.</p>
<p>Callback hell is why Promises and async/await exist. They solve the same coordination problem with much flatter code. That's the next part of this series.</p>
<h2><strong>What Callbacks Actually Give You</strong></h2>
<img src="https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/b5139a60-6750-4aee-ac1a-4a36a6ad6141.png" alt="" style="display:block;margin:0 auto" />

<p><strong>REFERENCES:</strong></p>
<ul>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Glossary/Callback_function">MDN Web Docs : Callback function</a></p>
</li>
<li><p>JavaScript.info : Introduction: callb<a href="https://javascript.info/callbacks">JavaScript.info : Introduction: callbacks</a></p>
</li>
<li><p><a href="https://nodejs.org/en/learn/asynchronous-work/javascript-asynchronous-programming-and-callbacks">JavaScript Asynchronous Programming and Callbacks</a></p>
</li>
<li><p><a href="http://callbackhell.com/">Callback Hell</a></p>
</li>
</ul>
]]></content:encoded></item></channel></rss>