<?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[Nitesh's Blogs]]></title><description><![CDATA[MERN Stack developer. Techie nerd, love coding, gaming. I use Vim BTW.]]></description><link>https://blog.thecodingant.in</link><image><url>https://cdn.hashnode.com/uploads/logos/69d007f5e466e2b7625cd1df/279f65fe-bb2e-4cb3-9060-4857910259cd.png</url><title>Nitesh&apos;s Blogs</title><link>https://blog.thecodingant.in</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 09 Apr 2026 09:57:16 GMT</lastBuildDate><atom:link href="https://blog.thecodingant.in/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Git Basics You Actually Need to Know - Part 1]]></title><description><![CDATA[You've cloned a repo, made some changes, and now you're staring at the terminal wondering what comes next. git add ., git commit -m "fix", git push — you've copy-pasted these commands a hundred times,]]></description><link>https://blog.thecodingant.in/git-basics-you-actually-need-to-know-part-1</link><guid isPermaLink="true">https://blog.thecodingant.in/git-basics-you-actually-need-to-know-part-1</guid><category><![CDATA[GitHub]]></category><category><![CDATA[Git]]></category><category><![CDATA[versioncontrol]]></category><category><![CDATA[version control]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Programming Tips]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Nitesh Kumar Tudu]]></dc:creator><pubDate>Thu, 09 Apr 2026 05:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/0db8ade6-f435-4b2b-8643-8f50cb9613e9.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You've cloned a repo, made some changes, and now you're staring at the terminal wondering what comes next. <code>git add .</code>, <code>git commit -m "fix"</code>, <code>git push</code> — you've copy-pasted these commands a hundred times, but do you actually know what they're doing?</p>
<p>Let's fix that. This is Part 1 of understanding Git from the ground up — the commands you use every day, what they actually do, and when to use them.</p>
<h2>How Git Actually Works</h2>
<p>Before touching any commands, you need to understand Git's mental model. Git has four places your code can exist:</p>
<ol>
<li><p><strong>Working Directory</strong> - Your actual files, the ones you edit</p>
</li>
<li><p><strong>Staging Area (Index)</strong> - Changes you've marked to commit</p>
</li>
<li><p><strong>Local Repository</strong> - Commits on your machine</p>
</li>
<li><p><strong>Remote Repository</strong> - Commits on GitHub/GitLab</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/230146c9-dfe9-4744-9f4f-e937e7e8a2d3.svg" alt="" style="display:block;margin:0 auto" />

<p>Every Git command moves your code between these areas. Once you see this, Git stops being magic.</p>
<h2>Starting a Repository: git init</h2>
<p>Creating a new Git repository is simple:</p>
<pre><code class="language-bash">git init
</code></pre>
<p>This creates a hidden <code>.git</code> folder in your current directory. That folder contains the entire history of your project. Delete it and your Git history is gone — but your files remain.</p>
<p>When you clone a repository, <code>git clone</code> does <code>git init</code> plus downloads all the commits from the remote:</p>
<pre><code class="language-bash">git clone https://github.com/user/repo.git
</code></pre>
<p>This creates a new folder, initializes Git, downloads the history, and checks out the default branch (usually <code>main</code> or <code>master</code>).</p>
<h2>Understanding Commits</h2>
<p>A commit is a snapshot of your code at a specific moment. Not a diff, not changes — a complete snapshot. Git is smart enough to store this efficiently, but conceptually, each commit contains the full state of your project.</p>
<p>Every commit has:</p>
<ul>
<li><p>A unique SHA hash (like <code>a3f2d91</code>)</p>
</li>
<li><p>A parent commit (except the first commit)</p>
</li>
<li><p>Author and timestamp</p>
</li>
<li><p>A commit message</p>
</li>
<li><p>A complete snapshot of all tracked files</p>
</li>
</ul>
<p>Commits form a chain:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/3f6ecd95-6a65-48d7-a8dc-c46fa775c42b.svg" alt="" style="display:block;margin:0 auto" />

<h2>The Three-Step Commit Process</h2>
<p>Making a commit is actually three separate actions:</p>
<h3>Step 1: Edit Files (Working Directory)</h3>
<p>You change a file. Git sees it as "modified" but hasn't done anything with it yet.</p>
<pre><code class="language-bash"># Check what changed
git status
</code></pre>
<p>Output:</p>
<pre><code class="language-plaintext">Changes not staged for commit:
  modified:   src/app.js
</code></pre>
<h3>Step 2: Stage Changes (Staging Area)</h3>
<p>You mark which changes should go into the next commit:</p>
<pre><code class="language-bash"># Stage a specific file
git add src/app.js

# Stage all changes
git add .

# Stage all changes in current directory
git add -A
</code></pre>
<p>Now <code>git status</code> shows:</p>
<pre><code class="language-plaintext">Changes to be committed:
  modified:   src/app.js
</code></pre>
<h3>Step 3: Commit (Local Repository)</h3>
<p>You save the staged changes as a commit:</p>
<pre><code class="language-bash">git commit -m "Add user authentication"
</code></pre>
<p>Your changes are now permanently saved in Git's history. But they're still only on your machine.</p>
<p><strong>Shortcut:</strong> Stage and commit modified files in one step (doesn't work for new files):</p>
<pre><code class="language-bash">git commit -am "Update user model"
</code></pre>
<h2>Branches: Parallel Universes</h2>
<p>A branch is just a pointer to a commit. That's it. When you create a branch, Git creates a new pointer. When you commit, the pointer moves forward.</p>
<pre><code class="language-bash"># Create a new branch
git branch feature/login

# Switch to it
git checkout feature/login

# Or do both at once
git checkout -b feature/login
</code></pre>
<p>Modern Git also has:</p>
<pre><code class="language-bash">git switch feature/login        # Switch branches
git switch -c feature/login     # Create and switch
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/af529c21-3c93-40c1-bac3-1e547972affe.svg" alt="" style="display:block;margin:0 auto" />

<p>When you're on a branch and make a commit, only that branch pointer moves. Other branches stay where they are. This lets you work on multiple features simultaneously without them interfering.</p>
<p><strong>List all branches:</strong></p>
<pre><code class="language-bash">git branch           # Local branches only
git branch -a        # Include remote branches
</code></pre>
<p><strong>Delete a branch:</strong></p>
<pre><code class="language-bash">git branch -d feature/login      # Safe delete (won't delete if unmerged)
git branch -D feature/login      # Force delete
</code></pre>
<h2>Connecting to Remote: git remote</h2>
<p>A remote is a URL where your repository lives online (GitHub, GitLab, etc). When you clone, Git automatically adds a remote called <code>origin</code>.</p>
<pre><code class="language-bash"># View remotes
git remote -v
</code></pre>
<p>Output:</p>
<pre><code class="language-plaintext">origin  https://github.com/user/repo.git (fetch)
origin  https://github.com/user/repo.git (push)
</code></pre>
<p><strong>Add a remote manually:</strong></p>
<pre><code class="language-bash">git remote add origin https://github.com/user/repo.git
</code></pre>
<p><strong>Change remote URL:</strong></p>
<pre><code class="language-bash">git remote set-url origin https://github.com/user/new-repo.git
</code></pre>
<p><strong>Remove a remote:</strong></p>
<pre><code class="language-bash">git remote remove origin
</code></pre>
<p>You can have multiple remotes. Common pattern for open source:</p>
<pre><code class="language-bash">git remote add upstream https://github.com/original/repo.git
git remote add origin https://github.com/yourfork/repo.git
</code></pre>
<p>Now you can pull from <code>upstream</code> (the original project) and push to <code>origin</code> (your fork).</p>
<h2>Pushing and Pulling</h2>
<p>Push sends your commits to the remote. Pull downloads commits from the remote.</p>
<p><strong>Push to remote:</strong></p>
<pre><code class="language-bash"># First time on a new branch
git push -u origin feature/login

# After that, just
git push
</code></pre>
<p>The <code>-u</code> flag sets the upstream tracking relationship. After that, Git knows where to push when you type <code>git push</code>.</p>
<p><strong>Pull from remote:</strong></p>
<pre><code class="language-bash">git pull
</code></pre>
<p>This does two things:</p>
<ol>
<li><p><code>git fetch</code> - Downloads commits from remote</p>
</li>
<li><p><code>git merge</code> - Merges them into your current branch</p>
</li>
</ol>
<p><strong>Fetch without merging:</strong></p>
<pre><code class="language-bash">git fetch origin
</code></pre>
<p>This updates your local knowledge of what's on the remote, but doesn't change your working directory. Useful when you want to see what changed before merging.</p>
<h2>Git Reset: Undoing Things</h2>
<p>Reset moves the branch pointer backward. It comes in three flavors:</p>
<h3>git reset --soft</h3>
<p>Moves the branch pointer, keeps staging area and working directory:</p>
<pre><code class="language-bash">git reset --soft HEAD~1
</code></pre>
<p>This undoes the last commit but keeps your changes staged. Useful when you committed too early or want to recommit with a better message.</p>
<p><strong>Use case:</strong> You committed "WIP" and want to add more changes to that commit:</p>
<pre><code class="language-bash">git reset --soft HEAD~1
# Make more changes
git add .
git commit -m "Complete feature implementation"
</code></pre>
<h3>git reset --mixed (default)</h3>
<p>Moves the branch pointer, unstages changes, keeps working directory:</p>
<pre><code class="language-bash">git reset HEAD~1
# Same as: git reset --mixed HEAD~1
</code></pre>
<p>This undoes the commit and unstages the changes, but your file edits remain. The most common reset type.</p>
<p><strong>Use case:</strong> You committed the wrong files:</p>
<pre><code class="language-bash">git reset HEAD~1
# Now selectively stage the right files
git add src/correct-file.js
git commit -m "Fix authentication bug"
</code></pre>
<h3>git reset --hard</h3>
<p>Moves the branch pointer, clears staging area, resets working directory:</p>
<pre><code class="language-bash">git reset --hard HEAD~1
</code></pre>
<p>⚠️ <strong>Danger zone:</strong> This deletes your changes. They're gone. Use with caution.</p>
<p><strong>Use case:</strong> You completely messed up and want to start over:</p>
<pre><code class="language-bash">git reset --hard HEAD  # Discard all uncommitted changes
git reset --hard origin/main  # Match remote exactly
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/c4547444-f1b9-4c17-86a3-7f0d873c1afe.svg" alt="" style="display:block;margin:0 auto" />

<p><strong>HEAD~1 vs HEAD^:</strong></p>
<ul>
<li><p><code>HEAD~1</code> - One commit before HEAD</p>
</li>
<li><p><code>HEAD~2</code> - Two commits before HEAD</p>
</li>
<li><p><code>HEAD^</code> - First parent (same as HEAD~1 for linear history)</p>
</li>
</ul>
<h2>Checking History</h2>
<p>See what commits exist:</p>
<pre><code class="language-bash"># Basic log
git log

# One line per commit
git log --oneline

# With branch graph
git log --oneline --graph --all

# Last 5 commits
git log -5

# Commits by author
git log --author="John"

# Commits in date range
git log --since="2 weeks ago"
</code></pre>
<p><strong>See what changed in a commit:</strong></p>
<pre><code class="language-bash">git show a3f2d91
</code></pre>
<p><strong>See what changed in a file:</strong></p>
<pre><code class="language-bash">git log -p src/app.js
</code></pre>
<h2>Practical Workflows</h2>
<h3>Starting a New Feature</h3>
<pre><code class="language-bash"># Make sure you're on main and up to date
git checkout main
git pull

# Create feature branch
git checkout -b feature/user-profile

# Work and commit
git add src/profile.js
git commit -m "Add user profile component"

# Push to remote
git push -u origin feature/user-profile
</code></pre>
<h3>Fixing a Mistake in Last Commit</h3>
<p><strong>Wrong commit message:</strong></p>
<pre><code class="language-bash">git commit --amend -m "Better commit message"
</code></pre>
<p><strong>Forgot to add a file:</strong></p>
<pre><code class="language-bash">git add forgotten-file.js
git commit --amend --no-edit
</code></pre>
<p>The <code>--amend</code> flag replaces the last commit. Don't amend commits that you've already pushed to a shared branch — it rewrites history.</p>
<h3>Discarding Uncommitted Changes</h3>
<p><strong>Discard changes in one file:</strong></p>
<pre><code class="language-bash">git checkout -- src/app.js
# Or in modern Git:
git restore src/app.js
</code></pre>
<p><strong>Discard all changes:</strong></p>
<pre><code class="language-bash">git reset --hard HEAD
</code></pre>
<p><strong>Unstage a file (keep changes):</strong></p>
<pre><code class="language-bash">git reset HEAD src/app.js
# Or:
git restore --staged src/app.js
</code></pre>
<h2>What We Covered</h2>
<p>You now understand:</p>
<ul>
<li><p>Git's four areas: working directory, staging, local repo, remote repo</p>
</li>
<li><p>How commits form a chain of snapshots</p>
</li>
<li><p>Creating and switching branches</p>
</li>
<li><p>The three-step commit process (edit, stage, commit)</p>
</li>
<li><p>Connecting to remotes and pushing/pulling</p>
</li>
<li><p>The three types of reset and when to use each</p>
</li>
<li><p>Basic history exploration</p>
</li>
</ul>
<p>In Part 2, we'll cover merging, rebasing, handling conflicts, stashing, and recovering lost commits.</p>
<h2>Common Mistakes to Avoid</h2>
<p><strong>Committing to main directly:</strong> Always create a feature branch. Keep main clean.</p>
<p><strong>Not pulling before pushing:</strong> Always pull before starting work. Prevents conflicts.</p>
<p><strong>Using</strong> <code>reset --hard</code> <strong>carelessly:</strong> Your changes are gone. Make sure you want that.</p>
<p><strong>Amending pushed commits:</strong> Rewrites history. Only amend commits that haven't been pushed.</p>
<p><strong>Forgetting to track new files:</strong> <code>git add .</code> includes new files. <code>git commit -am</code> doesn't.</p>
<hr />
<h2>TLDR;</h2>
<p>Git has four areas: working directory (your files), staging area (changes marked for commit), local repository (commits on your machine), and remote repository (commits on GitHub/GitLab). The basic workflow is: edit files → <code>git add</code> to stage → <code>git commit</code> to save → <code>git push</code> to upload. Branches are pointers to commits — create them with <code>git checkout -b branch-name</code>. Connect to remotes with <code>git remote add origin url</code>. Reset moves the branch pointer: <code>--soft</code> keeps changes staged, <code>--mixed</code> (default) unstages them, <code>--hard</code> deletes everything. Use <code>git log</code> to see history. Always pull before pushing. Never reset hard unless you want to lose changes. Part 2 will cover merging, rebasing, and conflict resolution.</p>
]]></content:encoded></item><item><title><![CDATA[JSDoc: The TypeScript Experience Without TypeScript]]></title><description><![CDATA[Remember that time you called a function, forgot what parameters it needed, then had to scroll up 200 lines to check? Or when you refactored a function and broke 12 files because nothing told you wher]]></description><link>https://blog.thecodingant.in/jsdoc-the-typescript-experience-without-typescript</link><guid isPermaLink="true">https://blog.thecodingant.in/jsdoc-the-typescript-experience-without-typescript</guid><category><![CDATA[jsdocs]]></category><category><![CDATA[js autocomplete]]></category><category><![CDATA[js documentation]]></category><category><![CDATA[webdevelopment]]></category><category><![CDATA[webdev]]></category><category><![CDATA[Developer Tools]]></category><dc:creator><![CDATA[Nitesh Kumar Tudu]]></dc:creator><pubDate>Wed, 08 Apr 2026 05:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/c6636be0-4499-41f5-88e8-b9bf30bc90c6.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Remember that time you called a function, forgot what parameters it needed, then had to scroll up 200 lines to check? Or when you refactored a function and broke 12 files because nothing told you where it was being used incorrectly?</p>
<p>JSDoc fixes this. It gives you TypeScript-level autocomplete, type checking, and documentation — all in plain JavaScript. No build step, no configuration, just comments.</p>
<h2>The Problem with Undocumented Code</h2>
<p>Here's a function without documentation:</p>
<pre><code class="language-javascript">function calculatePrice(item, quantity, discount) {
  const subtotal = item.price * quantity;
  const discountAmount = subtotal * (discount / 100);
  return subtotal - discountAmount;
}
</code></pre>
<p>When you call this function three months later, you have questions: Is <code>discount</code> a percentage (15) or a decimal (0.15)? Is <code>item</code> an object or just the price? What shape does it have?</p>
<p>Here's the same function with JSDoc:</p>
<pre><code class="language-javascript">/**
 * Calculate final price after discount
 * @param {Object} item - The product item
 * @param {number} item.price - Unit price
 * @param {string} item.name - Product name
 * @param {number} quantity - Number of items
 * @param {number} discount - Discount percentage (0-100)
 * @returns {number} Final price after discount
 */
function calculatePrice(item, quantity, discount) {
  const subtotal = item.price * quantity;
  const discountAmount = subtotal * (discount / 100);
  return subtotal - discountAmount;
}
</code></pre>
<p>Now when you type <code>calculatePrice(</code>, your editor shows you exactly what each parameter needs. No guessing. No context switching.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/d68e2140-df11-4f08-b60a-0c4c0892d8e0.png" alt="" style="display:block;margin:0 auto" />

<h2>Custom Types with @typedef</h2>
<p>For objects you use frequently, define them once and reuse everywhere:</p>
<pre><code class="language-javascript">/**
 * @typedef {Object} User
 * @property {string} id - User ID
 * @property {string} email - User email
 * @property {string} name - User name
 * @property {string[]} roles - User roles
 * @property {Date} createdAt - Account creation date
 */

/**
 * @typedef {Object} AuthToken
 * @property {string} token - JWT token
 * @property {Date} expiresAt - Token expiration
 */

/**
 * Authenticate user and return token
 * @param {string} email - User's email
 * @param {string} password - User's password
 * @returns {Promise&lt;{user: User, auth: AuthToken}&gt;}
 */
async function login(email, password) {
  const user = await User.findOne({ email });
  if (!user) throw new Error('User not found');
  
  const valid = await bcrypt.compare(password, user.password);
  if (!valid) throw new Error('Invalid password');
  
  const token = jwt.sign({ userId: user.id }, SECRET);
  
  return {
    user: {
      id: user.id,
      email: user.email,
      name: user.name,
      roles: user.roles,
      createdAt: user.createdAt
    },
    auth: {
      token,
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
    }
  };
}
</code></pre>
<p>When you call <code>login()</code>, your editor knows the returned object has <code>user</code> and <code>auth</code> properties with their full structure. Type <code>user.</code> and see all five properties. Type <code>auth.</code> and see token and expiration.</p>
<h2>Importing Types from Libraries</h2>
<p>You don't have to define everything yourself. Import types from npm packages:</p>
<pre><code class="language-javascript">/**
 * @typedef {import('express').Request} Request
 * @typedef {import('express').Response} Response
 * @typedef {import('express').NextFunction} NextFunction
 */

/**
 * Get user by ID
 * @param {Request} req - Express request
 * @param {Response} res - Express response
 * @returns {Promise&lt;void&gt;}
 */
async function getUser(req, res) {
  const userId = req.params.id;
  const user = await User.findById(userId);
  
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  res.json(user);
}
</code></pre>
<p>Type <code>req.</code> and your editor suggests <code>params</code>, <code>query</code>, <code>body</code>, <code>headers</code> — everything Express provides. Same with <code>res.status()</code>, <code>res.json()</code>, <code>res.send()</code>.</p>
<h2>Type Checking with @ts-check</h2>
<p>Add one comment at the top of your file and get TypeScript error checking:</p>
<pre><code class="language-javascript">// @ts-check

/**
 * @param {number} a
 * @param {number} b
 * @returns {number}
 */
function add(a, b) {
  return a + b;
}

const result = add(5, '10'); // ❌ Error: Argument of type 'string' is not assignable to parameter of type 'number'
</code></pre>
<p>Your editor warns you about type mismatches before you run the code. No TypeScript compiler needed.</p>
<p>This catches bugs at write-time:</p>
<pre><code class="language-javascript">// @ts-check

/**
 * @typedef {Object} Product
 * @property {string} id
 * @property {string} name
 * @property {number} price
 */

/**
 * @param {Product[]} products
 * @returns {number}
 */
function calculateTotal(products) {
  return products.reduce((sum, product) =&gt; sum + product.price, 0);
}

const products = [
  { id: '1', name: 'Book', price: 20 },
  { id: '2', name: 'Pen', price: 5 },
  { id: '3', name: 'Notebook', price: '15' } // ❌ Error: Type 'string' is not assignable to type 'number'
];

calculateTotal(products);
</code></pre>
<p>The error shows up immediately in your editor. Not in production. Not in testing. Right here.</p>
<h2>Enums and Literal Types</h2>
<p>Document allowed values to prevent invalid inputs:</p>
<pre><code class="language-javascript">/**
 * @typedef {'pending' | 'active' | 'completed' | 'cancelled'} OrderStatus
 */

/**
 * Update order status
 * @param {string} orderId - Order ID
 * @param {OrderStatus} status - New status
 * @returns {Promise&lt;void&gt;}
 */
async function updateOrderStatus(orderId, status) {
  await Order.updateOne(
    { _id: orderId },
    { status }
  );
}

// Editor suggests: 'pending', 'active', 'completed', 'cancelled'
await updateOrderStatus('order-123', 'active');

// Editor warns about invalid value
await updateOrderStatus('order-123', 'shipped'); // ❌ Not a valid OrderStatus
</code></pre>
<h2>Class Documentation</h2>
<p>JSDoc works great with ES6 classes:</p>
<pre><code class="language-javascript">/**
 * User service for managing user accounts
 */
class UserService {
  /**
   * @param {Object} database - Database connection
   */
  constructor(database) {
    this.db = database;
  }

  /**
   * Find user by email
   * @param {string} email - User email address
   * @returns {Promise&lt;User|null&gt;} User object or null
   */
  async findByEmail(email) {
    return await this.db.users.findOne({ email });
  }

  /**
   * Create new user
   * @param {Object} userData - User data
   * @param {string} userData.email - Email address
   * @param {string} userData.password - Password
   * @param {string} userData.name - Full name
   * @returns {Promise&lt;User&gt;} Created user
   */
  async create(userData) {
    const hashedPassword = await this.hashPassword(userData.password);
    return await this.db.users.create({
      ...userData,
      password: hashedPassword
    });
  }

  /**
   * @private
   * @param {string} password
   * @returns {Promise&lt;string&gt;}
   */
  async hashPassword(password) {
    return await bcrypt.hash(password, 10);
  }
}

const userService = new UserService(db);
const user = await userService.findByEmail('test@example.com');
</code></pre>
<p>The <code>@private</code> tag tells your editor that <code>hashPassword</code> is internal. It might not show in autocomplete or might show with a warning.</p>
<h2>Callbacks and Higher-Order Functions</h2>
<p>Document function parameters that are themselves functions:</p>
<pre><code class="language-javascript">/**
 * @callback ValidatorFn
 * @param {any} value - Value to validate
 * @returns {boolean} True if valid
 */

/**
 * @callback TransformFn
 * @param {any} value - Value to transform
 * @returns {any} Transformed value
 */

/**
 * Process array with validation and transformation
 * @param {any[]} items - Array to process
 * @param {ValidatorFn} validate - Validation function
 * @param {TransformFn} transform - Transform function
 * @returns {any[]} Processed array
 */
function processArray(items, validate, transform) {
  return items
    .filter(item =&gt; validate(item))
    .map(item =&gt; transform(item));
}

const numbers = [1, 2, 3, 4, 5];
const result = processArray(
  numbers,
  (num) =&gt; num &gt; 2,     // Editor knows this should return boolean
  (num) =&gt; num * 2      // Editor knows this can return any type
);
</code></pre>
<h2>Real-World Express Example</h2>
<p>Here's how JSDoc makes Express development better:</p>
<pre><code class="language-javascript">// @ts-check
/**
 * @typedef {import('express').Request} Request
 * @typedef {import('express').Response} Response
 * @typedef {import('express').NextFunction} NextFunction
 */

/**
 * @typedef {Object} AuthUser
 * @property {string} id - User ID
 * @property {string[]} roles - User roles
 */

/**
 * @typedef {Request &amp; {user: AuthUser}} AuthRequest
 */

/**
 * Authenticate request using JWT
 * @param {Request} req
 * @param {Response} res
 * @param {NextFunction} next
 */
function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }

  try {
    const decoded = jwt.verify(token, SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
}

/**
 * Get current user profile
 * @param {AuthRequest} req
 * @param {Response} res
 */
async function getProfile(req, res) {
  const user = await User.findById(req.user.id);
  res.json(user);
}
</code></pre>
<p>When you type <code>req.user.</code>, your editor suggests <code>id</code> and <code>roles</code>. When you type <code>res.</code>, it suggests all Express response methods.</p>
<h2>Common JSDoc Tags Reference</h2>
<p>Here are the tags you'll use most:</p>
<pre><code class="language-javascript">/**
 * @param {Type} name - Description
 * @returns {Type} Description
 * @throws {Error} Description
 * @typedef {Object} TypeName
 * @property {Type} name - Description
 * @callback CallbackName
 * @template T - Generic type parameter
 * @example
 * functionName(arg1, arg2)
 * @deprecated Use newFunction() instead
 * @see {@link OtherFunction}
 * @private
 * @async
 */
</code></pre>
<h2>When Not to Use JSDoc</h2>
<p>JSDoc isn't always the answer:</p>
<p><strong>Don't use JSDoc when:</strong></p>
<ul>
<li><p>Your team is already using TypeScript (just use TypeScript)</p>
</li>
<li><p>The function is so simple that docs add no value</p>
</li>
<li><p>You're documenting implementation details instead of the API</p>
</li>
</ul>
<p><strong>Do use JSDoc when:</strong></p>
<ul>
<li><p>You're in a JavaScript codebase and want better developer experience</p>
</li>
<li><p>You need type safety but can't add a build step</p>
</li>
<li><p>You're maintaining a library and want to help users</p>
</li>
<li><p>You want autocomplete without migrating to TypeScript</p>
</li>
</ul>
<h2>Making It a Habit</h2>
<p>The best way to start:</p>
<ol>
<li><p>Add <code>// @ts-check</code> to the top of new files</p>
</li>
<li><p>Document function signatures as you write them</p>
</li>
<li><p>Define <code>@typedef</code> for objects you use frequently</p>
</li>
<li><p>Import types from <code>@types</code> packages for libraries you use</p>
</li>
</ol>
<p>Type <code>/**</code> above any function and hit Enter. Most editors auto-generate a JSDoc template. Fill in the types. Done.</p>
<p>Five extra seconds per function. Saves hours of debugging later.</p>
<h2>The Documentation Bonus</h2>
<p>JSDoc comments can generate actual documentation sites. Tools like <code>jsdoc</code> or <code>documentation.js</code> turn your comments into HTML docs:</p>
<pre><code class="language-bash">npm install -g jsdoc
jsdoc src/**/*.js -d docs
</code></pre>
<p>Your inline comments become browsable documentation. One source of truth for both developers and docs.</p>
<hr />
<h2>TLDR;</h2>
<p>JSDoc gives you TypeScript-level autocomplete, type checking, and documentation in plain JavaScript. Add <code>/** */</code> comments above functions with <code>@param</code>, <code>@returns</code>, and <code>@typedef</code> tags. Your editor parses these and provides autocomplete, parameter hints, and error checking. Use <code>// @ts-check</code> at the top of files to enable type validation. Import types from npm packages with <code>@typedef {import('package').Type}</code>. No build step, no configuration — just better developer experience. Five seconds to document a function, hours saved in debugging and onboarding.</p>
]]></content:encoded></item><item><title><![CDATA[Writing Better APIs: Error Handling and Logging That Actually Works]]></title><description><![CDATA[You know that moment when your API throws a 500 error and you have no idea why? Or when you're hunting through 47 route files trying to find where that specific error message comes from? Let's fix tha]]></description><link>https://blog.thecodingant.in/writing-better-apis-error-handling-and-logging-that-actually-works</link><guid isPermaLink="true">https://blog.thecodingant.in/writing-better-apis-error-handling-and-logging-that-actually-works</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[node]]></category><category><![CDATA[error handleing]]></category><category><![CDATA[Express.js]]></category><category><![CDATA[logging]]></category><dc:creator><![CDATA[Nitesh Kumar Tudu]]></dc:creator><pubDate>Tue, 07 Apr 2026 05:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/4f6cbc83-202f-4dc5-b3d1-49e80dcc41f2.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You know that moment when your API throws a 500 error and you have no idea why? Or when you're hunting through 47 route files trying to find where that specific error message comes from? Let's fix that.</p>
<h2>The Copy-Paste Problem</h2>
<p>Most Express.js codebases look like this:</p>
<pre><code class="language-javascript">app.get('/users/: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.json(user);
  } catch (error) {
    console.log(error);
    res.status(500).json({ error: 'Something went wrong' });
  }
});

app.post('/users', async (req, res) =&gt; {
  try {
    const user = await User.create(req.body);
    res.status(201).json(user);
  } catch (error) {
    console.log(error);
    res.status(500).json({ error: 'Something went wrong' });
  }
});

// ... 45 more routes with identical try-catch blocks
</code></pre>
<p>Every route has the same error handling code. Every route logs errors differently. When production breaks, you're searching through console.log statements hoping to find something useful.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/0689e90d-30ba-4434-9d05-87f8ecb76934.svg" alt="" style="display:block;margin:0 auto" />

<h2>The Async Wrapper Pattern</h2>
<p>Here's a tiny function that changes everything:</p>
<pre><code class="language-javascript">const asyncHandler = (fn) =&gt; (req, res, next) =&gt; {
  Promise.resolve(fn(req, res, next)).catch(next);
};
</code></pre>
<p>This wrapper catches any error from your async route handlers and passes them to Express's error handling middleware. Now your routes look like this:</p>
<pre><code class="language-javascript">app.get('/users/:id', asyncHandler(async (req, res) =&gt; {
  const user = await User.findById(req.params.id);
  if (!user) {
    throw new NotFoundError('User not found');
  }
  res.json(user);
}));

app.post('/users', asyncHandler(async (req, res) =&gt; {
  const user = await User.create(req.body);
  res.status(201).json(user);
}));
</code></pre>
<p>No try-catch blocks. Just throw errors when something goes wrong. The wrapper handles everything.</p>
<h2>Centralized Error Handling</h2>
<p>Now that errors flow to one place, you need middleware to catch them properly:</p>
<pre><code class="language-javascript">class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true;
    Error.captureStackTrace(this, this.constructor);
  }
}

class NotFoundError extends AppError {
  constructor(message = 'Resource not found') {
    super(message, 404);
  }
}

class ValidationError extends AppError {
  constructor(message = 'Validation failed') {
    super(message, 400);
  }
}

class UnauthorizedError extends AppError {
  constructor(message = 'Unauthorized') {
    super(message, 401);
  }
}
</code></pre>
<p>The <code>isOperational</code> flag is crucial. It separates expected errors (user not found, validation failed) from unexpected errors (database connection lost, null reference).</p>
<p>Here's the error handler middleware:</p>
<pre><code class="language-javascript">app.use((err, req, res, next) =&gt; {
  err.statusCode = err.statusCode || 500;
  err.status = err.status || 'error';

  if (process.env.NODE_ENV === 'development') {
    res.status(err.statusCode).json({
      status: err.status,
      error: err,
      message: err.message,
      stack: err.stack
    });
  } else {
    // Production: don't leak error details
    if (err.isOperational) {
      res.status(err.statusCode).json({
        status: err.status,
        message: err.message
      });
    } else {
      // Programming error: log and hide details
      console.error('ERROR 💥', err);
      res.status(500).json({
        status: 'error',
        message: 'Something went wrong'
      });
    }
  }
});
</code></pre>
<p>In development, you get full stack traces. In production, users see clean error messages while unexpected errors are hidden and logged for investigation.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/10bb22dd-f89c-4855-8761-348d328b9280.svg" alt="" style="display:block;margin:0 auto" />

<h2>Why This Pattern Works</h2>
<p><strong>Operational errors</strong> are part of normal application flow. A user requests a resource that doesn't exist? That's operational. Invalid input? Operational. These errors should be handled gracefully and returned to the user.</p>
<p><strong>Programming errors</strong> are bugs in your code. Trying to read a property of undefined? Programming error. Database connection string is wrong? Programming error. These should never be shown to users.</p>
<p>The <code>isOperational</code> flag lets you handle both correctly in production.</p>
<h2>Logging That Actually Helps</h2>
<p><code>console.log(error)</code> tells you something broke. It doesn't tell you which user triggered it, what they were trying to do, or how to reproduce it.</p>
<p>Here's a proper request logger:</p>
<pre><code class="language-javascript">const requestLogger = (req, res, next) =&gt; {
  const start = Date.now();
  
  // Capture the original end function
  const originalEnd = res.end;
  
  res.end = function(...args) {
    const duration = Date.now() - start;
    
    console.log(JSON.stringify({
      timestamp: new Date().toISOString(),
      method: req.method,
      url: req.url,
      status: res.statusCode,
      duration: `${duration}ms`,
      userAgent: req.get('user-agent'),
      ip: req.ip,
      userId: req.user?.id || 'anonymous',
      // Add correlation ID for tracking across services
      correlationId: req.headers['x-correlation-id'] || generateId()
    }));
    
    // Call the original end function
    return originalEnd.apply(this, args);
  };
  
  next();
};

app.use(requestLogger);
</code></pre>
<p>This logs every request with context. Now when something breaks, you know who did what and how long it took.</p>
<h2>Structured Logging for Production</h2>
<p>JSON logs are searchable. String concatenation is not.</p>
<pre><code class="language-javascript">// Bad: impossible to search or filter
console.log('User john@example.com failed login attempt from 192.168.1.1');

// Good: structured and queryable
logger.warn({
  event: 'login_failed',
  email: 'john@example.com',
  ip: '192.168.1.1',
  timestamp: new Date().toISOString(),
  attemptCount: 3
});
</code></pre>
<p>Use a logging library like Winston or Pino:</p>
<pre><code class="language-javascript">const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ 
      filename: 'error.log', 
      level: 'error' 
    }),
    new winston.transports.File({ 
      filename: 'combined.log' 
    })
  ]
});

// Console logging for development
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}

module.exports = logger;
</code></pre>
<p>Now you can search your logs by event type, user ID, status code, or any field you include.</p>
<h2>Error Logging with Context</h2>
<p>When an error happens, log everything you need to debug it:</p>
<pre><code class="language-javascript">app.use((err, req, res, next) =&gt; {
  const errorLog = {
    timestamp: new Date().toISOString(),
    error: {
      message: err.message,
      stack: err.stack,
      statusCode: err.statusCode
    },
    request: {
      method: req.method,
      url: req.url,
      headers: req.headers,
      body: req.body,
      params: req.params,
      query: req.query,
      userId: req.user?.id
    }
  };

  if (err.isOperational) {
    logger.warn(errorLog);
  } else {
    logger.error(errorLog);
  }

  // Send response
  if (process.env.NODE_ENV === 'production') {
    if (err.isOperational) {
      res.status(err.statusCode).json({
        status: err.status,
        message: err.message
      });
    } else {
      res.status(500).json({
        status: 'error',
        message: 'Something went wrong'
      });
    }
  } else {
    res.status(err.statusCode).json({
      status: err.status,
      error: err,
      message: err.message,
      stack: err.stack
    });
  }
});
</code></pre>
<p>This logs the full context of failed requests. When debugging, you can see exactly what the user sent and what went wrong.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/4c64cd4c-53e6-4bf4-b0d4-f4c8057c39a6.svg" alt="" style="display:block;margin:0 auto" />

<h2>Monitoring Real Performance</h2>
<p>Logging tells you what happened after the fact. Monitoring tells you what's happening right now.</p>
<p>Track key metrics in your routes:</p>
<pre><code class="language-javascript">const metrics = {
  requestCount: 0,
  errorCount: 0,
  slowRequestCount: 0,
  totalResponseTime: 0
};

const monitoringMiddleware = (req, res, next) =&gt; {
  const start = Date.now();
  metrics.requestCount++;

  res.on('finish', () =&gt; {
    const duration = Date.now() - start;
    metrics.totalResponseTime += duration;

    if (duration &gt; 1000) {
      metrics.slowRequestCount++;
      logger.warn({
        event: 'slow_request',
        duration,
        method: req.method,
        url: req.url,
        userId: req.user?.id
      });
    }

    if (res.statusCode &gt;= 500) {
      metrics.errorCount++;
    }
  });

  next();
};

// Endpoint to expose metrics
app.get('/metrics', (req, res) =&gt; {
  const avgResponseTime = metrics.totalResponseTime / metrics.requestCount;
  const errorRate = (metrics.errorCount / metrics.requestCount) * 100;
  
  res.json({
    requestCount: metrics.requestCount,
    errorCount: metrics.errorCount,
    errorRate: `${errorRate.toFixed(2)}%`,
    slowRequestCount: metrics.slowRequestCount,
    avgResponseTime: `${avgResponseTime.toFixed(2)}ms`
  });
});
</code></pre>
<p>Now you can track error rates, response times, and slow requests. Hook this up to Prometheus, Datadog, or any monitoring service.</p>
<h2>Alert on What Matters</h2>
<p>Not every error needs an alert. A single 404? Normal. A 50% error rate? Critical.</p>
<pre><code class="language-javascript">const alerting = {
  errorThreshold: 100, // Alert after 100 errors
  errorWindow: 60000,  // in 1 minute
  errors: []
};

const checkAlertThreshold = () =&gt; {
  const now = Date.now();
  const recentErrors = alerting.errors.filter(
    timestamp =&gt; now - timestamp &lt; alerting.errorWindow
  );
  
  alerting.errors = recentErrors;

  if (recentErrors.length &gt;= alerting.errorThreshold) {
    logger.error({
      event: 'error_threshold_exceeded',
      errorCount: recentErrors.length,
      timeWindow: `${alerting.errorWindow / 1000}s`,
      message: 'High error rate detected - investigate immediately'
    });
    
    // Send alert to your monitoring service
    // sendSlackAlert(), sendPagerDutyAlert(), etc.
    
    // Reset to avoid spam
    alerting.errors = [];
  }
};

app.use((err, req, res, next) =&gt; {
  // Track errors for alerting
  if (!err.isOperational) {
    alerting.errors.push(Date.now());
    checkAlertThreshold();
  }

  // ... rest of error handling
});
</code></pre>
<p>This alerts when error rates spike, not on every single error.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/105ecbdc-9da2-4347-935b-8acfda7a2004.svg" alt="" style="display:block;margin:0 auto" />

<h2>Complete Setup</h2>
<p>Here's what a complete setup looks like:</p>
<pre><code class="language-javascript">const express = require('express');
const winston = require('winston');

const app = express();

// Logger setup
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

// Async wrapper
const asyncHandler = (fn) =&gt; (req, res, next) =&gt; {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// Custom errors
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true;
  }
}

// Request logger
app.use((req, res, next) =&gt; {
  const start = Date.now();
  res.on('finish', () =&gt; {
    logger.info({
      method: req.method,
      url: req.url,
      status: res.statusCode,
      duration: Date.now() - start,
      ip: req.ip
    });
  });
  next();
});

// Your routes
app.get('/users/:id', asyncHandler(async (req, res) =&gt; {
  const user = await User.findById(req.params.id);
  if (!user) throw new AppError('User not found', 404);
  res.json(user);
}));

// Error handler (must be last)
app.use((err, req, res, next) =&gt; {
  const errorLog = {
    message: err.message,
    stack: err.stack,
    url: req.url,
    method: req.method,
    userId: req.user?.id
  };

  if (err.isOperational) {
    logger.warn(errorLog);
    res.status(err.statusCode).json({
      status: 'fail',
      message: err.message
    });
  } else {
    logger.error(errorLog);
    res.status(500).json({
      status: 'error',
      message: 'Something went wrong'
    });
  }
});

app.listen(3000);
</code></pre>
<p>Clean routes, centralized error handling, structured logging, and proper monitoring. This is how production APIs should work.</p>
<h2>Key Takeaways</h2>
<p><strong>Error handling:</strong></p>
<ul>
<li><p>Use async wrappers to eliminate try-catch boilerplate</p>
</li>
<li><p>Centralize error handling in one middleware</p>
</li>
<li><p>Distinguish operational errors from programming errors</p>
</li>
<li><p>Never leak error details in production</p>
</li>
</ul>
<p><strong>Logging and monitoring:</strong></p>
<ul>
<li><p>Log structured JSON, not string messages</p>
</li>
<li><p>Include context with every log (user, URL, timestamp)</p>
</li>
<li><p>Track metrics like error rates and response times</p>
</li>
<li><p>Alert on patterns, not individual errors</p>
</li>
</ul>
<p>The async wrapper pattern alone cuts hundreds of lines of repetitive code. Combined with proper logging, you go from hunting through console statements to querying structured logs. Your debugging time drops from hours to minutes.</p>
<p>Stop copy-pasting try-catch blocks. Stop using console.log. Start building APIs that are actually maintainable.</p>
<hr />
<h2>TLDR;</h2>
<p>Use an async wrapper to remove try-catch blocks from every route. Create centralized error handling middleware with custom error classes that distinguish operational errors (expected) from programming errors (bugs). Implement structured JSON logging with Winston or Pino instead of console.log. Track metrics like error rates, response times, and slow requests. Alert on error rate spikes, not individual errors. This approach reduces boilerplate by 70%+, makes debugging dramatically faster, and gives you production visibility you can actually use.</p>
]]></content:encoded></item><item><title><![CDATA[Design for Developers:  Making Your UI Actually Work Everywhere (Part 2)]]></title><description><![CDATA[In Part 1, we covered the fundamentals: spacing, typography, color, hierarchy, and alignment. Those rules fix 80% of ugly developer UIs.
But there's a problem. You followed all the rules. Your spacing]]></description><link>https://blog.thecodingant.in/design-for-developers-making-your-ui-actually-work-everywhere-part-2</link><guid isPermaLink="true">https://blog.thecodingant.in/design-for-developers-making-your-ui-actually-work-everywhere-part-2</guid><category><![CDATA[dark mode design]]></category><category><![CDATA[design patt]]></category><category><![CDATA[Design]]></category><category><![CDATA[Responsive Web Design]]></category><category><![CDATA[Web Design]]></category><category><![CDATA[animation principles]]></category><category><![CDATA[accessible UI]]></category><category><![CDATA[responsive design]]></category><category><![CDATA[React developer]]></category><category><![CDATA[React]]></category><category><![CDATA[CSS]]></category><category><![CDATA[HTML5]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Nitesh Kumar Tudu]]></dc:creator><pubDate>Mon, 06 Apr 2026 04:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/0099dfaf-2928-400d-9a9c-5efd18bd42da.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In Part 1, we covered the fundamentals: spacing, typography, color, hierarchy, and alignment. Those rules fix 80% of ugly developer UIs.</p>
<p>But there's a problem. You followed all the rules. Your spacing is consistent. Your typography has hierarchy. Your colors make sense.</p>
<p>Then someone opens your app on a phone. Or switches to dark mode. Or hovers over a button and nothing happens.</p>
<p>Your UI falls apart.</p>
<p>This is Part 2. We're going beyond "making it look good" to "making it work everywhere." We'll cover layout patterns, component states, dark mode, animations, and responsive design.</p>
<p>Same deal: no theory, just concrete patterns you can copy.</p>
<h2>Rule 1: Layout Patterns That Actually Work</h2>
<p>Developers often think layout is complicated. It's not. You need like three patterns, and they cover 95% of use cases.</p>
<h3>The Three Layouts You Actually Need</h3>
<p><strong>1. Stack Layout</strong> - Everything vertical</p>
<pre><code class="language-css">.stack {
  display: flex;
  flex-direction: column;
  gap: 16px;
}
</code></pre>
<p>Use for: Forms, article content, mobile views, anything that flows top to bottom.</p>
<p><strong>2. Sidebar Layout</strong> - Fixed side, flexible main</p>
<pre><code class="language-css">.sidebar-layout {
  display: grid;
  grid-template-columns: 250px 1fr;
  gap: 24px;
}
</code></pre>
<p>Use for: Dashboards, settings pages, admin panels.</p>
<p><strong>3. Grid Layout</strong> - Equal cards</p>
<pre><code class="language-css">.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 24px;
}
</code></pre>
<p>Use for: Product grids, image galleries, card lists.</p>
<p>That's it. Three patterns. Everything else is a variation.</p>
<h3>Container Width Rules</h3>
<p>Developers either make content 100% width (unreadable on big screens) or pick a random max-width.</p>
<p>The rule:</p>
<pre><code class="language-css">/* Text content - narrow */
.prose {
  max-width: 65ch; /* About 65 characters per line */
  margin: 0 auto;
}

/* General content - medium */
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 24px;
}

/* Full-width data - wide */
.table-container {
  max-width: 1400px;
  margin: 0 auto;
}
</code></pre>
<p><strong>Why</strong> <code>ch</code> <strong>units for text?</strong> Because readability is about line length, not screen size. 50-75 characters per line is optimal. <code>65ch</code> gives you that regardless of font size.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/154f20b3-8c32-4ace-9b9f-32b7ee820782.svg" alt="" style="display:block;margin:0 auto" />

<p>The diagram above shows the three core layout patterns in action. Stack layout flows vertically with consistent gaps. Sidebar layout has a fixed navigation column and flexible content area. Grid layout auto-fills with cards that maintain minimum width. Notice how each pattern has a specific use case - these aren't just random choices.</p>
<h2>Rule 2: Component States Aren't Optional</h2>
<p>Here's what developers do:</p>
<pre><code class="language-css">.button {
  background: blue;
  color: white;
}
</code></pre>
<p>That's it. One state. No hover. No focus. No disabled. No active.</p>
<p>Users have no idea if the button is clickable, loading, or broken.</p>
<h3>The Five States Every Interactive Element Needs</h3>
<pre><code class="language-css">/* 1. Default - How it looks normally */
.button {
  background: #3b82f6;
  color: white;
  border: 2px solid transparent;
  transition: all 0.15s ease;
}

/* 2. Hover - Mouse is over it */
.button:hover {
  background: #2563eb;
  transform: translateY(-1px);
  box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3);
}

/* 3. Focus - Keyboard navigation */
.button:focus {
  outline: none;
  border-color: #3b82f6;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}

/* 4. Active - Being clicked */
.button:active {
  transform: translateY(0);
  box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
}

/* 5. Disabled - Can't be clicked */
.button:disabled {
  background: #d1d5db;
  color: #9ca3af;
  cursor: not-allowed;
  transform: none;
}
</code></pre>
<p>Every state tells the user something:</p>
<ul>
<li><p>Hover: "I'm interactive"</p>
</li>
<li><p>Focus: "I'm selected (keyboard users need this)"</p>
</li>
<li><p>Active: "I'm responding to your click"</p>
</li>
<li><p>Disabled: "I'm not available right now"</p>
</li>
</ul>
<h3>Form Input States</h3>
<p>Inputs need even more states because they handle validation:</p>
<pre><code class="language-css">/* Default */
.input {
  border: 1px solid #d1d5db;
  transition: all 0.15s ease;
}

/* Focus - User is typing */
.input:focus {
  border-color: #3b82f6;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
  outline: none;
}

/* Error - Validation failed */
.input.error {
  border-color: #ef4444;
}

.input.error:focus {
  box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}

/* Success - Validation passed */
.input.success {
  border-color: #10b981;
}

/* Disabled - Can't edit */
.input:disabled {
  background: #f3f4f6;
  color: #9ca3af;
  cursor: not-allowed;
}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/303d5bd3-d699-4a7e-857e-dabd77ca1b54.svg" alt="" style="display:block;margin:0 auto" />

<p>This diagram shows all five states for a button and an input field. Notice how each state provides visual feedback: hover lifts slightly, focus shows a ring for keyboard users, active presses down, and disabled is grayed out. The input field adds error and success states with color-coded borders. These aren't decorative - they're functional feedback.</p>
<h2>Rule 3: Dark Mode Is Not "Invert Colors"</h2>
<p>Developers who add dark mode:</p>
<pre><code class="language-css">/* Bad - Just flip everything */
body.dark {
  background: black;
  color: white;
}
</code></pre>
<p>This creates problems:</p>
<ul>
<li><p>Pure black (#000) is harsh on OLED screens</p>
</li>
<li><p>Pure white text (#fff) on pure black is too high contrast</p>
</li>
<li><p>Shadows disappear (they're designed for light backgrounds)</p>
</li>
<li><p>Colors need adjustment (bright colors hurt on dark backgrounds)</p>
</li>
</ul>
<h3>Dark Mode Color System</h3>
<p>You need a separate palette:</p>
<pre><code class="language-css">:root {
  /* Light mode */
  --bg-primary: #ffffff;
  --bg-secondary: #f3f4f6;
  --text-primary: #1a1a1a;
  --text-secondary: #6b7280;
  --border: #e5e7eb;
}

[data-theme="dark"] {
  /* Dark mode - NOT just inverted */
  --bg-primary: #1a1a1a;      /* Dark gray, not black */
  --bg-secondary: #2d2d2d;    /* Lighter for cards */
  --text-primary: #e5e7eb;    /* Light gray, not white */
  --text-secondary: #9ca3af;  /* Reduced contrast */
  --border: #404040;          /* Subtle borders */
}
</code></pre>
<p><strong>Key principles:</strong></p>
<ol>
<li><p>Use dark gray (#1a1a1a), not black (#000)</p>
</li>
<li><p>Use light gray (#e5e7eb), not white (#fff)</p>
</li>
<li><p>Reduce contrast slightly (easier on eyes)</p>
</li>
<li><p>Make borders more subtle</p>
</li>
<li><p>Adjust brand colors to be less saturated</p>
</li>
</ol>
<h3>Elevation in Dark Mode</h3>
<p>In light mode, you use shadows for depth. In dark mode, shadows don't work (dark on dark).</p>
<p>Solution: Use lighter backgrounds for elevated elements.</p>
<pre><code class="language-css">/* Light mode - elevation with shadows */
.card {
  background: white;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

/* Dark mode - elevation with lighter backgrounds */
[data-theme="dark"] .card {
  background: #2d2d2d;  /* Lighter than page background */
  box-shadow: none;     /* No shadow needed */
}

[data-theme="dark"] .modal {
  background: #353535;  /* Even lighter for modals */
}
</code></pre>
<p>The rule: Elevated elements = lighter background in dark mode.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/89a83f4a-472b-4f06-ab62-c23ffcf9f463.svg" alt="" style="display:block;margin:0 auto" />

<p>The diagram compares light mode and dark mode implementations. Notice light mode uses pure white backgrounds with shadows for depth. Dark mode uses a dark gray base (#1a1a1a) with lighter grays for elevated surfaces. Colors are desaturated in dark mode - the bright blue becomes a softer blue. Text contrast is slightly reduced to prevent eye strain.</p>
<h2>Rule 4: Animation Should Feel Natural</h2>
<p>Developers either skip animations entirely (boring, static UI) or go overboard (everything bounces and spins).</p>
<p>The right approach: animate state changes, keep it subtle.</p>
<h3>What to Animate</h3>
<p><strong>Good candidates:</strong></p>
<ul>
<li><p>Buttons on hover/click</p>
</li>
<li><p>Modal/drawer open/close</p>
</li>
<li><p>Accordion expand/collapse</p>
</li>
<li><p>Loading states</p>
</li>
<li><p>Toast notifications appearing</p>
</li>
<li><p>Form validation feedback</p>
</li>
</ul>
<p><strong>Bad candidates:</strong></p>
<ul>
<li><p>Page loads (users just want content)</p>
</li>
<li><p>Scrolling (interferes with user control)</p>
</li>
<li><p>Every single thing (overwhelming)</p>
</li>
</ul>
<h3>Timing and Easing</h3>
<p>This is where developers mess up. They use <code>linear</code> (looks robotic) or random timing values.</p>
<p>The standards:</p>
<pre><code class="language-css">/* Quick interactions - 150ms */
.button {
  transition: all 0.15s ease-out;
}

/* Medium transitions - 250ms */
.dropdown {
  transition: all 0.25s ease-in-out;
}

/* Slow transitions - 350ms */
.modal {
  transition: all 0.35s ease-out;
}
</code></pre>
<p><strong>Easing functions:</strong></p>
<ul>
<li><p><code>ease-out</code> - Starts fast, ends slow (feels responsive)</p>
</li>
<li><p><code>ease-in</code> - Starts slow, ends fast (feels like falling)</p>
</li>
<li><p><code>ease-in-out</code> - Smooth both ends (feels polished)</p>
</li>
</ul>
<p>Never use <code>linear</code> for UI animations. It looks robotic.</p>
<h3>Practical Examples</h3>
<pre><code class="language-css">/* Button - Quick response */
.button {
  transform: translateY(0);
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  transition: all 0.15s ease-out;
}

.button:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}

/* Modal - Medium entrance */
.modal {
  opacity: 0;
  transform: scale(0.95);
  transition: all 0.25s ease-out;
}

.modal.open {
  opacity: 1;
  transform: scale(1);
}

/* Loading spinner - Continuous */
@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.spinner {
  animation: spin 1s linear infinite;
}
</code></pre>
<p><strong>Performance rule:</strong> Only animate <code>transform</code> and <code>opacity</code>. Everything else (width, height, margin, etc.) causes layout recalculation and is slow.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/bf7dc21d-2c12-4a16-8ae7-d3ade4533725.png" alt="" style="display:block;margin:0 auto" />

<p>This diagram illustrates timing curves and animation examples. The top shows the difference between linear (robotic), ease-out (natural), and ease-in-out (smooth) timing functions. Below are three examples: a button hover with 150ms ease-out, a modal entrance with 250ms ease-out, and a toast notification sliding in. Each uses the appropriate timing for its interaction type.</p>
<h2>Rule 5: Responsive Design Is Mobile-First</h2>
<p>Developers design for desktop first. Then they try to "make it work on mobile" by hiding things and shrinking fonts.</p>
<p>This breaks everything.</p>
<h3>The Mobile-First Approach</h3>
<p>Start with mobile. Add complexity for larger screens.</p>
<pre><code class="language-css">/* Mobile first - Default styles */
.container {
  padding: 16px;
}

.grid {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.nav {
  display: none; /* Hidden on mobile */
}

/* Tablet - 768px and up */
@media (min-width: 768px) {
  .container {
    padding: 24px;
  }
  
  .grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 24px;
  }
  
  .nav {
    display: flex; /* Show on tablet+ */
  }
}

/* Desktop - 1024px and up */
@media (min-width: 1024px) {
  .container {
    padding: 32px;
    max-width: 1200px;
    margin: 0 auto;
  }
  
  .grid {
    grid-template-columns: repeat(3, 1fr);
    gap: 32px;
  }
}
</code></pre>
<p>Why this works:</p>
<ul>
<li><p>Mobile loads minimal CSS (faster)</p>
</li>
<li><p>Desktop enhances the mobile base</p>
</li>
<li><p>You design for constraints first (forces simplicity)</p>
</li>
<li><p>No "fixing" mobile as an afterthought</p>
</li>
</ul>
<h3>Touch Target Sizes</h3>
<p>Developers use 32px buttons on mobile. Users tap wrong buttons constantly.</p>
<p>The rule: <strong>Minimum 44px x 44px for touch targets.</strong></p>
<pre><code class="language-css">/* Desktop - can be smaller */
.button {
  padding: 8px 16px;
  font-size: 14px;
}

/* Mobile - needs to be tappable */
@media (max-width: 767px) {
  .button {
    padding: 12px 24px;  /* At least 44px tall */
    font-size: 16px;     /* Prevents zoom on iOS */
  }
}
</code></pre>
<p><strong>iOS zoom prevention:</strong> If input font-size is less than 16px, iOS zooms in when you tap. Always use 16px minimum on mobile inputs.</p>
<h3>Responsive Typography</h3>
<p>Don't just shrink fonts. Adjust the scale.</p>
<pre><code class="language-css">/* Mobile - smaller scale */
h1 { font-size: 24px; }
h2 { font-size: 20px; }
p  { font-size: 16px; }

/* Desktop - larger scale */
@media (min-width: 768px) {
  h1 { font-size: 32px; }
  h2 { font-size: 24px; }
  p  { font-size: 16px; } /* Body stays same */
}
</code></pre>
<p>Body text should stay 16px (readability). Only scale headings.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/196e797e-9139-45e5-89eb-1f65acdcd8b7.png" alt="" style="display:block;margin:0 auto" />

<p>The diagram shows how layouts transform across breakpoints. A mobile stack (single column) becomes a 2-column grid on tablet, then a 3-column grid on desktop. Navigation goes from hidden (hamburger menu) to visible horizontal nav. Container padding increases from 16px to 32px. Touch targets grow from 32px to 44px minimum. This is mobile-first progressive enhancement.</p>
<h2>Common Patterns Developers Get Wrong</h2>
<h3>1. Modals on Mobile</h3>
<p>Desktop modals are centered overlays. On mobile, they should be full-screen (or bottom sheets).</p>
<pre><code class="language-css">/* Desktop - Centered modal */
.modal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 90%;
  max-width: 500px;
}

/* Mobile - Full screen */
@media (max-width: 767px) {
  .modal {
    top: 0;
    left: 0;
    transform: none;
    width: 100%;
    height: 100%;
    max-width: none;
  }
}
</code></pre>
<h3>2. Tables on Mobile</h3>
<p>Tables don't work on small screens. You have two options:</p>
<p><strong>Option 1: Horizontal scroll</strong></p>
<pre><code class="language-css">.table-container {
  overflow-x: auto;
}
</code></pre>
<p><strong>Option 2: Reformat as cards (better UX)</strong></p>
<pre><code class="language-css">/* Desktop - Table */
@media (min-width: 768px) {
  .data-list {
    display: table;
  }
}

/* Mobile - Cards */
@media (max-width: 767px) {
  .data-row {
    display: block;
    margin-bottom: 16px;
    padding: 16px;
    border: 1px solid #e5e7eb;
    border-radius: 8px;
  }
}
</code></pre>
<h3>3. Navigation Patterns</h3>
<p>Desktop: horizontal nav bar. Mobile: hamburger menu or bottom nav.</p>
<pre><code class="language-css">/* Mobile - Bottom nav */
@media (max-width: 767px) {
  .nav {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    display: flex;
    justify-content: space-around;
    background: white;
    border-top: 1px solid #e5e7eb;
    padding: 12px;
  }
}

/* Desktop - Top nav */
@media (min-width: 768px) {
  .nav {
    position: static;
    justify-content: flex-start;
    gap: 24px;
    border-top: none;
  }
}
</code></pre>
<h2>Accessibility Checklist</h2>
<p>These are non-negotiable:</p>
<ul>
<li><p>[ ] <strong>Keyboard navigation works</strong> - Tab through everything</p>
</li>
<li><p>[ ] <strong>Focus states are visible</strong> - You can see what's selected</p>
</li>
<li><p>[ ] <strong>Color isn't the only indicator</strong> - Use icons + text, not just color</p>
</li>
<li><p>[ ] <strong>Touch targets are 44px minimum</strong> - On mobile</p>
</li>
<li><p>[ ] <strong>Text is at least 16px</strong> - On mobile (prevents zoom)</p>
</li>
<li><p>[ ] <strong>Contrast ratios meet WCAG</strong> - 4.5:1 for text, 3:1 for UI</p>
</li>
<li><p>[ ] <strong>Alt text on images</strong> - Screen readers need descriptions</p>
</li>
<li><p>[ ] <strong>Forms have labels</strong> - Not just placeholders</p>
</li>
</ul>
<h2>Tools for Building Responsive UIs</h2>
<p><strong>Testing:</strong></p>
<ul>
<li><p>Chrome DevTools device mode</p>
</li>
<li><p>Responsively App (tests multiple devices at once)</p>
</li>
<li><p>BrowserStack (real device testing)</p>
</li>
</ul>
<p><strong>Design systems with good responsive patterns:</strong></p>
<ul>
<li><p>Tailwind CSS (mobile-first utilities)</p>
</li>
<li><p>Chakra UI (responsive props)</p>
</li>
<li><p>Radix UI (accessible primitives)</p>
</li>
</ul>
<p><strong>Animation libraries:</strong></p>
<ul>
<li><p>Framer Motion (React)</p>
</li>
<li><p>GSAP (vanilla JS)</p>
</li>
<li><p>CSS <code>@keyframes</code> (built-in)</p>
</li>
</ul>
<h2>The Bottom Line</h2>
<p>Part 1 taught you to make UIs look good. Part 2 teaches you to make them work everywhere.</p>
<p>Follow these rules:</p>
<ol>
<li><p><strong>Layouts</strong> - Use stack, sidebar, or grid patterns</p>
</li>
<li><p><strong>States</strong> - Every interactive element needs 5 states</p>
</li>
<li><p><strong>Dark mode</strong> - Use dark gray, not black; lighter surfaces = elevated</p>
</li>
<li><p><strong>Animation</strong> - Animate transforms/opacity only; 150ms for quick, 250ms for medium</p>
</li>
<li><p><strong>Responsive</strong> - Mobile-first; 44px touch targets; scale typography</p>
</li>
</ol>
<p>These aren't subjective. They're patterns that work across devices, themes, and interaction methods.</p>
<p>Start with mobile. Add states to your components. Test in dark mode. Add subtle animations. Make it responsive.</p>
<p>Your UI will work everywhere, for everyone.</p>
<hr />
<p><em>What responsive patterns do you find most useful? Drop them in the comments.</em></p>
]]></content:encoded></item><item><title><![CDATA[Design for Developers: Stop Making Your Apps Look Like Potato (Part 1)]]></title><description><![CDATA[You can write beautiful code. Your API is elegant. Your database queries are optimized. Your tests are green.
But your UI looks like it was designed in Microsoft Word 97.
I get it. You're a developer,]]></description><link>https://blog.thecodingant.in/design-for-developers-stop-making-your-apps-look-like-potato-part-1</link><guid isPermaLink="true">https://blog.thecodingant.in/design-for-developers-stop-making-your-apps-look-like-potato-part-1</guid><category><![CDATA[Design]]></category><category><![CDATA[design as developer]]></category><category><![CDATA[design patterns]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Web Design]]></category><category><![CDATA[Web design & Development]]></category><category><![CDATA[UI]]></category><category><![CDATA[ui desi]]></category><category><![CDATA[UI Design]]></category><category><![CDATA[ui design for developers]]></category><category><![CDATA[color system]]></category><category><![CDATA[scaling]]></category><category><![CDATA[Developer]]></category><category><![CDATA[design principles]]></category><dc:creator><![CDATA[Nitesh Kumar Tudu]]></dc:creator><pubDate>Sun, 05 Apr 2026 07:35:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/1daed425-4619-4635-ae62-f5bd826a95b3.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You can write beautiful code. Your API is elegant. Your database queries are optimized. Your tests are green.</p>
<p>But your UI looks like it was designed in Microsoft Word 97.</p>
<p>I get it. You're a developer, not a designer. But here's the thing: your users don't care how clean your code is if your app is painful to look at.</p>
<p>The good news? You don't need to become a designer. You just need to understand a few fundamental principles. And unlike learning a new framework, these principles don't change every six months.</p>
<p>This is Part 1 of a practical design guide for developers. No theory. No "make it pop." Just concrete rules you can apply immediately.</p>
<h2>The Core Problem: Developers Think Visually Different</h2>
<p>When developers build UIs, we think in terms of:</p>
<ul>
<li><p>Data structures</p>
</li>
<li><p>Component hierarchies</p>
</li>
<li><p>Input/output</p>
</li>
<li><p>Function signatures</p>
</li>
</ul>
<p>When designers build UIs, they think in terms of:</p>
<ul>
<li><p>Visual weight</p>
</li>
<li><p>Flow and rhythm</p>
</li>
<li><p>Emotional response</p>
</li>
<li><p>Spatial relationships</p>
</li>
</ul>
<p>Neither is wrong. But if you only think like a developer, your UI will look like a JSON object rendered to HTML.</p>
<h2>Rule 1: Whitespace Is Not Wasted Space</h2>
<p>The biggest mistake developers make: cramming everything together to "maximize screen real estate."</p>
<p>Bad developer thinking: "The user paid for a 1920px screen, I should use all 1920 pixels."</p>
<p>Good design thinking: "Whitespace helps the user focus on what matters."</p>
<p>Look at any app you think is ugly. I guarantee it has almost no breathing room. Everything touches everything else.</p>
<h3>The Spacing Scale You Actually Need</h3>
<p>Forget random pixel values. Use a spacing scale:</p>
<pre><code class="language-plaintext">4px  - Tiny gaps (icon to text)
8px  - Small gaps (within a component)
16px - Medium gaps (between related items)
24px - Large gaps (between sections)
32px - XL gaps (between major sections)
48px - XXL gaps (page margins)
</code></pre>
<p>That's it. Pick from these six values for 95% of your spacing decisions.</p>
<p><strong>Bad spacing</strong>: 7px here, 11px there, 19px somewhere else. Everything feels random.</p>
<p><strong>Good spacing</strong>: Consistent use of the scale. Your UI develops rhythm.</p>
<h3>Practical Example</h3>
<pre><code class="language-css">/* Bad - random spacing */
.card {
  padding: 13px;
  margin-bottom: 17px;
}

.card-header {
  margin-bottom: 9px;
}

/* Good - spacing scale */
.card {
  padding: 24px;        /* Large internal padding */
  margin-bottom: 16px;  /* Medium gap between cards */
}

.card-header {
  margin-bottom: 16px;  /* Medium gap to content */
}
</code></pre>
<p>Here's the visual difference between cramped and spacious layouts:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/20e2ca66-e4d8-4f0c-a87b-342bcefc490b.svg" alt="" style="display:block;margin:0 auto" />

<p>The left side shows everything crammed together with 4-8px gaps. The right side uses 16-24px consistently. Notice how the spacious version is easier to scan and naturally groups related content.</p>
<h2>Rule 2: Typography Hierarchy Isn't Optional</h2>
<p>Developers often do this:</p>
<pre><code class="language-css">* {
  font-size: 14px;
}
</code></pre>
<p>Everything is the same size. Headings look like paragraphs. Buttons look like labels. It's visual chaos.</p>
<p>You need hierarchy. Not because it looks nice – because it tells users what to read first.</p>
<h3>The Type Scale</h3>
<p>You need exactly 5-6 font sizes:</p>
<pre><code class="language-plaintext">12px - Small text (captions, helper text, timestamps)
14px - Body text (paragraphs, form inputs, most UI)
16px - Emphasized body (slightly larger for readability)
20px - Subheadings (section titles)
24px - Headings (page titles, card headings)
32px - Large headings (main page titles, hero text)
</code></pre>
<p>And two font weights:</p>
<pre><code class="language-plaintext">400 - Normal (body text)
600 - Semi-bold (headings, emphasis)
</code></pre>
<p>That's it. Not 300, 400, 500, 600, 700, 800, 900. Just two weights.</p>
<h3>Why This Works</h3>
<p>Your eye immediately knows:</p>
<ul>
<li><p>32px + bold = most important</p>
</li>
<li><p>24px + bold = section heading</p>
</li>
<li><p>14px + normal = body text</p>
</li>
<li><p>12px + normal = metadata</p>
</li>
</ul>
<p>No guessing. Clear visual hierarchy.</p>
<h3>The Line Height Rule</h3>
<p>Developers often forget line-height. Result: text that's either cramped or weirdly spaced.</p>
<p>Simple rule:</p>
<pre><code class="language-css">/* Headings - tighter line height */
h1, h2, h3 {
  line-height: 1.2;
}

/* Body text - relaxed line height */
p, li, label {
  line-height: 1.6;
}
</code></pre>
<p>Short text (headings) needs less space. Long text (paragraphs) needs more space for readability.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/25fca600-ba34-4e92-9496-8d17ba59699a.svg" alt="" style="display:block;margin:0 auto" />

<p>The diagram above shows two layouts side by side. On the left, everything is 14px with no hierarchy – you can't tell what's important. On the right, using 32px/20px/14px/12px creates instant visual structure. Your eye knows exactly where to start reading.</p>
<h2>Rule 3: Color Is A System, Not A Palette</h2>
<p>Developers pick colors like this:</p>
<ol>
<li><p>Choose a blue</p>
</li>
<li><p>Use it everywhere</p>
</li>
<li><p>Need another color</p>
</li>
<li><p>Pick a random red</p>
</li>
<li><p>Repeat until you have 47 colors</p>
</li>
</ol>
<p>This is wrong.</p>
<h3>The Minimal Color System</h3>
<p>You need exactly these colors:</p>
<pre><code class="language-plaintext">Gray scale: 
  - 100 (lightest - backgrounds)
  - 300 (light - borders)
  - 500 (medium - disabled text)
  - 700 (dark - body text)
  - 900 (darkest - headings)

Primary color (your brand):
  - 100 (lightest - backgrounds)
  - 500 (medium - buttons, links)
  - 700 (dark - hover states)

Semantic colors:
  - Success green (confirmations, success states)
  - Warning yellow (warnings, pending states)
  - Danger red (errors, destructive actions)
  - Info blue (info messages, neutral highlights)
</code></pre>
<p>That's 5 grays + 3 primary shades + 4 semantic colors = 12 colors total.</p>
<p>Every color has a purpose. No random colors.</p>
<h3>How to Use Them</h3>
<pre><code class="language-css">/* Text */
.heading { color: gray-900; }
.body-text { color: gray-700; }
.caption { color: gray-500; }

/* Backgrounds */
.page-bg { background: gray-100; }
.card-bg { background: white; }

/* Borders */
.border { border: 1px solid gray-300; }

/* Interactive elements */
.button-primary { background: primary-500; }
.button-primary:hover { background: primary-700; }
.link { color: primary-500; }

/* States */
.success-message { background: success-100; color: success-700; }
.error-message { background: danger-100; color: danger-700; }
</code></pre>
<p>See the pattern? Each color has a job.</p>
<h3>Contrast Matters</h3>
<p>The most common accessibility mistake: poor contrast.</p>
<p><strong>Rule</strong>: Text on background needs 4.5:1 contrast ratio minimum.</p>
<pre><code class="language-css">/* Bad - 2.1:1 contrast */
color: #999;
background: #fff;

/* Good - 7:1 contrast */
color: #333;
background: #fff;
</code></pre>
<p>Use a contrast checker. Don't guess. Light gray on white might look subtle to you, but it's unreadable for many users.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/c94e15aa-783c-4fcd-98bd-715fc60eee58.svg" alt="" style="display:block;margin:0 auto" />

<p>This diagram shows the complete 12-color system with actual color swatches. You'll see the 5-shade gray scale, the 3-shade primary system, and the 4 semantic colors. Each color includes its hex code, shade number, and specific use case. The bottom section shows contrast examples – good vs bad contrast ratios with actual text samples.</p>
<h2>Rule 4: Visual Hierarchy Guides The Eye</h2>
<p>When a user opens your app, their eye needs to know where to start.</p>
<p>Bad developer UIs: everything has equal weight. The logo is the same size as a button. The heading is the same weight as body text. Nothing stands out.</p>
<p>Good design: clear hierarchy. You know exactly where to look first.</p>
<h3>The Hierarchy Stack</h3>
<ol>
<li><p><strong>Primary action</strong> - The one thing you want users to do</p>
<ul>
<li><p>Largest, most colorful, high contrast</p>
</li>
<li><p>Example: "Sign Up" button on landing page</p>
</li>
</ul>
</li>
<li><p><strong>Secondary actions</strong> - Alternative actions</p>
<ul>
<li><p>Less prominent, outline style or gray</p>
</li>
<li><p>Example: "Learn More" next to "Sign Up"</p>
</li>
</ul>
</li>
<li><p><strong>Tertiary actions</strong> - Minor actions</p>
<ul>
<li><p>Text links, small buttons</p>
</li>
<li><p>Example: "Cancel" or "Skip"</p>
</li>
</ul>
</li>
</ol>
<h3>Practical Example</h3>
<pre><code class="language-html">&lt;!-- Bad - all buttons look the same --&gt;
&lt;button&gt;Delete Account&lt;/button&gt;
&lt;button&gt;Cancel&lt;/button&gt;
&lt;button&gt;Save Changes&lt;/button&gt;

&lt;!-- Good - visual hierarchy --&gt;
&lt;button class="primary"&gt;Save Changes&lt;/button&gt;
&lt;button class="secondary"&gt;Cancel&lt;/button&gt;
&lt;button class="danger-link"&gt;Delete Account&lt;/button&gt;
</code></pre>
<pre><code class="language-css">.primary {
  background: blue-500;
  color: white;
  padding: 12px 24px;
  font-weight: 600;
}

.secondary {
  background: white;
  border: 1px solid gray-300;
  color: gray-700;
  padding: 12px 24px;
}

.danger-link {
  background: none;
  border: none;
  color: red-500;
  font-size: 14px;
  text-decoration: underline;
}
</code></pre>
<p>Your eye naturally goes: primary → secondary → tertiary.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/d97b8a0c-2f51-4da5-b5e9-732058cf8d05.png" alt="" style="display:block;margin:0 auto" />

<p>The visual above compares two button layouts. The left side shows three buttons with identical styling – you have to read each one to know what to do. The right side uses visual weight: the primary "Save Changes" button is filled and bold (most important), "Cancel" is outlined (alternative), and "Delete Account" is just a text link (destructive, less prominent). Your eye immediately knows which action is primary.</p>
<h2>Rule 5: Alignment Creates Order</h2>
<p>Misalignment is the hallmark of developer-designed UIs.</p>
<p>Text that doesn't line up. Buttons at random positions. Form fields with inconsistent widths.</p>
<h3>The Alignment Rules</h3>
<ol>
<li><p><strong>Left-align text</strong> (in left-to-right languages)</p>
<ul>
<li><p>Easiest to read</p>
</li>
<li><p>Creates a clean edge</p>
</li>
<li><p>Never center long paragraphs</p>
</li>
</ul>
</li>
<li><p><strong>Align related items</strong></p>
<ul>
<li><p>Form labels align with inputs</p>
</li>
<li><p>Icons align with text baselines</p>
</li>
<li><p>List items align vertically</p>
</li>
</ul>
</li>
<li><p><strong>Use a grid</strong></p>
<ul>
<li><p>12-column grid is standard</p>
</li>
<li><p>Or simple: left column + right column</p>
</li>
<li><p>Consistent margins on both sides</p>
</li>
</ul>
</li>
</ol>
<h3>Example: Form Alignment</h3>
<pre><code class="language-html">&lt;!-- Bad - misaligned --&gt;
&lt;div&gt;
  &lt;label&gt;Email&lt;/label&gt;
  &lt;input type="email"&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;label&gt;Password&lt;/label&gt;
  &lt;input type="password"&gt;
&lt;/div&gt;

&lt;!-- Good - aligned with grid --&gt;
&lt;div class="form-row"&gt;
  &lt;label class="form-label"&gt;Email&lt;/label&gt;
  &lt;input class="form-input" type="email"&gt;
&lt;/div&gt;
&lt;div class="form-row"&gt;
  &lt;label class="form-label"&gt;Password&lt;/label&gt;
  &lt;input class="form-input" type="password"&gt;
&lt;/div&gt;
</code></pre>
<pre><code class="language-css">.form-row {
  display: grid;
  grid-template-columns: 120px 1fr;
  gap: 16px;
  align-items: center;
  margin-bottom: 16px;
}

.form-label {
  text-align: right; /* Labels align right to inputs */
}
</code></pre>
<p>Everything lines up. Creates visual order.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/cacb7848-0ced-4ac5-b934-9bde2cdc839e.svg" alt="" style="display:block;margin:0 auto" />

<h2>Common Mistakes Developers Make</h2>
<h3>1. Using Too Many Fonts</h3>
<pre><code class="language-css">/* Bad */
h1 { font-family: 'Fancy Display Font'; }
h2 { font-family: 'Another Font'; }
body { font-family: 'Different Body Font'; }
code { font-family: 'Yet Another Monospace'; }
</code></pre>
<p>You need TWO fonts max:</p>
<ul>
<li><p>Sans-serif for UI (Inter, SF Pro, Roboto)</p>
</li>
<li><p>Monospace for code (Fira Code, JetBrains Mono)</p>
</li>
</ul>
<p>That's it.</p>
<h3>2. Over-Using Bold</h3>
<p>Developers discover <code>font-weight: 700</code> and suddenly everything is bold.</p>
<p>Bold is for emphasis. If everything is bold, nothing is bold.</p>
<p>Use bold sparingly:</p>
<ul>
<li><p>Headings</p>
</li>
<li><p>Active navigation items</p>
</li>
<li><p>Key metrics/numbers</p>
</li>
<li><p>Strong emphasis in body text (rarely)</p>
</li>
</ul>
<h3>3. Ignoring Mobile</h3>
<p>You design for desktop. It looks great on your 27" monitor. Then someone opens it on their phone and it's a disaster.</p>
<p><strong>Always design mobile-first:</strong></p>
<pre><code class="language-css">/* Mobile first (default) */
.container {
  padding: 16px;
}

.grid {
  display: block; /* Stack on mobile */
}

/* Desktop (progressive enhancement) */
@media (min-width: 768px) {
  .container {
    padding: 32px;
    max-width: 1200px;
    margin: 0 auto;
  }
  
  .grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 24px;
  }
}
</code></pre>
<p>Start with mobile. Enhance for desktop. Not the other way around.</p>
<h3>4. Inconsistent Spacing</h3>
<p>5px here, 17px there, 23px somewhere else.</p>
<p>Use the spacing scale. Always. No exceptions.</p>
<h3>5. Low Contrast</h3>
<p>Gray text on light gray background might look "modern" to you. It's unreadable.</p>
<p>Use a contrast checker. Aim for 4.5:1 minimum for body text, 3:1 for large text.</p>
<h2>The Quick Checklist</h2>
<p>Before you ship your UI, ask:</p>
<ul>
<li><p>[ ] Is there enough whitespace? (16-24px between sections)</p>
</li>
<li><p>[ ] Do I have clear typography hierarchy? (5-6 sizes max)</p>
</li>
<li><p>[ ] Am I using my color system consistently? (12 colors total)</p>
</li>
<li><p>[ ] Is there a clear visual hierarchy? (can you tell what's important?)</p>
</li>
<li><p>[ ] Is everything aligned properly? (using a grid)</p>
</li>
<li><p>[ ] Does it work on mobile? (test it!)</p>
</li>
<li><p>[ ] Is the contrast good enough? (use a checker)</p>
</li>
<li><p>[ ] Am I using 2 fonts max? (not 5 different fonts)</p>
</li>
</ul>
<p>If you can check all these boxes, your UI is already better than 80% of developer-designed apps.</p>
<h2>Tools That Help</h2>
<p><strong>Color contrast checkers:</strong></p>
<ul>
<li><p>WebAIM Contrast Checker</p>
</li>
<li><p>Colorable</p>
</li>
<li><p>Accessible Colors</p>
</li>
</ul>
<p><strong>Type scale generators:</strong></p>
<ul>
<li><p>Type Scale</p>
</li>
<li><p>Modular Scale</p>
</li>
</ul>
<p><strong>Spacing system:</strong></p>
<ul>
<li><p>Tailwind CSS (has great defaults)</p>
</li>
<li><p>Just use multiples of 4 or 8</p>
</li>
</ul>
<p><strong>Design inspiration:</strong></p>
<ul>
<li><p>Dribbble (but don't copy, learn)</p>
</li>
<li><p>Refactoring UI (book specifically for developers)</p>
</li>
<li><p>Land-book (real websites)</p>
</li>
</ul>
<h2>The Bottom Line</h2>
<p>You don't need to become a designer. You just need to follow these rules:</p>
<ol>
<li><p><strong>Whitespace</strong> - Use consistent spacing (16-24px between sections)</p>
</li>
<li><p><strong>Typography</strong> - 5-6 sizes, 2 weights, clear hierarchy</p>
</li>
<li><p><strong>Color</strong> - 12 colors total, each with a purpose</p>
</li>
<li><p><strong>Hierarchy</strong> - Make important things look important</p>
</li>
<li><p><strong>Alignment</strong> - Everything lines up, use a grid</p>
</li>
</ol>
<p>These aren't subjective "make it pretty" suggestions. They're concrete rules that make interfaces usable.</p>
<p>Start with one rule. Apply it to your app. See the difference. Then add another.</p>
<p>Your code is beautiful. Your UI should be too.</p>
<hr />
<p><strong>Part 2 Coming Soon</strong>: Layout patterns, component design, dark mode, animations, and responsive design.</p>
<hr />
<p><em>What design mistakes do you see developers make most often? Drop them in the comments.</em></p>
]]></content:encoded></item><item><title><![CDATA[Your Site is Slow: Now What? A Developer's Guide to Actually Finding the Problem]]></title><description><![CDATA[You know that feeling. Your app feels sluggish. Users are complaining. Your boss is asking questions. But where do you even start?
I've been there too many times. Staring at code, making random "optim]]></description><link>https://blog.thecodingant.in/your-site-is-slow-now-what-a-developer-s-guide-to-actually-finding-the-problem</link><guid isPermaLink="true">https://blog.thecodingant.in/your-site-is-slow-now-what-a-developer-s-guide-to-actually-finding-the-problem</guid><category><![CDATA[devtools]]></category><category><![CDATA[#chrome_devtools]]></category><category><![CDATA[react dev tools]]></category><category><![CDATA[react dev]]></category><dc:creator><![CDATA[Nitesh Kumar Tudu]]></dc:creator><pubDate>Sat, 04 Apr 2026 05:50:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/1a952c19-d2e2-4d50-b54a-f4ddaeaa0057.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You know that feeling. Your app <em>feels</em> sluggish. Users are complaining. Your boss is asking questions. But where do you even start?</p>
<p>I've been there too many times. Staring at code, making random "optimizations" that don't help, getting frustrated. Then I learned to actually <em>measure</em> instead of guess.</p>
<p>Here's the thing: performance debugging isn't magic. It's a process. And the browser gives you ridiculously powerful tools to figure out exactly what's slowing you down.</p>
<p>Let me show you how I debug performance issues, from "this feels slow" to "I found the exact function causing the problem."</p>
<h2>Step 1: Confirm You Actually Have a Problem</h2>
<p>Before you start optimizing, you need to know if there's actually a problem and where it is.</p>
<p>Open Chrome DevTools (F12 or right-click → Inspect) and go to the <strong>Performance</strong> tab. This is your new best friend.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/65e2a328-4555-4f89-83b6-3c5370605f4b.png" alt="" style="display:block;margin:0 auto" />

<p>Hit the record button (or Cmd+E / Ctrl+E), interact with your app for a few seconds, then stop recording. You'll get a waterfall chart showing everything that happened.</p>
<h3>What You're Looking For</h3>
<p><strong>Long tasks</strong> - Anything that blocks the main thread for more than 50ms shows up as a red flag. These are your enemies. The browser can't respond to user input during long tasks.</p>
<p><strong>Frame drops</strong> - If you see the FPS (frames per second) graph dipping below 60, that's where your app is janky. Smooth scrolling and animations need 60fps. Anything less feels choppy.</p>
<p><strong>Excessive layout and paint</strong> - See a ton of purple (layout) or green (paint) bars? You're probably triggering layout thrash or unnecessary repaints.</p>
<p>Here's what a healthy performance profile looks like versus a problematic one:</p>
<p>The visual above shows the key differences. In a healthy profile, the FPS line stays steady at 60, and all tasks are short green/blue/purple bars. In a problematic one, the FPS dips, and you see those red triangle corners on tasks—that's the browser warning you about long tasks.</p>
<h2>Step 2: Find the Slow Code</h2>
<p>Once you know where the jank is, you need to find <em>what</em> code is causing it.</p>
<p>Click on one of those long red-flagged tasks in the Performance tab. The bottom panel shows you the call stack—every function that was running during that task.</p>
<p>This is gold. You can see:</p>
<ul>
<li><p>Which functions took the most time (wider bars = more time)</p>
</li>
<li><p>The full call tree from your code down to browser internals</p>
</li>
<li><p>Source code snippets for each function</p>
</li>
</ul>
<h3>The Bottom-Up Tab is Your Friend</h3>
<p>After recording, switch to the "Bottom-Up" view. This groups time by function, sorted from most expensive to least expensive.</p>
<p>You'll see something like:</p>
<ul>
<li><p><code>calculateExpensiveThing</code> - 342ms (self time: 98ms)</p>
</li>
<li><p><code>Array.prototype.map</code> - 244ms</p>
</li>
<li><p><code>renderComponent</code> - 189ms</p>
</li>
</ul>
<p>The "self time" is what matters—that's time spent <em>in</em> that function, not calling other functions. If <code>calculateExpensiveThing</code> has 98ms of self time, that's where your bottleneck is.</p>
<p>Click on a function to jump to the source code. Then you can actually see what's slow.</p>
<h2>Step 3: React Dev Tools Profiler (React Only)</h2>
<p>If you're using React, install the React DevTools extension. It has a Profiler tab that's specifically designed for React performance.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/ede7b26c-b15b-4c33-bfc0-a27a0b4ab443.png" alt="" style="display:block;margin:0 auto" />

<p>Record a profile the same way—click record, interact with your app, stop recording.</p>
<h3>What the Profiler Shows You</h3>
<p><strong>Flamegraph view</strong> - Each component is a bar. Wider = took longer to render. Color indicates why it rendered:</p>
<ul>
<li><p>Gray - didn't render</p>
</li>
<li><p>Yellow - rendered because props/state changed</p>
</li>
<li><p>Green - rendered because parent rendered</p>
</li>
</ul>
<p><strong>Ranked view</strong> - Components sorted by render time. The slowest component is at the top. This is where you start optimizing.</p>
<h3>The Most Common React Performance Issues</h3>
<ol>
<li><p><strong>Unnecessary re-renders</strong> - A component renders even though nothing changed. Look for green bars in the flamegraph. Usually fixed with <code>React.memo()</code> or <code>useMemo()</code>.</p>
</li>
<li><p><strong>Expensive calculations in render</strong> - Doing heavy work every render instead of memoizing it. Look for components with long self-times. Fix with <code>useMemo()</code>.</p>
</li>
<li><p><strong>Passing new object/array/function references every render</strong> - This breaks memoization. The classic mistake:</p>
<pre><code class="language-javascript">// Bad - new object every render
&lt;Component config={{ setting: true }} /&gt;

// Good - stable reference
const config = useMemo(() =&gt; ({ setting: true }), []);
&lt;Component config={config} /&gt;
</code></pre>
</li>
</ol>
<p>The Profiler's "Why did this render?" feature tells you <em>exactly</em> which prop changed and triggered the render. No more guessing.</p>
<h2>Step 4: Memory Leaks (When Your Site Gets Slower Over Time)</h2>
<p>Your app starts fast but gets slower the longer it runs? That's probably a memory leak.</p>
<h3>Using the Memory Tab</h3>
<p>Open DevTools → Memory tab → Take a heap snapshot.</p>
<p>Use your app for a bit (click around, navigate, do stuff). Take another snapshot.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/5746cfb7-a8ff-49a7-89ba-2fb273ec5c2c.png" alt="" style="display:block;margin:0 auto" />

<p>Compare the snapshots. You're looking for:</p>
<ul>
<li><p>Objects that keep growing in count (leaked listeners, timers, subscriptions)</p>
</li>
<li><p>Detached DOM nodes (components unmounted but still in memory)</p>
</li>
<li><p>Large strings or arrays that don't get cleaned up</p>
</li>
</ul>
<h3>Common Memory Leaks</h3>
<p><strong>Event listeners not removed:</strong></p>
<pre><code class="language-javascript">// Bad
useEffect(() =&gt; {
  window.addEventListener('scroll', handleScroll);
  // No cleanup!
}, []);

// Good
useEffect(() =&gt; {
  window.addEventListener('scroll', handleScroll);
  return () =&gt; window.removeEventListener('scroll', handleScroll);
}, []);
</code></pre>
<p><strong>Intervals/timeouts not cleared:</strong></p>
<pre><code class="language-javascript">// Bad
useEffect(() =&gt; {
  const timer = setInterval(doSomething, 1000);
  // Timer keeps running after component unmounts
}, []);

// Good  
useEffect(() =&gt; {
  const timer = setInterval(doSomething, 1000);
  return () =&gt; clearInterval(timer);
}, []);
</code></pre>
<p><strong>Subscriptions not unsubscribed:</strong></p>
<pre><code class="language-javascript">// Bad
useEffect(() =&gt; {
  const subscription = observable.subscribe(handleData);
  // Subscription leaks
}, []);

// Good
useEffect(() =&gt; {
  const subscription = observable.subscribe(handleData);
  return () =&gt; subscription.unsubscribe();
}, []);
</code></pre>
<p>The Memory tab will show you "Detached HTMLDivElement" or similar if you have detached DOM nodes. Those are components that unmounted but something is still holding a reference to their DOM.</p>
<h2>Step 5: Custom Performance Marks</h2>
<p>Sometimes you need more granular timing than DevTools provides. That's where <code>performance.mark()</code> and <code>performance.measure()</code> come in.</p>
<h3>Using the Performance API</h3>
<p>Wrap the code you want to measure:</p>
<pre><code class="language-javascript">performance.mark('expensive-start');

// Your expensive code here
const result = doExpensiveCalculation();

performance.mark('expensive-end');
performance.measure('expensive-operation', 'expensive-start', 'expensive-end');

// Get the measurements
const measures = performance.getEntriesByType('measure');
const expensiveOp = measures.find(m =&gt; m.name === 'expensive-operation');
console.log(`Operation took ${expensiveOp.duration}ms`);
</code></pre>
<p>These marks show up in the Performance tab timeline. You'll see your custom labels alongside the browser's internal timing.</p>
<h3>Real-World Example</h3>
<p>I use this to measure API calls, data transformations, and component render times:</p>
<pre><code class="language-javascript">function ProductList({ products }) {
  performance.mark('product-list-start');
  
  const filtered = useMemo(() =&gt; {
    performance.mark('filter-start');
    const result = products.filter(p =&gt; p.inStock);
    performance.mark('filter-end');
    performance.measure('filter-products', 'filter-start', 'filter-end');
    return result;
  }, [products]);
  
  performance.mark('product-list-end');
  performance.measure('product-list-render', 'product-list-start', 'product-list-end');
  
  return &lt;div&gt;{/* render products */}&lt;/div&gt;;
}
</code></pre>
<p>Now when I record a Performance profile, I can see exactly how long filtering takes versus how long the component render takes.</p>
<p>You can also send these measurements to your analytics to track real-user performance in production.</p>
<h2>Step 6: Lighthouse Performance Audit</h2>
<p>For a quick overall health check, run a Lighthouse audit.</p>
<p>Open DevTools → Lighthouse tab → Generate report.</p>
<p>Lighthouse gives you:</p>
<ul>
<li><p>Performance score (0-100)</p>
</li>
<li><p>Specific issues ranked by impact</p>
</li>
<li><p>Actionable suggestions with code examples</p>
</li>
<li><p>Metrics like First Contentful Paint, Time to Interactive, Total Blocking Time</p>
</li>
</ul>
<p>It won't tell you <em>which line of code</em> is slow, but it'll tell you <em>what categories</em> of problems you have:</p>
<ul>
<li><p>Unused JavaScript</p>
</li>
<li><p>Images not optimized</p>
</li>
<li><p>Render-blocking resources</p>
</li>
<li><p>Poor layout shift scores</p>
</li>
</ul>
<p>Use Lighthouse to find the high-level problems, then use the Performance tab to drill into the specific code.</p>
<h2>The Tools I Actually Reach For</h2>
<p>Here's my actual debugging workflow, in order:</p>
<ol>
<li><p><strong>Feel something is slow</strong> → Open Performance tab, record, look for red flags</p>
</li>
<li><p><strong>Found a slow task</strong> → Bottom-Up view to find the function</p>
</li>
<li><p><strong>React component rendering slowly</strong> → React Profiler to see why it rendered</p>
</li>
<li><p><strong>Still not clear</strong> → Add <code>performance.mark()</code> around suspicious code</p>
</li>
<li><p><strong>Memory growing over time</strong> → Memory tab, take snapshots, compare</p>
</li>
<li><p><strong>General health check</strong> → Run Lighthouse</p>
</li>
</ol>
<p>I don't use all of these every time. Usually the Performance tab + React Profiler catches 90% of issues.</p>
<h2>One More Thing: Network Tab</h2>
<p>Performance isn't always about JavaScript. Sometimes it's about how much you're downloading.</p>
<p>Open DevTools → Network tab → Reload the page.</p>
<p>Look for:</p>
<ul>
<li><p>Large bundle sizes (anything over 500KB should raise an eyebrow)</p>
</li>
<li><p>Waterfalls where resources block each other</p>
</li>
<li><p>Too many requests (40+ is usually a problem)</p>
</li>
<li><p>Unoptimized images (multi-megabyte PNGs)</p>
</li>
</ul>
<p>The Network tab won't tell you if your code is slow, but it'll tell you if you're shipping too much code in the first place.</p>
<h2>The Bottom Line</h2>
<p>Stop guessing. Start measuring.</p>
<p>The browser gives you incredible tools to find <em>exactly</em> what's slow. Use them:</p>
<ul>
<li><p><strong>Performance tab</strong> for finding slow JavaScript</p>
</li>
<li><p><strong>React Profiler</strong> for unnecessary renders (React only)</p>
</li>
<li><p><strong>Memory tab</strong> for leaks</p>
</li>
<li><p><strong>performance.mark()</strong> for custom timing</p>
</li>
<li><p><strong>Lighthouse</strong> for overall health</p>
</li>
</ul>
<p>Record a profile. Find the red flags. Look at the call stack. Fix the actual problem.</p>
<p>Your users will thank you for the smooth, fast experience. And you'll thank yourself for not wasting time optimizing the wrong things.</p>
<hr />
<p><em>What performance tools do you use? Found any tricky performance bugs lately? Let me know in the comments.</em></p>
]]></content:encoded></item><item><title><![CDATA[React 19: The Update That Actually Changes How You Build]]></title><description><![CDATA[Look, I've been through enough React updates to know when one is just polish and when one is actually different. React 19? This is the latter.
I spent the weekend porting a side project to React 19, a]]></description><link>https://blog.thecodingant.in/react-19-the-update-that-actually-changes-how-you-build</link><guid isPermaLink="true">https://blog.thecodingant.in/react-19-the-update-that-actually-changes-how-you-build</guid><category><![CDATA[React]]></category><category><![CDATA[React 19]]></category><category><![CDATA[react hooks]]></category><category><![CDATA[Performance Optimization]]></category><dc:creator><![CDATA[Nitesh Kumar Tudu]]></dc:creator><pubDate>Fri, 03 Apr 2026 03:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/c7855a3d-5766-490d-ad02-7879391afc6d.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Look, I've been through enough React updates to know when one is just polish and when one is <em>actually</em> different. React 19? This is the latter.</p>
<p>I spent the weekend porting a side project to React 19, and I kept catching myself thinking "wait, I can just... do that now?" So many patterns I've been dancing around for years – they're just... gone. Simplified. Fixed.</p>
<p>Let me show you what I mean.</p>
<h2>Actions: Forms That Don't Suck Anymore</h2>
<p>Remember this dance? You've written it a hundred times:</p>
<pre><code class="language-javascript">function UpdateProfile() {
  const [name, setName] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const handleSubmit = async (e) =&gt; {
    e.preventDefault();
    setLoading(true);
    setError(null);
    
    try {
      await updateProfile(name);
      // redirect or show success
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    &lt;form onSubmit={handleSubmit}&gt;
      &lt;input 
        value={name} 
        onChange={(e) =&gt; setName(e.target.value)}
        disabled={loading}
      /&gt;
      &lt;button disabled={loading}&gt;
        {loading ? 'Updating...' : 'Update'}
      &lt;/button&gt;
      {error &amp;&amp; &lt;p&gt;{error}&lt;/p&gt;}
    &lt;/form&gt;
  );
}
</code></pre>
<p>Three pieces of state. Manual loading states. Error handling. Preventing default. Disabling inputs. It works, but man, it's verbose.</p>
<p>React 19 introduces <strong>Actions</strong> – and suddenly that whole pattern collapses:</p>
<pre><code class="language-javascript">function UpdateProfile() {
  const [state, formAction, isPending] = useActionState(
    async (previousState, formData) =&gt; {
      const name = formData.get('name');
      const error = await updateProfile(name);
      if (error) return { error };
      redirect('/profile');
      return null;
    },
    null
  );

  return (
    &lt;form action={formAction}&gt;
      &lt;input name="name" disabled={isPending} /&gt;
      &lt;button disabled={isPending}&gt;
        {isPending ? 'Updating...' : 'Update'}
      &lt;/button&gt;
      {state?.error &amp;&amp; &lt;p&gt;{state.error}&lt;/p&gt;}
    &lt;/form&gt;
  );
}
</code></pre>
<p>Same functionality. Half the code. No <code>useState</code> for three different things. No <code>e.preventDefault()</code>. The form action gets the <code>FormData</code> automatically.</p>
<p>The <code>isPending</code> state? That's built-in. React tracks it for you. Error handling? Also built-in. You just return the error from your action.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/f8b86dfb-eaef-4cfa-87e3-d1ceb8fc994e.svg" alt="" style="display:block;margin:0 auto" />

<p>This is the kind of update that makes you go back and refactor old code just because the new way is <em>so much cleaner</em>.</p>
<h2>Server Actions: The API Routes You Don't Write</h2>
<p>Here's where it gets wild. You know how every form submission means:</p>
<ol>
<li><p>Write the client-side form</p>
</li>
<li><p>Write an API route</p>
</li>
<li><p>Call the API from the form</p>
</li>
<li><p>Handle the response</p>
</li>
</ol>
<p>React 19 says: what if you just... didn't write the API route?</p>
<p>Create a file called <code>actions.ts</code>:</p>
<pre><code class="language-typescript">'use server'

export async function createTodo(formData: FormData) {
  const title = formData.get('title');
  
  // This runs on the server
  await db.todos.create({
    title,
    userId: auth.currentUser.id
  });
  
  revalidatePath('/todos');
}
</code></pre>
<p>Then use it in your component:</p>
<pre><code class="language-typescript">'use client'

import { createTodo } from './actions';

export function TodoForm() {
  return (
    &lt;form action={createTodo}&gt;
      &lt;input name="title" placeholder="What needs doing?" /&gt;
      &lt;button type="submit"&gt;Add&lt;/button&gt;
    &lt;/form&gt;
  );
}
</code></pre>
<p>That's it. The form calls a server function directly. No API route. No fetch call. No JSON serialization. It just... works.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/dcedeadc-f981-4257-b05d-5d8c7aac9f3c.svg" alt="" style="display:block;margin:0 auto" />

<p>The <code>'use server'</code> directive tells your bundler "this function only runs on the server." Your framework handles the RPC call automatically. From the client's perspective, it's just calling a function. From the server's perspective, it's handling a secure POST request.</p>
<p>This is kind of mind-blowing when you first see it.</p>
<h2>The <code>use</code> Hook: Promises, But Make It React</h2>
<p>Okay, this one's subtle but powerful. You've probably done this pattern:</p>
<pre><code class="language-javascript">function Comments() {
  const [comments, setComments] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() =&gt; {
    fetchComments().then(data =&gt; {
      setComments(data);
      setLoading(false);
    });
  }, []);

  if (loading) return &lt;div&gt;Loading...&lt;/div&gt;;
  return comments.map(c =&gt; &lt;Comment key={c.id} {...c} /&gt;);
}
</code></pre>
<p>React 19 introduces <code>use()</code> – it lets you read promises directly in render:</p>
<pre><code class="language-javascript">import { use } from 'react';

function Comments({ commentsPromise }) {
  // This suspends until the promise resolves
  const comments = use(commentsPromise);
  
  return comments.map(c =&gt; &lt;Comment key={c.id} {...c} /&gt;);
}

function Page() {
  const commentsPromise = fetchComments();
  
  return (
    &lt;Suspense fallback={&lt;div&gt;Loading...&lt;/div&gt;}&gt;
      &lt;Comments commentsPromise={commentsPromise} /&gt;
    &lt;/Suspense&gt;
  );
}
</code></pre>
<p>No <code>useState</code>. No <code>useEffect</code>. No loading state. You just <code>use()</code> the promise and React suspends until it's ready.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/d0647ebe-596d-4694-8c93-2228f8989f38.svg" alt="" style="display:block;margin:0 auto" />

<p>The really cool part? Unlike hooks, <code>use()</code> can be called conditionally:</p>
<pre><code class="language-javascript">function UserProfile({ userId }) {
  if (!userId) {
    return &lt;div&gt;Please log in&lt;/div&gt;;
  }
  
  // This early return would break useContext
  // But use() is totally fine with it
  const theme = use(ThemeContext);
  
  return &lt;div style={{ color: theme.color }}&gt;Profile&lt;/div&gt;;
}
</code></pre>
<p>You can also use it to read context after early returns. It's like hooks, but without the rules-of-hooks constraints.</p>
<h2>Goodbye <code>forwardRef</code>, Hello Sanity</h2>
<p>Quick: what's the most annoying React API? For me, it's always been <code>forwardRef</code>.</p>
<pre><code class="language-javascript">const Input = forwardRef((props, ref) =&gt; {
  return &lt;input {...props} ref={ref} /&gt;;
});
</code></pre>
<p>Why do I need this wrapper? Why can't <code>ref</code> just be a prop like everything else?</p>
<p>React 19 agrees with me. Now it's just:</p>
<pre><code class="language-javascript">function Input({ ref, ...props }) {
  return &lt;input {...props} ref={ref} /&gt;;
}
</code></pre>
<p>That's it. <code>ref</code> is a prop now. Like it always should have been.</p>
<p>Same with Context providers:</p>
<pre><code class="language-javascript">// Before
&lt;ThemeContext.Provider value="dark"&gt;
  &lt;App /&gt;
&lt;/ThemeContext.Provider&gt;

// After
&lt;ThemeContext value="dark"&gt;
  &lt;App /&gt;
&lt;/ThemeContext&gt;
</code></pre>
<p>No more <code>.Provider</code>. Just use the context directly.</p>
<p>These are small changes that make the API feel more consistent and less magical.</p>
<h2>Document Metadata That Actually Makes Sense</h2>
<p>You know what's always been weird? Managing page titles and meta tags in React.</p>
<p>You'd either:</p>
<ol>
<li><p>Use a library like <code>react-helmet</code></p>
</li>
<li><p>Manually manipulate the DOM in <code>useEffect</code></p>
</li>
<li><p>Give up and set it once at the root level</p>
</li>
</ol>
<p>React 19 says: just render them wherever you want.</p>
<pre><code class="language-javascript">function BlogPost({ post }) {
  return (
    &lt;article&gt;
      &lt;title&gt;{post.title}&lt;/title&gt;
      &lt;meta name="description" content={post.excerpt} /&gt;
      &lt;meta property="og:image" content={post.coverImage} /&gt;
      
      &lt;h1&gt;{post.title}&lt;/h1&gt;
      &lt;p&gt;{post.content}&lt;/p&gt;
    &lt;/article&gt;
  );
}
</code></pre>
<p>React will automatically hoist these to the <code>&lt;head&gt;</code>. They work in Server Components. They work in Client Components. They just work.</p>
<p>You can finally colocate SEO metadata with the component that needs it. No more hunting through the codebase to find where titles are set.</p>
<h2>Optimistic Updates Made Easy</h2>
<p>You know that pattern where you update the UI immediately, then sync to the server in the background? It's tricky to get right.</p>
<p>React 19 adds <code>useOptimistic</code> to handle this:</p>
<pre><code class="language-javascript">function TodoList({ todos }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, newTodo) =&gt; [...state, newTodo]
  );

  async function addTodo(formData) {
    const newTodo = { 
      id: crypto.randomUUID(), 
      text: formData.get('text'),
      pending: true 
    };
    
    // Update UI immediately
    addOptimisticTodo(newTodo);
    
    // Sync to server
    await createTodo(formData);
  }

  return (
    &lt;&gt;
      &lt;form action={addTodo}&gt;
        &lt;input name="text" /&gt;
        &lt;button&gt;Add&lt;/button&gt;
      &lt;/form&gt;
      {optimisticTodos.map(todo =&gt; (
        &lt;div key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}&gt;
          {todo.text}
        &lt;/div&gt;
      ))}
    &lt;/&gt;
  );
}
</code></pre>
<p>The todo appears instantly with reduced opacity. When the server responds, it updates with full opacity. If the server returns an error, React automatically reverts the optimistic update.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/c49668fd-bf20-44b2-ba61-294f3099d0ae.svg" alt="" style="display:block;margin:0 auto" />

<p>This is the kind of UX polish that used to require a ton of custom state management. Now it's built-in.</p>
<h2>Better Errors (Finally!)</h2>
<p>React's error messages have always been... let's say "verbose." You'd get the same error logged three times, with stack traces everywhere, making it hard to figure out what actually went wrong.</p>
<p>React 19 cleans this up. One error. One stack trace. Actual useful information about what went wrong and where.</p>
<p>Hydration errors got especially better. Instead of:</p>
<pre><code class="language-plaintext">Warning: Text content did not match. Server: "Server" Client: "Client"
Warning: An error occurred during hydration...
Uncaught Error: Text content does not match...
</code></pre>
<p>You get:</p>
<pre><code class="language-plaintext">Error: Hydration failed because the server rendered HTML didn't 
match the client. This can happen if:
- You used Date.now() or Math.random()
- A browser extension modified the HTML
- Server/client environment differences

  &lt;span&gt;
  + Client
  - Server
</code></pre>
<p>It shows you the diff! And it tells you common causes! This is the kind of DX improvement that saves hours of debugging.</p>
<h2>The Things You'll Barely Notice (But Matter)</h2>
<p>Some changes are more behind-the-scenes but still important:</p>
<p><strong>Stylesheet precedence</strong>: You can now control when stylesheets load relative to each other. No more FOUC (flash of unstyled content) because a critical stylesheet loaded too late.</p>
<p><strong>Async scripts</strong>: Render script tags anywhere in your component tree. React deduplicates them and loads them in the right order automatically.</p>
<p><strong>Preloading resources</strong>: New APIs to prefetch DNS, preconnect to origins, and preload resources. Your framework probably handles this for you, but it's nice that it's standardized.</p>
<p><strong>Custom Elements support</strong>: If you're using Web Components, they finally work properly in React. Props are passed as properties (not attributes), and everything just works.</p>
<h2>What This Means For You</h2>
<p>React 19 isn't just adding features. It's removing friction.</p>
<p>All those patterns you've memorized – the boilerplate, the workarounds, the "React way" of doing things – a lot of them are just... gone now. Simpler. More direct.</p>
<p>Forms don't need three pieces of state. Refs are just props. Metadata goes where it belongs. Errors are readable.</p>
<p>And Server Actions? That's not just a convenience feature. That's a fundamental shift in how you build React apps. The line between client and server is blurring in a way that actually makes sense.</p>
<p>I spent years writing API routes that were literally just "take this form data and put it in the database." And now with React 19, Just write the database code. Although <code>Next js</code> was doing it before react, but most of the software still prefer a dedicated backend system for handling database queries. Good to know we can do this with directly with React.</p>
<p>For years we've used a different package or configuring custom functions it to handle these features, But now they're out of the box features. It is simpler now.</p>
<h2>Should You Upgrade?</h2>
<p>If you're starting a new project? Absolutely. React 19 is stable, and the major frameworks (Next.js, Remix, etc.) all support it.</p>
<p>Existing projects? The migration is pretty smooth. There are codemods for most breaking changes. The React team documented the upgrade path well.</p>
<p>The biggest decision is whether to go all-in on Server Components and Actions. If your framework supports them (Next.js App Router, Remix with React Router 7), it's worth trying. The patterns are cleaner, and the user experience improvements are real.</p>
<p>Just start small. Port one form to use Actions. Try Server Components on one page. See how it feels.</p>
<p>For me? I'm not going back. This is how I want to build React apps from now on.</p>
<hr />
<p><em>Have you tried React 19 yet? What feature are you most excited about? Let me know in the comments – I'm curious what patterns people discover that I haven't thought of.</em></p>
]]></content:encoded></item><item><title><![CDATA[CSS Grid: The Layout System You're Probably Underusing]]></title><description><![CDATA[I'll be honest with you – for the longest time, I avoided CSS Grid. Flexbox felt comfortable, like an old pair of jeans. Grid felt... complicated. Too many lines, too many terms, too much to remember.]]></description><link>https://blog.thecodingant.in/css-grid-the-layout-system-you-re-probably-underusing</link><guid isPermaLink="true">https://blog.thecodingant.in/css-grid-the-layout-system-you-re-probably-underusing</guid><category><![CDATA[CSS]]></category><category><![CDATA[CSS Grid]]></category><category><![CDATA[css-grid-layout]]></category><category><![CDATA[ CSS Grid Properties Explained]]></category><dc:creator><![CDATA[Nitesh Kumar Tudu]]></dc:creator><pubDate>Thu, 02 Apr 2026 03:30:00 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1555066931-4365d14bab8c?ixid=M3wyNjEwMzZ8MHwxfHNlYXJjaHw2fHxjb2RlJTIwbGFwdG9wfGVufDB8MHx8fDE3NzUyNDM4Mjh8MA&amp;ixlib=rb-4.1.0&amp;w=1600&amp;q=80" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'll be honest with you – for the longest time, I avoided CSS Grid. Flexbox felt comfortable, like an old pair of jeans. Grid felt... complicated. Too many lines, too many terms, too much to remember.</p>
<p>Then I actually sat down and learned it properly. And now? I kick myself for not doing it sooner.</p>
<p>CSS Grid isn't just "flexbox but in 2D" (though that's not a bad mental model to start with). It's a fundamentally different way of thinking about layouts. Once it clicks, you'll find yourself reaching for it constantly.</p>
<p>Let me show you why.</p>
<h2>The Mental Model That Changed Everything</h2>
<p>Here's the thing most tutorials get wrong: they start by throwing syntax at you. <code>grid-template-columns</code>, <code>grid-auto-flow</code>, <code>grid-gap</code> – it's overwhelming.</p>
<p>Instead, think of Grid like this: <strong>you're drawing lines on a page, then placing things between those lines.</strong></p>
<p>That's it. Everything else is just different ways to define where those lines go and what sits between them.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/7a716c53-a006-46a1-be3d-e0f90cdba2f8.svg" alt="" style="display:block;margin:0 auto" />

<p>When you create a grid, you're creating <strong>grid lines</strong> – both horizontal and vertical. Your items sit in the spaces between these lines. The visual above shows how items occupy the cells formed by these intersecting lines.</p>
<h2>The Basics (But Actually Useful This Time)</h2>
<p>Let's start with the minimum you need to know:</p>
<pre><code class="language-css">.container {
  display: grid;
  grid-template-columns: 200px 200px 200px;
  grid-template-rows: 100px 100px;
  gap: 20px;
}
</code></pre>
<p>This creates a 3×2 grid. Three columns, two rows. Simple.</p>
<p>But here's where it gets interesting: instead of fixed pixels, you can use <code>fr</code> units:</p>
<pre><code class="language-css">.container {
  display: grid;
  grid-template-columns: 1fr 2fr 1fr;
  gap: 20px;
}
</code></pre>
<p>Think of <code>fr</code> as "fraction of available space". In the code above, you're saying: "Give the middle column twice as much space as the side columns."</p>
<p>This is where Grid starts to feel magical – it does the math for you. No more <code>calc()</code> nightmares.</p>
<h2>Grid Template Areas: The Game Changer</h2>
<p>Alright, here's the feature that made me fall in love with Grid. You can literally <strong>draw your layout in CSS</strong>.</p>
<p>Check this out:</p>
<pre><code class="language-css">.container {
  display: grid;
  grid-template-columns: 200px 1fr 1fr;
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "header header header"
    "sidebar content content"
    "footer footer footer";
  gap: 16px;
}

.header  { grid-area: header; }
.sidebar { grid-area: sidebar; }
.content { grid-area: content; }
.footer  { grid-area: footer; }
</code></pre>
<p>Look at those <code>grid-template-areas</code>. That's not some abstract syntax – <strong>that's literally what your layout looks like</strong>. The header stretches across the top, sidebar on the left, content takes up the remaining space, footer at the bottom.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/c9b0d859-37f1-43dc-a8f8-d82f78c753e0.svg" alt="" style="display:block;margin:0 auto" />

<p>The visual above shows exactly how this ASCII-art-like syntax maps to the actual layout. It's readable. It's maintainable. It's genius.</p>
<p>Want to move the sidebar to the right? Just rearrange the strings:</p>
<pre><code class="language-css">grid-template-areas:
  "header header header"
  "content content sidebar"
  "footer footer footer";
</code></pre>
<p>Done. No math. No recalculating widths. Just move the words around.</p>
<h2>Grid Hacks That'll Make You Look Like a Wizard</h2>
<h3>1. The "Dot Trick" for Empty Cells</h3>
<p>Sometimes you want gaps in your grid – actual empty spaces, not just gaps between items. Use a dot (<code>.</code>) to represent an empty cell:</p>
<pre><code class="language-css">grid-template-areas:
  "logo . . nav"
  "sidebar content content content"
  ". footer footer .";
</code></pre>
<p>This creates empty cells in your grid. The first row has the logo on the left, nav on the right, and empty space in between. The footer row has padding on both sides. Simple, visual, perfect.</p>
<h3>2. Overlapping Items (Yes, Really)</h3>
<p>Here's something that blew my mind: you can place multiple items in the same grid cell. They'll overlap.</p>
<pre><code class="language-css">.container {
  display: grid;
  grid-template-columns: 1fr;
}

.background-image {
  grid-column: 1;
  grid-row: 1;
  z-index: 1;
}

.overlay-text {
  grid-column: 1;
  grid-row: 1;
  z-index: 2;
  align-self: center;
  justify-self: center;
}
</code></pre>
<p>Both items occupy column 1, row 1. They stack on top of each other. Use <code>z-index</code> to control the layering. This is perfect for hero sections, image overlays, card designs – anywhere you'd normally reach for <code>position: absolute</code>.</p>
<h3>3. Auto-Fit and Auto-Fill: Responsive Without Media Queries</h3>
<p>This one's a crowd-pleaser. Want cards that automatically wrap and resize based on container width? No media queries needed:</p>
<pre><code class="language-css">.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 20px;
}
</code></pre>
<p>Let me break that down:</p>
<ul>
<li><p><code>repeat()</code> creates multiple columns</p>
</li>
<li><p><code>auto-fit</code> makes as many columns as will fit</p>
</li>
<li><p><code>minmax(250px, 1fr)</code> says each column should be at least 250px, but can grow to fill available space</p>
</li>
</ul>
<p>The grid automatically adjusts the number of columns based on available space. On a wide screen? Maybe 4 columns. On a phone? Drops down to 1. All automatic. All smooth.</p>
<p>The difference between <code>auto-fit</code> and <code>auto-fill</code> is subtle but important:</p>
<ul>
<li><p><code>auto-fit</code> collapses empty tracks to zero</p>
</li>
<li><p><code>auto-fill</code> keeps empty tracks (maintains column count)</p>
</li>
</ul>
<p>Use <code>auto-fit</code> when you want items to stretch and fill space. Use <code>auto-fill</code> when you want a consistent grid structure even with fewer items.</p>
<h3>4. Grid Line Numbers: Your Secret Weapon</h3>
<p>Remember how I said Grid is all about lines? Well, those lines have numbers.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/fbbbf9ea-e120-40e8-874e-ce3c07343752.svg" alt="" style="display:block;margin:0 auto" />

<p>The visual above shows how grid lines are numbered. Columns and rows both start at 1 and count up from the top-left corner. But here's the clever bit: they <strong>also</strong> count backwards from the bottom-right using negative numbers.</p>
<p>So line <code>-1</code> is always the last line, whether you have 3 columns or 30. This is incredibly useful:</p>
<pre><code class="language-css">.full-width {
  /* Stretch from first to last line */
  grid-column: 1 / -1;
}

.header {
  /* Also stretches full width */
  grid-column: 1 / -1;
  /* But only in the first row */
  grid-row: 1 / 2;
}
</code></pre>
<p>The <code>-1</code> trick means you never have to count your columns. Want something to span the entire width? <code>grid-column: 1 / -1</code>. Always works.</p>
<p>You can also use the <code>span</code> keyword:</p>
<pre><code class="language-css">.featured-card {
  /* Start at column 2, span across 3 columns */
  grid-column: 2 / span 3;
}
</code></pre>
<p>This is cleaner than counting endpoints manually.</p>
<h3>5. Dense Packing with grid-auto-flow</h3>
<p>Got items of different sizes and want Grid to pack them efficiently? Add this one line:</p>
<pre><code class="language-css">.masonry-style {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  grid-auto-flow: dense;
  gap: 16px;
}
</code></pre>
<p>The <code>grid-auto-flow: dense</code> tells Grid to fill in gaps whenever possible. If a large item doesn't fit in the current row, Grid will skip it temporarily, place smaller items in the gaps, then come back to the large one.</p>
<blockquote>
<p>Warning: this can mess up tab order and screen reader navigation since items won't flow in source order anymore. Use it for image galleries or decorative layouts, not for critical content.</p>
</blockquote>
<h3>6. Implicit Grids: Let Grid Do the Work</h3>
<p>You don't always need to define every row. Sometimes you just want Grid to create rows as needed:</p>
<pre><code class="language-css">.auto-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  /* Don't define rows – let them be created automatically */
  grid-auto-rows: minmax(100px, auto);
  gap: 20px;
}
</code></pre>
<p>This creates 3 columns, and rows are added automatically as you add more items. Each row will be at least 100px tall but can grow to fit content. Perfect for card layouts where you don't know how many rows you'll need.</p>
<h2>A Real-World Example: Dashboard Layout</h2>
<p>Let's put it all together with something you'd actually build – a dashboard:</p>
<pre><code class="language-css">.dashboard {
  display: grid;
  grid-template-columns: 250px 1fr 1fr;
  grid-template-rows: 60px 1fr 1fr;
  grid-template-areas:
    "sidebar header header"
    "sidebar main-chart main-chart"
    "sidebar stats-1 stats-2";
  gap: 20px;
  height: 100vh;
  padding: 20px;
}

.sidebar { grid-area: sidebar; }
.header { grid-area: header; }
.main-chart { grid-area: main-chart; }
.stats-1 { grid-area: stats-1; }
.stats-2 { grid-area: stats-2; }

/* Make it responsive */
@media (max-width: 768px) {
  .dashboard {
    grid-template-columns: 1fr;
    grid-template-areas:
      "header"
      "main-chart"
      "stats-1"
      "stats-2"
      "sidebar";
  }
}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/20e95747-aac5-4401-aacd-4ee0a7367250.svg" alt="" style="display:block;margin:0 auto" />

<p>On desktop: sidebar on the left, header across the top, big chart in the middle, two stat cards at the bottom.</p>
<p>On mobile: just stack everything vertically by redefining the template areas. No position changes, no moving elements around in the DOM. Just change the grid definition.</p>
<h2>Common Gotchas (So You Don't Have To Learn The Hard Way)</h2>
<h3>1. Grid Items Create New Stacking Contexts</h3>
<p>When you use <code>grid-column</code> or <code>grid-row</code>, you're creating a positioned element. This means it gets its own stacking context. If your z-indexes aren't working like you expect, this might be why.</p>
<h3>2. Margins Don't Collapse</h3>
<p>Unlike block elements, margins on grid items don't collapse. If you have <code>margin-bottom: 20px</code> on one item and <code>margin-top: 20px</code> on the item below, you get 40px of space, not 20px. Use <code>gap</code> instead for consistent spacing.</p>
<h3>3. Percentage Heights Need Explicit Row Heights</h3>
<p>This one trips people up:</p>
<pre><code class="language-css">.grid-item {
  height: 100%; /* This won't work */
}
</code></pre>
<p>For percentage heights to work, the grid row needs an explicit height:</p>
<pre><code class="language-css">.container {
  grid-template-rows: 300px; /* Or 1fr, or minmax(...) */
}
</code></pre>
<p>Or use <code>align-self: stretch</code> (which is the default anyway).</p>
<h2>When NOT to Use Grid</h2>
<p>Real talk: Grid isn't always the answer.</p>
<p>Use <strong>Flexbox</strong> when:</p>
<ul>
<li><p>You're laying out items in a single direction (row or column)</p>
</li>
<li><p>You want items to size themselves based on content</p>
</li>
<li><p>You're building a navigation bar, button group, or simple centered layout</p>
</li>
</ul>
<p>Use <strong>Grid</strong> when:</p>
<ul>
<li><p>You need to control both rows and columns</p>
</li>
<li><p>You want to overlap items intentionally</p>
</li>
<li><p>You're building page layouts, dashboards, card grids, or complex forms</p>
</li>
<li><p>You want named template areas for clarity</p>
</li>
</ul>
<p>Sometimes you'll use both in the same project. Grid for the page layout, Flexbox for the navbar. That's fine. Use the right tool for the job.</p>
<h2>The Bottom Line</h2>
<p>CSS Grid is one of those features that fundamentally changes how you approach layout. Once you internalize the mental model – drawing lines and placing things between them – it becomes second nature.</p>
<p>Start with <code>grid-template-areas</code> for your page layouts. It's the most intuitive way to work with Grid, and it makes your CSS incredibly readable. Then layer in line numbers, auto-fit, and the other tricks as you need them.</p>
<p>And remember: you're not fighting the layout anymore. You're describing what you want, and Grid figures out how to make it happen.</p>
<p>That's the real superpower.</p>
<hr />
<p><em>What's your favorite Grid trick? Hit me up in the comments – I'm always looking for new techniques to add to my toolbox.</em></p>
]]></content:encoded></item><item><title><![CDATA[Axios Interceptors: The Middleware Pattern That'll Save You Hours]]></title><description><![CDATA[If you're making HTTP requests in JavaScript, you're probably using Axios. And if you're using Axios but not using interceptors, you're writing the same code over and over again.
Let me show you what ]]></description><link>https://blog.thecodingant.in/axios-interceptors-the-middleware-pattern-that-ll-save-you-hours</link><guid isPermaLink="true">https://blog.thecodingant.in/axios-interceptors-the-middleware-pattern-that-ll-save-you-hours</guid><category><![CDATA[axios]]></category><category><![CDATA[axios in react]]></category><category><![CDATA[axios-interceptor]]></category><category><![CDATA[axios middleware]]></category><category><![CDATA[AI]]></category><category><![CDATA[axios intercep]]></category><dc:creator><![CDATA[Nitesh Kumar Tudu]]></dc:creator><pubDate>Wed, 01 Apr 2026 18:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/c4644972-6484-4368-bbb4-785f3124ee0f.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you're making HTTP requests in JavaScript, you're probably using Axios. And if you're using Axios but not using interceptors, you're writing the same code over and over again.</p>
<p>Let me show you what I mean.</p>
<p>Every single API request in your app probably needs:</p>
<ul>
<li><p>An auth token in the header</p>
</li>
<li><p>Error handling for 401s (redirect to login)</p>
</li>
<li><p>Error handling for 500s (show a toast notification)</p>
</li>
<li><p>Loading state management</p>
</li>
<li><p>Request logging for debugging</p>
</li>
<li><p>Maybe retry logic for failed requests</p>
</li>
</ul>
<p>Without interceptors, you're doing this:</p>
<pre><code class="language-javascript">async function getUser() {
  try {
    const token = localStorage.getItem('token');
    const response = await axios.get('/api/user', {
      headers: { Authorization: `Bearer ${token}` }
    });
    return response.data;
  } catch (error) {
    if (error.response?.status === 401) {
      localStorage.removeItem('token');
      window.location.href = '/login';
    }
    throw error;
  }
}

async function updateProfile(data) {
  try {
    const token = localStorage.getItem('token');
    const response = await axios.put('/api/profile', data, {
      headers: { Authorization: `Bearer ${token}` }
    });
    return response.data;
  } catch (error) {
    if (error.response?.status === 401) {
      localStorage.removeItem('token');
      window.location.href = '/login';
    }
    throw error;
  }
}

// ... copy-pasted 47 more times
</code></pre>
<p>See the problem? Same auth logic. Same error handling. Copy-pasted everywhere.</p>
<p>Interceptors fix this. They're like middleware for your HTTP requests – code that runs before every request and after every response.</p>
<h2>What Are Axios Interceptors?</h2>
<p>Think of interceptors as checkpoints. Every request passes through them on the way out, and every response passes through them on the way back.</p>
<p>Here's how it works: when you make an API call, the request interceptor runs first. It can modify the request – add headers, log it, cancel it, whatever you need. Then the request goes to the server.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/7c70142c-2150-4dbd-9437-975e84644e6a.svg" alt="" style="display:block;margin:0 auto" />

<p>When the response comes back, the response interceptor runs. It can transform the data, handle errors globally, retry failed requests, all before your code even sees the response.</p>
<h2>Setting Up Interceptors</h2>
<p>Here's the basic pattern:</p>
<pre><code class="language-javascript">// Request interceptor
axios.interceptors.request.use(
  (config) =&gt; {
    // Modify the request config before it's sent
    console.log('Request sent:', config.url);
    return config;
  },
  (error) =&gt; {
    // Handle request errors
    return Promise.reject(error);
  }
);

// Response interceptor
axios.interceptors.response.use(
  (response) =&gt; {
    // Any status code 2xx triggers this
    console.log('Response received:', response.status);
    return response;
  },
  (error) =&gt; {
    // Any status code outside 2xx triggers this
    console.log('Response error:', error.message);
    return Promise.reject(error);
  }
);
</code></pre>
<p>That's it. Now every Axios request in your app goes through these interceptors.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/650bfa13-5614-42f5-a25c-0405430acc14.svg" alt="" style="display:block;margin:0 auto" />

<h2>Use Case 1: Auto-Injecting Auth Tokens</h2>
<p>This is probably the most common use case. Instead of manually adding the auth header to every request:</p>
<pre><code class="language-javascript">// Before interceptors - manual token on every request
axios.get('/api/user', {
  headers: { Authorization: `Bearer ${token}` }
});

axios.post('/api/posts', data, {
  headers: { Authorization: `Bearer ${token}` }
});

// After interceptors - automatic
axios.interceptors.request.use((config) =&gt; {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Now just make requests normally
axios.get('/api/user');
axios.post('/api/posts', data);
</code></pre>
<p>Every request automatically gets the auth token. No copy-paste. No forgetting to add it.</p>
<h2>Use Case 2: Global Error Handling</h2>
<p>This one saved me so much time. Handle 401s (auth expired), 500s (server errors), and network errors in one place:</p>
<pre><code class="language-javascript">axios.interceptors.response.use(
  (response) =&gt; response,
  (error) =&gt; {
    if (error.response?.status === 401) {
      // Token expired - redirect to login
      localStorage.removeItem('token');
      window.location.href = '/login';
    } else if (error.response?.status === 500) {
      // Server error - show notification
      toast.error('Something went wrong. Please try again.');
    } else if (!error.response) {
      // Network error - no response from server
      toast.error('Network error. Check your connection.');
    }
    
    return Promise.reject(error);
  }
);
</code></pre>
<p>Now every API error in your entire app is handled consistently. Users get logged out on 401s, see error messages on 500s, and get network error alerts automatically.</p>
<h2>Use Case 3: Request/Response Logging</h2>
<p>Great for debugging. See every API call in the console:</p>
<pre><code class="language-javascript">axios.interceptors.request.use((config) =&gt; {
  console.log(`➡️  \({config.method.toUpperCase()} \){config.url}`, config.data);
  return config;
});

axios.interceptors.response.use(
  (response) =&gt; {
    console.log(`✅ \({response.config.method.toUpperCase()} \){response.config.url}`, response.data);
    return response;
  },
  (error) =&gt; {
    console.log(`❌ \({error.config?.method?.toUpperCase()} \){error.config?.url}`, error.message);
    return Promise.reject(error);
  }
);
</code></pre>
<p>Now you can see the entire request/response flow in the console without adding console.logs everywhere.</p>
<h2>Use Case 4: Loading State Management</h2>
<p>Show a loading spinner while requests are in progress:</p>
<pre><code class="language-javascript">let activeRequests = 0;

axios.interceptors.request.use((config) =&gt; {
  activeRequests++;
  if (activeRequests === 1) {
    // Show loading spinner
    document.getElementById('loading').style.display = 'block';
  }
  return config;
});

axios.interceptors.response.use(
  (response) =&gt; {
    activeRequests--;
    if (activeRequests === 0) {
      // Hide loading spinner
      document.getElementById('loading').style.display = 'none';
    }
    return response;
  },
  (error) =&gt; {
    activeRequests--;
    if (activeRequests === 0) {
      document.getElementById('loading').style.display = 'none';
    }
    return Promise.reject(error);
  }
);
</code></pre>
<p>The loading spinner shows automatically for any request and hides when all requests finish.</p>
<h2>Use Case 5: Retry Failed Requests</h2>
<p>Automatically retry requests that fail due to network issues:</p>
<pre><code class="language-javascript">axios.interceptors.response.use(
  (response) =&gt; response,
  async (error) =&gt; {
    const config = error.config;
    
    // Don't retry if we've already retried
    if (!config || config._retry) {
      return Promise.reject(error);
    }
    
    // Only retry on network errors or 5xx server errors
    if (!error.response || error.response.status &gt;= 500) {
      config._retry = true;
      
      // Wait 1 second before retrying
      await new Promise((resolve) =&gt; setTimeout(resolve, 1000));
      
      // Retry the request
      return axios(config);
    }
    
    return Promise.reject(error);
  }
);
</code></pre>
<p>Now if a request fails due to a temporary network hiccup or server error, it automatically retries once after a second.</p>
<h2>Use Case 6: Transform Response Data</h2>
<p>Extract the data you actually need from the response:</p>
<pre><code class="language-javascript">// API returns: { data: { user: {...} }, meta: {...} }
// You want: { user: {...} }

axios.interceptors.response.use((response) =&gt; {
  // If response has a 'data' property, unwrap it
  if (response.data &amp;&amp; response.data.data) {
    response.data = response.data.data;
  }
  return response;
});

// Now this works:
const response = await axios.get('/api/user');
console.log(response.data.user); // Direct access, no extra .data
</code></pre>
<p>Your code stays clean because the interceptor handles the data unwrapping.</p>
<h2>Advanced Pattern: Creating Axios Instances</h2>
<p>Instead of adding interceptors to the global <code>axios</code> object, create custom instances with their own interceptors. This is useful when you have multiple APIs with different auth schemes:</p>
<pre><code class="language-javascript">// API for your main backend
const apiClient = axios.create({
  baseURL: 'https://api.yourapp.com',
  timeout: 10000,
});

apiClient.interceptors.request.use((config) =&gt; {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// API for a third-party service
const externalAPI = axios.create({
  baseURL: 'https://external-api.com',
  timeout: 5000,
});

externalAPI.interceptors.request.use((config) =&gt; {
  config.headers['X-API-Key'] = process.env.EXTERNAL_API_KEY;
  return config;
});

// Use them separately
apiClient.get('/user');        // Gets Bearer token
externalAPI.get('/data');      // Gets API key
</code></pre>
<p>Each instance has its own interceptors. No conflicts, no mixing auth schemes.</p>
<h2>Removing Interceptors</h2>
<p>Sometimes you need to remove an interceptor. Save the interceptor ID when you add it:</p>
<pre><code class="language-javascript">const requestInterceptor = axios.interceptors.request.use(
  (config) =&gt; {
    // Interceptor logic
    return config;
  }
);

// Later, remove it
axios.interceptors.request.eject(requestInterceptor);
</code></pre>
<p>This is useful for temporarily disabling interceptors or cleaning up when a component unmounts.</p>
<h2>Real-World Example: Auth Refresh Flow</h2>
<p>Here's a complete example that handles token refresh when the access token expires:</p>
<pre><code class="language-javascript">let isRefreshing = false;
let failedQueue = [];

const processQueue = (error, token = null) =&gt; {
  failedQueue.forEach((prom) =&gt; {
    if (error) {
      prom.reject(error);
    } else {
      prom.resolve(token);
    }
  });
  failedQueue = [];
};

axios.interceptors.response.use(
  (response) =&gt; response,
  async (error) =&gt; {
    const originalRequest = error.config;

    if (error.response?.status === 401 &amp;&amp; !originalRequest._retry) {
      if (isRefreshing) {
        // Another request is already refreshing the token
        // Queue this request to retry after refresh completes
        return new Promise((resolve, reject) =&gt; {
          failedQueue.push({ resolve, reject });
        })
          .then((token) =&gt; {
            originalRequest.headers.Authorization = `Bearer ${token}`;
            return axios(originalRequest);
          })
          .catch((err) =&gt; Promise.reject(err));
      }

      originalRequest._retry = true;
      isRefreshing = true;

      try {
        const refreshToken = localStorage.getItem('refreshToken');
        const response = await axios.post('/auth/refresh', { refreshToken });
        const newAccessToken = response.data.accessToken;
        
        localStorage.setItem('token', newAccessToken);
        
        // Update the failed request with new token
        originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
        
        // Process all queued requests
        processQueue(null, newAccessToken);
        
        // Retry the original request
        return axios(originalRequest);
      } catch (refreshError) {
        processQueue(refreshError, null);
        localStorage.removeItem('token');
        localStorage.removeItem('refreshToken');
        window.location.href = '/login';
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    }

    return Promise.reject(error);
  }
);
</code></pre>
<p>This handles the complex case where multiple requests fail with 401 simultaneously. Instead of all of them trying to refresh the token, only the first one does. The others wait and retry with the new token.</p>
<h2>Common Gotchas</h2>
<h3>1. Don't Mutate Original Config Objects</h3>
<pre><code class="language-javascript">// Bad - mutates the original config
axios.interceptors.request.use((config) =&gt; {
  config.headers.common['X-Custom'] = 'value';
  return config;
});

// Good - modify config.headers directly
axios.interceptors.request.use((config) =&gt; {
  config.headers['X-Custom'] = 'value';
  return config;
});
</code></pre>
<h3>2. Always Return Config or Response</h3>
<pre><code class="language-javascript">// Bad - doesn't return anything
axios.interceptors.request.use((config) =&gt; {
  console.log('Request:', config.url);
  // Missing return!
});

// Good - always return
axios.interceptors.request.use((config) =&gt; {
  console.log('Request:', config.url);
  return config;
});
</code></pre>
<p>If you don't return, the request won't be sent.</p>
<h3>3. Handle Both Success and Error Callbacks</h3>
<pre><code class="language-javascript">// Bad - only handles success
axios.interceptors.request.use((config) =&gt; {
  return config;
});

// Good - handles both
axios.interceptors.request.use(
  (config) =&gt; {
    return config;
  },
  (error) =&gt; {
    return Promise.reject(error);
  }
);
</code></pre>
<p>Always provide both callbacks, even if the error callback just rejects the promise.</p>
<h3>4. Be Careful with Async Interceptors</h3>
<pre><code class="language-javascript">// This works - async is fine
axios.interceptors.request.use(async (config) =&gt; {
  const token = await getTokenFromSecureStorage();
  config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// But be aware it makes ALL requests wait
// If getting the token is slow, every request is slow
</code></pre>
<p>Async interceptors work, but they block all requests. Keep them fast.</p>
<h2>Practical Setup: Complete Production Example</h2>
<p>Here's how I structure interceptors in production apps:</p>
<pre><code class="language-javascript">// src/api/interceptors.js
import axios from 'axios';
import { toast } from 'react-toastify';

// Create API client
const apiClient = axios.create({
  baseURL: process.env.REACT_APP_API_URL,
  timeout: 10000,
});

// Request interceptor - add auth token
apiClient.interceptors.request.use(
  (config) =&gt; {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) =&gt; {
    return Promise.reject(error);
  }
);

// Response interceptor - handle errors globally
apiClient.interceptors.response.use(
  (response) =&gt; {
    return response.data; // Unwrap data
  },
  (error) =&gt; {
    // Network error
    if (!error.response) {
      toast.error('Network error. Please check your connection.');
      return Promise.reject(error);
    }

    const { status } = error.response;

    // Handle specific status codes
    switch (status) {
      case 401:
        localStorage.removeItem('token');
        window.location.href = '/login';
        toast.error('Session expired. Please login again.');
        break;
      case 403:
        toast.error('You don\'t have permission to do that.');
        break;
      case 404:
        toast.error('Resource not found.');
        break;
      case 500:
        toast.error('Server error. Please try again later.');
        break;
      default:
        toast.error('Something went wrong.');
    }

    return Promise.reject(error);
  }
);

export default apiClient;
</code></pre>
<p>Then use it throughout the app:</p>
<pre><code class="language-javascript">// src/services/userService.js
import apiClient from './api/interceptors';

export const getUser = () =&gt; apiClient.get('/user');
export const updateProfile = (data) =&gt; apiClient.put('/profile', data);
export const uploadAvatar = (file) =&gt; {
  const formData = new FormData();
  formData.append('avatar', file);
  return apiClient.post('/avatar', formData);
};
</code></pre>
<p>Clean, consistent, no repetition.</p>
<h2>TypeScript Support</h2>
<p>If you're using TypeScript, you can type your interceptors:</p>
<pre><code class="language-typescript">import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';

axios.interceptors.request.use(
  (config: AxiosRequestConfig) =&gt; {
    // config is typed
    return config;
  },
  (error: AxiosError) =&gt; {
    return Promise.reject(error);
  }
);

axios.interceptors.response.use(
  (response: AxiosResponse) =&gt; {
    return response;
  },
  (error: AxiosError) =&gt; {
    return Promise.reject(error);
  }
);
</code></pre>
<p>TypeScript will catch errors like typos in config properties or incorrect return types.</p>
<h2>When NOT to Use Interceptors</h2>
<p>Interceptors are powerful, but don't use them for everything:</p>
<p><strong>Don't use for request-specific logic</strong>: If only one endpoint needs special handling, do it in that request, not in an interceptor.</p>
<pre><code class="language-javascript">// Bad - interceptor for one-off logic
axios.interceptors.request.use((config) =&gt; {
  if (config.url === '/special-endpoint') {
    config.headers['X-Special'] = 'value';
  }
  return config;
});

// Good - handle it in the request
axios.get('/special-endpoint', {
  headers: { 'X-Special': 'value' }
});
</code></pre>
<p><strong>Don't use for component state</strong>: Interceptors run at the API layer. Don't try to update React state from an interceptor.</p>
<pre><code class="language-javascript">// Bad - trying to update component state
axios.interceptors.response.use((response) =&gt; {
  setLoading(false); // This doesn't work - no access to component state
  return response;
});

// Good - handle state in the component
const fetchData = async () =&gt; {
  setLoading(true);
  try {
    const data = await axios.get('/data');
    setData(data);
  } finally {
    setLoading(false);
  }
};
</code></pre>
<p><strong>Don't use for business logic</strong>: Keep business logic in services, not interceptors. Interceptors should handle cross-cutting concerns like auth, logging, and error handling.</p>
<h2>The Bottom Line</h2>
<p>Axios interceptors are middleware for your HTTP requests. They let you:</p>
<ul>
<li><p>Write auth logic once instead of on every request</p>
</li>
<li><p>Handle errors globally instead of in every try-catch</p>
</li>
<li><p>Log requests automatically for debugging</p>
</li>
<li><p>Transform responses consistently</p>
</li>
<li><p>Manage loading states centrally</p>
</li>
</ul>
<p>The key insight: if you're doing the same thing before or after every request, it belongs in an interceptor.</p>
<p>Start simple. Add a request interceptor for auth tokens. Add a response interceptor for error handling. You'll immediately see how much cleaner your code becomes.</p>
<p>Then explore the advanced patterns – custom instances, token refresh, retry logic – as you need them.</p>
<hr />
<p><em>Using Axios interceptors in your projects? Have a clever pattern to share? Drop it in the comments.</em></p>
]]></content:encoded></item><item><title><![CDATA[PostHog: All-in-One Production Monitoring That Actually Works]]></title><description><![CDATA[Most development teams use a scattered toolkit for production monitoring: one service for analytics, another for session replay, a third for error tracking, and yet another for feature flags. Each too]]></description><link>https://blog.thecodingant.in/posthog-all-in-one-production-monitoring-that-actually-works</link><guid isPermaLink="true">https://blog.thecodingant.in/posthog-all-in-one-production-monitoring-that-actually-works</guid><category><![CDATA[posthog]]></category><category><![CDATA[monitoring tool]]></category><category><![CDATA[error monitoring]]></category><dc:creator><![CDATA[Nitesh Kumar Tudu]]></dc:creator><pubDate>Mon, 30 Mar 2026 05:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/7f377138-35bf-4ccb-898e-e8b61ddbb413.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most development teams use a scattered toolkit for production monitoring: one service for analytics, another for session replay, a third for error tracking, and yet another for feature flags. Each tool has its own dashboard, its own API, and its own way of identifying users.</p>
<p>The problem? When a user hits an error, you can't easily connect it to their session replay. When analytics show a conversion drop, you can't filter by which feature flags were enabled. Everything exists in silos.</p>
<p>PostHog solves this by putting everything in one platform with shared user data and connected insights.</p>
<h2>What PostHog Actually Is</h2>
<p>PostHog is an all-in-one developer platform that consolidates your monitoring stack:</p>
<ul>
<li><p><strong>Product analytics</strong> (like Mixpanel/Amplitude)</p>
</li>
<li><p><strong>Session replay</strong> (like FullStory/Hotjar)</p>
</li>
<li><p><strong>Error tracking</strong> (like Sentry)</p>
</li>
<li><p><strong>Feature flags</strong> (like LaunchDarkly)</p>
</li>
<li><p><strong>A/B testing</strong> (like Optimizely)</p>
</li>
<li><p><strong>User surveys</strong></p>
</li>
<li><p><strong>And more</strong></p>
</li>
</ul>
<p>All in one place. Same dashboard. Same user data. Same event stream.</p>
<p>The game-changer? <strong>Everything is connected</strong>. You don't just see that an error happened – you can watch the session replay of exactly what the user did before the error. You don't just see analytics – you can filter for users with specific feature flags enabled and watch their sessions.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/4a87c3ca-ba0e-4805-b6fc-1682918b4374.png" alt="" style="display:block;margin:0 auto" />

<h2>Session Replay: See Exactly What Users Do</h2>
<p>When users report issues like "your checkout is broken," reproducing the problem can be nearly impossible without seeing what actually happened. Session replay solves this by recording every interaction.</p>
<p>PostHog records every click, scroll, mouse movement, and interaction. Here's what makes it different:</p>
<h3>It's Not Just a Video</h3>
<p>PostHog doesn't record video – it records the DOM. This means:</p>
<ul>
<li><p><strong>Tiny file sizes</strong> (a 10-minute session might be 50KB instead of 50MB)</p>
</li>
<li><p><strong>Searchable</strong> (you can search for text that appeared on screen)</p>
</li>
<li><p><strong>Privacy-friendly</strong> (automatic masking of sensitive inputs)</p>
</li>
<li><p><strong>Lower bandwidth</strong> (doesn't slow down your users' experience)</p>
</li>
</ul>
<h3>Console Logs Are Included</h3>
<p>When watching a session replay, you can see the actual console output from that user's browser. This eliminates the need to ask users to check their console and send screenshots.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/ffc08922-1094-4465-81a8-25612f453b01.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Real example</strong>: A user reported that a payment form "stops working." The session replay showed a JavaScript error firing when they entered a Canadian postal code. The validation regex was breaking on Canadian formats. The issue was identified and fixed in minutes by simply watching what happened.</p>
<h3>Network Requests Are Captured Too</h3>
<p>You can see every API call made during the session:</p>
<ul>
<li><p>Request/response status codes</p>
</li>
<li><p>Headers (automatically scrubbed of sensitive data)</p>
</li>
<li><p>Timing information</p>
</li>
<li><p>Failed requests</p>
</li>
</ul>
<p>This is invaluable when debugging "it's slow" complaints. You can see exactly which API call took 4 seconds and made the UI feel sluggish.</p>
<h3>Filtering by Behavior</h3>
<p>Here's where it gets smart. You can filter replays by:</p>
<ul>
<li><p><strong>Rage clicks</strong> (user clicked the same thing 5+ times – usually means something's broken)</p>
</li>
<li><p><strong>Console errors</strong> (sessions where JavaScript errors occurred)</p>
</li>
<li><p><strong>Dead clicks</strong> (clicked things that do nothing)</p>
</li>
<li><p><strong>Specific events</strong> (show me everyone who started checkout but didn't complete it)</p>
</li>
<li><p><strong>User properties</strong> (show me sessions from enterprise customers only)</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/57397927-9ee5-441f-848c-55af7593fe94.png" alt="" style="display:block;margin:0 auto" />

<p>You can create saved filters for common debugging scenarios. For example, filtering for "rage clicks + console errors" provides a weekly report of things that are likely broken. Users don't always report bugs, but they do rage-click when things don't work.</p>
<h2>Error Tracking: Sentry, But Connected</h2>
<p>PostHog's error tracking is relatively new, but it's already better than using Sentry for one simple reason: <strong>you can watch the session replay.</strong></p>
<h3>How It Works</h3>
<p>Install the SDK (one line of code), and PostHog automatically captures:</p>
<ul>
<li><p>JavaScript exceptions</p>
</li>
<li><p>Unhandled promise rejections</p>
</li>
<li><p>Console errors</p>
</li>
<li><p>Custom errors you log</p>
</li>
</ul>
<p>But unlike traditional error monitoring, every error is linked to:</p>
<ul>
<li><p>The session replay showing what the user did</p>
</li>
<li><p>The user's properties (browser, OS, location, custom properties)</p>
</li>
<li><p>The sequence of events leading up to the error</p>
</li>
<li><p>Network requests that might have failed</p>
</li>
</ul>
<h3>The Error Dashboard</h3>
<p>Errors are grouped by stack trace, so you don't get 1,000 reports of the same error. You get one error with "1,000 occurrences."</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/f1aa6a26-b7a6-4f58-bb98-a5a638c64e55.png" alt="" style="display:block;margin:0 auto" />

<p>For each error group, you can see:</p>
<ul>
<li><p>How many times it occurred</p>
</li>
<li><p>How many users were affected</p>
</li>
<li><p>When it first appeared (useful for knowing which deploy broke it)</p>
</li>
<li><p>Which browser/OS combinations trigger it</p>
</li>
<li><p><strong>A link to watch any of the session replays where it happened</strong></p>
</li>
</ul>
<p>That last one is the key. Click "Watch session" and you're immediately watching the exact moment the user hit the error. No guessing. No trying to reproduce it. Just watch what happened.</p>
<h3>Stack Traces That Make Sense</h3>
<p>PostHog supports source maps, so your minified production errors show the actual source code. No more trying to figure out what <code>a.b.c is undefined</code> means when it's actually <code>user.profile.avatar</code>.</p>
<p>Upload your source maps once (automatic with most build tools), and every stack trace shows readable code.</p>
<h2>Product Analytics: Understand Your Users</h2>
<p>The analytics part is where PostHog really shines, because it's not just numbers – it's numbers with context.</p>
<h3>Events and Properties</h3>
<p>Everything in PostHog is built around events. Every interaction can be tracked:</p>
<pre><code class="language-javascript">posthog.capture('user_signed_up', {
  plan: 'pro',
  trial: true,
  source: 'landing_page'
});
</code></pre>
<p>Then you can:</p>
<ul>
<li><p>Count how many signups you got</p>
</li>
<li><p>Filter by plan type</p>
</li>
<li><p>See which sources convert best</p>
</li>
<li><p><strong>Watch session replays of users who signed up</strong></p>
</li>
</ul>
<p>That last part keeps coming up because it's so damn useful. Analytics tell you <em>what</em> is happening. Session replay tells you <em>why</em>.</p>
<h3>Funnels</h3>
<p>Create a signup funnel:</p>
<ol>
<li><p>Visited landing page</p>
</li>
<li><p>Clicked "Sign Up"</p>
</li>
<li><p>Completed form</p>
</li>
<li><p>Verified email</p>
</li>
<li><p>Completed onboarding</p>
</li>
</ol>
<p>PostHog shows you:</p>
<ul>
<li><p>Conversion rate at each step</p>
</li>
<li><p>Where people drop off</p>
</li>
<li><p><strong>Click any drop-off point to watch replays of users who abandoned the flow</strong></p>
</li>
</ul>
<p><strong>Example</strong>: A 40% drop-off at an email verification step led to watching session replays, which revealed that the verification redirect was broken on mobile Safari. After fixing the CSS issue, conversion improved significantly.</p>
<h3>User Paths</h3>
<p>This visualization shows the actual paths users take through your app. Not what you <em>think</em> they do – what they <em>actually</em> do.</p>
<p>You can see patterns like:</p>
<ul>
<li><p>60% of users who visit pricing go to the docs before signing up (they want to see if it's technical enough)</p>
</li>
<li><p>Users who watch the demo video convert 3x better</p>
</li>
<li><p>People keep clicking the logo expecting it to go home, but it doesn't (oops)</p>
</li>
</ul>
<p>And yes, you can click any node in the path and watch replays of users who took that path.</p>
<h2>Feature Flags: Ship Fearlessly</h2>
<p>Feature flags let you deploy code without showing it to everyone. This is table stakes nowadays, but PostHog's integration makes it powerful.</p>
<h3>The Basic Use Case</h3>
<pre><code class="language-javascript">if (posthog.isFeatureEnabled('new-checkout')) {
  // Show new checkout flow
} else {
  // Show old checkout
}
</code></pre>
<p>Deploy the code to everyone, but only turn on the flag for:</p>
<ul>
<li><p>Internal users (test it first)</p>
</li>
<li><p>5% of users (gradual rollout)</p>
</li>
<li><p>Enterprise customers (they get features early)</p>
</li>
<li><p>Users in specific regions</p>
</li>
<li><p>Whatever conditions you want</p>
</li>
</ul>
<h3>Why It's Better in PostHog</h3>
<p>Because it's connected to everything else:</p>
<p><strong>Analytics</strong>: Create a funnel comparing new-checkout vs old-checkout. Which one converts better? You have data.</p>
<p><strong>Session replay</strong>: Filter replays by "has flag new-checkout enabled." Watch how people actually use the new feature.</p>
<p><strong>Errors</strong>: Filter errors by feature flag. Is the new checkout causing errors? You'll know immediately.</p>
<p><strong>Surveys</strong>: Show a survey only to users with a specific flag enabled. "How was the new checkout experience?"</p>
<p>All of this happens automatically. You're not manually trying to connect Amplitude analytics to LaunchDarkly flags to Hotjar recordings. It's one system.</p>
<h2>Quick and Simple Setup</h2>
<p>Setting up PostHog is straightforward. Add this script to your HTML:</p>
<pre><code class="language-html">&lt;script&gt;
  !function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&amp;&amp;(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&amp;&amp;(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys".split(" "),n=0;n&lt;o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
  posthog.init('YOUR_PROJECT_API_KEY', {api_host: 'https://us.posthog.com'})
&lt;/script&gt;
</code></pre>
<p>That's it. You now have:</p>
<ul>
<li><p>Analytics</p>
</li>
<li><p>Session replay</p>
</li>
<li><p>Error tracking</p>
</li>
<li><p>Feature flags</p>
</li>
<li><p>Everything</p>
</li>
</ul>
<p>Want to track custom events? One line:</p>
<pre><code class="language-javascript">posthog.capture('button_clicked', { button_name: 'pricing_cta' });
</code></pre>
<p>Want to identify a user?</p>
<pre><code class="language-javascript">posthog.identify('user_123', {
  email: 'user@example.com',
  plan: 'pro'
});
</code></pre>
<p>For backend events (Node, Python, Go, whatever), there's an SDK:</p>
<pre><code class="language-javascript">// Node.js
const posthog = require('posthog-node');
const client = new PostHog('YOUR_PROJECT_API_KEY');

client.capture({
  distinctId: 'user_123',
  event: 'server_side_event',
  properties: { source: 'api' }
});
</code></pre>
<h2>Real-World Debugging Examples</h2>
<h3>1. Intermittent Bugs</h3>
<p><strong>Problem</strong>: Users reported a bug that only happened "sometimes" and couldn't be reproduced in development.</p>
<p><strong>Solution</strong>: Filtered PostHog for sessions with console errors in the past week. Found 47 sessions with the same error. After watching three replays, the pattern emerged: the bug only occurred when users navigated directly to a page rather than through the normal app flow. The state wasn't being initialized correctly on direct page loads.</p>
<p><strong>Result</strong>: Issue identified and fixed in one day instead of weeks of back-and-forth debugging.</p>
<h3>2. Sudden Conversion Drop</h3>
<p><strong>Problem</strong>: Signup conversion dropped 15% overnight. Analytics showed the drop but not the cause.</p>
<p><strong>Solution</strong>: Watched 20 session replays of users who started signup but didn't finish. The issue was found in the first 5 replays: the email verification link was returning a 404 error.</p>
<p><strong>Result</strong>: A routing refactor had broken the verification endpoint. Quick fix once the problem was identified.</p>
<h3>3. Low Feature Adoption</h3>
<p><strong>Problem</strong>: A new dashboard feature that took two weeks to build had almost no usage according to analytics.</p>
<p><strong>Solution</strong>: Watched replays of users who should have been using the feature based on their behavior patterns. The issue: users literally couldn't find it. The button was buried in the settings menu that nobody opened.</p>
<p><strong>Result</strong>: Moved the button to the main navigation, and usage increased 10x.</p>
<h3>4. Platform-Specific Bug</h3>
<p><strong>Problem</strong>: "Your app is broken on my phone" – but it worked fine in testing on iPhone and mobile emulators.</p>
<p><strong>Solution</strong>: Found the user's session in PostHog. They were on an older iOS version that hadn't been tested. The replay showed the exact UI element that wasn't rendering.</p>
<p><strong>Result</strong>: Identified a Safari 14 on iOS 14.3 specific CSS issue, fixed it, and confirmed the fix with another session replay from that user.</p>
<h2>What It Costs (And Why It's Worth It)</h2>
<p>PostHog has a generous free tier:</p>
<ul>
<li><p>1 million events per month</p>
</li>
<li><p>5,000 session recordings per month</p>
</li>
<li><p>Unlimited feature flags</p>
</li>
<li><p>Unlimited team members</p>
</li>
</ul>
<p>For most small apps, this is enough.</p>
<p>When you outgrow it, you pay for what you use:</p>
<ul>
<li><p>Events: starts around $0.00005 per event</p>
</li>
<li><p>Recordings: around $0.005 per recording</p>
</li>
<li><p>Feature flag requests: almost free</p>
</li>
</ul>
<p>There's also a self-hosted option if you want to run PostHog on your own infrastructure. It's fully open source.</p>
<p><strong>Cost comparison</strong>: For a production app with ~300K monthly active users, PostHog costs around \(50/month. Using separate tools (Mixpanel + FullStory + Sentry + LaunchDarkly) typically costs \)400+/month.</p>
<h2>The "But What About..." Questions</h2>
<p><strong>"Isn't this just Google Analytics with extra steps?"</strong></p>
<p>No. Google Analytics tells you pageviews and bounce rates. PostHog tells you that user #42 rage-clicked the submit button 7 times, saw an error, and left. Then lets you watch exactly what happened.</p>
<p><strong>"What about GDPR / privacy?"</strong></p>
<p>PostHog is built with privacy in mind:</p>
<ul>
<li><p>Automatic masking of password/credit card inputs</p>
</li>
<li><p>Can self-host in your own region</p>
</li>
<li><p>Respects Do Not Track</p>
</li>
<li><p>Full data control and deletion</p>
</li>
<li><p>GDPR compliant</p>
</li>
</ul>
<p><strong>"Does it slow down my site?"</strong></p>
<p>The script is ~40KB gzipped. Session replay data is sent asynchronously. In practice, the performance impact is negligible.</p>
<p>We've tested it extensively – no measurable impact on page load times or Core Web Vitals.</p>
<p><strong>"Why not just use the browser DevTools?"</strong></p>
<p>Because DevTools only work when you're the one using the app. PostHog works for all your users, in production, all the time.</p>
<h2>Key Considerations</h2>
<p>PostHog consolidates multiple monitoring tools into one platform with connected data. This solves the common problem of needing to manually correlate information across separate analytics, session replay, error tracking, and feature flag services.</p>
<p><strong>Strengths:</strong></p>
<ul>
<li><p>Everything is connected (errors link to session replays, analytics filter by feature flags)</p>
</li>
<li><p>Single source of truth for user data</p>
</li>
<li><p>Significant cost savings compared to multiple tools</p>
</li>
<li><p>Privacy-focused with GDPR compliance</p>
</li>
</ul>
<p><strong>Limitations:</strong></p>
<ul>
<li><p>UI can be overwhelming initially due to the breadth of features</p>
</li>
<li><p>Documentation is comprehensive but dense</p>
</li>
<li><p>Costs can scale up significantly for very large apps (10M+ events/day)</p>
</li>
</ul>
<p><strong>Best fit for:</strong></p>
<ul>
<li><p>Teams currently using 3+ separate monitoring tools</p>
</li>
<li><p>Apps with 100K - 5M monthly active users</p>
</li>
<li><p>Development teams that want integrated debugging workflows</p>
</li>
</ul>
<h3>Getting Started</h3>
<p>The recommended approach:</p>
<ol>
<li><p><strong>Sign up</strong> for the free tier at posthog.com</p>
</li>
<li><p><strong>Add the snippet</strong> to your site (2-minute setup)</p>
</li>
<li><p><strong>Create a simple funnel</strong> for your main conversion flow</p>
</li>
<li><p><strong>Enable session recordings</strong></p>
</li>
<li><p><strong>Review sessions</strong> the next day to identify improvement opportunities</p>
</li>
</ol>
<p>Within 24 hours, most teams identify at least one actionable insight from the combination of analytics and session replays.</p>
<hr />
<p><em>Have experience using PostHog for production monitoring? Share your insights in the comments.</em></p>
]]></content:encoded></item></channel></rss>