<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ software development - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ software development - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Thu, 27 Aug 2026 09:50:49 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/software-development/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Flashcard Study App with Next.js and MongoDB ]]>
                </title>
                <description>
                    <![CDATA[ If you've ever crammed for an exam the night before, you know how hard it is to remember everything. Flashcards are one of the most effective study tools because they use active recall: you actively t ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-flashcard-study-app-with-next-js-and-mongodb/</link>
                <guid isPermaLink="false">6a8f26c8d7f59479db2c21fd</guid>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ MongoDB ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ David Aniebo ]]>
                </dc:creator>
                <pubDate>Wed, 26 Aug 2026 17:47:52 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/ed3602f9-f68c-4917-bc61-338d3ffba6e7.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you've ever crammed for an exam the night before, you know how hard it is to remember everything.</p>
<p>Flashcards are one of the most effective study tools because they use <strong>active recall</strong>: you actively try to remember the answer instead of passively reading notes. Research shows this strengthens memory and helps information stick.</p>
<p>In this tutorial, you'll build a full-stack flashcard app that lets students:</p>
<ul>
<li><p><strong>Create subjects</strong> (like "Biology 101" or "Calculus")</p>
</li>
<li><p><strong>Add flashcards</strong> with a question on the front and answer on the back</p>
</li>
<li><p><strong>Study</strong> by flipping cards and marking them correct or wrong</p>
</li>
<li><p><strong>Track progress</strong> to see how well they're doing</p>
</li>
</ul>
<p>By the end, you'll have a working app that stores data in MongoDB and runs on Next.js. No prior experience with these tools is required. We'll explain everything as we go.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-youll-learn">What You'll Learn</a></p>
</li>
<li><p><a href="#heading-tech-stack-overview">Tech Stack Overview</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
<ul>
<li><p><a href="#heading-step-1-create-the-nextjs-project">Step 1: Create the Next.js Project</a></p>
</li>
<li><p><a href="#heading-step-2-install-mongoose">Step 2: Install Mongoose</a></p>
</li>
<li><p><a href="#heading-step-3-set-up-mongodb">Step 3: Set Up MongoDB</a></p>
</li>
<li><p><a href="#heading-step-4-create-the-environment-file">Step 4: Create the Environment File</a></p>
</li>
<li><p><a href="#heading-step-5-understand-the-folder-structure">Step 5: Understand the Folder Structure</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-building-the-features">Building the Features</a></p>
<ul>
<li><p><a href="#heading-part-1-connecting-to-mongodb">Part 1: Connecting to MongoDB</a></p>
</li>
<li><p><a href="#heading-part-2-defining-the-data-models">Part 2: Defining the Data Models</a></p>
<ul>
<li><p><a href="#heading-subject-model">Subject Model</a></p>
</li>
<li><p><a href="#heading-flashcard-model">Flashcard Model</a></p>
</li>
<li><p><a href="#heading-progress-model">Progress Model</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-part-3-creating-subjects-and-flashcards-api-routes">Part 3: Creating Subjects and Flashcards (API Routes)</a></p>
<ul>
<li><p><a href="#heading-subjects-api-list-and-create">Subjects API – List and Create</a></p>
</li>
<li><p><a href="#heading-flashcards-api-list-and-create">Flashcards API – List and Create</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-part-4-editing-and-deleting-dynamic-api-routes">Part 4: Editing and Deleting (Dynamic API Routes)</a></p>
<ul>
<li><p><a href="#heading-subject-by-id-get-update-delete">Subject by ID – Get, Update, Delete</a></p>
</li>
<li><p><a href="#heading-flashcard-by-id-get-update-delete">Flashcard by ID – Get, Update, Delete</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-part-5-progress-tracking-api">Part 5: Progress Tracking API</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-ui-implementation">UI Implementation</a></p>
<ul>
<li><p><a href="#heading-the-subjects-page">The Subjects Page</a></p>
</li>
<li><p><a href="#heading-the-subject-detail-page-creating-and-editing-flashcards">The Subject Detail Page (Creating and Editing Flashcards)</a></p>
</li>
<li><p><a href="#heading-the-study-page-flipping-cards">The Study Page – Flipping Cards</a></p>
<ul>
<li><p><a href="#heading-the-flip-animation">The Flip Animation</a></p>
</li>
<li><p><a href="#heading-recording-progress">Recording Progress</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-the-progress-page">The Progress Page</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-best-practices">Best Practices</a></p>
<ul>
<li><p><a href="#heading-code-organization">Code Organization</a></p>
</li>
<li><p><a href="#heading-error-handling">Error Handling</a></p>
</li>
<li><p><a href="#performance-tips">Performance Tips</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-youll-learn">What You'll Learn</h2>
<ul>
<li><p>How to set up a Next.js project with TypeScript</p>
</li>
<li><p>How to connect to MongoDB and store data</p>
</li>
<li><p>How to build API routes for creating, reading, updating, and deleting data</p>
</li>
<li><p>How to build a React UI with forms, lists, and interactive flashcards</p>
</li>
<li><p>How to add a flip animation and progress tracking</p>
</li>
</ul>
<h2 id="heading-tech-stack-overview">Tech Stack Overview</h2>
<p>Before we start coding, here's what we're using and why.</p>
<h3 id="heading-nextjs">Next.js</h3>
<p>Next.js is a React framework for building web applications. It handles routing, server-side rendering, and API routes out of the box.</p>
<p>Instead of building a separate frontend and backend, Next.js lets us put both in one project. We can create API routes (like <code>/api/flashcards</code>) that talk to the database, and pages that display the UI, all in the same codebase.</p>
<h3 id="heading-mongodb">MongoDB</h3>
<p>MongoDB is a NoSQL database that stores data as JSON-like documents. Unlike traditional tables with rows and columns, you store flexible "documents" in "collections."</p>
<p>MongoDB is beginner-friendly, works well with JavaScript/TypeScript, and has a generous free tier (MongoDB Atlas) or can run locally with Docker.</p>
<h3 id="heading-mongoose">Mongoose</h3>
<p>Mongoose is a library that lets you define schemas and models for MongoDB. It adds structure and validation so you don't accidentally save invalid data.</p>
<p>Without Mongoose, you'd write raw MongoDB queries. With Mongoose, you define a "Flashcard" model once and use simple methods like <code>Flashcard.create()</code> or <code>Flashcard.find()</code>.</p>
<h3 id="heading-tailwind-css">Tailwind CSS</h3>
<p>Tailwind is a utility-first CSS framework. Instead of writing custom CSS, you add classes like <code>rounded-xl</code> or <code>bg-blue-500</code> directly in your HTML.</p>
<p>Tailwind speeds up styling and keeps the design consistent. Next.js supports it out of the box.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<h3 id="heading-step-1-create-the-nextjs-project">Step 1: Create the Next.js Project</h3>
<p>Open your terminal and run:</p>
<pre><code class="language-bash">npx create-next-app@latest flash-cards --typescript --tailwind --eslint --app --src-dir --import-alias "@/*" --use-npm
</code></pre>
<p>When prompted, choose:</p>
<ul>
<li><p>TypeScript: <strong>Yes</strong></p>
</li>
<li><p>ESLint: <strong>Yes</strong></p>
</li>
<li><p>Tailwind CSS: <strong>Yes</strong></p>
</li>
<li><p><code>src/</code> directory: <strong>Yes</strong></p>
</li>
<li><p>App Router: <strong>Yes</strong></p>
</li>
<li><p>Import alias: <strong>@/</strong>*</p>
</li>
</ul>
<p>This creates a new folder called <code>flash-cards</code> with a basic Next.js app.</p>
<h3 id="heading-step-2-install-mongoose">Step 2: Install Mongoose</h3>
<p>Mongoose is not included by default. Run the commands below to Install it.</p>
<pre><code class="language-bash">cd flash-cards
npm install mongoose
</code></pre>
<h3 id="heading-step-3-set-up-mongodb">Step 3: Set Up MongoDB</h3>
<p>You have two options:</p>
<h4 id="heading-option-a-docker-recommended-for-local-development">Option A: Docker (recommended for local development)</h4>
<p>Create a file called <code>docker-compose.yml</code> in your project root:</p>
<pre><code class="language-yaml">services:
  mongodb:
    image: mongo:7
    container_name: flashstudy-mongodb
    ports:
      - "27017:27017"
    volumes:
      - mongodb_data:/data/db

volumes:
  mongodb_data:
</code></pre>
<p>Then run:</p>
<pre><code class="language-bash">docker compose up -d
</code></pre>
<p>This starts MongoDB in the background. Your data is stored in a Docker volume, so it persists even if you stop the container.</p>
<h4 id="heading-option-b-mongodb-atlas-cloud">Option B: MongoDB Atlas (cloud)</h4>
<ol>
<li><p>Go to <a href="https://www.mongodb.com/cloud/atlas">mongodb.com/cloud/atlas</a></p>
</li>
<li><p>Create a free account and cluster</p>
</li>
<li><p>Create a database user and get your connection string</p>
</li>
<li><p>Add your IP to the network access list</p>
</li>
</ol>
<h3 id="heading-step-4-create-the-environment-file">Step 4: Create the Environment File</h3>
<p>Create a file named <code>.env.local</code> in your project root (this file is ignored by Git for security):</p>
<pre><code class="language-env">MONGODB_URI=mongodb://localhost:27017/flashcards
</code></pre>
<p>If you're using Atlas, replace this with your connection string, for example:</p>
<pre><code class="language-env">MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/flashcards?retryWrites=true&amp;w=majority
</code></pre>
<h3 id="heading-step-5-understand-the-folder-structure">Step 5: Understand the Folder Structure</h3>
<p>After setup, your project looks like this:</p>
<pre><code class="language-plaintext">flash-cards/
├── src/
│   ├── app/              # Pages and API routes
│   │   ├── api/          # Backend API endpoints
│   │   ├── subjects/     # Subject list and detail pages
│   │   ├── study/        # Study mode page
│   │   └── progress/     # Progress tracking page
│   ├── components/       # Reusable UI components
│   └── lib/              # Utilities and database code
│       ├── db.ts         # MongoDB connection
│       └── models/       # Mongoose schemas
├── .env.local            # Environment variables (you create this)
├── docker-compose.yml    # Docker config for MongoDB
└── package.json
</code></pre>
<p>The <code>app</code> folder uses Next.js App Router: each folder can have a <code>page.tsx</code> (the UI) and <code>route.ts</code> (API endpoints). We'll build these step by step.</p>
<h2 id="heading-building-the-features">Building the Features</h2>
<h3 id="heading-part-1-connecting-to-mongodb">Part 1: Connecting to MongoDB</h3>
<p>Before we can store or retrieve flashcards, we need to connect our application to MongoDB.</p>
<p>We'll create a small database utility that handles this connection for us. Because Next.js can handle multiple requests and reload modules during development, we don't want to create a new MongoDB connection every time an API route runs. Instead, we'll cache the connection and reuse it whenever possible.</p>
<p>Let's start by creating a <code>db.ts</code> file inside the <code>src/lib</code> directory.</p>
<pre><code class="language-typescript">import mongoose from "mongoose";

const MONGODB_URI = process.env.MONGODB_URI || "mongodb://localhost:27017/flashcards";

interface MongooseCache {
  conn: typeof mongoose | null;
  promise: Promise&lt;typeof mongoose&gt; | null;
}

declare global {
  var mongoose: MongooseCache | undefined;
}

let cached: MongooseCache = global.mongoose || { conn: null, promise: null };

if (!global.mongoose) {
  global.mongoose = cached;
}

async function dbConnect(): Promise&lt;typeof mongoose&gt; {
  if (cached.conn) return cached.conn;

  if (!cached.promise) {
    cached.promise = mongoose.connect(MONGODB_URI, {
      bufferCommands: false,
    });
  }

  cached.conn = await cached.promise;
  return cached.conn;
}

export default dbConnect;
</code></pre>
<p>Here's a line-by-line explanation of this code:</p>
<ul>
<li><p><code>MONGODB_URI</code>: Reads the connection string from <code>.env.local</code>. Falls back to local MongoDB if not set.</p>
</li>
<li><p><code>MongooseCache</code>: A TypeScript interface describing our cache: we store either a connection (<code>conn</code>) or a promise that will eventually give us one.</p>
</li>
<li><p><code>global.mongoose</code>: In development, Next.js may reload modules. Using <code>global</code> keeps our cache across reloads so we don't create duplicate connections.</p>
</li>
<li><p><code>dbConnect()</code>: If we already have a connection, return it. Otherwise, create one, cache it, and return it. Every API route will call <code>await dbConnect()</code> before touching the database.</p>
</li>
</ul>
<h3 id="heading-part-2-defining-the-data-models">Part 2: Defining the Data Models</h3>
<p>Now that our application can connect to MongoDB, let's define the data we'll store in the database.</p>
<p>Our flashcard app needs three types of data:</p>
<ul>
<li><p><strong>Subjects</strong>: Categories such as Biology 101 or Calculus.</p>
</li>
<li><p><strong>Flashcards</strong>: Questions and answers that belong to a subject.</p>
</li>
<li><p><strong>Progress</strong>: Records of how well the user performs when studying.</p>
</li>
</ul>
<p>We'll use Mongoose schemas to define the structure of each type of data. A schema describes the fields a document can have and the type of data each field should contain.</p>
<p>Let's start with the <code>Subject</code> model.</p>
<h4 id="heading-subject-model">Subject Model</h4>
<p>A subject represents a category of flashcards. For example, a student might create a subject called <strong>Biology 101</strong> and use it to organize their biology flashcards.</p>
<p>Each subject will have a name, an optional description, and a color that we'll use when displaying the subject in the UI.</p>
<p>First, create a <code>models</code> directory inside <code>src/lib</code> if you haven't already. Then create a file named <code>Subject.ts</code> inside it.</p>
<pre><code class="language-typescript">import mongoose, { Schema, model, models } from "mongoose";

export interface ISubject {
  _id: string;
  name: string;
  description?: string;
  color: string;
  createdAt: Date;
  updatedAt: Date;
}

const SubjectSchema = new Schema(
  {
    name: { type: String, required: true },
    description: { type: String },
    color: { type: String, default: "#6366f1" },
  },
  { timestamps: true }
);

export default models.Subject || model&lt;ISubject&gt;("Subject", SubjectSchema);
</code></pre>
<p>In this code:</p>
<ul>
<li><p><code>ISubject</code>: TypeScript interface. Describes what a subject object looks like in our app.</p>
</li>
<li><p><code>SubjectSchema</code> – Mongoose schema. <code>name</code> is required, while <code>description</code> and <code>color</code> are optional. <code>color</code> defaults to a purple hex.</p>
</li>
<li><p><code>timestamps: true</code>: Mongoose automatically adds <code>createdAt</code> and <code>updatedAt</code> to every document.</p>
</li>
<li><p><code>models.Subject || model(...)</code>: In development, modules can reload. This prevents "model already defined" errors by reusing the existing model if it exists.</p>
</li>
</ul>
<h4 id="heading-flashcard-model">Flashcard Model</h4>
<p>A flashcard belongs to a subject and contains a question on the front and an answer on the back.</p>
<p>Next, let's create the <code>Flashcard</code> model. Inside <code>src/lib/models</code>, create a file named <code>Flashcard.ts</code> and add the code below:</p>
<pre><code class="language-typescript">import mongoose, { Schema, model, models } from "mongoose";

export interface IFlashcard {
  _id: string;
  subjectId: string;
  front: string;
  back: string;
  createdAt: Date;
  updatedAt: Date;
}

const FlashcardSchema = new Schema(
  {
    subjectId: { type: Schema.Types.ObjectId, ref: "Subject", required: true },
    front: { type: String, required: true },
    back: { type: String, required: true },
  },
  { timestamps: true }
);

export default models.Flashcard || model&lt;IFlashcard&gt;("Flashcard", FlashcardSchema);
</code></pre>
<p>In this code:</p>
<ul>
<li><p><code>subjectId</code>: References a Subject by its <code>_id</code>. <code>ref: "Subject"</code> lets Mongoose populate this field (replace the ID with the full subject object when we fetch).</p>
</li>
<li><p><code>front</code> and <code>back</code>: The question and answer text.</p>
</li>
</ul>
<h4 id="heading-progress-model">Progress Model</h4>
<p>When a user studies, we need to record whether they answered each flashcard correctly or incorrectly. We'll use this information to display their progress on the dashboard.</p>
<p>Next, let's create the <code>Progress</code> model. Inside <code>src/lib/models</code>, create a file named <code>Progress.ts</code> and add the code below:</p>
<pre><code class="language-typescript">import mongoose, { Schema, model, models } from "mongoose";

export interface IProgress {
  _id: string;
  flashcardId: string;
  subjectId: string;
  correct: boolean;
  reviewedAt: Date;
}

const ProgressSchema = new Schema(
  {
    flashcardId: { type: Schema.Types.ObjectId, ref: "Flashcard", required: true },
    subjectId: { type: Schema.Types.ObjectId, ref: "Subject", required: true },
    correct: { type: Boolean, required: true },
    reviewedAt: { type: Date, default: Date.now },
  },
  { timestamps: true }
);

export default models.Progress || model&lt;IProgress&gt;("Progress", ProgressSchema);
</code></pre>
<p>In this code:</p>
<ul>
<li><p><code>correct</code>: <code>true</code> if the user knew the answer, <code>false</code> if not.</p>
</li>
<li><p><code>reviewedAt</code>: When the review happened. We use this for sorting and future features like spaced repetition.</p>
</li>
</ul>
<h3 id="heading-subjects-api-list-and-create">Subjects API – List and Create</h3>
<p>Now that we've defined our data models, let's create the API routes that will allow the application to work with that data.</p>
<p>API routes handle requests from the frontend and communicate with MongoDB. In this section, we'll create routes for creating and retrieving subjects and flashcards.</p>
<p>We'll start with the subjects API. This route will support two operations:</p>
<ul>
<li><p><strong>GET</strong>: Retrieve all subjects.</p>
</li>
<li><p><strong>POST</strong>: Create a new subject.</p>
</li>
</ul>
<p>Inside <code>src/app/api/subjects</code>, create a file named <code>route.ts</code> and add the code below:</p>
<pre><code class="language-typescript">import { NextResponse } from "next/server";
import dbConnect from "@/lib/db";
import Subject from "@/lib/models/Subject";

export async function GET() {
  try {
    await dbConnect();
    const subjects = await Subject.find({}).sort({ createdAt: -1 });
    return NextResponse.json(subjects);
  } catch (error) {
    console.error("Error fetching subjects:", error);
    return NextResponse.json(
      { error: "Failed to fetch subjects" },
      { status: 500 }
    );
  }
}

export async function POST(request: Request) {
  try {
    await dbConnect();
    const body = await request.json();
    const { name, description, color } = body;

    if (!name) {
      return NextResponse.json(
        { error: "Subject name is required" },
        { status: 400 }
      );
    }

    const subject = await Subject.create({
      name,
      description: description || "",
      color: color || "#6366f1",
    });

    return NextResponse.json(subject);
  } catch (error) {
    console.error("Error creating subject:", error);
    return NextResponse.json(
      { error: "Failed to create subject" },
      { status: 500 }
    );
  }
}
</code></pre>
<p>In this code:</p>
<ul>
<li><p><code>GET</code>: Fetches all subjects, sorted by newest first. <code>find({})</code> means "find all." Returns them as JSON.</p>
</li>
<li><p><code>POST</code>: Creates a new subject. Reads <code>name</code>, <code>description</code>, and <code>color</code> from the request body. Validates that <code>name</code> exists. Uses <code>Subject.create()</code> to save to MongoDB. Returns the created subject.</p>
</li>
<li><p><code>status: 400</code>: Bad request (missing data). <code>status: 500</code>: Server error (for example, database failure).</p>
</li>
</ul>
<h4 id="heading-flashcards-api-list-and-create">Flashcards API – List and Create</h4>
<p>Next, let's create the API route for working with flashcards. This route will let us retrieve existing flashcards and create new ones.</p>
<p>Inside <code>src/app/api/flashcards</code>, create a file named <code>route.ts</code> and add the code block:</p>
<pre><code class="language-typescript">import { NextResponse } from "next/server";
import dbConnect from "@/lib/db";
import Flashcard from "@/lib/models/Flashcard";

export async function GET(request: Request) {
  try {
    await dbConnect();
    const { searchParams } = new URL(request.url);
    const subjectId = searchParams.get("subjectId");

    const query = subjectId ? { subjectId } : {};
    const flashcards = await Flashcard.find(query)
      .populate("subjectId", "name color")
      .sort({ createdAt: -1 });

    return NextResponse.json(flashcards);
  } catch (error) {
    console.error("Error fetching flashcards:", error);
    return NextResponse.json(
      { error: "Failed to fetch flashcards" },
      { status: 500 }
    );
  }
}

export async function POST(request: Request) {
  try {
    await dbConnect();
    const body = await request.json();
    const { subjectId, front, back } = body;

    if (!subjectId || !front || !back) {
      return NextResponse.json(
        { error: "Subject, front, and back are required" },
        { status: 400 }
      );
    }

    const flashcard = await Flashcard.create({
      subjectId,
      front,
      back,
    });

    const populated = await Flashcard.findById(flashcard._id).populate(
      "subjectId",
      "name color"
    );

    return NextResponse.json(populated);
  } catch (error) {
    console.error("Error creating flashcard:", error);
    return NextResponse.json(
      { error: "Failed to create flashcard" },
      { status: 500 }
    );
  }
}
</code></pre>
<p>In this code:</p>
<ul>
<li><p><code>searchParams.get("subjectId")</code>: For <code>GET /api/flashcards?subjectId=abc123</code>, we filter by that subject. If no <code>subjectId</code>, we return all flashcards.</p>
</li>
<li><p><code>.populate("subjectId", "name color")</code>: Replaces the raw ID with the subject object, but only includes <code>name</code> and <code>color</code>. Makes it easy to display the subject name in the UI.</p>
</li>
<li><p><code>POST</code>: Requires <code>subjectId</code>, <code>front</code>, and <code>back</code>. After creating, we fetch the flashcard again with <code>populate</code> so the response includes the subject details.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/6904c2dbd42ef6b1f9e61c3e/f15cd9c8-87d1-461b-92ac-4ec091481338.jpg" alt="Flashcard-create-study-form" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-part-4-editing-and-deleting-dynamic-api-routes">Part 4: Editing and Deleting (Dynamic API Routes)</h3>
<p>For individual subjects and flashcards, we'll use <strong>dynamic routes</strong>. In Next.js, placing <code>[id]</code> in a folder name creates a route that can handle different IDs. For example, <code>/api/subjects/123</code> and <code>/api/subjects/456</code> can use the same route.</p>
<h4 id="heading-subject-api-route">Subject API Route</h4>
<p>Let's start by creating the dynamic route for individual subjects. This route will let us retrieve, update, or delete a subject.</p>
<p>Inside <code>src/app/api/subjects/[id]</code>, create a file named <code>route.ts</code> and add the code below:</p>
<pre><code class="language-typescript">import { NextResponse } from "next/server";
import dbConnect from "@/lib/db";
import Subject from "@/lib/models/Subject";
import Flashcard from "@/lib/models/Flashcard";
import Progress from "@/lib/models/Progress";

export async function GET(
  _request: Request,
  { params }: { params: Promise&lt;{ id: string }&gt; }
) {
  try {
    await dbConnect();
    const { id } = await params;
    const subject = await Subject.findById(id);

    if (!subject) {
      return NextResponse.json({ error: "Subject not found" }, { status: 404 });
    }

    return NextResponse.json(subject);
  } catch (error) {
    console.error("Error fetching subject:", error);
    return NextResponse.json(
      { error: "Failed to fetch subject" },
      { status: 500 }
    );
  }
}

export async function PUT(
  request: Request,
  { params }: { params: Promise&lt;{ id: string }&gt; }
) {
  try {
    await dbConnect();
    const { id } = await params;
    const body = await request.json();
    const { name, description, color } = body;

    const subject = await Subject.findByIdAndUpdate(
      id,
      { name, description, color },
      { new: true }
    );

    if (!subject) {
      return NextResponse.json({ error: "Subject not found" }, { status: 404 });
    }

    return NextResponse.json(subject);
  } catch (error) {
    console.error("Error updating subject:", error);
    return NextResponse.json(
      { error: "Failed to update subject" },
      { status: 500 }
    );
  }
}

export async function DELETE(
  _request: Request,
  { params }: { params: Promise&lt;{ id: string }&gt; }
) {
  try {
    await dbConnect();
    const { id } = await params;

    await Flashcard.deleteMany({ subjectId: id });
    await Progress.deleteMany({ subjectId: id });
    const subject = await Subject.findByIdAndDelete(id);

    if (!subject) {
      return NextResponse.json({ error: "Subject not found" }, { status: 404 });
    }

    return NextResponse.json({ message: "Subject deleted" });
  } catch (error) {
    console.error("Error deleting subject:", error);
    return NextResponse.json(
      { error: "Failed to delete subject" },
      { status: 500 }
    );
  }
}
</code></pre>
<p>In this code:</p>
<ul>
<li><p><code>params</code>: In Next.js 15+, <code>params</code> is a Promise. We <code>await</code> it to get <code>{ id: "abc123" }</code>.</p>
</li>
<li><p><code>findByIdAndUpdate(id, updates, { new: true })</code>: Updates the document and returns the updated version. <code>{ new: true }</code> means "return the new document, not the old one."</p>
</li>
<li><p><code>DELETE</code>: When we delete a subject, we also delete its flashcards and progress records. Otherwise we'd have orphaned data.</p>
</li>
</ul>
<h4 id="heading-flashcard-by-id-get-update-delete">Flashcard by ID – Get, Update, Delete</h4>
<p>Now, let's create the dynamic route for individual flashcards. This route will let us retrieve, update, or delete a flashcard.</p>
<p>Inside <code>src/app/api/flashcards/[id]</code>, create a file named <code>route.ts</code> and add the code below:</p>
<pre><code class="language-typescript">import { NextResponse } from "next/server";
import dbConnect from "@/lib/db";
import Flashcard from "@/lib/models/Flashcard";

export async function GET(
  _request: Request,
  { params }: { params: Promise&lt;{ id: string }&gt; }
) {
  try {
    await dbConnect();
    const { id } = await params;
    const flashcard = await Flashcard.findById(id).populate(
      "subjectId",
      "name color"
    );

    if (!flashcard) {
      return NextResponse.json(
        { error: "Flashcard not found" },
        { status: 404 }
      );
    }

    return NextResponse.json(flashcard);
  } catch (error) {
    console.error("Error fetching flashcard:", error);
    return NextResponse.json(
      { error: "Failed to fetch flashcard" },
      { status: 500 }
    );
  }
}

export async function PUT(
  request: Request,
  { params }: { params: Promise&lt;{ id: string }&gt; }
) {
  try {
    await dbConnect();
    const { id } = await params;
    const body = await request.json();
    const { front, back } = body;

    const flashcard = await Flashcard.findByIdAndUpdate(
      id,
      { front, back },
      { new: true }
    ).populate("subjectId", "name color");

    if (!flashcard) {
      return NextResponse.json(
        { error: "Flashcard not found" },
        { status: 404 }
      );
    }

    return NextResponse.json(flashcard);
  } catch (error) {
    console.error("Error updating flashcard:", error);
    return NextResponse.json(
      { error: "Failed to update flashcard" },
      { status: 500 }
    );
  }
}

export async function DELETE(
  _request: Request,
  { params }: { params: Promise&lt;{ id: string }&gt; }
) {
  try {
    await dbConnect();
    const { id } = await params;
    const flashcard = await Flashcard.findByIdAndDelete(id);

    if (!flashcard) {
      return NextResponse.json(
        { error: "Flashcard not found" },
        { status: 404 }
      );
    }

    return NextResponse.json({ message: "Flashcard deleted" });
  } catch (error) {
    console.error("Error deleting flashcard:", error);
    return NextResponse.json(
      { error: "Failed to delete flashcard" },
      { status: 500 }
    );
  }
}
</code></pre>
<h3 id="heading-part-5-progress-tracking-api">Part 5: Progress Tracking API</h3>
<p>When a user marks a flashcard as correct or incorrect during a study session, we need to save that result. We'll also use this data to calculate progress statistics, such as the percentage of correct answers for each subject.</p>
<p>Next, let's create the API route for tracking progress. Inside <code>src/app/api/progress</code>, create a file named <code>route.ts</code> and add the code below:</p>
<pre><code class="language-typescript">import { NextResponse } from "next/server";
import dbConnect from "@/lib/db";
import Progress from "@/lib/models/Progress";

export async function GET(request: Request) {
  try {
    await dbConnect();
    const { searchParams } = new URL(request.url);
    const subjectId = searchParams.get("subjectId");

    const query = subjectId ? { subjectId } : {};
    const progress = await Progress.find(query).sort({ reviewedAt: -1 });

    const stats = await Progress.aggregate([
      { $match: query },
      {
        $group: {
          _id: "$subjectId",
          total: { $sum: 1 },
          correct: { $sum: { $cond: ["$correct", 1, 0] } },
        },
      },
    ]);

    return NextResponse.json({ progress, stats });
  } catch (error) {
    console.error("Error fetching progress:", error);
    return NextResponse.json(
      { error: "Failed to fetch progress" },
      { status: 500 }
    );
  }
}

export async function POST(request: Request) {
  try {
    await dbConnect();
    const body = await request.json();
    const { flashcardId, subjectId, correct } = body;

    if (!flashcardId || !subjectId || typeof correct !== "boolean") {
      return NextResponse.json(
        { error: "flashcardId, subjectId, and correct are required" },
        { status: 400 }
      );
    }

    const progress = await Progress.create({
      flashcardId,
      subjectId,
      correct,
    });

    return NextResponse.json(progress);
  } catch (error) {
    console.error("Error recording progress:", error);
    return NextResponse.json(
      { error: "Failed to record progress" },
      { status: 500 }
    );
  }
}
</code></pre>
<p>In this code:</p>
<ul>
<li><p><code>aggregate</code>: MongoDB's aggregation pipeline. We group by <code>subjectId</code> and count total reviews and correct answers. <code>$cond: ["$correct", 1, 0]</code> means "if correct is true, add 1, else add 0."</p>
</li>
<li><p><code>stats</code>: Returns something like <code>[{ _id: "subjectId123", total: 20, correct: 16 }]</code>. The frontend uses this to show "80% accuracy" per subject.</p>
</li>
</ul>
<h2 id="heading-ui-implementation">UI Implementation</h2>
<p>Now we'll build the pages users see. We'll use React hooks (<code>useState</code>, <code>useEffect</code>) to manage data and <code>fetch</code> to call our API.</p>
<h3 id="heading-the-subjects-page">The Subjects Page</h3>
<p>On load, we fetch subjects from the API. We show a form to create new subjects. Each subject is a card that links to its detail page.</p>
<p>Key logic:</p>
<ol>
<li><p><code>useEffect</code> runs once on mount and calls <code>fetch("/api/subjects")</code>.</p>
</li>
<li><p>The form's <code>onSubmit</code> calls <code>fetch("/api/subjects", { method: "POST", ... })</code>.</p>
</li>
<li><p>After a successful create, we clear the form and call <code>fetchSubjects()</code> again to refresh the list.</p>
</li>
</ol>
<pre><code class="language-tsx">// Simplified structure - see full code in src/app/subjects/page.tsx
const [subjects, setSubjects] = useState&lt;Subject[]&gt;([]);
const [showForm, setShowForm] = useState(false);

useEffect(() =&gt; {
  fetch("/api/subjects")
    .then((res) =&gt; res.json())
    .then((data) =&gt; setSubjects(data));
}, []);

const handleSubmit = async (e) =&gt; {
  e.preventDefault();
  await fetch("/api/subjects", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ name, description, color }),
  });
  fetchSubjects(); 
};
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6904c2dbd42ef6b1f9e61c3e/30d83725-4d91-4642-b8ee-f3a89726843f.jpg" alt="Flashcard-study-list" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-the-subject-detail-page-creating-and-editing-flashcards">The Subject Detail Page (Creating and Editing Flashcards)</h3>
<p>This page shows one subject and its flashcards. Users can add new cards or edit/delete existing ones. The URL is <code>/subjects/[id]</code>, so we use <code>useParams()</code> to get the subject ID.</p>
<p>Key logic:</p>
<ol>
<li><p><code>useParams()</code> gives us the <code>id</code> from the URL.</p>
</li>
<li><p>We fetch the subject and its flashcards on mount.</p>
</li>
<li><p>"Add Flashcard" shows a form. On submit, we POST to <code>/api/flashcards</code> with <code>subjectId</code>, <code>front</code>, and <code>back</code>.</p>
</li>
<li><p>Each card has Edit and Delete buttons. Edit switches to an inline form, while Delete calls <code>DELETE /api/flashcards/[id]</code>.</p>
</li>
</ol>
<pre><code class="language-tsx">// Creating a flashcard
const handleCreate = async (e) =&gt; {
  e.preventDefault();
  await fetch("/api/flashcards", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ subjectId: id, front, back }),
  });
  fetchFlashcards(); // Refresh
};

// Updating a flashcard
const handleUpdate = async (e) =&gt; {
  e.preventDefault();
  await fetch(`/api/flashcards/${editingId}`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ front: editFront, back: editBack }),
  });
  setEditingId(null);
  fetchFlashcards();
};
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6904c2dbd42ef6b1f9e61c3e/e60ba077-b22b-4f71-895f-5fce4f9e66d3.jpg" alt="Study-details-page" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-the-study-page-flipping-cards">The Study Page – Flipping Cards</h3>
<p>The study page has three main states:</p>
<ol>
<li><p><strong>Subject selection</strong>: User picks which subject to study.</p>
</li>
<li><p><strong>Ready to start</strong>: Shows "Start Studying" with the card count.</p>
</li>
<li><p><strong>Studying</strong>: Shows one card at a time. User clicks to flip, then marks correct or wrong. We advance to the next card and record progress.</p>
</li>
</ol>
<h4 id="heading-the-flip-animation">The Flip Animation</h4>
<p>We use CSS 3D transforms to create the flip animation. The flashcard has two faces: a front for the question and a back for the answer. When <code>flipped</code> is <code>true</code>, we rotate the card container 180 degrees. We also use <code>backface-visibility: hidden</code> so that only the appropriate face is visible during the rotation.</p>
<p>To add the styles for the flip animation, open <code>src/app/globals.css</code> and add the following code:</p>
<pre><code class="language-css">/* Flashcard flip animation */
.perspective-1000 {
  perspective: 1000px;
}

.preserve-3d {
  transform-style: preserve-3d;
}

.backface-hidden {
  backface-visibility: hidden;
}

/* Lined paper effect for the card background */
.lined-paper {
  background-image: repeating-linear-gradient(
    transparent,
    transparent 27px,
    #e5e7eb 27px,
    #e5e7eb 28px
  );
}
</code></pre>
<p>The <code>lined-paper</code> class creates horizontal grey lines (like notebook paper) using a repeating gradient. This gives the flashcard a familiar, study-friendly look.</p>
<p>The card structure:</p>
<pre><code class="language-tsx">&lt;div
  className={`preserve-3d transition-transform duration-500 ${
    flipped ? "[transform:rotateY(180deg)]" : ""
  }`}
&gt;
  {/* Front face - Question */}
  &lt;div className="backface-hidden [transform:rotateY(0deg)]"&gt;
    {currentCard.front}
  &lt;/div&gt;
  {/* Back face - Answer */}
  &lt;div className="backface-hidden [transform:rotateY(180deg)]"&gt;
    {currentCard.back}
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<p>When the user clicks the card, we toggle <code>flipped</code>. The parent rotates, and the correct face becomes visible.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6904c2dbd42ef6b1f9e61c3e/a92a03c7-c562-4600-ba2c-ecd6e85ab00c.jpg" alt="a92a03c7-c562-4600-ba2c-ecd6e85ab00c" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h4 id="heading-recording-progress">Recording Progress</h4>
<p>When the user clicks "Got it!" or "Didn't know", we:</p>
<ol>
<li><p>POST to <code>/api/progress</code> with <code>flashcardId</code>, <code>subjectId</code>, and <code>correct</code>.</p>
</li>
<li><p>Update local state (<code>sessionCorrect</code> or <code>sessionWrong</code>) for the live stats.</p>
</li>
<li><p>Move to the next card. If we've finished all cards, we show the "Start Studying" screen again.</p>
</li>
</ol>
<pre><code class="language-tsx">const handleKnow = () =&gt; {
  recordProgress(true);
  setFlipped(false);
  if (currentIndex &lt; flashcards.length - 1) {
    setCurrentIndex((i) =&gt; i + 1);
  } else {
    setStudyStarted(false);
    setCurrentIndex(0);
  }
};
</code></pre>
<h3 id="heading-the-progress-page">The Progress Page</h3>
<p>Here, we fetch subjects and progress stats. For each subject, we look up its stats (total reviews, correct count) and compute the percentage. We display overall stats at the top and per-subject breakdown below.</p>
<pre><code class="language-tsx">const getSubjectStats = (subjectId) =&gt; {
  const stat = stats.find((s) =&gt; s._id === subjectId);
  return stat
    ? {
        total: stat.total,
        correct: stat.correct,
        pct: Math.round((stat.correct / stat.total) * 100),
      }
    : null;
};
</code></pre>
<h3 id="heading-optional-lined-paper-and-paperclip-icon">Optional: Lined Paper and Paperclip Icon</h3>
<p>The app includes a lined-paper effect and a paperclip icon to make the flashcard feel more tactile. The paperclip is a simple SVG component:</p>
<pre><code class="language-tsx">// src/components/PaperclipIcon.tsx
export default function PaperclipIcon({ className }: { className?: string }) {
  return (
    &lt;svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"&gt;
      &lt;path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" /&gt;
    &lt;/svg&gt;
  );
}
</code></pre>
<p>Place it at the top center of the flashcard. The <code>lined-paper</code> class is applied to the card content area for the notebook effect.</p>
<h2 id="heading-best-practices">Best Practices</h2>
<h3 id="heading-code-organization">Code Organization</h3>
<ul>
<li><p><strong>Models</strong> in <code>lib/models/</code>: One file per model. Keeps schemas in one place.</p>
</li>
<li><p><strong>API routes</strong> in <code>app/api/</code>: Group by resource (subjects, flashcards, progress). Use <code>[id]</code> for dynamic routes.</p>
</li>
<li><p><strong>Reusable components</strong>: The <code>PaperclipIcon</code> is in <code>components/</code>. Use this pattern for any UI you repeat.</p>
</li>
</ul>
<h3 id="heading-error-handling">Error Handling</h3>
<ul>
<li><p><strong>API routes</strong>: Always wrap logic in <code>try/catch</code>. Return appropriate status codes (400 for bad input, 404 for not found, 500 for server errors).</p>
</li>
<li><p><strong>Frontend</strong>: Check <code>res.ok</code> before using <code>res.json()</code>. Show loading and error states to the user.</p>
</li>
</ul>
<h3 id="heading-performance-tips">Performance Tips</h3>
<ul>
<li><p><strong>Database connection</strong>: Reuse the connection (our <code>dbConnect</code> does this). Don't connect on every request.</p>
</li>
<li><p><strong>Populate sparingly</strong>: Only <code>.populate()</code> fields you need. Specify which fields: <code>.populate("subjectId", "name color")</code>.</p>
</li>
<li><p><strong>Loading states</strong>: Show a spinner while fetching. Prevents layout shift and gives feedback.</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You've built a full-stack flashcard app with:</p>
<ul>
<li><p><strong>Next.js</strong> for the app and API routes</p>
</li>
<li><p><strong>MongoDB + Mongoose</strong> for storing subjects, flashcards, and progress</p>
</li>
<li><p><strong>React</strong> for the UI with forms, lists, and a flip animation</p>
</li>
<li><p><strong>Tailwind CSS</strong> for styling</p>
</li>
</ul>
<h3 id="heading-possible-improvements">Possible Improvements</h3>
<p>There are a few features you could build to improve this app.</p>
<p>First, you could add authentication. Add login so each user has their own subjects and cards. Consider NextAuth.js or Clerk.</p>
<p>Second, you could add a spaced repetition feature. Use the progress data to show cards at optimal intervals (for example, cards you got wrong more often).</p>
<p>Next, you could add some animations, like transitions between cards or a confetti effect when a session is complete.</p>
<p>You could also build in mobile responsiveness. The current layout works on desktop, but you could optimize the study view for phones.</p>
<p>And finally, an export/import feature could be useful: let users export their flashcards as JSON or CSV for backup.</p>
<h3 id="heading-next-steps">Next Steps</h3>
<p>To take this further, run <code>npm run dev</code> and explore the app. You can add a few subjects and flashcards, then try the study mode.</p>
<p>After that, open MongoDB Compass or Atlas to inspect your data. Experiment with the code: change colors, add fields, or tweak the flip animation.</p>
<p>Happy studying!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Fix a Leaked API Key: A Developer’s Guide to Git Security ]]>
                </title>
                <description>
                    <![CDATA[ Imagine this: you're working late, your code finally works, and you're ready to push it to GitHub. You run: git add . git commit -m "Fix API integration" git push A few minutes later, you notice some ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-fix-a-leaked-api-key/</link>
                <guid isPermaLink="false">6a8ddd55902e76128f1985bb</guid>
                
                    <category>
                        <![CDATA[ Git ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ api ]]>
                    </category>
                
                    <category>
                        <![CDATA[ GitHub ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Eva J Patel ]]>
                </dc:creator>
                <pubDate>Tue, 25 Aug 2026 18:22:13 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/0903575c-822b-481b-af12-07b864bebc67.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Imagine this: you're working late, your code finally works, and you're ready to push it to GitHub.</p>
<p>You run:</p>
<pre><code class="language-bash">git add .
git commit -m "Fix API integration"
git push
</code></pre>
<p>A few minutes later, you notice something strange. Your API usage has suddenly increased. Maybe there are unexpected requests, new cloud resources, or even a bill that looks much larger than expected.</p>
<p>Then you find it:</p>
<pre><code class="language-javascript">const apiKey = "sk_live_123456789";
</code></pre>
<p>Your API key is sitting in a Git repository.</p>
<p>This situation is stressful, but it's fixable.</p>
<p>The most important rule is:</p>
<blockquote>
<p><strong>If an API key has been committed to Git, assume it has been copied and compromised, even if you delete it immediately.</strong></p>
</blockquote>
<p>Deleting the key from the latest version of your file doesn't make the old key safe. Git keeps previous versions of files in its history, and exposed credentials can be discovered by automated scanners.</p>
<p>In this guide, you'll learn the following:</p>
<ul>
<li><p><a href="#heading-what-is-an-api-key">What Is an API Key?</a></p>
</li>
<li><p><a href="#heading-the-emergency-response-what-to-do-first">The Emergency Response: What to Do First</a></p>
</li>
<li><p><a href="#heading-step-1-revoke-or-rotate-the-leaked-key">Step 1: Revoke or Rotate the Leaked Key</a></p>
</li>
<li><p><a href="#heading-step-2-investigate-suspicious-activity">Step 2: Investigate Suspicious Activity</a></p>
</li>
<li><p><a href="#heading-step-3-remove-the-secret-from-your-current-code">Step 3: Remove the Secret From Your Current Code</a></p>
</li>
<li><p><a href="#heading-step-4-use-a-env-file-for-local-development">Step 4: Use a.envFile for Local Development</a></p>
</li>
<li><p><a href="#heading-step-5-create-a-safe-envexample">Step 5: Create a Safe.env.example</a></p>
</li>
<li><p><a href="#heading-step-6-determine-whether-the-secret-is-still-in-git-history">Step 6: Determine Whether the Secret Is Still in Git History</a></p>
</li>
<li><p><a href="#heading-when-do-you-need-to-rewrite-git-history">When Do You Need to Rewrite Git History?</a></p>
</li>
<li><p><a href="#heading-step-7-remove-the-secret-from-git-history">Step 7: Remove the Secret From Git History</a></p>
</li>
<li><p><a href="#heading-step-8-verify-that-the-secret-is-gone">Step 8: Verify That the Secret Is Gone</a></p>
</li>
<li><p><a href="#heading-step-9-push-the-cleaned-history-carefully">Step 9: Push the Cleaned History Carefully</a></p>
</li>
<li><p><a href="#heading-step-10-replace-the-credential-everywhere">Step 10: Replace the Credential Everywhere</a></p>
</li>
<li><p><a href="#heading-step-11-restrict-the-replacement-key">Step 11: Restrict the Replacement Key</a></p>
</li>
<li><p><a href="#heading-what-about-frontend-applications">What About Frontend Applications?</a></p>
</li>
<li><p><a href="#heading-environment-variables-vs-secret-managers">Environment Variables vs Secret Managers</a></p>
</li>
<li><p><a href="#heading-add-secret-scanning-to-your-workflow">Add Secret Scanning to Your Workflow</a></p>
</li>
<li><p><a href="#heading-use-git-hooks-as-an-extra-safety-net">Use Git Hooks as an Extra Safety Net</a></p>
</li>
<li><p><a href="#heading-review-your-staged-diff-before-committing">Review Your Staged Diff Before Committing</a></p>
</li>
<li><p><a href="#heading-common-mistakes-developers-make">Common Mistakes Developers Make</a></p>
</li>
<li><p><a href="#heading-a-complete-api-key-incident-checklist">A Complete API-Key Incident Checklist</a></p>
</li>
<li><p><a href="#heading-a-secure-project-structure">A Secure Project Structure</a></p>
</li>
</ul>
<p>We'll use this basic workflow throughout the article:</p>
<pre><code class="language-text">Invalidate → Investigate → Remove → Replace → Prevent
</code></pre>
<p>Let's start with what an API key actually is before we get to the most important part: what to do <strong>right now</strong> after a key is exposed.</p>
<h2 id="heading-what-is-an-api-key">What Is an API Key?</h2>
<p>An API key is a credential that allows an application to communicate with another service.</p>
<p>For example, an application might use an API key to access:</p>
<ul>
<li><p>A weather service</p>
</li>
<li><p>A payment provider</p>
</li>
<li><p>A mapping service</p>
</li>
<li><p>An artificial intelligence API</p>
</li>
<li><p>A cloud platform</p>
</li>
<li><p>A database</p>
</li>
<li><p>An email provider</p>
</li>
<li><p>A private company API</p>
</li>
</ul>
<p>A key might look something like this:</p>
<pre><code class="language-javascript">const apiKey = "your-real-api-key";
</code></pre>
<p>Or it might appear in a configuration file:</p>
<pre><code class="language-json">{
  "apiKey": "your-real-api-key",
  "databasePassword": "your-real-password"
}
</code></pre>
<p>API keys are often called <strong>secrets</strong> because possessing one may allow someone to make requests, access data, create resources, or generate charges on your account.</p>
<p>Not every API key is equally sensitive. Some services provide browser keys that are intentionally visible to users. Those keys should still have appropriate restrictions, quotas, and permissions.</p>
<p>As a general rule:</p>
<blockquote>
<p><strong>If a credential can access private data, create resources, modify records, or generate charges, it shouldn't be stored directly in your source code.</strong></p>
</blockquote>
<h2 id="heading-the-emergency-response-what-to-do-first">The Emergency Response: What to Do First</h2>
<p>When you discover a leaked credential, a common reaction is to delete the key from the file and push another commit.</p>
<p>Don't start there.</p>
<p>Your first priority is to <strong>make the leaked credential useless</strong>.</p>
<p>Use this order of operations:</p>
<pre><code class="language-text">1. Invalidate the leaked credential
2. Investigate suspicious activity
3. Remove the secret from your code
4. Replace it with a new credential
5. Clean the Git history if necessary
6. Verify the cleanup
7. Add protections against future leaks
</code></pre>
<p>Think of an API key like a house key that was dropped in a crowded street.</p>
<p>Deleting a picture of the key doesn't matter if someone already picked up the physical key.</p>
<p><strong>Change the lock first.</strong></p>
<h2 id="heading-step-1-revoke-or-rotate-the-leaked-key">Step 1: Revoke or Rotate the Leaked Key</h2>
<p>Go to the dashboard of the service that issued the credential.</p>
<p>Depending on the provider, you may see options such as:</p>
<ul>
<li><p>Revoke</p>
</li>
<li><p>Delete</p>
</li>
<li><p>Disable</p>
</li>
<li><p>Rotate</p>
</li>
<li><p>Regenerate</p>
</li>
<li><p>Create new key</p>
</li>
</ul>
<p>If the provider supports key rotation, create a replacement credential before disabling the old one if possible. This can reduce application downtime while you update your configuration.</p>
<p>The important thing is that the original credential must no longer be usable.</p>
<p><strong>Do not reuse the leaked key.</strong> Don't rename it. Don't encode it. Don't move it to another file and assume it is safe. Don't assume nobody saw it.</p>
<p>Treat it as compromised.</p>
<h2 id="heading-step-2-investigate-suspicious-activity">Step 2: Investigate Suspicious Activity</h2>
<p>After disabling the credential, check the provider's usage dashboard and logs.</p>
<p>Look for things such as:</p>
<ul>
<li><p>Sudden spikes in requests</p>
</li>
<li><p>Requests from unfamiliar locations</p>
</li>
<li><p>Unexpected database queries</p>
</li>
<li><p>New cloud resources</p>
</li>
<li><p>Changes to permissions</p>
</li>
<li><p>Unexpected downloads</p>
</li>
<li><p>Unusual payment activity</p>
</li>
<li><p>New deployments</p>
</li>
<li><p>Requests at times when your application was inactive</p>
</li>
</ul>
<p>If the credential had broad permissions, assume that anything within its permission scope <strong>MAY have been accessed or modified</strong>.</p>
<p>For example, if a cloud credential could create virtual machines, check whether unexpected machines were created.</p>
<p>If a credential could access a database, review:</p>
<ul>
<li><p>Authentication logs</p>
</li>
<li><p>Read operations</p>
</li>
<li><p>Write operations</p>
</li>
<li><p>Deleted records</p>
</li>
<li><p>Exported data</p>
</li>
<li><p>Newly created accounts</p>
</li>
<li><p>Permission changes</p>
</li>
</ul>
<p>Also check your billing information if the credential could generate usage-based charges.</p>
<p>Write down what you discover. A simple timeline can help:</p>
<pre><code class="language-text">10:15 - API key committed
10:23 - Repository pushed publicly
10:41 - Unusual usage detected
10:45 - Key revoked
11:00 - Logs reviewed
11:30 - Replacement key deployed
12:00 - Git history cleaned
</code></pre>
<p>This can be especially useful if you need to report the incident to a team or service provider.</p>
<h2 id="heading-step-3-remove-the-secret-from-your-current-code">Step 3: Remove the Secret From Your Current Code</h2>
<p>Once the original credential has been disabled, remove it from your working files.</p>
<p>This is unsafe:</p>
<pre><code class="language-javascript">const apiKey = "your-real-api-key";
</code></pre>
<p>Instead, load the credential from the environment:</p>
<pre><code class="language-javascript">const apiKey = process.env.API_KEY;

if (!apiKey) {
  throw new Error("API_KEY is not configured");
}
</code></pre>
<p>In Python:</p>
<pre><code class="language-python">import os

api_key = os.environ.get("API_KEY")

if not api_key:
    raise RuntimeError("API_KEY is not configured")
</code></pre>
<p>The important idea is simple:</p>
<pre><code class="language-text">Source code → environment variable → secret value
</code></pre>
<p>instead of:</p>
<pre><code class="language-text">Source code → hardcoded secret
</code></pre>
<p>Environment variables aren't the only way to manage secrets, but they are a common and practical solution for local development and many deployment environments.</p>
<h2 id="heading-step-4-use-a-env-file-for-local-development">Step 4: Use a <code>.env</code> File for Local Development</h2>
<p>For local development, you can store environment variables in a <code>.env</code> file.</p>
<p>For example:</p>
<pre><code class="language-env">API_KEY=your-local-development-key
DATABASE_URL=your-local-database-url
</code></pre>
<p>A Node.js project can load these values with a package such as <code>dotenv</code>.</p>
<p>Install it with:</p>
<pre><code class="language-bash">npm install dotenv
</code></pre>
<p>Then:</p>
<pre><code class="language-javascript">import "dotenv/config";

const apiKey = process.env.API_KEY;
</code></pre>
<p>The important part is that the <code>.env</code> file normally <strong>should not be committed to Git</strong>.</p>
<p>Add it to <code>.gitignore</code>:</p>
<pre><code class="language-gitignore"># Environment files
.env
.env.*
!.env.example

# Credential files
*.pem
*.key
credentials.json
service-account.json

# Local development files
.DS_Store
</code></pre>
<p>But there's an important detail here: the <code>.gitignore</code> <strong>does NOT remove files that Git is already tracking.</strong></p>
<p>If <code>.env</code> has already been committed, adding it to <code>.gitignore</code> won't erase it from Git.</p>
<p>You can stop tracking the file while keeping it on your computer:</p>
<pre><code class="language-bash">git rm --cached .env
</code></pre>
<p>Then commit the <code>.gitignore</code> change:</p>
<pre><code class="language-bash">git add .gitignore
git commit -m "Ignore local environment files"
</code></pre>
<p>But remember: this only removes the file from future commits. It does <strong>not</strong> remove the secret from previous commits.</p>
<p>That's where Git history comes in.</p>
<h2 id="heading-step-5-create-a-safe-envexample">Step 5: Create a Safe <code>.env.example</code></h2>
<p>Other developers still need to know which environment variables the application requires.</p>
<p>Instead of committing <code>.env</code>, create <code>.env.example</code>:</p>
<pre><code class="language-env">API_KEY=
DATABASE_URL=
PORT=3000
LOG_LEVEL=info
</code></pre>
<p>This file contains variable names rather than real credentials, so it can be committed to the repository.</p>
<p>You can also provide comments:</p>
<pre><code class="language-env"># Required API credential
API_KEY=

# PostgreSQL connection string
DATABASE_URL=

# Optional application port
PORT=3000
</code></pre>
<p>A new developer can then copy the file:</p>
<pre><code class="language-bash">cp .env.example .env
</code></pre>
<p>and provide their own values.</p>
<p>Use clearly fake placeholders in examples:</p>
<pre><code class="language-env">API_KEY=replace-me-with-your-own-key
</code></pre>
<p>Avoid putting realistic-looking production credentials into <code>.env.example</code>.</p>
<h2 id="heading-step-6-determine-whether-the-secret-is-still-in-git-history">Step 6: Determine Whether the Secret Is Still in Git History</h2>
<p>This is one of the most important parts of fixing a leaked credential.</p>
<p>Suppose your Git history looks like this:</p>
<pre><code class="language-text">Commit A: Add API key to config.js
Commit B: Update API integration
Commit C: Delete API key
</code></pre>
<p>Even though Commit C no longer contains the key, Commit A still does.</p>
<p>Git remembers previous versions of your files.</p>
<p>You can inspect the history of a file with:</p>
<pre><code class="language-bash">git log --all -- config.js
</code></pre>
<p>To display a file from an older commit:</p>
<pre><code class="language-bash">git show COMMIT_ID:config.js
</code></pre>
<p>You can also search Git history for a known leaked value:</p>
<pre><code class="language-bash">git log --all -S"your-leaked-key" --oneline
</code></pre>
<p>If you know the secret was committed, you should assume that it exists somewhere in the repository's history until you've verified otherwise.</p>
<h2 id="heading-when-do-you-need-to-rewrite-git-history">When Do You Need to Rewrite Git History?</h2>
<p>Not every accidental secret requires a history rewrite. Consider these situations:</p>
<h3 id="heading-the-secret-was-never-committed">The Secret Was Never Committed</h3>
<p>If the secret exists only in your working directory and was never committed, you generally don't need to rewrite history.</p>
<p>Remove it, add the appropriate file to <code>.gitignore</code>, and continue.</p>
<h3 id="heading-the-secret-was-committed-locally-but-never-pushed">The Secret Was Committed Locally But Never Pushed</h3>
<p>If the secret exists in local commits but hasn't been shared with a remote repository, you may be able to clean up those commits before pushing.</p>
<h3 id="heading-the-secret-was-pushed-to-a-remote-repository">The Secret Was Pushed to a Remote Repository</h3>
<p>Treat the credential as compromised. Revoke or rotate it immediately.</p>
<p>Then determine whether removing the secret from the repository's history is appropriate.</p>
<h3 id="heading-the-repository-was-public">The Repository Was Public</h3>
<p>Assume that someone or something may already have copied the secret.</p>
<p>This is why <strong>revocation comes before Git cleanup</strong>.</p>
<h3 id="heading-the-secret-was-in-a-private-repository">The Secret Was in a Private Repository</h3>
<p>A private repository is safer than a public repository, but it isn't a secret vault.</p>
<p>Credentials can still escape through:</p>
<ul>
<li><p>Compromised accounts</p>
</li>
<li><p>Contractors</p>
</li>
<li><p>Integrations</p>
</li>
<li><p>CI logs</p>
</li>
<li><p>Forks</p>
</li>
<li><p>Backups</p>
</li>
<li><p>Screenshots</p>
</li>
<li><p>Copied code</p>
</li>
<li><p>Pull requests</p>
</li>
</ul>
<p>So the safest rule remains:</p>
<blockquote>
<p><strong>Never intentionally commit credentials to Git, even in a private repository.</strong></p>
</blockquote>
<h2 id="heading-step-7-remove-the-secret-from-git-history">Step 7: Remove the Secret From Git History</h2>
<p>If the credential was committed, you may need to remove it from the repository's history.</p>
<p>Before rewriting history, create a backup:</p>
<pre><code class="language-bash">git clone --mirror https://github.com/your-username/your-repository.git repository-backup.git
</code></pre>
<p>A mirror clone includes branches and tags, which makes it useful for recovery if something goes wrong.</p>
<h3 id="heading-option-1-remove-an-entire-file">Option 1: Remove an Entire File</h3>
<p>If the secret was stored in a file such as <code>.env</code>, you can remove that file from the entire history:</p>
<pre><code class="language-bash">git filter-repo --path .env --invert-paths
</code></pre>
<p>For a file inside a directory:</p>
<pre><code class="language-bash">git filter-repo --path config/production.json --invert-paths
</code></pre>
<p>This removes the file from the repository's rewritten history.</p>
<h3 id="heading-option-2-replace-a-secret-inside-a-file">Option 2: Replace a Secret Inside a File</h3>
<p>Sometimes you need to keep the file but remove the secret from previous versions.</p>
<p>Create a temporary replacements file:</p>
<p>Then run:</p>
<pre><code class="language-bash">git filter-repo --replace-text replacements.txt
</code></pre>
<p>You can replace the value with a placeholder:</p>
<pre><code class="language-text">your-leaked-key==&gt;YOUR_API_KEY_HERE
</code></pre>
<p>Be extremely careful with <code>replacements.txt</code>. It contains the original secret, so <strong>do not commit it.</strong></p>
<p>Delete it after the cleanup:</p>
<pre><code class="language-bash">rm replacements.txt
</code></pre>
<p>On Windows PowerShell:</p>
<pre><code class="language-powershell">Remove-Item replacements.txt
</code></pre>
<p>For multiple secrets:</p>
<pre><code class="language-text">old-api-key==&gt;REMOVED_API_KEY
old-database-password==&gt;REMOVED_DATABASE_PASSWORD
old-token==&gt;REMOVED_TOKEN
</code></pre>
<p>Then:</p>
<pre><code class="language-bash">git filter-repo --replace-text replacements.txt
</code></pre>
<p>Test the cleanup on your backup clone first.</p>
<h2 id="heading-step-8-verify-that-the-secret-is-gone">Step 8: Verify That the Secret Is Gone</h2>
<p>Never assume the cleanup worked just because the command completed successfully.</p>
<p>Search for the known leaked value again:</p>
<pre><code class="language-bash">git log --all -S"your-leaked-key" --oneline
</code></pre>
<p>You can also inspect relevant files and commits:</p>
<pre><code class="language-bash">git log --all -- config.js
</code></pre>
<p>and:</p>
<pre><code class="language-bash">git show COMMIT_ID:config.js
</code></pre>
<p>If your repository uses branches and tags, make sure you aren't checking only the branch you currently have checked out.</p>
<p>You should also inspect other locations where the secret may have appeared, including pull requests, CI/CD logs, build artifacts, release files, Docker images, package releases, documentation, issue comments, and screenshots</p>
<p>Remember:</p>
<blockquote>
<p><strong>Rewriting your repository doesn't erase copies that already exist somewhere else.</strong></p>
</blockquote>
<p>That's another reason why the original credential must be revoked.</p>
<h2 id="heading-step-9-push-the-cleaned-history-carefully">Step 9: Push the Cleaned History Carefully</h2>
<p>Once you've verified the cleanup, you may need to push the rewritten history:</p>
<pre><code class="language-bash">git push --force --all origin
git push --force --tags origin
</code></pre>
<h3 id="heading-important-warning">Important Warning</h3>
<p><strong>Force-pushing rewritten history is disruptive.</strong> It changes commit hashes and can affect collaborators who have existing clones of the repository.</p>
<p>Before doing this on a shared project:</p>
<ol>
<li><p>Tell your collaborators.</p>
</li>
<li><p>Make sure everyone understands that history is being rewritten.</p>
</li>
<li><p>Coordinate the cleanup.</p>
</li>
<li><p>Follow your organization's incident-response process if one exists.</p>
</li>
</ol>
<p>After the rewrite, collaborators may need to reclone the repository:</p>
<pre><code class="language-bash">git clone https://github.com/your-username/your-repository.git
</code></pre>
<p>They shouldn't blindly merge their old repository history back into the cleaned repository.</p>
<h2 id="heading-step-10-replace-the-credential-everywhere">Step 10: Replace the Credential Everywhere</h2>
<p>Now create or use the replacement credential. Update every environment where the application runs. Common locations include:</p>
<ul>
<li><p>Local development</p>
</li>
<li><p>Testing</p>
</li>
<li><p>Staging</p>
</li>
<li><p>Production</p>
</li>
<li><p>Docker containers</p>
</li>
<li><p>Kubernetes secrets</p>
</li>
<li><p>CI/CD systems</p>
</li>
<li><p>Hosting platforms</p>
</li>
<li><p>Scheduled jobs</p>
</li>
<li><p>Serverless functions</p>
</li>
</ul>
<p>A common mistake is updating production but forgetting the deployment pipeline.</p>
<p>For example, your local application may work because <code>.env</code> contains the new key, while your CI/CD system still contains the old one.</p>
<p>Make a checklist:</p>
<pre><code class="language-text">1. Local development
2. Automated tests
3. Staging
4. Production
5. CI/CD variables
6. Docker configuration
7. Cloud deployment settings
8. Scheduled scripts
9. Serverless functions
</code></pre>
<p>After updating the credential, test the application in each important environment.</p>
<h2 id="heading-step-11-restrict-the-replacement-key">Step 11: Restrict the Replacement Key</h2>
<p>Replacing a leaked credential is only part of the solution.</p>
<p>The new credential should have <strong>only the permissions it actually needs</strong>.</p>
<p>Useful restrictions can include:</p>
<ul>
<li><p>Read-only permissions</p>
</li>
<li><p>Specific API scopes</p>
</li>
<li><p>Allowed IP addresses</p>
</li>
<li><p>Allowed domains</p>
</li>
<li><p>Environment-specific access</p>
</li>
<li><p>Request quotas</p>
</li>
<li><p>Rate limits</p>
</li>
<li><p>Expiration dates</p>
</li>
</ul>
<p>For example, a weather application may only need permission to read weather data.</p>
<p>It shouldn't have permission to manage users, modify billing, or delete unrelated resources.</p>
<p>This is the <strong>principle of least privilege</strong>:</p>
<blockquote>
<p><strong>Give each credential the smallest amount of access necessary to perform its job.</strong></p>
</blockquote>
<p>It's also a good idea to use different credentials for different environments:</p>
<pre><code class="language-text">local-development-key
testing-key
staging-key
production-key
</code></pre>
<p>That way, a development credential leak doesn't automatically expose production resources.</p>
<h2 id="heading-what-about-frontend-applications">What About Frontend Applications?</h2>
<p>This is where API-key security gets confusing.</p>
<p>Frontend code runs on the user's device.</p>
<p>That means users can inspect it.</p>
<p>For example:</p>
<pre><code class="language-javascript">const apiKey = "browser-key";
</code></pre>
<p>A user can inspect the JavaScript bundle, browser developer tools, or network requests and potentially see the value.</p>
<p>Some services intentionally provide browser API keys that are designed to be publicly visible.</p>
<p>Those keys should still be restricted by things such as:</p>
<ul>
<li><p>Allowed domains</p>
</li>
<li><p>Website origins</p>
</li>
<li><p>API operations</p>
</li>
<li><p>Usage quotas</p>
</li>
<li><p>Referrer restrictions</p>
</li>
<li><p>Time limits</p>
</li>
</ul>
<p>But a truly private credential should <strong>never be placed in browser code</strong>.</p>
<p>Instead of:</p>
<pre><code class="language-javascript">fetch("https://private-api.example.com/data", {
  headers: {
    Authorization: "Bearer private-secret-token"
  }
});
</code></pre>
<p>have the browser call your own backend:</p>
<pre><code class="language-javascript">fetch("/api/data");
</code></pre>
<p>Then the backend communicates with the private service:</p>
<pre><code class="language-javascript">const response = await fetch(
  "https://private-api.example.com/data",
  {
    headers: {
      Authorization: `Bearer ${process.env.PRIVATE_API_TOKEN}`
    }
  }
);
</code></pre>
<p>The backend can then return only the information the browser is allowed to receive.</p>
<p>The important distinction is:</p>
<pre><code class="language-text">Public/browser credential
        ↓
Can be visible, but should be restricted

Private credential
        ↓
Must remain on a trusted backend or secret-management system
</code></pre>
<h2 id="heading-environment-variables-vs-secret-managers">Environment Variables vs Secret Managers</h2>
<p>Environment variables are useful, but they're not a universal secret-management solution.</p>
<p>For a small application or local development environment, something like:</p>
<pre><code class="language-env">API_KEY=your-secret
</code></pre>
<p>may be perfectly reasonable.</p>
<p>For larger production systems, you may want a dedicated <strong>secret manager</strong>.</p>
<p>A secret-management system can provide features such as:</p>
<ul>
<li><p>Centralized credential storage</p>
</li>
<li><p>Access controls</p>
</li>
<li><p>Auditing</p>
</li>
<li><p>Credential rotation</p>
</li>
<li><p>Versioning</p>
</li>
<li><p>Separation between environments</p>
</li>
<li><p>Integration with deployment systems</p>
</li>
</ul>
<p>The important idea is that your source code shouldn't be responsible for storing production secrets.</p>
<p>Instead:</p>
<pre><code class="language-text">Application
    ↓
Secret management system
    ↓
Credential
</code></pre>
<p>rather than:</p>
<pre><code class="language-text">Application
    ↓
Hardcoded production credential
</code></pre>
<p>Which solution you use depends on the size and requirements of your project.</p>
<h2 id="heading-add-secret-scanning-to-your-workflow">Add Secret Scanning to Your Workflow</h2>
<p>Humans are excellent programmers and occasionally terrible search engines.</p>
<p>Automated secret scanning can catch credentials before they make it into a repository.</p>
<p>Popular tools include:</p>
<ul>
<li><p>Gitleaks</p>
</li>
<li><p>TruffleHog</p>
</li>
<li><p>detect-secrets</p>
</li>
<li><p>Pre-commit hooks</p>
</li>
<li><p>Git hosting secret scanning</p>
</li>
<li><p>CI security scanners</p>
</li>
</ul>
<p>For example, you can run Gitleaks locally:</p>
<pre><code class="language-bash">gitleaks detect --source . --verbose
</code></pre>
<p>You can also integrate secret scanning into CI.</p>
<p>A basic GitHub Actions workflow might look like this:</p>
<pre><code class="language-yaml">name: Secret Scan

on:
  push:
  pull_request:

jobs:
  scan:
    runs-on: ubuntu-latest

    steps:
      - name: Check out repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Scan for secrets
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
</code></pre>
<p>Review the documentation for your chosen tool and pin versions according to your project's security practices.</p>
<p>Secret scanners can produce false positives, so you may need to configure exceptions for safe test values.</p>
<p>Be careful with allowlists, though. An overly broad exception can hide a real credential.</p>
<h2 id="heading-use-git-hooks-as-an-extra-safety-net">Use Git Hooks as an Extra Safety Net</h2>
<p>You can also scan files before they're committed.</p>
<p>For example, a simple pre-commit script could search for suspicious words:</p>
<pre><code class="language-bash">#!/usr/bin/env bash

if grep -RniE "api[_-]?key|password|secret|token|private[_-]?key" . \
  --exclude-dir=.git \
  --exclude=".env.example"; then

  echo "Possible secret detected. Commit cancelled."
  exit 1
fi
</code></pre>
<p>This isn't a complete security scanner, but it can catch obvious mistakes.</p>
<p>For stronger protection, use a dedicated secret-scanning tool through a pre-commit framework.</p>
<p>The goal isn't to make committing miserable. The goal is to make accidentally publishing a credential harder.</p>
<h2 id="heading-review-your-staged-diff-before-committing">Review Your Staged Diff Before Committing</h2>
<p>One of the simplest security habits you can develop is checking what you're actually about to commit.</p>
<p>First:</p>
<pre><code class="language-bash">git status
</code></pre>
<p>Then stage only the files you intend to commit:</p>
<pre><code class="language-bash">git add src/api.js README.md
</code></pre>
<p>Now inspect the staged changes:</p>
<pre><code class="language-bash">git diff --cached
</code></pre>
<p>Look for:</p>
<ul>
<li><p>API keys</p>
</li>
<li><p>Passwords</p>
</li>
<li><p>Tokens</p>
</li>
<li><p>Private URLs</p>
</li>
<li><p>Internal hostnames</p>
</li>
<li><p>Customer data</p>
</li>
<li><p>Debug output</p>
</li>
<li><p>Personal information</p>
</li>
<li><p>Private certificates</p>
</li>
</ul>
<p>Only commit after the staged diff looks correct:</p>
<pre><code class="language-bash">git commit -m "Load API key from environment"
</code></pre>
<p>Be cautious with:</p>
<pre><code class="language-bash">git add .
</code></pre>
<p>It can stage files you never intended to publish, including <code>.env</code> files, database exports, generated files, or local configuration.</p>
<h2 id="heading-common-mistakes-developers-make">Common Mistakes Developers Make</h2>
<h3 id="heading-mistake-1-i-deleted-it-so-its-fine">Mistake 1: "I Deleted It, So It's Fine"</h3>
<p>Deleting a secret from the current version of a file doesn't delete it from Git history.</p>
<p><strong>Correct response:</strong> Revoke the credential and clean the repository history when appropriate.</p>
<h3 id="heading-mistake-2-the-repository-is-private">Mistake 2: "The Repository Is Private"</h3>
<p>Private repositories aren't vaults.</p>
<p>Credentials can still escape through compromised accounts, integrations, CI logs, forks, backups, or copied code.</p>
<p><strong>Correct response:</strong> Don't commit secrets even to private repositories.</p>
<h3 id="heading-mistake-3-ill-just-encode-it">Mistake 3: "I'll Just Encode It"</h3>
<p>These don't make a credential secret:</p>
<pre><code class="language-javascript">const key = atob("c29tZS1rZXk=");
</code></pre>
<p>or:</p>
<pre><code class="language-javascript">const key = "some-" + "secret-" + "value";
</code></pre>
<p>Encoding, splitting, renaming, or hiding a credential doesn't protect it.</p>
<p>If your application can reconstruct the credential, someone analyzing the application may be able to do the same.</p>
<h3 id="heading-mistake-4-logging-the-secret">Mistake 4: Logging the Secret</h3>
<p>Don't do this:</p>
<pre><code class="language-javascript">console.log(process.env.API_KEY);
</code></pre>
<p>Logs can be stored by your terminal, CI system, hosting provider, monitoring platform, or cloud service.</p>
<p>Instead:</p>
<pre><code class="language-javascript">console.log(
  "API key configured:",
  Boolean(process.env.API_KEY)
);
</code></pre>
<p>If you absolutely need to inspect a value during debugging, avoid printing the full credential.</p>
<p>For example:</p>
<pre><code class="language-javascript">function maskSecret(value) {
  if (!value) return "not configured";
  if (value.length &lt;= 8) return "********";

  return `${value.slice(0, 4)}...${value.slice(-4)}`;
}

console.log(maskSecret(process.env.API_KEY));
</code></pre>
<p>Even masked credentials should be handled carefully.</p>
<h3 id="heading-mistake-5-using-the-same-credential-everywhere">Mistake 5: Using the Same Credential Everywhere</h3>
<p>If local development, testing, staging, and production all use the same credential, one leak can affect everything.</p>
<p><strong>Correct response:</strong> Use separate credentials with separate permissions.</p>
<h3 id="heading-mistake-6-cleaning-only-the-current-branch">Mistake 6: Cleaning Only the Current Branch</h3>
<p>A secret can remain in:</p>
<ul>
<li><p>Old branches</p>
</li>
<li><p>Tags</p>
</li>
<li><p>Pull requests</p>
</li>
<li><p>Other references</p>
</li>
</ul>
<p><strong>Correct response:</strong> Consider the entire repository when investigating and cleaning a leaked credential.</p>
<h3 id="heading-mistake-7-forgetting-build-artifacts">Mistake 7: Forgetting Build Artifacts</h3>
<p>A secret might also appear in:</p>
<ul>
<li><p>Compiled JavaScript bundles</p>
</li>
<li><p>Docker images</p>
</li>
<li><p>Downloadable releases</p>
</li>
<li><p>Published packages</p>
</li>
<li><p>Generated documentation</p>
</li>
</ul>
<p><strong>Correct response:</strong> Revoke the credential and identify affected artifacts that may need to be removed or replaced.</p>
<h2 id="heading-a-complete-api-key-incident-checklist">A Complete API-Key Incident Checklist</h2>
<p>If you discover that you've exposed an API key, use this checklist:</p>
<pre><code class="language-text">1. Revoke or rotate the leaked key
2. Create a replacement credential
3. Restrict the replacement credential
4. Review provider logs
5. Review billing and usage
6. Check for unauthorized resources
7. Remove the key from current files
8. Add secret files to .gitignore
9. Create or update .env.example
10. Search Git history
11. Check branches and tags
12. Remove the secret from Git history if necessary
13. Verify the old secret is gone
14. Force-push cleaned history if appropriate
15. Check pull requests and forks
16. Check CI and deployment logs
17. Update local configuration
18. Update staging configuration
19. Update production configuration
20. Update CI/CD secrets
21. Run a secret scanner
22. Document the incident
23. Add preventive security checks
</code></pre>
<p>The exact steps will depend on your provider and project, but the order matters: <strong>Invalidate first. Clean up second.</strong></p>
<h2 id="heading-a-secure-project-structure">A Secure Project Structure</h2>
<p>A simple Node.js project might look like this:</p>
<pre><code class="language-text">my-project/
├── src/
│   └── api.js
├── .env
├── .env.example
├── .gitignore
├── package.json
└── README.md
</code></pre>
<p>The local <code>.env</code> file contains the actual development value:</p>
<pre><code class="language-env">API_KEY=your-local-key
</code></pre>
<p>The <code>.env.example</code> file contains no real credential:</p>
<pre><code class="language-env">API_KEY=replace-me-with-your-own-key
</code></pre>
<p>The application reads the environment variable:</p>
<pre><code class="language-javascript">import "dotenv/config";

const apiKey = process.env.API_KEY;

if (!apiKey) {
  throw new Error("Missing API_KEY environment variable");
}

export async function getData() {
  const response = await fetch(
    "https://api.example.com/data",
    {
      headers: {
        Authorization: `Bearer ${apiKey}`
      }
    }
  );

  if (!response.ok) {
    throw new Error(
      `API request failed: ${response.status}`
    );
  }

  return response.json();
}
</code></pre>
<p>And <code>.gitignore</code> keeps the local environment file out of future commits:</p>
<pre><code class="language-gitignore">.env
.env.*
!.env.example

node_modules/
</code></pre>
<p>Finally, your README can explain the setup without exposing credentials:</p>
<p>Step 1: Copy the example environment file on your bash <code>cp .env.example .env</code></p>
<p>Step 2: Add your own API key to <code>.env</code>.</p>
<p>And last but not least, start your application!</p>
<pre><code class="language-bash">npm start
</code></pre>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Leaking an API key doesn't mean you're a terrible developer. It just means your development workflow needs better guardrails.</p>
<p>The important thing is knowing how to respond quickly and how to prevent the same mistake from happening again.</p>
<p>Remember the emergency formula:</p>
<pre><code class="language-text">Invalidate → Investigate → Remove → Replace → Prevent
</code></pre>
<p>The important thing is knowing how to respond quickly and how to prevent the same mistake from happening again.</p>
<ul>
<li><p>Invalidate the leaked credential so it can no longer be used.</p>
</li>
<li><p>Investigate your logs, usage, and billing to determine whether it was abused.</p>
</li>
<li><p>Remove the secret from your current code and, when necessary, from Git history.</p>
</li>
<li><p>Replace it with a new credential that has only the permissions it needs.</p>
</li>
<li><p>Prevent future leaks with environment variables, secret managers, secret scanning, and careful Git practices.</p>
</li>
</ul>
<p>Git is excellent at remembering your project's history. That's useful when you accidentally delete an important function. But it's much less useful when that history contains a password.</p>
<p>So keep your code public when appropriate. And <strong>keep your secrets somewhere else.</strong></p>
<p>Happy coding!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Neural Networks Explained: What They Are and How to Build One in Python  ]]>
                </title>
                <description>
                    <![CDATA[ Have you ever wondered how a computer can recognize a handwritten number, predict whether an email is spam, recommend a video, or understand a sentence? A lot of modern AI systems rely on something ca ]]>
                </description>
                <link>https://www.freecodecamp.org/news/neural-networks-explained-simply-in-python/</link>
                <guid isPermaLink="false">6a88c8b8c9a055790ae586e7</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Artificial Intelligence ]]>
                    </category>
                
                    <category>
                        <![CDATA[ DeepLearning ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Software Engineering ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Eva J Patel ]]>
                </dc:creator>
                <pubDate>Fri, 21 Aug 2026 21:52:56 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/e140594f-daab-4c39-8b59-91bc794d6430.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Have you ever wondered how a computer can recognize a handwritten number, predict whether an email is spam, recommend a video, or understand a sentence?</p>
<p>A lot of modern AI systems rely on something called a <strong>neural network</strong>.</p>
<p>Now, the name can make them sound much more complicated than they really are. You might imagine that you need advanced calculus, a huge computer, and thousands of lines of code to build one.</p>
<p>You don't.</p>
<p>At its most basic level, a neural network is a mathematical model that takes some numbers as input, performs calculations on those numbers, makes a prediction, checks how far that prediction was from the correct answer, and then adjusts itself so it can do a little better next time.</p>
<p>In this tutorial, we're going to build one ourselves using Python and NumPy.</p>
<h3 id="heading-heres-what-well-cover">Here's What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-1-what-is-a-neural-network">1. What Is a Neural Network?</a></p>
</li>
<li><p><a href="#heading-2-why-are-they-called-neural-networks">2. Why Are They Called Neural Networks?</a></p>
</li>
<li><p><a href="#heading-3-the-three-main-parts-of-a-neural-network">3. The Three Main Parts of a Neural Network</a></p>
</li>
<li><p><a href="#heading-4-what-is-a-neuron">4. What Is a Neuron?</a></p>
</li>
<li><p><a href="#heading-5-what-is-a-weight">5. What Is a Weight?</a></p>
</li>
<li><p><a href="#heading-6-what-is-a-bias">6. What Is a Bias?</a></p>
</li>
<li><p><a href="#heading-7-why-do-we-need-activation-functions">7. Why Do We Need Activation Functions?</a></p>
</li>
<li><p><a href="#heading-8-building-our-first-neuron-in-python">8. Building Our First Neuron in Python</a></p>
</li>
<li><p><a href="#heading-9-from-one-neuron-to-a-layer">9. From One Neuron to a Layer</a></p>
</li>
<li><p><a href="#heading-10-how-does-a-neural-network-actually-learn">10. How Does a Neural Network Actually Learn?</a></p>
</li>
<li><p><a href="#heading-11-predictions-and-loss">11. Predictions and Loss</a></p>
</li>
<li><p><a href="#heading-12-what-are-gradients">12. What Are Gradients?</a></p>
</li>
<li><p><a href="#heading-13-what-is-gradient-descent">13. What Is Gradient Descent?</a></p>
</li>
<li><p><a href="#heading-14-what-is-backpropagation">14. What Is Backpropagation?</a></p>
</li>
<li><p><a href="#heading-15-the-complete-learning-cycle">15. The Complete Learning Cycle</a></p>
</li>
<li><p><a href="#heading-16-lets-build-a-neural-network-from-scratch">16. Let's Build a Neural Network From Scratch</a></p>
</li>
<li><p><a href="#heading-17-understanding-the-network-architecture">17. Understanding the Network Architecture</a></p>
</li>
<li><p><a href="#heading-18-setting-up-the-data">18. Setting Up the Data</a></p>
</li>
<li><p><a href="#heading-19-creating-the-weights-and-biases">19. Creating the Weights and Biases</a></p>
</li>
<li><p><a href="#heading-20-the-sigmoid-function">20. The Sigmoid Function</a></p>
</li>
<li><p><a href="#heading-21-forward-propagation">21. Forward Propagation</a></p>
</li>
<li><p><a href="#heading-22-calculating-the-loss">22. Calculating the Loss</a></p>
</li>
<li><p><a href="#heading-23-backpropagation-in-code">23. Backpropagation in Code</a></p>
</li>
<li><p><a href="#heading-24-updating-the-weights">24. Updating the Weights</a></p>
</li>
<li><p><a href="#heading-25-the-complete-numpy-neural-network">25. The Complete NumPy Neural Network</a></p>
</li>
<li><p><a href="#heading-26-testing-the-network">26. Testing the Network</a></p>
</li>
<li><p><a href="#heading-27-why-did-we-need-a-hidden-layer">27. Why Did We Need a Hidden Layer?</a></p>
</li>
<li><p><a href="#heading-28-what-happens-in-a-larger-neural-network">28. What Happens in a Larger Neural Network?</a></p>
</li>
<li><p><a href="#heading-29-do-you-have-to-build-neural-networks-from-scratch">29. Do You Have to Build Neural Networks From Scratch?</a></p>
</li>
<li><p><a href="#heading-30-building-the-same-network-with-pytorch">30. Building the Same Network With PyTorch</a></p>
</li>
<li><p><a href="#heading-31-training-the-network-with-pytorch">31. Training the Network With PyTorch</a></p>
</li>
<li><p><a href="#heading-32-numpy-vs-pytorch">32. NumPy vs. PyTorch</a></p>
</li>
<li><p><a href="#heading-33-what-is-deep-learning">33. What Is Deep Learning?</a></p>
</li>
<li><p><a href="#heading-34-where-are-neural-networks-used">34. Where Are Neural Networks Used?</a></p>
</li>
<li><p><a href="#heading-35-the-whole-process-in-one-picture">35. The Whole Process in One Picture</a></p>
</li>
<li><p><a href="#heading-36-the-most-important-ideas-to-remember">36. The Most Important Ideas to Remember</a></p>
</li>
<li><p><a href="#heading-37-what-should-you-learn-next">37. What Should You Learn Next?</a></p>
</li>
<li><p><a href="#heading-final-takeaway">Final Takeaway</a></p>
</li>
</ul>
<p>We'll start with a single artificial neuron, then gradually put together a complete neural network. By the end, you'll understand what weights and biases are, what activation functions do, how a network learns from its mistakes, what backpropagation and gradient descent actually mean, and how all of those pieces fit together.</p>
<p>You don't need to know advanced machine learning to follow along. Some basic Python and algebra will help, but I'll explain the important math as we go.</p>
<h2 id="heading-1-what-is-a-neural-network">1. What Is a Neural Network?</h2>
<p>Let's start with a simple example.</p>
<p>Imagine that we want a computer to predict whether a student will pass an exam.</p>
<p>We could give the computer information such as:</p>
<ul>
<li><p>How many hours the student studied</p>
</li>
<li><p>How many practice questions they completed</p>
</li>
<li><p>Their previous test score</p>
</li>
</ul>
<p>For example:</p>
<p><code>Study Hours = 5 Practice Questions = 80 Previous Score = 82</code></p>
<p>We also know whether the student actually passed:</p>
<p><code>Passed = 1</code></p>
<p>After seeing many examples like this, we want the computer to learn a pattern.</p>
<p>Maybe students who study more tend to perform better. Maybe previous test scores are useful. Maybe practice questions are helpful, too.</p>
<p>Instead of writing all of those rules ourselves, we can give the examples to a neural network and let it learn the relationships.</p>
<p>The basic idea looks like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a581501af6af179dc1987d5/26be21fa-5503-402c-ac6c-7f77c5689e1e.png" alt="Visual idea about how a neural network works" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The prediction could be something like: <code>0.92</code></p>
<p>If we're predicting the probability of passing, we could interpret that as approximately a 92% predicted chance of passing.</p>
<p>The important thing is that we didn't tell the network that...</p>
<blockquote>
<p>"Study hours are important, and previous scores are slightly more important."</p>
</blockquote>
<p>Instead, the network learns numbers called <strong>weights</strong> that determine how strongly different inputs affect its predictions.</p>
<h2 id="heading-2-why-are-they-called-neural-networks">2. Why Are They Called Neural Networks?</h2>
<p>The name comes from biological brains.</p>
<p>Your brain contains neurons that receive signals, process information, and pass signals to other neurons.</p>
<p>Artificial neural networks are <strong>not artificial brains</strong>. They don't work exactly like biological neurons. But the general idea of connecting many simple processing units inspired the name.</p>
<p>A very simplified artificial neuron looks like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a581501af6af179dc1987d5/bd68424f-e1be-4dea-8f6a-7fc1ed5abb15.png" alt="Input and Output through a neural network" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The neuron receives numbers, performs some mathematical operations, and produces another number.</p>
<p>A neural network is made by connecting many of these artificial neurons together.</p>
<h2 id="heading-3-the-three-main-parts-of-a-neural-network">3. The Three Main Parts of a Neural Network</h2>
<p>A simple neural network can be divided into three types of layers:</p>
<ol>
<li><p>Input Layer</p>
</li>
<li><p>Hidden Layer(s)</p>
</li>
<li><p>Output Layer</p>
</li>
</ol>
<p>Let's look at each one.</p>
<h3 id="heading-the-input-layer">The Input Layer</h3>
<p>The input layer contains the information we give the network.</p>
<p>For our student example, we could have three inputs:</p>
<pre><code class="language-text">Input 1 = Study Hours
Input 2 = Practice Questions
Input 3 = Previous Score
</code></pre>
<p>So one student's input might look like:</p>
<pre><code class="language-text">[5, 80, 82]
</code></pre>
<p>The network doesn't necessarily understand that these numbers mean "study hours" or "test score." To the mathematical part of the network, they're simply numbers.</p>
<p>That's an important idea to remember:</p>
<blockquote>
<p>Neural networks work with numbers.</p>
</blockquote>
<p>Images, text, audio, and other information must eventually be represented as numbers before a neural network can process them.</p>
<h3 id="heading-hidden-layers">Hidden Layers</h3>
<p>After the input layer come the hidden layers.</p>
<p>A network might look like:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a581501af6af179dc1987d5/6f177121-ce84-4a9d-a2b2-0ba98ad0e7d5.png" alt="Image showing how data moves from the Input Layer to the Hidden Layer and then to the Output layer" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The hidden layer contains neurons that perform calculations on the inputs.</p>
<p>A network can have one hidden layer or many hidden layers.</p>
<p>When a network has many layers, we often call it a <strong>deep neural network</strong>.</p>
<h3 id="heading-the-output-layer">The Output Layer</h3>
<p>The output layer produces the final result.</p>
<p>For a simple yes/no problem, we might represent the answers as:</p>
<pre><code class="language-text">0 = No
1 = Yes
</code></pre>
<p>For example:</p>
<pre><code class="language-text">0.12 → probably No
0.91 → probably Yes
</code></pre>
<p>For a problem with multiple categories, the output could contain several numbers:</p>
<pre><code class="language-text">Cat  = 0.05
Dog  = 0.90
Bird = 0.05
</code></pre>
<p>The largest value is associated with "Dog," so the model would predict Dog.</p>
<h2 id="heading-4-what-is-a-neuron">4. What Is a Neuron?</h2>
<p>Now let's zoom in on one neuron.</p>
<p>Suppose our neuron receives three inputs:</p>
<pre><code class="language-text">x₁
x₂
x₃
</code></pre>
<p>Each input has a corresponding <strong>weight</strong>:</p>
<pre><code class="language-text">w₁
w₂
w₃
</code></pre>
<p>The neuron multiplies each input by its weight and adds the results together.</p>
<p>It also adds something called a <strong>bias</strong>.</p>
<p>The equation is:</p>
<pre><code class="language-text">z = x₁w₁ + x₂w₂ + x₃w₃ + b
</code></pre>
<p>Don't worry if that equation looks intimidating.</p>
<p>It's basically just:</p>
<pre><code class="language-text">input × weight
+
input × weight
+
input × weight
+
bias
</code></pre>
<p>Let's use actual numbers.</p>
<p>Suppose:</p>
<pre><code class="language-text">x₁ = 2
x₂ = 3
x₃ = 4

w₁ = 0.5
w₂ = 0.2
w₃ = 0.8

b = 1
</code></pre>
<p>Then:</p>
<pre><code class="language-text">z = (2 × 0.5) + (3 × 0.2) + (4 × 0.8) + 1
</code></pre>
<p>Calculate each part:</p>
<pre><code class="language-text">2 × 0.5 = 1.0
3 × 0.2 = 0.6
4 × 0.8 = 3.2
</code></pre>
<p>Now add them:</p>
<pre><code class="language-text">z = 1.0 + 0.6 + 3.2 + 1
z = 5.8
</code></pre>
<p>The neuron has produced <code>5.8</code>.</p>
<p>But we're not finished yet.</p>
<h2 id="heading-5-what-is-a-weight">5. What Is a Weight?</h2>
<p>A weight controls how strongly an input affects a neuron.</p>
<p>Imagine we have:</p>
<pre><code class="language-text">x = 5
</code></pre>
<p>If the weight is:</p>
<pre><code class="language-text">w = 2
</code></pre>
<p>then:</p>
<pre><code class="language-text">x × w = 5 × 2
      = 10
</code></pre>
<p>But if the weight is:</p>
<pre><code class="language-text">w = 0.1
</code></pre>
<p>then:</p>
<pre><code class="language-text">x × w = 5 × 0.1
      = 0.5
</code></pre>
<p>The same input produced a very different result because the weight changed.</p>
<p>You can think of a weight as a volume knob.</p>
<p>A large positive weight makes an input have a stronger positive influence. A weight close to zero makes the input have little influence. A negative weight can push the result in the opposite direction.</p>
<p>The network learns these weights during training.</p>
<h2 id="heading-6-what-is-a-bias">6. What Is a Bias?</h2>
<p>The bias is another number added to the neuron's calculation.</p>
<p>Without the bias, we would have:</p>
<pre><code class="language-text">z = x₁w₁ + x₂w₂ + x₃w₃
</code></pre>
<p>With the bias:</p>
<pre><code class="language-text">z = x₁w₁ + x₂w₂ + x₃w₃ + b
</code></pre>
<p>Why add another number? Because it gives the neuron more flexibility.</p>
<p>Think of it like adjusting the starting point of the neuron's calculation.</p>
<p>The network learns the bias during training just like it learns the weights.</p>
<p>So when you see:</p>
<pre><code class="language-text">weights + bias
</code></pre>
<p>you're looking at some of the parameters the neural network can change while it learns.</p>
<h2 id="heading-7-why-do-we-need-activation-functions">7. Why Do We Need Activation Functions?</h2>
<p>At this point, our neuron can calculate a weighted sum:</p>
<pre><code class="language-text">z = x₁w₁ + x₂w₂ + ... + b
</code></pre>
<p>But neural networks need to learn more complicated relationships than simple weighted sums.</p>
<p>That's where <strong>activation functions</strong> come in. An activation function takes the neuron's calculated value and transforms it.</p>
<p>One common activation function is <strong>ReLU</strong>. ReLU stands for <strong>Rectified Linear Unit</strong>.</p>
<p>Its equation is:</p>
<pre><code class="language-text">ReLU(x) = max(0, x)
</code></pre>
<p>In simple terms:</p>
<ul>
<li><p>If the number is positive, keep it.</p>
</li>
<li><p>If the number is negative, turn it into zero.</p>
</li>
</ul>
<p>For example:</p>
<pre><code class="language-text">ReLU(-5) = 0
ReLU(-2) = 0
ReLU(0)  = 0
ReLU(3)  = 3
ReLU(10) = 10
</code></pre>
<p>In Python:</p>
<pre><code class="language-python">def relu(x):
    return max(0, x)
</code></pre>
<p>With NumPy arrays, we can use:</p>
<pre><code class="language-python">def relu(x):
    return np.maximum(0, x)
</code></pre>
<p>Activation functions are important because they allow neural networks with multiple layers to learn more complicated patterns.</p>
<h2 id="heading-8-building-our-first-neuron-in-python">8. Building Our First Neuron in Python</h2>
<p>Let's turn the math into Python.</p>
<p>First, import NumPy:</p>
<pre><code class="language-python">import numpy as np
</code></pre>
<p>NumPy gives us tools for working with numbers, arrays, vectors, and matrices.</p>
<p>Now let's create our inputs:</p>
<pre><code class="language-python">x = np.array([2, 3, 4])
</code></pre>
<p>This creates an array containing three values:</p>
<pre><code class="language-text">[2, 3, 4]
</code></pre>
<p>Now create the weights:</p>
<pre><code class="language-python">weights = np.array([0.5, 0.2, 0.8])
</code></pre>
<p>We have one weight for each input:</p>
<pre><code class="language-text">x₁ = 2    w₁ = 0.5
x₂ = 3    w₂ = 0.2
x₃ = 4    w₃ = 0.8
</code></pre>
<p>Next, create the bias:</p>
<pre><code class="language-python">bias = 1
</code></pre>
<p>Now we calculate the weighted sum:</p>
<pre><code class="language-python">z = np.dot(x, weights) + bias
</code></pre>
<p><code>np.dot()</code> performs the multiplication-and-addition operation we described earlier.</p>
<p>In this case:</p>
<pre><code class="language-text">np.dot(x, weights)
</code></pre>
<p>is equivalent to:</p>
<pre><code class="language-text">(2 × 0.5) + (3 × 0.2) + (4 × 0.8)
</code></pre>
<p>which equals:</p>
<pre><code class="language-text">4.8
</code></pre>
<p>Then we add the bias:</p>
<pre><code class="language-text">4.8 + 1 = 5.8
</code></pre>
<p>Now apply ReLU:</p>
<pre><code class="language-python">output = np.maximum(0, z)
</code></pre>
<p>Since <code>z</code> is <code>5.8</code>, ReLU leaves it unchanged:</p>
<pre><code class="language-text">output = 5.8
</code></pre>
<p>Finally:</p>
<pre><code class="language-python">print(output)
</code></pre>
<p>prints:</p>
<pre><code class="language-text">5.8
</code></pre>
<p>So our entire neuron is:</p>
<pre><code class="language-python">import numpy as np

x = np.array([2, 3, 4])
weights = np.array([0.5, 0.2, 0.8])
bias = 1

z = np.dot(x, weights) + bias
output = np.maximum(0, z)

print(output)
</code></pre>
<p>We have just created a tiny artificial neuron.</p>
<h2 id="heading-9-from-one-neuron-to-a-layer">9. From One Neuron to a Layer</h2>
<p>One neuron isn't enough for most interesting problems.</p>
<p>Instead, we can connect several neurons together.</p>
<p>For example:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a581501af6af179dc1987d5/62aeadb8-9f22-4b3a-b7a4-a450df33a55b.png" alt="Input, Output and Hidden Layer depicted with neurons" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Those neurons together form a <strong>layer</strong>.</p>
<p>A small neural network might look like:</p>
<pre><code class="language-text">Input Layer
     ↓
Hidden Layer
     ↓
Output Layer
</code></pre>
<p>Every neuron in one layer can send its output to neurons in the next layer.</p>
<p>This is where neural networks start becoming much more powerful.</p>
<h2 id="heading-10-how-does-a-neural-network-actually-learn">10. How Does a Neural Network Actually Learn?</h2>
<p>So far, we've manually chosen the weights:</p>
<pre><code class="language-text">0.5
0.2
0.8
</code></pre>
<p>But a real neural network doesn't start out knowing the correct weights.</p>
<p>Instead, it starts with weights that are usually initialized to small random values.</p>
<p>Then it goes through a cycle:</p>
<pre><code class="language-text">Make a prediction
       ↓
Compare prediction with correct answer
       ↓
Measure the error
       ↓
Figure out how to change the weights
       ↓
Update the weights
       ↓
Try again
</code></pre>
<p>This process happens over and over, and the network gradually adjusts its parameters to make better predictions on the training data.</p>
<p>Let's break each part down.</p>
<h2 id="heading-11-predictions-and-loss">11. Predictions and Loss</h2>
<p>Suppose the correct answer is:</p>
<pre><code class="language-text">1
</code></pre>
<p>but our network predicts:</p>
<pre><code class="language-text">0.3
</code></pre>
<p>The prediction isn't very close to the target.</p>
<p>We need a way to measure how wrong it is. That's what a <strong>loss function</strong> does.</p>
<p>A loss function takes the prediction and the correct answer and produces a number representing the model's error.</p>
<p>For a simple example, we could use squared error:</p>
<pre><code class="language-text">Loss = (prediction - actual)²
</code></pre>
<p>Using our numbers:</p>
<pre><code class="language-text">Loss = (0.3 - 1)²
</code></pre>
<p>First:</p>
<pre><code class="language-text">0.3 - 1 = -0.7
</code></pre>
<p>Then square it:</p>
<pre><code class="language-text">(-0.7)² = 0.49
</code></pre>
<p>So:</p>
<pre><code class="language-text">Loss = 0.49
</code></pre>
<p>Generally, a smaller loss means the prediction is closer to the target.</p>
<p>In real neural networks, different problems use different loss functions. For binary classification, binary cross-entropy is commonly used.</p>
<h2 id="heading-12-what-are-gradients">12. What Are Gradients?</h2>
<p>Now we have a problem.</p>
<p>We know that the prediction was wrong, but how should we change the weights?</p>
<p>This is where <strong>gradients</strong> become useful. A gradient tells us how changing a parameter would affect the loss.</p>
<p>You can think of it like standing on a hill. Imagine that your goal is to reach the lowest point. If you know which direction slopes upward, you can move in the opposite direction to go downhill.</p>
<p>Training a neural network works with a similar idea. We want to reduce the loss. The gradients give us information about which direction the parameters should move.</p>
<h2 id="heading-13-what-is-gradient-descent">13. What Is Gradient Descent?</h2>
<p><strong>Gradient descent</strong> is the process of using gradients to adjust the network's parameters.</p>
<p>A simplified update rule is:</p>
<pre><code class="language-text">new weight = old weight - learning rate × gradient
</code></pre>
<p>In Python:</p>
<pre><code class="language-python">weight = weight - learning_rate * gradient
</code></pre>
<p>The <strong>learning rate</strong> controls how large the update is.</p>
<p>For example:</p>
<pre><code class="language-python">learning_rate = 0.01
</code></pre>
<p>If the learning rate is too large, the network can make huge changes and potentially jump around instead of settling on a good solution.</p>
<p>If it's too small, learning can take a very long time.</p>
<p>So training involves finding parameter updates that move the model toward lower loss without making the process unstable.</p>
<h2 id="heading-14-what-is-backpropagation">14. What Is Backpropagation?</h2>
<p>There's still one important question:</p>
<p>If a neural network has thousands or millions of weights, how does it figure out which weights contributed to the error?</p>
<p>That's where <strong>backpropagation</strong> comes in. Backpropagation calculates gradients for the parameters by working backward through the network.</p>
<p>Imagine a network like this:</p>
<pre><code class="language-text">Input
  ↓
Hidden Layer
  ↓
Output
  ↓
Loss
</code></pre>
<p>During the forward pass, information moves:</p>
<pre><code class="language-text">Input → Hidden Layer → Output
</code></pre>
<p>During backpropagation, gradient information moves backward:</p>
<pre><code class="language-text">Loss → Output → Hidden Layer → Input
</code></pre>
<p>The network uses these gradients to determine how its weights and biases should change.</p>
<p>You don't normally calculate all of these derivatives by hand when building real neural networks. Libraries such as PyTorch can calculate them automatically.</p>
<p>But understanding the basic idea is important:</p>
<blockquote>
<p>Backpropagation calculates how the parameters contributed to the error, and gradient descent uses that information to update them.</p>
</blockquote>
<h2 id="heading-15-the-complete-learning-cycle">15. The Complete Learning Cycle</h2>
<p>Now we can put everything together.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a581501af6af179dc1987d5/cdd30bf8-25b9-4a3e-b62e-ee764642c05a.png" alt="Learning cycle of neural network: input, prediction, loss, gradients, update (and then back to prediction...)" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>More specifically:</p>
<pre><code class="language-text">Give the network data
          ↓
Calculate a prediction
          ↓
Compare it with the correct answer
          ↓
Calculate the loss
          ↓
Calculate gradients
          ↓
Update weights and biases
          ↓
Repeat
</code></pre>
<p>One complete pass through the training data is often called an <strong>epoch</strong>.</p>
<p>For example:</p>
<pre><code class="language-text">Epoch 1 → Loss: 0.82
Epoch 2 → Loss: 0.61
Epoch 3 → Loss: 0.43
Epoch 4 → Loss: 0.29
Epoch 5 → Loss: 0.18
</code></pre>
<p>These numbers are just an example, but ideally the loss decreases as training progresses.</p>
<h2 id="heading-16-lets-build-a-neural-network-from-scratch">16. Let's Build a Neural Network From Scratch</h2>
<p>Congrats! You now understand the basics of neural networks. Now it's time to put these ideas together.</p>
<p>We're going to build a small neural network using only:</p>
<pre><code class="language-text">Python + NumPy
</code></pre>
<p>Our network will learn a classic machine learning problem called <strong>XOR</strong>.</p>
<p>XOR is a logical operation with two inputs.</p>
<p>Its rules are:</p>
<pre><code class="language-text">0 XOR 0 → 0
0 XOR 1 → 1
1 XOR 0 → 1
1 XOR 1 → 0
</code></pre>
<p>In other words, the output is <code>1</code> when exactly one of the inputs is <code>1</code>.</p>
<p>Our training data will therefore be:</p>
<pre><code class="language-python">X = np.array([
    [0, 0],
    [0, 1],
    [1, 0],
    [1, 1]
])
</code></pre>
<p>And the correct answers are:</p>
<pre><code class="language-python">y = np.array([
    [0],
    [1],
    [1],
    [0]
])
</code></pre>
<p>We want our neural network to learn this pattern.</p>
<h2 id="heading-17-understanding-the-network-architecture">17. Understanding the Network Architecture</h2>
<p>Our network will contain:</p>
<pre><code class="language-text">2 input neurons
       ↓
4 hidden neurons
       ↓
1 output neuron
</code></pre>
<p>The two inputs represent the two numbers in each XOR example.</p>
<p>The four hidden neurons give the network enough flexibility to learn the XOR relationship.</p>
<p>The output neuron produces a number between <code>0</code> and <code>1</code>.</p>
<h2 id="heading-18-setting-up-the-data">18. Setting Up the Data</h2>
<p>Let's start our Python program.</p>
<pre><code class="language-python">import numpy as np
</code></pre>
<p>This imports NumPy. We'll use NumPy for arrays, matrix multiplication, and mathematical operations.</p>
<p>Next:</p>
<pre><code class="language-python">X = np.array([
    [0, 0],
    [0, 1],
    [1, 0],
    [1, 1]
])
</code></pre>
<p><code>X</code> contains our four training examples.</p>
<p>Each row is one example:</p>
<pre><code class="language-text">[0, 0]
[0, 1]
[1, 0]
[1, 1]
</code></pre>
<p>Now create the correct answers:</p>
<pre><code class="language-python">y = np.array([
    [0],
    [1],
    [1],
    [0]
])
</code></pre>
<p>The first row of <code>X</code> corresponds to the first row of <code>y</code>.</p>
<p>So:</p>
<pre><code class="language-text">[0, 0] → 0
[0, 1] → 1
[1, 0] → 1
[1, 1] → 0
</code></pre>
<h2 id="heading-19-creating-the-weights-and-biases">19. Creating the Weights and Biases</h2>
<p>Now we need the parameters of our network.</p>
<p>First:</p>
<pre><code class="language-python">np.random.seed(42)
</code></pre>
<p>This makes our random numbers reproducible.</p>
<p>Without this line, the network would receive different random starting weights each time we ran the program.</p>
<p>Now create the first layer's weights:</p>
<pre><code class="language-python">W1 = np.random.randn(2, 4)
</code></pre>
<p>Why <code>(2, 4)</code>?</p>
<p>Because:</p>
<ul>
<li><p>We have 2 input values.</p>
</li>
<li><p>We have 4 neurons in the hidden layer.</p>
</li>
</ul>
<p>So <code>W1</code> needs a weight connecting each input to each hidden neuron.</p>
<p>There are:</p>
<pre><code class="language-text">2 × 4 = 8
</code></pre>
<p>weights.</p>
<p>Next:</p>
<pre><code class="language-python">b1 = np.zeros((1, 4))
</code></pre>
<p>This creates four biases, one for each hidden neuron.</p>
<p>Now the second layer:</p>
<pre><code class="language-python">W2 = np.random.randn(4, 1)
</code></pre>
<p>There are four hidden neurons and one output neuron, so we need:</p>
<pre><code class="language-text">4 × 1 = 4
</code></pre>
<p>weights.</p>
<p>Finally:</p>
<pre><code class="language-python">b2 = np.zeros((1, 1))
</code></pre>
<p>This gives the output neuron one bias.</p>
<p>Our network parameters are therefore:</p>
<pre><code class="language-text">W1 → input-to-hidden weights
b1 → hidden-layer biases

W2 → hidden-to-output weights
b2 → output-layer bias
</code></pre>
<h2 id="heading-20-the-sigmoid-function">20. The Sigmoid Function</h2>
<p>Our output represents a probability, so we'd like it to be between <code>0</code> and <code>1</code>.</p>
<p>We can use the <strong>sigmoid function</strong>.</p>
<p>Its equation is:</p>
<pre><code class="language-text">sigmoid(x) = 1 / (1 + e⁻ˣ)
</code></pre>
<p>In Python:</p>
<pre><code class="language-python">def sigmoid(x):
    return 1 / (1 + np.exp(-x))
</code></pre>
<p>Let's see what it does:</p>
<pre><code class="language-text">sigmoid(-5) ≈ 0.007
sigmoid(0)  = 0.5
sigmoid(5)  ≈ 0.993
</code></pre>
<p>No matter how large or small the input is, the result stays between <code>0</code> and <code>1</code>.</p>
<p>That's useful when our output represents a probability.</p>
<h2 id="heading-21-forward-propagation">21. Forward Propagation</h2>
<p>Now we can send the data through the network. This is called <strong>forward propagation</strong>.</p>
<p>First, calculate the hidden layer:</p>
<pre><code class="language-python">z1 = X @ W1 + b1
</code></pre>
<p>There's a new symbol here:</p>
<pre><code class="language-text">@
</code></pre>
<p>In Python, <code>@</code> performs matrix multiplication.</p>
<p>You can think of this operation as performing many weighted sums at once.</p>
<p>Instead of manually calculating every neuron:</p>
<pre><code class="language-text">input × weight + input × weight + bias
</code></pre>
<p>NumPy can calculate all of them together.</p>
<p>The result is stored in <code>z1</code>.</p>
<p>Next:</p>
<pre><code class="language-python">a1 = np.tanh(z1)
</code></pre>
<p>Here we're using the <strong>tanh activation function</strong> for the hidden layer.</p>
<p>Tanh converts its input into values between <code>-1</code> and <code>1</code>.</p>
<p>Why use tanh here?</p>
<p>Because XOR isn't something a single simple linear calculation can solve. The nonlinear activation gives the hidden layer the flexibility it needs to learn the pattern.</p>
<p>Now calculate the output layer:</p>
<pre><code class="language-python">z2 = a1 @ W2 + b2
</code></pre>
<p>This takes the hidden layer's outputs and combines them using the second set of weights.</p>
<p>Finally:</p>
<pre><code class="language-python">a2 = sigmoid(z2)
</code></pre>
<p>Now <code>a2</code> contains our predictions.</p>
<p>For example, before training, the network might produce something like:</p>
<pre><code class="language-text">0.52
0.61
0.48
0.55
</code></pre>
<p>Those predictions aren't useful yet, but that's expected. The network hasn't learned anything yet.</p>
<h2 id="heading-22-calculating-the-loss">22. Calculating the Loss</h2>
<p>Now we need to measure how good those predictions are.</p>
<p>For binary classification, we'll use <strong>binary cross-entropy</strong>, which is a loss function used in machine learning for binary classification. It measures the performance of a model whose output is a probability value between 0 and 1.</p>
<p>The formula is:</p>
<pre><code class="language-text">Loss = -mean(
    y × log(prediction)
    +
    (1 - y) × log(1 - prediction)
)
</code></pre>
<p>That looks much more complicated than the squared-error example from earlier, but we don't need to memorize the formula.</p>
<p>In Python:</p>
<pre><code class="language-python">loss = -np.mean(
    y * np.log(a2 + 1e-8) +
    (1 - y) * np.log(1 - a2 + 1e-8)
)
</code></pre>
<p>The <code>1e-8</code> is a very small number.</p>
<p>It prevents problems if <code>a2</code> gets extremely close to <code>0</code> or <code>1</code>, because taking the logarithm of exactly zero isn't valid.</p>
<p>At the beginning of training, the loss will probably be relatively high. But as the network learns, we'd like it to decrease.</p>
<h2 id="heading-23-backpropagation-in-code">23. Backpropagation in Code</h2>
<p>Now comes the most mathematical part of our program.</p>
<p>We need to calculate the gradients.</p>
<p>Start with:</p>
<pre><code class="language-python">dz2 = a2 - y
</code></pre>
<p>This gives us the gradient of the loss with respect to the output layer's pre-activation value for the sigmoid + binary cross-entropy combination.</p>
<p>Next:</p>
<pre><code class="language-python">dW2 = (a1.T @ dz2) / len(X)
</code></pre>
<p>This calculates the gradient for <code>W2</code>.</p>
<p>The <code>.T</code> means transpose.</p>
<p>Our hidden-layer output has four neurons, while <code>dz2</code> represents the output layer's error. Matrix multiplication combines them to determine how each hidden-to-output weight contributed to the loss.</p>
<p>We divide by:</p>
<pre><code class="language-python">len(X)
</code></pre>
<p>because we have four training examples and we're calculating the average gradient.</p>
<p>Now calculate the output bias gradient:</p>
<pre><code class="language-python">db2 = np.mean(dz2, axis=0, keepdims=True)
</code></pre>
<p>This calculates the average gradient for the output bias.</p>
<p>Next:</p>
<pre><code class="language-python">da1 = dz2 @ W2.T
</code></pre>
<p>This sends the gradient information backward from the output layer toward the hidden layer.</p>
<p>Now we need to account for the derivative of the tanh activation function.</p>
<p>The derivative of tanh can be written as:</p>
<pre><code class="language-text">1 - tanh(x)²
</code></pre>
<p>Since we already have the hidden layer's activated values in <code>a1</code>, we can write:</p>
<pre><code class="language-python">dz1 = da1 * (1 - a1**2)
</code></pre>
<p>This tells us how the hidden layer's pre-activation values affected the loss.</p>
<p>Now calculate the gradients for the first layer's weights:</p>
<pre><code class="language-python">dW1 = (X.T @ dz1) / len(X)
</code></pre>
<p>And the hidden-layer biases:</p>
<pre><code class="language-python">db1 = np.mean(dz1, axis=0, keepdims=True)
</code></pre>
<p>At this point, we have gradients for all of our trainable parameters.</p>
<h2 id="heading-24-updating-the-weights">24. Updating the Weights</h2>
<p>Now we use gradient descent.</p>
<p>First:</p>
<pre><code class="language-python">W2 -= learning_rate * dW2
</code></pre>
<p>This updates the second layer's weights.</p>
<p>The <code>-=</code> means:</p>
<pre><code class="language-python">W2 = W2 - learning_rate * dW2
</code></pre>
<p>Then:</p>
<pre><code class="language-python">b2 -= learning_rate * db2
</code></pre>
<p>updates the output bias.</p>
<p>And:</p>
<pre><code class="language-python">W1 -= learning_rate * dW1
</code></pre>
<p>updates the first layer's weights.</p>
<p>Finally:</p>
<pre><code class="language-python">b1 -= learning_rate * db1
</code></pre>
<p>updates the hidden-layer biases.</p>
<p>These updates are what actually allow the network to learn.</p>
<h2 id="heading-25-the-complete-numpy-neural-network">25. The Complete NumPy Neural Network</h2>
<p>Now let's put everything together.</p>
<pre><code class="language-python">import numpy as np

# 1. Training data

X = np.array([
    [0, 0],
    [0, 1],
    [1, 0],
    [1, 1]
])

y = np.array([
    [0],
    [1],
    [1],
    [0]
])

# 2. Initialize parameters

np.random.seed(42)

W1 = np.random.randn(2, 4)
b1 = np.zeros((1, 4))

W2 = np.random.randn(4, 1)
b2 = np.zeros((1, 1))

learning_rate = 0.1

# 3. Activation functions

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

# 4. Training

for epoch in range(10000):

    # Forward propagation

    z1 = X @ W1 + b1
    a1 = np.tanh(z1)

    z2 = a1 @ W2 + b2
    a2 = sigmoid(z2)

    # Calculate loss

    loss = -np.mean(
        y * np.log(a2 + 1e-8) +
        (1 - y) * np.log(1 - a2 + 1e-8)
    )

    # Backpropagation

    dz2 = a2 - y

    dW2 = (a1.T @ dz2) / len(X)
    db2 = np.mean(dz2, axis=0, keepdims=True)

    da1 = dz2 @ W2.T

    dz1 = da1 * (1 - a1**2)

    dW1 = (X.T @ dz1) / len(X)
    db1 = np.mean(dz1, axis=0, keepdims=True)

    # Update parameters

    W2 -= learning_rate * dW2
    b2 -= learning_rate * db2

    W1 -= learning_rate * dW1
    b1 -= learning_rate * db1

    # Display progress

    if epoch % 1000 == 0:
        print(f"Epoch {epoch}, Loss: {loss:.4f}")
</code></pre>
<p>Let's go through the program from top to bottom.</p>
<h3 id="heading-line-by-line-explanation-of-the-full-code">Line-by-Line Explanation of the Full Code</h3>
<h4 id="heading-importing-numpy">Importing NumPy:</h4>
<pre><code class="language-python">import numpy as np
</code></pre>
<p>We import NumPy because our network will work with arrays and matrix operations.</p>
<h4 id="heading-creating-the-inputs">Creating the inputs</h4>
<pre><code class="language-python">X = np.array([
    [0, 0],
    [0, 1],
    [1, 0],
    [1, 1]
])
</code></pre>
<p>Each row is one XOR example.</p>
<p>There are four examples and two input values per example.</p>
<p>So the shape of <code>X</code> is:</p>
<pre><code class="language-text">4 × 2
</code></pre>
<h4 id="heading-creating-the-answers">Creating the answers</h4>
<pre><code class="language-python">y = np.array([
    [0],
    [1],
    [1],
    [0]
])
</code></pre>
<p>There are four correct answers, one for each row in <code>X</code>.</p>
<h4 id="heading-making-random-initialization-reproducible">Making random initialization reproducible</h4>
<pre><code class="language-python">np.random.seed(42)
</code></pre>
<p>This makes NumPy generate the same starting random values each time.</p>
<p>The number <code>42</code> isn't special. You could use another number.</p>
<h4 id="heading-creating-the-first-weight-matrix">Creating the first weight matrix</h4>
<pre><code class="language-python">W1 = np.random.randn(2, 4)
</code></pre>
<p>This creates a matrix containing random numbers.</p>
<p>Its shape is 2*4</p>
<p>There are two inputs and four hidden neurons.</p>
<h4 id="heading-creating-the-first-biases">Creating the first biases</h4>
<pre><code class="language-python">b1 = np.zeros((1, 4))
</code></pre>
<p>This creates four zeros:</p>
<pre><code class="language-text">[0, 0, 0, 0]
</code></pre>
<p>There is one bias for every hidden neuron.</p>
<h4 id="heading-creating-the-second-weight-matrix">Creating the second weight matrix</h4>
<pre><code class="language-python">W2 = np.random.randn(4, 1)
</code></pre>
<p>There are four hidden neurons and one output neuron.</p>
<p>Therefore:</p>
<pre><code class="language-text">4 × 1
</code></pre>
<p>weights are needed.</p>
<h4 id="heading-creating-the-output-bias">Creating the output bias</h4>
<pre><code class="language-python">b2 = np.zeros((1, 1))
</code></pre>
<p>The output layer has one neuron, so it needs one bias.</p>
<h4 id="heading-setting-the-learning-rate">Setting the learning rate</h4>
<pre><code class="language-python">learning_rate = 0.1
</code></pre>
<p>This controls how strongly the gradients affect each update.</p>
<h4 id="heading-creating-sigmoid">Creating sigmoid</h4>
<pre><code class="language-python">def sigmoid(x):
    return 1 / (1 + np.exp(-x))
</code></pre>
<p>This converts the output into a value between <code>0</code> and <code>1</code>.</p>
<h3 id="heading-starting-the-training-loop">Starting the Training Loop</h3>
<pre><code class="language-python">for epoch in range(10000):
</code></pre>
<p>This tells Python to repeat the training process 10,000 times.</p>
<p>Each repetition is an epoch, which is one complete pass of the entire training dataset through a neural network</p>
<h4 id="heading-calculating-the-hidden-layer">Calculating the hidden layer</h4>
<pre><code class="language-python">z1 = X @ W1 + b1
</code></pre>
<p>This performs the weighted-sum calculation for all four hidden neurons and all four training examples.</p>
<h4 id="heading-applying-tanh">Applying tanh</h4>
<pre><code class="language-python">a1 = np.tanh(z1)
</code></pre>
<p>This applies the nonlinear activation function to the hidden layer.</p>
<h4 id="heading-calculating-the-output-layer">Calculating the output layer</h4>
<pre><code class="language-python">z2 = a1 @ W2 + b2
</code></pre>
<p>This takes the hidden layer's values and calculates the output neuron's weighted sum.</p>
<h4 id="heading-applying-sigmoid">Applying sigmoid</h4>
<pre><code class="language-python">a2 = sigmoid(z2)
</code></pre>
<p>This turns the output into probabilities between <code>0</code> and <code>1</code>.</p>
<h4 id="heading-calculating-the-loss">Calculating the loss</h4>
<pre><code class="language-python">loss = -np.mean(
    y * np.log(a2 + 1e-8) +
    (1 - y) * np.log(1 - a2 + 1e-8)
)
</code></pre>
<p>This measures how different the predictions are from the correct answers.</p>
<p>A lower value generally means the predictions are better.</p>
<h4 id="heading-calculating-the-output-gradient">Calculating the output gradient</h4>
<pre><code class="language-python">dz2 = a2 - y
</code></pre>
<p>This calculates the gradient needed to update the output layer.</p>
<h4 id="heading-updating-the-second-layer-weight-gradients">Updating the second-layer weight gradients</h4>
<pre><code class="language-python">dW2 = (a1.T @ dz2) / len(X)
</code></pre>
<p>This determines how each weight connecting the hidden layer to the output layer contributed to the loss.</p>
<h4 id="heading-updating-the-output-bias-gradient">Updating the output bias gradient</h4>
<pre><code class="language-python">db2 = np.mean(dz2, axis=0, keepdims=True)
</code></pre>
<p>This calculates the average gradient for the output bias.</p>
<h4 id="heading-moving-backward-toward-the-hidden-layer">Moving backward toward the hidden layer</h4>
<pre><code class="language-python">da1 = dz2 @ W2.T
</code></pre>
<p>This passes the gradient information backward through the output layer.</p>
<h4 id="heading-applying-the-tanh-derivative">Applying the tanh derivative</h4>
<pre><code class="language-python">dz1 = da1 * (1 - a1**2)
</code></pre>
<p>This accounts for the effect of the tanh activation function.</p>
<h4 id="heading-calculating-the-first-layer-gradients">Calculating the first-layer gradients</h4>
<pre><code class="language-python">dW1 = (X.T @ dz1) / len(X)
</code></pre>
<p>This determines how the input-to-hidden weights contributed to the loss.</p>
<p>Then:</p>
<pre><code class="language-python">db1 = np.mean(dz1, axis=0, keepdims=True)
</code></pre>
<p>calculates the gradients for the hidden-layer biases.</p>
<h4 id="heading-updating-the-parameters">Updating the parameters</h4>
<pre><code class="language-python">W2 -= learning_rate * dW2
b2 -= learning_rate * db2

W1 -= learning_rate * dW1
b1 -= learning_rate * db1
</code></pre>
<p>These four lines are where the network changes what it has learned.</p>
<p>The gradients tell us which direction to move, while the learning rate determines how large the movement should be.</p>
<h4 id="heading-printing-the-loss">Printing the loss</h4>
<pre><code class="language-python">if epoch % 1000 == 0:
    print(f"Epoch {epoch}, Loss: {loss:.4f}")
</code></pre>
<p>The <code>%</code> operator gives us the remainder after division.</p>
<p>So:</p>
<pre><code class="language-python">epoch % 1000 == 0
</code></pre>
<p>is true every 1,000 epochs.</p>
<p>That means we don't print something 10,000 times. Instead, we get occasional updates such as:</p>
<pre><code class="language-text">Epoch 0, Loss: ...
Epoch 1000, Loss: ...
Epoch 2000, Loss: ...
...
</code></pre>
<p>If training is working well, the loss should generally decrease.</p>
<h2 id="heading-26-testing-the-network">26. Testing the Network</h2>
<p>After training, we can use the network to make predictions.</p>
<pre><code class="language-python">z1 = X @ W1 + b1
a1 = np.tanh(z1)

z2 = a1 @ W2 + b2
predictions = sigmoid(z2)

print(predictions)
</code></pre>
<p>The network should produce values close to:</p>
<pre><code class="language-text">[[0],
 [1],
 [1],
 [0]]
</code></pre>
<p>The actual values probably won't be exactly <code>0</code> and <code>1</code>.</p>
<p>You might get something more like:</p>
<pre><code class="language-text">[[0.01],
 [0.98],
 [0.99],
 [0.02]]
</code></pre>
<p>That's fine.</p>
<p>The network is producing probabilities.</p>
<p>We can convert those probabilities into classes using a threshold:</p>
<pre><code class="language-python">classes = (predictions &gt;= 0.5).astype(int)

print(classes)
</code></pre>
<p>The result should be:</p>
<pre><code class="language-text">[[0],
 [1],
 [1],
 [0]]
</code></pre>
<p>Our network has learned the XOR pattern.</p>
<h2 id="heading-27-why-did-we-need-a-hidden-layer">27. Why Did We Need a Hidden Layer?</h2>
<p>You might wonder why we couldn't just connect the two inputs directly to the output.</p>
<p>The reason is that XOR isn't something a single linear layer can represent.</p>
<p>The hidden layer gives the network additional transformations that allow it to learn the more complicated relationship.</p>
<p>This is one of the most important ideas behind neural networks: a network doesn't necessarily learn one giant rule. Instead, different layers can transform information step by step.</p>
<p>For an image recognition system, you can imagine a simplified process like:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a581501af6af179dc1987d5/1bf62ced-92e8-4ee2-ba7f-fba16fde006f.png" alt="Image recognition system visually depicted" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Real neural networks don't literally create neat layers called "edges," "shapes," and "objects." This is just an intuition for how increasingly complex representations can emerge through multiple layers.</p>
<h2 id="heading-28-what-happens-in-a-larger-neural-network">28. What Happens in a Larger Neural Network?</h2>
<p>The network we built is tiny. Modern neural networks can have millions, billions, or even more parameters.</p>
<p>A simplified network might look like:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a581501af6af179dc1987d5/9007ed47-0c1f-4b72-99e6-194010fdfc20.png" alt="Simplified neural network visually depicted" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Each connection can have its own weight.</p>
<p>The more neurons and connections a network has, the more parameters it may need to learn.</p>
<p>Large models therefore require significant amounts of computing power and memory.</p>
<p>But remember the basic process:</p>
<pre><code class="language-text">Input
 ↓
Calculations
 ↓
Prediction
 ↓
Loss
 ↓
Gradients
 ↓
Parameter Updates
</code></pre>
<p>The size of the network changes dramatically, but the basic training idea remains.</p>
<h2 id="heading-29-do-you-have-to-build-neural-networks-from-scratch">29. Do You Have to Build Neural Networks From Scratch?</h2>
<p>No. Building a neural network from scratch is useful for learning because it forces you to understand what's happening underneath the libraries.</p>
<p>But you normally wouldn't manually calculate every gradient when building a real machine learning application.</p>
<p>That's where machine learning frameworks come in. Some commonly used Python libraries include:</p>
<ul>
<li><p>NumPy</p>
</li>
<li><p>PyTorch</p>
</li>
<li><p>TensorFlow</p>
</li>
<li><p>Keras</p>
</li>
<li><p>scikit-learn</p>
</li>
</ul>
<p>For deep learning, <strong>PyTorch</strong> is one of the most commonly used frameworks. It can automatically calculate gradients and handle many of the mathematical operations involved in training.</p>
<h2 id="heading-30-building-the-same-network-with-pytorch">30. Building the Same Network With PyTorch</h2>
<p>Let's see how much shorter the network becomes with PyTorch.</p>
<p>First, install it:</p>
<pre><code class="language-bash">pip install torch
</code></pre>
<p>Then import it:</p>
<pre><code class="language-python">import torch
import torch.nn as nn
</code></pre>
<p>Now create the model:</p>
<pre><code class="language-python">model = nn.Sequential(
    nn.Linear(2, 4),
    nn.Tanh(),
    nn.Linear(4, 1),
    nn.Sigmoid()
)
</code></pre>
<p>Let's break that down.</p>
<pre><code class="language-python">nn.Linear(2, 4)
</code></pre>
<p>creates a layer that takes two inputs and produces four outputs.</p>
<p>That's our hidden layer.</p>
<p>Next:</p>
<pre><code class="language-python">nn.Tanh()
</code></pre>
<p>applies the tanh activation function.</p>
<p>Then:</p>
<pre><code class="language-python">nn.Linear(4, 1)
</code></pre>
<p>connects the four hidden neurons to one output neuron.</p>
<p>Finally:</p>
<pre><code class="language-python">nn.Sigmoid()
</code></pre>
<p>converts the output into a value between <code>0</code> and <code>1</code>.</p>
<p>So the architecture is:</p>
<pre><code class="language-text">2 inputs
   ↓
4 hidden neurons
   ↓
Tanh
   ↓
1 output neuron
   ↓
Sigmoid
</code></pre>
<p>Notice how much shorter this is than our NumPy implementation.</p>
<p>That's because PyTorch handles many of the calculations for us.</p>
<h2 id="heading-31-training-the-network-with-pytorch">31. Training the Network With PyTorch</h2>
<p>First, create the training data:</p>
<pre><code class="language-python">X = torch.tensor([
    [0., 0.],
    [0., 1.],
    [1., 0.],
    [1., 1.]
])

y = torch.tensor([
    [0.],
    [1.],
    [1.],
    [0.]
])
</code></pre>
<p>The decimal points are important because neural networks normally work with floating-point numbers.</p>
<p>Now create the model:</p>
<pre><code class="language-python">model = nn.Sequential(
    nn.Linear(2, 4),
    nn.Tanh(),
    nn.Linear(4, 1),
    nn.Sigmoid()
)
</code></pre>
<p>Next, choose our loss function:</p>
<pre><code class="language-python">loss_function = nn.BCELoss()
</code></pre>
<p><code>BCELoss</code> calculates binary cross-entropy loss.</p>
<p>Now create an optimizer:</p>
<pre><code class="language-python">optimizer = torch.optim.Adam(
    model.parameters(),
    lr=0.01
)
</code></pre>
<p>Adam is an optimization algorithm that updates the model's parameters during training.</p>
<p><code>model.parameters()</code> tells the optimizer which values it should update.</p>
<p><code>lr=0.01</code> sets the learning rate.</p>
<p>Now we can train:</p>
<pre><code class="language-python">for epoch in range(5000):

    predictions = model(X)

    loss = loss_function(predictions, y)

    optimizer.zero_grad()

    loss.backward()

    optimizer.step()

    if epoch % 500 == 0:
        print(
            f"Epoch {epoch}, Loss: {loss.item():.4f}"
        )
</code></pre>
<p>Let's look at the important parts.</p>
<p>First:</p>
<pre><code class="language-python">predictions = model(X)
</code></pre>
<p>This sends the training data through the network.</p>
<p>Then:</p>
<pre><code class="language-python">loss = loss_function(predictions, y)
</code></pre>
<p>compares the predictions with the correct answers.</p>
<p>Next:</p>
<pre><code class="language-python">optimizer.zero_grad()
</code></pre>
<p>clears gradients from the previous training step.</p>
<p>Then:</p>
<pre><code class="language-python">loss.backward()
</code></pre>
<p>calculates the gradients automatically using backpropagation.</p>
<p>Finally:</p>
<pre><code class="language-python">optimizer.step()
</code></pre>
<p>uses those gradients to update the model's parameters.</p>
<p>That's the same basic learning process we implemented manually with NumPy. The difference is that PyTorch takes care of many of the calculations.</p>
<h2 id="heading-32-numpy-vs-pytorch">32. NumPy vs. PyTorch</h2>
<p>So why did we build the network twice? Well, because the two versions teach different things.</p>
<p>With NumPy, we manually handled weights, biases, forward propagation,<br>loss, gradients, backpropagation, and parameter updates. That makes the mechanics easier to see.</p>
<p>With PyTorch, we can write the same general idea in much less code because the framework handles many of those calculations.</p>
<p>You can think of it like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a581501af6af179dc1987d5/6230c12c-34ef-4ab3-9f9f-f99eaebcf295.png" alt="Comparison between NumPy and PyTorch" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Learning how the NumPy version works makes the PyTorch version much less mysterious.</p>
<h2 id="heading-33-what-is-deep-learning">33. What Is Deep Learning?</h2>
<p>You may have heard the term <strong>deep learning</strong>. Deep learning is a part of machine learning that uses neural networks with multiple layers.</p>
<p>For example:</p>
<pre><code class="language-text">Input
  ↓
Layer 1
  ↓
Layer 2
  ↓
Layer 3
  ↓
Layer 4
  ↓
Output
</code></pre>
<p>The word "deep" refers to the depth of the network, or the number of layers involved.</p>
<p>There isn't a magical point where a neural network suddenly becomes intelligent. Adding layers simply gives the model more opportunities to transform the input into useful representations.</p>
<h2 id="heading-34-where-are-neural-networks-used">34. Where Are Neural Networks Used?</h2>
<p>Neural networks are used in many different areas. Here are a few examples...</p>
<h3 id="heading-computer-vision">Computer Vision</h3>
<p>Neural networks can process images.</p>
<p>For example:</p>
<pre><code class="language-text">Image
  ↓
Neural Network
  ↓
Prediction
</code></pre>
<p>They can be used for tasks such as image classification and object detection.</p>
<h3 id="heading-natural-language-processing">Natural Language Processing</h3>
<p>Neural networks can also process text.</p>
<p>For example:</p>
<pre><code class="language-text">Text
  ↓
Neural Network
  ↓
Prediction
</code></pre>
<p>Modern language models use neural networks to process and generate text.</p>
<h3 id="heading-speech-recognition">Speech Recognition</h3>
<p>Neural networks can process audio and help convert spoken language into text.</p>
<pre><code class="language-text">Audio
  ↓
Neural Network
  ↓
Words
</code></pre>
<h3 id="heading-recommendation-systems">Recommendation Systems</h3>
<p>Neural networks can learn patterns from user behavior and help predict which content or products might be useful to someone.</p>
<h3 id="heading-generative-ai">Generative AI</h3>
<p>Large neural networks can also be used to generate text, images, audio, code, video, and much more.</p>
<p>These systems are much more complicated than the small XOR network we built, but they still rely on the same general idea of learning parameters from data.</p>
<h2 id="heading-35-the-whole-process-in-one-picture">35. The Whole Process in One Picture</h2>
<p>At this point, we've covered a lot.</p>
<p>Here's the entire training process:</p>
<pre><code class="language-text">Data
   ↓
Neural Network
   ↓
Prediction
   ↓
Loss
  ↓
Backpropagation
  ↓
Update Parameters
  ↓
Repeat
</code></pre>
<p>Once training is finished, we use the learned parameters to make predictions on new data:</p>
<pre><code class="language-text">New Data
   ↓
Trained Neural Network
   ↓
Prediction
</code></pre>
<p>That's the basic idea behind neural network training.</p>
<h2 id="heading-36-the-most-important-ideas-to-remember">36. The Most Important Ideas to Remember</h2>
<p>If you don't remember every equation from this tutorial, that's okay.</p>
<p>Start with these concepts.</p>
<h3 id="heading-inputs">Inputs</h3>
<p>The numbers we give to the network.</p>
<pre><code class="language-text">x₁, x₂, x₃...
</code></pre>
<h3 id="heading-weights">Weights</h3>
<p>Numbers that determine how strongly inputs affect neurons.</p>
<pre><code class="language-text">w₁, w₂, w₃...
</code></pre>
<h3 id="heading-biases">Biases</h3>
<p>Additional values that give neurons more flexibility.</p>
<pre><code class="language-text">b
</code></pre>
<h3 id="heading-activation-functions">Activation Functions</h3>
<p>Functions that transform neuron outputs and allow networks to learn nonlinear patterns.</p>
<p>Examples include:</p>
<pre><code class="language-text">ReLU
Tanh
Sigmoid
</code></pre>
<h3 id="heading-forward-propagation">Forward Propagation</h3>
<p>Sending data from the input toward the output.</p>
<pre><code class="language-text">Input → Hidden Layers → Output
</code></pre>
<h3 id="heading-loss">Loss</h3>
<p>A measurement of how different the prediction is from the correct answer.</p>
<h3 id="heading-backpropagation">Backpropagation</h3>
<p>Calculating gradients by working backward through the network.</p>
<h3 id="heading-gradient-descent">Gradient Descent</h3>
<p>Using those gradients to update the network's parameters.</p>
<p>And the entire learning process can be summarized as:</p>
<pre><code class="language-text">Predict
   ↓
Measure Error
   ↓
Calculate Gradients
   ↓
Update Parameters
   ↓
Repeat
</code></pre>
<h2 id="heading-37-what-should-you-learn-next">37. What Should You Learn Next?</h2>
<p>If you want to continue learning neural networks with Python, you don't need to jump directly into complicated research papers.</p>
<p>A useful learning path is:</p>
<pre><code class="language-text">Python
  ↓
NumPy
  ↓
Basic Linear Algebra
  ↓
Probability &amp; Statistics
  ↓
Machine Learning Basics
  ↓
Neural Networks
  ↓
PyTorch
  ↓
Deep Learning
  ↓
Computer Vision / NLP / Generative AI
</code></pre>
<p>You can also learn by building small projects.</p>
<p>For example:</p>
<ol>
<li><p>XOR classifier</p>
</li>
<li><p>House price predictor</p>
</li>
<li><p>Handwritten digit classifier</p>
</li>
<li><p>Simple image classifier</p>
</li>
<li><p>Spam message classifier</p>
</li>
<li><p>Neural network that learns a mathematical function</p>
</li>
</ol>
<p>The projects don't need to be huge. A small project that you completely understand is often more useful than a large project where you copied code without understanding it.</p>
<h2 id="heading-final-takeaway">Final Takeaway</h2>
<p>Neural networks can look intimidating because the systems used in modern AI can contain enormous numbers of parameters.</p>
<p>But the basic idea is much smaller.</p>
<p>A neural network takes numbers as input, combines them using weights and biases, applies mathematical functions, produces a prediction, measures how wrong that prediction was, and then adjusts its parameters.</p>
<p>The cycle looks like this:</p>
<pre><code class="language-text">Input
  ↓
Weighted Calculations
  ↓
Activation Functions
  ↓
Prediction
  ↓
Loss
  ↓
Gradients
  ↓
Parameter Updates
  ↓
Repeat
</code></pre>
<p>That's the foundation.</p>
<p>The XOR network we built in this tutorial is tiny compared with the neural networks used in modern AI. But the ideas you just learned (parameters, layers, activation functions, forward propagation, loss, backpropagation, gradients, and optimization) are fundamental ideas that appear again and again in deep learning.</p>
<p>The next time you hear that an AI model has millions or billions of parameters, it might still sound overwhelming.</p>
<p>But underneath all that scale, the basic learning loop is still familiar:</p>
<ol>
<li><p>Make a prediction.</p>
</li>
<li><p>Measure the error.</p>
</li>
<li><p>Figure out how to improve.</p>
</li>
<li><p>Update the parameters.</p>
</li>
<li><p>Try again.</p>
</li>
</ol>
<p>And that's the core idea behind a neural network.</p>
<p>Happy coding and keep learning!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Basic Discord Storytelling, Chat, and Mental Wellness Bot with Python ]]>
                </title>
                <description>
                    <![CDATA[ Discord bots can look surprisingly complicated when you see them in action. A bot can respond to messages, tell stories, remember parts of conversations, and stay online around the clock. When I first ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-basic-discord-bot-with-python/</link>
                <guid isPermaLink="false">6a7f44fc58366ecdaf016624</guid>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ bot ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python 3 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Beginner Developers ]]>
                    </category>
                
                    <category>
                        <![CDATA[ techblog ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Eva J Patel ]]>
                </dc:creator>
                <pubDate>Fri, 14 Aug 2026 16:40:28 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/6a444c51-d332-4915-aa1f-326b57b17472.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Discord bots can look surprisingly complicated when you see them in action. A bot can respond to messages, tell stories, remember parts of conversations, and stay online around the clock.</p>
<p>When I first started looking into how they worked, I assumed there had to be a huge amount of complicated code behind all of it.</p>
<p>But the basic idea is actually pretty simple.</p>
<p>At its core, a Discord bot is just a Python program that connects to Discord, waits for something to happen, and then decides how to respond. Once you understand that basic idea, you can start adding features one at a time and turn a simple bot into something much more interesting.</p>
<p>In this tutorial, we'll start with a very small bot and gradually build it into something more capable. Along the way, you'll learn about Discord commands, events, asynchronous Python, user state, environment variables, and basic deployment.</p>
<p>One quick disclaimer before we start: the mental-wellness feature that we'll be integrating in this bot in this project is <strong>not therapy</strong>, and the bot is not a therapist or medical professional. It should only provide general supportive suggestions and encourage users to reach out to a trusted person when appropriate.</p>
<p>With that out of the way, let's get coding!</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-what-were-building">What We're Building</a></p>
</li>
<li><p><a href="#heading-what-you-need">What You Need</a></p>
</li>
<li><p><a href="#heading-create-the-discord-bot">Create the Discord Bot</a></p>
</li>
<li><p><a href="#heading-give-the-bot-permission-to-read-messages">Give the Bot Permission to Read Messages</a></p>
</li>
<li><p><a href="#heading-create-the-project">Create the Project</a></p>
</li>
<li><p><a href="#heading-create-a-virtual-environment">Create a Virtual Environment</a></p>
</li>
<li><p><a href="#heading-install-discordpy">Install discord.py</a></p>
</li>
<li><p><a href="#heading-create-your-first-bot">Create Your First Bot</a></p>
<ul>
<li><p><a href="#heading-importing-our-libraries">Importing Our Libraries</a></p>
</li>
<li><p><a href="#heading-loading-the-token">Loading the Token</a></p>
</li>
<li><p><a href="#heading-understanding-intents">Understanding Intents</a></p>
</li>
<li><p><a href="#heading-what-is-ctx">What Isctx?</a></p>
</li>
<li><p><a href="#heading-why-does-everything-say-async-and-await">Why Does Everything Sayasyncandawait?</a></p>
</li>
<li><p><a href="#heading-run-the-bot">Run the Bot</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-build-the-storytelling-system">Build the Storytelling System</a></p>
<ul>
<li><p><a href="#heading-lets-make-the-story-remember-the-user">Let's Make the Story Remember the User</a></p>
</li>
<li><p><a href="#heading-add-a-story-choice">Add a Story Choice</a></p>
</li>
<li><p><a href="#heading-add-a-casual-chat-command">Add a Casual Chat Command</a></p>
</li>
<li><p><a href="#heading-add-a-mental-wellness-support-feature">Add a Mental-Wellness Support Feature</a></p>
</li>
<li><p><a href="#heading-add-a-help-command">Add a Help Command</a></p>
</li>
<li><p><a href="#heading-improve-error-handling">Improve Error Handling</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-put-everything-together">Put Everything Together</a></p>
</li>
<li><p><a href="#heading-our-bot-doesnt-actually-remember-anything">Our Bot Doesn't Actually Remember Anything</a></p>
<ul>
<li><p><a href="#heading-create-the-database">Create the Database</a></p>
</li>
<li><p><a href="#heading-save-a-users-story">Save a User's Story</a></p>
</li>
<li><p><a href="#heading-get-the-story-back">Get the Story Back</a></p>
</li>
<li><p><a href="#heading-put-it-into-a-command">Put It Into a Command</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-adding-real-ai-chat">Adding Real AI Chat</a></p>
<ul>
<li><p><a href="#heading-install-the-hugging-face-library">Install the Hugging Face Library</a></p>
</li>
<li><p><a href="#heading-create-the-hugging-face-client">Create the Hugging Face Client</a></p>
</li>
<li><p><a href="#heading-connect-the-ai-model-to-the-bot">Connect the AI Model to the Bot</a></p>
</li>
<li><p><a href="#heading-handle-ai-errors">Handle AI Errors</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-how-do-we-keep-the-bot-online">How Do We Keep the Bot Online?</a></p>
<ul>
<li><p><a href="#heading-option-1-run-it-on-your-computer">Option 1: Run It on Your Computer</a></p>
</li>
<li><p><a href="#heading-option-2-host-it-on-a-server">Option 2: Host It on a Server</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-what-forever-actually-means">What "Forever" Actually Means</a></p>
</li>
<li><p><a href="#heading-dont-try-to-keep-it-awake-with-random-tricks">Don't Try to "Keep It Awake" With Random Tricks</a></p>
</li>
<li><p><a href="#heading-additional-features-and-where-to-go-next">Additional Features and Where to Go Next</a></p>
</li>
<li><p><a href="#heading-test-everything-locally-first">Test Everything Locally First</a></p>
</li>
<li><p><a href="#heading-deploying-the-bot">Deploying the Bot</a></p>
<ul>
<li><a href="#heading-the-start-command">The Start Command</a></li>
</ul>
</li>
<li><p><a href="#heading-remember-keep-your-secrets-secret">Remember: Keep Your Secrets Secret</a></p>
</li>
<li><p><a href="#heading-what-you-learned">What You Learned</a></p>
</li>
<li><p><a href="#heading-final-thoughts">Final Thoughts</a></p>
</li>
</ul>
<h2 id="heading-what-were-building">What We're Building</h2>
<p>Our finished bot will have several commands:</p>
<pre><code class="language-text">!hello
!story
!chat hello!
!support I'm having a stressful day
!help
</code></pre>
<p>For example:</p>
<pre><code class="language-text">User:
!story

Bot:
You wake up inside an abandoned library.

There are three doors in front of you:

1. A red wooden door
2. A metal door covered in strange symbols
3. A staircase leading underground

Which one do you choose?
</code></pre>
<p>The user can then continue the story.</p>
<p>For chat:</p>
<pre><code class="language-text">User:
!chat What's a good way to learn Python?

Bot:
Try building small projects instead of only reading tutorials.
A Discord bot is actually a pretty fun project to start with.
</code></pre>
<p>And for mental-wellness support:</p>
<pre><code class="language-text">User:
!support I'm really stressed about school.

Bot:
That sounds like a lot to deal with. You could try breaking
the work into one small task at a time and taking a short
break between tasks.

I'm a bot, not a therapist, so if you need personal support,
consider talking with someone you trust.
</code></pre>
<p>The goal isn't to make a magical robot therapist. It's to build a useful bot while learning how Discord APIs, Python functions, events, asynchronous programming, and basic conversational logic fit together.</p>
<h2 id="heading-what-you-need">What You Need</h2>
<p>You only need a few things:</p>
<ul>
<li><p>Python (version 3.8+ is recommended)</p>
</li>
<li><p>A Discord account</p>
</li>
<li><p>A Discord server where you have permission to add a bot</p>
</li>
<li><p>A code editor (I personally prefer VS Code or PyCharm)</p>
</li>
<li><p>The <code>discord.py</code> library</p>
</li>
</ul>
<p>We'll also use Python's built-in <code>os</code> module for reading environment variables.</p>
<p>If you don't already have Python installed, install a current supported version of Python from the official Python website.</p>
<p>Then check that Python works:</p>
<pre><code class="language-bash">python --version
</code></pre>
<p>You should see something similar to:</p>
<pre><code class="language-text">Python 3.x.x
</code></pre>
<h2 id="heading-create-the-discord-bot">Create the Discord Bot</h2>
<p>Before Python can control Discord, we need to create a Discord application.</p>
<p>Go to the Discord Developer Portal: <a href="https://discord.com/developers/applications">https://discord.com/developers/applications</a></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a581501af6af179dc1987d5/64a5aac8-fb53-42d0-9b91-fb6453b3eb1d.png" alt="Picture of the discord developer application page" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>This is what the page will look like, you might need to login with your discord email/username and password before you start.</p>
<p>Click on the "New Application" button on the top right and give your bot a name. For this tutorial, let's call ours <code>StoryBot</code>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a581501af6af179dc1987d5/84bd2f32-941c-4de2-8e6b-30ab22cc5ba1.png" alt="Picture of what it looks like when you click on the &quot;New Application&quot; button" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The application is basically the home for your bot.</p>
<p>Discord's developer platform provides the tools needed to create and configure applications and bots.</p>
<p>Once you've created the application, open its <strong>Bot</strong> section and create the bot user. You can add your own icon picture and your own banner if you want to.</p>
<p>You will then go to the <strong>Token</strong> section and click on "Reset Token" to generate your token. Treat that token like a password. Do <strong>NOT</strong> put it directly into your Python source code.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a581501af6af179dc1987d5/7537d64e-706c-43b9-a574-746294703f6d.png" alt="Picture of what the Token part in the Bots section looks like" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Never do this:</p>
<pre><code class="language-python">bot.run("my-secret-token")
</code></pre>
<p>And definitely don't upload a token to GitHub or commit it to source control. Instead, we'll store it in an environment variable, which we'll talk about later.</p>
<h2 id="heading-give-the-bot-permission-to-read-messages">Give the Bot Permission to Read Messages</h2>
<p>Our bot needs to see the messages that contain commands.</p>
<p>Discord uses something called <strong>Gateway Intents</strong> to control which types of events a bot receives. The <code>discord.py</code> documentation explains that intents must be enabled both in your code and, for privileged intents, in the Discord Developer Portal.</p>
<p>In the Developer Portal, find:</p>
<pre><code class="language-text">Bot
→ Privileged Gateway Intents
</code></pre>
<p>Enable:</p>
<pre><code class="language-text">Message Content Intent
</code></pre>
<p>It should look somewhat like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a581501af6af179dc1987d5/0273691f-8723-41d3-8cfc-d59656dfb6e2.png" alt="What should the &quot;Message Content Intent&quot; section look like" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>We'll also enable it in Python, which we will talk about later in this article.</p>
<h2 id="heading-create-the-project">Create the Project</h2>
<p>Create a folder:</p>
<pre><code class="language-text">discord-story-bot/
</code></pre>
<p>Inside it, we'll eventually have:</p>
<pre><code class="language-text">discord-story-bot/
│
├── bot.py
├── requirements.txt
└── .env
</code></pre>
<p>The three important files are:</p>
<ul>
<li><p><code>bot.py</code>: our Python program</p>
</li>
<li><p><code>requirements.txt</code>: text file that contains the name of the packages our bot needs</p>
</li>
<li><p><code>.env</code>: our secret token during local development</p>
</li>
</ul>
<h2 id="heading-create-a-virtual-environment">Create a Virtual Environment</h2>
<p>Open your terminal inside the project folder.</p>
<p>Run:</p>
<pre><code class="language-bash">python -m venv venv
</code></pre>
<p>Then activate it.</p>
<p>On Windows:</p>
<pre><code class="language-bash">venv\Scripts\activate
</code></pre>
<p>On macOS/Linux:</p>
<pre><code class="language-bash">source venv/bin/activate
</code></pre>
<p>A virtual environment gives this project its own little Python bubble.</p>
<p>That means packages installed for this bot won't randomly interfere with packages used by another project.</p>
<h2 id="heading-install-discordpy">Install discord.py</h2>
<p>Now install the Discord library:</p>
<pre><code class="language-bash">pip install -U discord.py
</code></pre>
<p>The official <code>discord.py</code> documentation uses this installation approach for setting up the library.</p>
<p>We'll also install <code>python-dotenv</code>, which makes reading our local <code>.env</code> file easier:</p>
<pre><code class="language-bash">pip install python-dotenv
</code></pre>
<p>Then save the dependencies:</p>
<pre><code class="language-bash">pip freeze &gt; requirements.txt
</code></pre>
<p>Your <code>requirements.txt</code> should contain the packages needed by the project.</p>
<h2 id="heading-create-your-first-bot">Create Your First Bot</h2>
<p>Let's start small.</p>
<p>Open <code>bot.py</code>:</p>
<pre><code class="language-python">import os

import discord
from discord.ext import commands
from dotenv import load_dotenv


load_dotenv()

TOKEN = os.getenv("DISCORD_TOKEN")

intents = discord.Intents.default()
intents.message_content = True

bot = commands.Bot(
    command_prefix="!",
    intents=intents
)


@bot.event
async def on_ready():
    print(f"Logged in as {bot.user}")


@bot.command()
async def hello(ctx):
    await ctx.send("Hello! I'm online.")


bot.run(TOKEN)
</code></pre>
<p>That is already a functional Discord bot.</p>
<p>Let's break it apart piece by piece.</p>
<h3 id="heading-importing-our-libraries">Importing Our Libraries</h3>
<p>First:</p>
<pre><code class="language-python">import os
</code></pre>
<p><code>os</code> lets Python communicate with parts of the operating system.</p>
<p>We'll use it to read environment variables.</p>
<p>Next:</p>
<pre><code class="language-python">import discord
</code></pre>
<p>This imports <code>discord.py</code>.</p>
<p>Then:</p>
<pre><code class="language-python">from discord.ext import commands
</code></pre>
<p>The <code>commands</code> extension makes creating commands much easier.</p>
<p>Instead of manually checking every message for something like <code>!hello</code>, we can write:</p>
<pre><code class="language-python">@bot.command()
async def hello(ctx):
    await ctx.send("Hello!")
</code></pre>
<p>The <code>discord.py</code> command system is built around Python functions decorated as commands.</p>
<p>Finally:</p>
<pre><code class="language-python">from dotenv import load_dotenv
</code></pre>
<p>This lets us load values from our <code>.env</code> file.</p>
<h3 id="heading-loading-the-token">Loading the Token</h3>
<p>Our bot needs a token to connect our Python program to Discord. Think of the token as a password that allows our program to authenticate as the bot.</p>
<p>We don't want to put this secret directly into our Python code. Instead, we'll store it in an environment variable.</p>
<p>First, install <code>python-dotenv</code>:</p>
<pre><code class="language-bash">pip install python-dotenv
</code></pre>
<p>This package lets Python read values from a <code>.env</code> file.</p>
<p>Now create a new file called <code>.env</code> in the same folder as <code>bot.py</code>.</p>
<p>Inside <code>.env</code>, add:</p>
<pre><code class="language-text">DISCORD_TOKEN=YOUR_BOT_TOKEN_HERE
</code></pre>
<p>Replace <code>YOUR_BOT_TOKEN_HERE</code> with the token you copied from the Discord Developer Portal.</p>
<p>Your file should look something like this:</p>
<pre><code class="language-text">DISCORD_TOKEN=your_actual_token_here
</code></pre>
<p>Don't share this token with anyone or upload your <code>.env</code> file to GitHub. Your bot token should be treated like a password.</p>
<p>To make sure Git doesn't accidentally include the <code>.env</code> file in a repository, create a file called <code>.gitignore</code> in your project folder and add:</p>
<pre><code class="language-text">.env
venv/
__pycache__/
</code></pre>
<p>Now let's load the token in Python.</p>
<p>At the top of <code>bot.py</code>, add:</p>
<pre><code class="language-python">import os
from dotenv import load_dotenv
</code></pre>
<p>Then add:</p>
<pre><code class="language-python">load_dotenv()
</code></pre>
<p>This tells Python to look for the <code>.env</code> file and load the variables inside it.</p>
<p>Now we can get our Discord token:</p>
<pre><code class="language-python">TOKEN = os.getenv("DISCORD_TOKEN")
</code></pre>
<p><code>os.getenv()</code> looks for the environment variable named <code>"DISCORD_TOKEN"</code> and gives us its value.</p>
<p>We can also check that the token was actually found:</p>
<pre><code class="language-python">if not TOKEN:
    raise RuntimeError("DISCORD_TOKEN is not set.")
</code></pre>
<p>If Python can't find the token, the program stops and gives us a clear error message instead of failing later in a confusing way.</p>
<h3 id="heading-understanding-intents">Understanding Intents</h3>
<p>Remember how we talked about enabling message content readability in python? We are going to do that now.</p>
<p>Add:</p>
<pre><code class="language-python">intents = discord.Intents.default()
intents.message_content = True
</code></pre>
<p>The first line creates a set of Discord's default intents.</p>
<p>The second line tells Discord that our bot needs access to message content.</p>
<p>Now we need to give these intents to our bot when we create it:</p>
<pre><code class="language-python">bot = commands.Bot(
    command_prefix="!",
    intents=intents
)
</code></pre>
<p>The <code>command_prefix="!"</code> means our bot will recognize commands that begin with <code>!</code>.</p>
<p>For example:</p>
<pre><code class="language-text">!hello
</code></pre>
<p>The <code>intents=intents</code> part gives our bot the permissions we configured above.</p>
<p>There are two steps here because Discord needs to know that our bot is allowed to receive message content, while our Python program also needs to tell Discord that it wants to receive it.</p>
<p>Our basic setup should now look like this:</p>
<pre><code class="language-python">import os
import discord

from dotenv import load_dotenv
from discord.ext import commands

load_dotenv()

TOKEN = os.getenv("DISCORD_TOKEN")

if not TOKEN:
    raise RuntimeError("DISCORD_TOKEN is not set.")

intents = discord.Intents.default()
intents.message_content = True

bot = commands.Bot(
    command_prefix="!",
    intents=intents
)
</code></pre>
<p>Now our bot has its token safely loaded and <code>discord.py</code> knows which intents to request when it connects to Discord.</p>
<h3 id="heading-what-is-ctx">What Is <code>ctx</code>?</h3>
<p>This part can look weird when you're learning Discord bots:</p>
<pre><code class="language-python">async def hello(ctx):
</code></pre>
<p>What is <code>ctx</code>? <code>ctx</code> stands for <strong>context</strong>. It contains information about the command that was used.</p>
<p>For example, it can tell us:</p>
<ul>
<li><p>Who ran the command</p>
</li>
<li><p>Which server it came from</p>
</li>
<li><p>Which channel it came from</p>
</li>
<li><p>What message triggered it</p>
</li>
</ul>
<p>Then:</p>
<pre><code class="language-python">await ctx.send("Hello!")
</code></pre>
<p>means:</p>
<blockquote>
<p>"Send this message back to the place where the command was used."</p>
</blockquote>
<h3 id="heading-why-does-everything-say-async-and-await">Why Does Everything Say <code>async</code> and <code>await</code>?</h3>
<p>You might notice:</p>
<pre><code class="language-python">async def hello(ctx):
</code></pre>
<p>and:</p>
<pre><code class="language-python">await ctx.send(...)
</code></pre>
<p>Discord bots spend a lot of time waiting.</p>
<p>They wait for:</p>
<ul>
<li><p>Messages</p>
</li>
<li><p>Discord responses</p>
</li>
<li><p>API requests</p>
</li>
<li><p>Timers</p>
</li>
<li><p>Other events</p>
</li>
</ul>
<p>Python's asynchronous programming features allow the bot to wait for these operations without freezing everything else.</p>
<p>You don't need to become an async-programming expert before building your first bot.</p>
<p>For now, think of <code>await</code> as:</p>
<blockquote>
<p>"Pause this task until this operation finishes, while letting the bot handle other things."</p>
</blockquote>
<h3 id="heading-run-the-bot">Run the Bot</h3>
<p>Start it with:</p>
<pre><code class="language-bash">python bot.py
</code></pre>
<p>If everything works, your terminal should print something similar to:</p>
<pre><code class="language-text">Logged in as StoryBot
</code></pre>
<p>Now go to your Discord server and type <code>!hello</code>. Your bot should respond.</p>
<p>Congratulations! You've officially made a Discord bot.</p>
<p>Now let's make it interesting.</p>
<h2 id="heading-build-the-storytelling-system">Build the Storytelling System</h2>
<p>First, we're going to create an interactive storytelling command.</p>
<p>At the top of <code>bot.py</code>, add:</p>
<pre><code class="language-python">import random
</code></pre>
<p>Then create some story ingredients:</p>
<pre><code class="language-python">story_locations = [
    "an abandoned library",
    "a mysterious island",
    "a futuristic city",
    "a hidden underground laboratory",
    "a forest that never appears on maps"
]

story_items = [
    "a glowing key",
    "an ancient notebook",
    "a strange compass",
    "a locked metal box",
    "a mysterious photograph"
]

story_events = [
    "You hear footsteps behind you.",
    "The lights suddenly turn off.",
    "A hidden door opens nearby.",
    "Your phone starts displaying a message from an unknown sender.",
    "You notice that the room has changed."
]
</code></pre>
<p>Now create the command:</p>
<pre><code class="language-python">@bot.command()
async def story(ctx):
    location = random.choice(story_locations)
    item = random.choice(story_items)
    event = random.choice(story_events)

    story_text = (
        f"You wake up in {location}.\n\n"
        f"Next to you is {item}.\n\n"
        f"{event}\n\n"
        "What do you do?"
    )

    await ctx.send(story_text)
</code></pre>
<p>Now <code>!story</code> might produce:</p>
<pre><code class="language-text">You wake up in a futuristic city.

Next to you is an ancient notebook.

A hidden door opens nearby.

What do you do?
</code></pre>
<p>Run it again and you might get something completely different.</p>
<p>That's because of:</p>
<pre><code class="language-python">random.choice(...)
</code></pre>
<p>Python randomly picks one item from each list.</p>
<p>It's a simple technique, but suddenly your bot can generate hundreds of different combinations.</p>
<h3 id="heading-lets-make-the-story-remember-the-user">Let's Make the Story Remember the User</h3>
<p>Random stories are fun, but interactive stories are much better when the bot remembers what happened.</p>
<p>We can create a dictionary:</p>
<pre><code class="language-python">user_stories = {}
</code></pre>
<p>The dictionary will store story information for each user.</p>
<p>For example:</p>
<pre><code class="language-text">user ID → current story
</code></pre>
<p>Now let's modify the story command:</p>
<pre><code class="language-python">@bot.command()
async def story(ctx):
    user_id = ctx.author.id

    location = random.choice(story_locations)
    item = random.choice(story_items)
    event = random.choice(story_events)

    user_stories[user_id] = {
        "location": location,
        "item": item,
        "event": event
    }

    await ctx.send(
        f"You wake up in {location}.\n\n"
        f"Next to you is {item}.\n\n"
        f"{event}\n\n"
        "What do you do?"
    )
</code></pre>
<p>Now each user can have their own active story.</p>
<h3 id="heading-add-a-story-choice">Add a Story Choice</h3>
<p>Let's give users choices.</p>
<pre><code class="language-python">@bot.command()
async def choose(ctx, choice: str):
    user_id = ctx.author.id

    if user_id not in user_stories:
        await ctx.send("You don't have an active story. Try `!story` first.")
        return

    choice = choice.lower()

    if choice == "left":
        response = (
            "You head left and discover a room filled with old maps. "
            "One of them has your name written on it."
        )

    elif choice == "right":
        response = (
            "You head right and find a staircase leading toward "
            "a strange blue light."
        )

    else:
        response = "Try choosing `left` or `right`."

    await ctx.send(response)
</code></pre>
<p>Now users can type:</p>
<pre><code class="language-text">!choose left
</code></pre>
<p>or:</p>
<pre><code class="language-text">!choose right
</code></pre>
<p>Notice this:</p>
<pre><code class="language-python">async def choose(ctx, choice: str):
</code></pre>
<p>The <code>choice</code> parameter receives the text after the command.</p>
<p>So:</p>
<pre><code class="language-text">!choose left
</code></pre>
<p>becomes approximately:</p>
<pre><code class="language-python">choice = "left"
</code></pre>
<p>This is one of the reasons command frameworks are so convenient. A <strong>command framework</strong> is a set of tools that makes it easier to create and manage commands in a program. In our case, <code>discord.py</code> provides the command framework that lets us turn Python functions into Discord commands using decorators like <code>@bot.command()</code>.</p>
<p>Instead of manually checking every message to figure out whether someone typed <code>!choose</code>, <code>discord.py</code> handles that work for us. It recognizes the command, takes the user's arguments, and passes them to our function.</p>
<p>So when someone types:</p>
<pre><code class="language-text">!choose left
</code></pre>
<p><code>discord.py</code> knows that choose is the command, <code>"left"</code> is the argument, and that it should call our <code>choose()</code> function with that information.</p>
<h3 id="heading-add-a-casual-chat-command">Add a Casual Chat Command</h3>
<p>Now let's make the bot capable of basic conversation.</p>
<p>We could connect it to a large language model API, but you don't actually need AI to learn how a chat command works. We'll start with a simple keyword-based response system.</p>
<p>First, we'll create a dictionary containing some keywords and possible responses:</p>
<pre><code class="language-python">chat_responses = {
    "hello": [
        "Hey! What's up?",
        "Hello! How's your day going?",
        "Hi! What are you working on?"
    ],
    "python": [
        "Python is a great language for beginners because its syntax is pretty readable.",
        "If you're learning Python, try building something instead of only watching tutorials."
    ],
    "discord": [
        "Discord bots are a fun way to practice Python because you get instant feedback.",
        "Once you understand commands and events, you can build some surprisingly complex bots."
    ]
}

Think of `chat_responses` as a small collection of things our bot knows how to talk about. Each key, such as `"python"` or `"discord"`, represents a keyword the bot can look for. The value associated with each key is a list of possible responses.

We use a list instead of a single response so the bot doesn't give exactly the same answer every time. Later, we'll randomly choose one of these responses.

Now let's create the actual `!chat` command:

```python
@bot.command()
async def chat(ctx, *, message: str):
    text = message.lower()

    for keyword, responses in chat_responses.items():
        if keyword in text:
            await ctx.send(random.choice(responses))
            return

    await ctx.send(
        "I'm still learning how to respond to that. "
        "Try talking to me about Python or Discord!"
    )
</code></pre>
<p>There are a few things happening here, so let's break it down.</p>
<p>First, this part:</p>
<pre><code class="language-python">@bot.command()
async def chat(ctx, *, message: str):
</code></pre>
<p>turns the <code>chat()</code> function into a Discord command. The <code>*</code> is important because it tells <code>discord.py</code> to treat everything after the command as one argument.</p>
<p>For example, if someone types:</p>
<pre><code class="language-text">!chat I want to learn Python
</code></pre>
<p>the entire phrase after <code>!chat</code> becomes the value of <code>message</code>:</p>
<pre><code class="language-python">message = "I want to learn Python"
</code></pre>
<p>Next, we have:</p>
<pre><code class="language-python">text = message.lower()
</code></pre>
<p>This converts the message to lowercase. That means <code>Python</code>, <code>python</code>, and <code>PYTHON</code> will all become <code>python</code>. Without this, our keyword check could miss a match simply because the user capitalized a word differently.</p>
<p>Now we get to the loop:</p>
<pre><code class="language-python">for keyword, responses in chat_responses.items():
</code></pre>
<p><code>.items()</code> lets us go through both the keyword and its corresponding list of responses. During each loop, <code>keyword</code> contains something like <code>"python"</code>, while <code>responses</code> contains the list of responses associated with it.</p>
<p>Then we check:</p>
<pre><code class="language-python">if keyword in text:
</code></pre>
<p>This asks whether the current keyword appears anywhere in the user's message.</p>
<p>If the user writes:</p>
<pre><code class="language-text">!chat I want to learn Python
</code></pre>
<p>the lowercase version becomes:</p>
<pre><code class="language-text">i want to learn python
</code></pre>
<p>Since <code>"python"</code> appears inside that text, the condition is true.</p>
<p>The bot can then choose a random response:</p>
<pre><code class="language-python">await ctx.send(random.choice(responses))
</code></pre>
<p><code>random.choice()</code> picks one item from the response list, while <code>ctx.send()</code> sends that response back to the Discord channel.</p>
<p>Finally, we have:</p>
<pre><code class="language-python">return
</code></pre>
<p>This stops the function after a matching keyword is found. Without it, the loop would continue checking the other keywords even after the bot had already responded.</p>
<p>But what happens if none of the keywords match?</p>
<p>That's what this part handles:</p>
<pre><code class="language-python">await ctx.send(
    "I'm still learning how to respond to that. "
    "Try talking to me about Python or Discord!"
)
</code></pre>
<p>If the loop finishes without finding a keyword, the bot sends this fallback message instead.</p>
<p>For example:</p>
<pre><code class="language-text">!chat I like pizza
</code></pre>
<p>doesn't contain <code>"hello"</code>, <code>"python"</code>, or <code>"discord"</code>, so the bot doesn't have a specific response to use.</p>
<p>This gives us a simple way for the bot to have conversations without needing an AI model.</p>
<h3 id="heading-add-a-mental-wellness-support-feature">Add a Mental-Wellness Support Feature</h3>
<p>Now for the feature that needs a little more care.</p>
<p>Instead of calling this a "therapy command" internally, we'll call it:</p>
<pre><code class="language-text">!support
</code></pre>
<p>Quick additional disclaimer before we start...this is just a fun wellness script, not a real therapist!</p>
<p>Create:</p>
<pre><code class="language-python">support_responses = {
    "stress": [
        "That sounds like a lot to handle. Try breaking the situation into one small task at a time.",
        "When everything feels overwhelming, it can help to pause and focus on what needs attention right now."
    ],

    "school": [
        "School can pile up quickly. Consider choosing one assignment to work on first instead of trying to solve everything at once.",
        "If school stress is getting difficult to manage, talking with a trusted person can make things feel less like something you have to handle alone."
    ],

    "sad": [
        "I'm sorry you're having a difficult moment. Taking a short break, doing something calming, or talking with someone you trust may help.",
        "You don't have to solve everything immediately. Give yourself some time and consider reaching out to someone you trust."
    ]
}
</code></pre>
<p>Now create the command:</p>
<pre><code class="language-python">@bot.command()
async def support(ctx, *, message: str):
    text = message.lower()

    for keyword, responses in support_responses.items():
        if keyword in text:
            response = random.choice(responses)

            await ctx.send(
                f"{response}\n\n"
                "I'm a bot, not a therapist or medical professional. "
                "If you need personal support, consider talking with "
                "someone you trust."
            )
            return

    await ctx.send(
        "It sounds like something is bothering you. "
        "I can offer general wellness suggestions, but I'm not a therapist. "
        "If you need personal support, consider reaching out to someone you trust."
    )
</code></pre>
<p>Now someone can type:</p>
<pre><code class="language-text">!support I'm stressed about school
</code></pre>
<p>The bot sees the word:</p>
<pre><code class="language-text">school
</code></pre>
<p>and chooses one of the school-related responses.</p>
<p>This is deliberately simple.</p>
<p>For a real public bot, you'd want much more careful safety handling, testing, moderation, privacy protection, and escalation logic before allowing users to rely on it for sensitive situations.</p>
<h3 id="heading-add-a-help-command">Add a Help Command</h3>
<p>A good bot should explain itself.</p>
<pre><code class="language-python">@bot.command()
async def commands_help(ctx):
    await ctx.send(
        "**Available commands:**\n"
        "`!hello` - Say hello\n"
        "`!story` - Start a new story\n"
        "`!choose left` - Choose the left path\n"
        "`!choose right` - Choose the right path\n"
        "`!chat &lt;message&gt;` - Have a casual conversation\n"
        "`!support &lt;message&gt;` - Get general wellness support"
    )
</code></pre>
<p>There's one small issue.</p>
<p>Discord's default help command is already called <code>help</code>.</p>
<p>So instead of:</p>
<pre><code class="language-python">async def help(ctx):
</code></pre>
<p>we've named ours:</p>
<pre><code class="language-python">commands_help
</code></pre>
<p>If you want the command itself to be called <code>!help</code>, you can write:</p>
<pre><code class="language-python">@bot.command(name="help")
async def commands_help(ctx):
    ...
</code></pre>
<p>That tells Discord:</p>
<blockquote>
<p>Use <code>!help</code> for this function even though the Python function has another name.</p>
</blockquote>
<h3 id="heading-improve-error-handling">Improve Error Handling</h3>
<p>Bots shouldn't crash just because someone enters an invalid command.</p>
<p>Add:</p>
<pre><code class="language-python">@bot.event
async def on_command_error(ctx, error):
    if isinstance(error, commands.MissingRequiredArgument):
        await ctx.send(
            "You're missing something. Try `!help` to see how the command works."
        )

    elif isinstance(error, commands.CommandNotFound):
        return

    else:
        print(f"Error: {error}")
</code></pre>
<p>Now if someone types:</p>
<pre><code class="language-text">!chat
</code></pre>
<p>without giving the bot a message, it can respond with a useful explanation instead of dumping a confusing error into the conversation.</p>
<h2 id="heading-put-everything-together">Put Everything Together</h2>
<p>At this point, your <code>bot.py</code> can look like this:</p>
<pre><code class="language-python">import os
import random

import discord
from discord.ext import commands
from dotenv import load_dotenv


load_dotenv()

TOKEN = os.getenv("DISCORD_TOKEN")

if not TOKEN:
    raise RuntimeError("DISCORD_TOKEN is not set.")


intents = discord.Intents.default()
intents.message_content = True

bot = commands.Bot(
    command_prefix="!",
    intents=intents
)


story_locations = [
    "an abandoned library",
    "a mysterious island",
    "a futuristic city",
    "a hidden underground laboratory",
    "a forest that never appears on maps"
]

story_items = [
    "a glowing key",
    "an ancient notebook",
    "a strange compass",
    "a locked metal box",
    "a mysterious photograph"
]

story_events = [
    "You hear footsteps behind you.",
    "The lights suddenly turn off.",
    "A hidden door opens nearby.",
    "Your phone starts displaying a message from an unknown sender.",
    "You notice that the room has changed."
]


user_stories = {}


chat_responses = {
    "hello": [
        "Hey! What's up?",
        "Hello! How's your day going?",
        "Hi! What are you working on?"
    ],

    "python": [
        "Python is a great language for beginners because its syntax is pretty readable.",
        "If you're learning Python, try building something instead of only watching tutorials."
    ],

    "discord": [
        "Discord bots are a fun way to practice Python because you get instant feedback.",
        "Once you understand commands and events, you can build some surprisingly complex bots."
    ]
}


support_responses = {
    "stress": [
        "That sounds like a lot to handle. Try breaking the situation into one small task at a time.",
        "When everything feels overwhelming, it can help to pause and focus on what needs attention right now."
    ],

    "school": [
        "School can pile up quickly. Consider choosing one assignment to work on first instead of trying to solve everything at once.",
        "If school stress is getting difficult to manage, talking with a trusted person can make things feel less like something you have to handle alone."
    ],

    "sad": [
        "I'm sorry you're having a difficult moment. Taking a short break, doing something calming, or talking with someone you trust may help.",
        "You don't have to solve everything immediately. Give yourself some time and consider reaching out to someone you trust."
    ]
}


@bot.event
async def on_ready():
    print(f"Logged in as {bot.user}")


@bot.command()
async def hello(ctx):
    await ctx.send("Hello! I'm online.")


@bot.command()
async def story(ctx):
    user_id = ctx.author.id

    location = random.choice(story_locations)
    item = random.choice(story_items)
    event = random.choice(story_events)

    user_stories[user_id] = {
        "location": location,
        "item": item,
        "event": event
    }

    await ctx.send(
        f"You wake up in {location}.\n\n"
        f"Next to you is {item}.\n\n"
        f"{event}\n\n"
        "What do you do?"
    )


@bot.command()
async def choose(ctx, choice: str):
    user_id = ctx.author.id

    if user_id not in user_stories:
        await ctx.send(
            "You don't have an active story. Try `!story` first."
        )
        return

    choice = choice.lower()

    if choice == "left":
        response = (
            "You head left and discover a room filled with old maps. "
            "One of them has your name written on it."
        )

    elif choice == "right":
        response = (
            "You head right and find a staircase leading toward "
            "a strange blue light."
        )

    else:
        response = "Try choosing `left` or `right`."

    await ctx.send(response)


@bot.command()
async def chat(ctx, *, message: str):
    text = message.lower()

    for keyword, responses in chat_responses.items():
        if keyword in text:
            await ctx.send(random.choice(responses))
            return

    await ctx.send(
        "I'm still learning how to respond to that. "
        "Try talking to me about Python or Discord!"
    )


@bot.command()
async def support(ctx, *, message: str):
    text = message.lower()

    for keyword, responses in support_responses.items():
        if keyword in text:
            response = random.choice(responses)

            await ctx.send(
                f"{response}\n\n"
                "I'm a bot, not a therapist or medical professional. "
                "If you need personal support, consider talking with "
                "someone you trust."
            )
            return

    await ctx.send(
        "It sounds like something is bothering you. "
        "I can offer general wellness suggestions, but I'm not a therapist. "
        "If you need personal support, consider reaching out to someone you trust."
    )


@bot.command(name="help")
async def commands_help(ctx):
    await ctx.send(
        "**Available commands:**\n"
        "`!hello` - Say hello\n"
        "`!story` - Start a new story\n"
        "`!choose left` - Choose the left path\n"
        "`!choose right` - Choose the right path\n"
        "`!chat &lt;message&gt;` - Have a casual conversation\n"
        "`!support &lt;message&gt;` - Get general wellness support"
    )


@bot.event
async def on_command_error(ctx, error):
    if isinstance(error, commands.MissingRequiredArgument):
        await ctx.send(
            "You're missing something. Try `!help` to see how the command works."
        )

    elif isinstance(error, commands.CommandNotFound):
        return

    else:
        print(f"Error: {error}")


bot.run(TOKEN)
</code></pre>
<p>This is enough to create a surprisingly capable beginner Discord project.</p>
<p>But there's an important limitation.</p>
<h2 id="heading-our-bot-doesnt-actually-remember-anything">Our Bot Doesn't Actually Remember Anything</h2>
<p>There's one small problem with our bot so far: it doesn't actually remember anything after it shuts down.</p>
<p>Right now, we're storing our story information in a Python dictionary:</p>
<pre><code class="language-python">user_stories = {}
</code></pre>
<p>This works while the bot is running. But if you stop the program and start it again, the dictionary starts empty.</p>
<p>To fix this, we need somewhere to permanently store our data. That's where a <strong>database</strong> comes in.</p>
<p>For this project, we'll use <strong>SQLite</strong>. SQLite is a lightweight database that stores information in a file on your computer. Python already includes SQLite through the built-in <code>sqlite3</code> module, so we don't need to install anything extra.</p>
<h3 id="heading-create-the-database">Create the Database</h3>
<p>First, add this import near the top of <code>bot.py</code>:</p>
<pre><code class="language-python">import sqlite3
</code></pre>
<p>Then create a connection to a database file:</p>
<pre><code class="language-python">db = sqlite3.connect("bot.db")
cursor = db.cursor()
</code></pre>
<p>The first line creates a database file called <code>bot.db</code> if one doesn't already exist. If the file already exists, SQLite simply opens it.</p>
<p>The second line creates a <strong>cursor</strong>. You can think of the cursor as the part of our Python program that lets us send instructions to the database.</p>
<p>Now we need to create a table where we can store our users' story information:</p>
<pre><code class="language-python">cursor.execute("""
    CREATE TABLE IF NOT EXISTS user_stories (
        user_id INTEGER PRIMARY KEY,
        location TEXT,
        item TEXT,
        event TEXT
    )
""")

db.commit()
</code></pre>
<p>Let's break this down.</p>
<p><code>cursor.execute()</code> tells SQLite to run the SQL command inside the parentheses.</p>
<p>The SQL command starts with:</p>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS user_stories
</code></pre>
<p>This tells SQLite to create a table called <code>user_stories</code>, but only if that table doesn't already exist.</p>
<p>Inside the parentheses, we define the information that each row can contain:</p>
<pre><code class="language-sql">user_id INTEGER PRIMARY KEY,
location TEXT,
item TEXT,
event TEXT
</code></pre>
<p><code>user_id</code> stores the Discord user's ID. We use it as the <code>PRIMARY KEY</code>, which means each user gets their own unique row.</p>
<p><code>location</code>, <code>item</code>, and <code>event</code> are all pieces of information about the user's current story.</p>
<p>Finally:</p>
<pre><code class="language-python">db.commit()
</code></pre>
<p>saves the changes to the database.</p>
<p>At this point, your project folder should contain a new file called:</p>
<pre><code class="language-text">bot.db
</code></pre>
<p>You don't need to open or edit this file manually. SQLite will manage it for us.</p>
<h3 id="heading-save-a-users-story">Save a User's Story</h3>
<p>Now let's actually put information into our database.</p>
<p>Suppose we have these variables:</p>
<pre><code class="language-python">user_id = ctx.author.id
location = "an abandoned castle"
item = "a mysterious key"
event = "a locked door"
</code></pre>
<p>We can save them using:</p>
<pre><code class="language-python">cursor.execute(
    """
    INSERT OR REPLACE INTO user_stories
    (user_id, location, item, event)
    VALUES (?, ?, ?, ?)
    """,
    (user_id, location, item, event)
)

db.commit()
</code></pre>
<p>The SQL statement tells SQLite to insert the information into the <code>user_stories</code> table.</p>
<p>The <code>?</code> symbols are placeholders for the actual values. The values are provided separately here:</p>
<pre><code class="language-python">(user_id, location, item, event)
</code></pre>
<p>This is safer than manually inserting values directly into the SQL string.</p>
<p><code>INSERT OR REPLACE</code> also means that if this user already has a saved story, their old story information can be replaced with the new information.</p>
<h3 id="heading-get-the-story-back">Get the Story Back</h3>
<p>Saving information is only half of the job. We also need to be able to retrieve it.</p>
<p>We can search the database for a user's story like this:</p>
<pre><code class="language-python">cursor.execute(
    """
    SELECT location, item, event
    FROM user_stories
    WHERE user_id = ?
    """,
    (user_id,)
)

story = cursor.fetchone()
</code></pre>
<p>This time, we're using <code>SELECT</code> to ask SQLite for information.</p>
<p>The <code>WHERE</code> part is important:</p>
<pre><code class="language-sql">WHERE user_id = ?
</code></pre>
<p>It tells SQLite to find the row belonging to this specific Discord user.</p>
<p>Then:</p>
<pre><code class="language-python">story = cursor.fetchone()
</code></pre>
<p>gets the first matching result.</p>
<p>If the user has a saved story, <code>story</code> will contain their information. If they don't, <code>story</code> will be <code>None</code>.</p>
<p>We can check for that:</p>
<pre><code class="language-python">if story:
    location, item, event = story

    await ctx.send(
        f"You're currently in {location}. "
        f"You have {item}, and you're facing {event}."
    )
else:
    await ctx.send("I don't have a saved story for you yet!")
</code></pre>
<p>Now the bot can retrieve information that was saved earlier, even after the Python program has been restarted.</p>
<h3 id="heading-put-it-into-a-command">Put It Into a Command</h3>
<p>We can turn this into a simple command that lets users check their saved story:</p>
<pre><code class="language-python">@bot.command()
async def status(ctx):
    user_id = ctx.author.id

    cursor.execute(
        """
        SELECT location, item, event
        FROM user_stories
        WHERE user_id = ?
        """,
        (user_id,)
    )

    story = cursor.fetchone()

    if story:
        location, item, event = story

        await ctx.send(
            f"You're currently in {location}. "
            f"You have {item}, and you're facing {event}."
        )
    else:
        await ctx.send(
            "You don't have a saved story yet. "
            "Start one with `!story`!"
        )
</code></pre>
<p>Now a user can type:</p>
<pre><code class="language-text">!status
</code></pre>
<p>and the bot can look up their story from the database.</p>
<p>This is a big improvement over our original dictionary. A dictionary only remembers information while the Python program is running. SQLite lets us save that information so it can still be there when the bot starts again.</p>
<p>For a larger bot, you could eventually store things like user preferences, story progress, inventory, conversation history, or other data. But for now, this simple database is enough to give our bot some real memory.</p>
<h2 id="heading-adding-real-ai-chat">Adding Real AI Chat</h2>
<p>Before we connect our bot to an AI model, let's quickly talk about <strong>Hugging Face</strong>.</p>
<p>If you've never used it before, Hugging Face is a platform where developers can find, share, and use machine learning models and datasets. Think of it as a huge community and library for AI tools.</p>
<p>Hugging Face also provides tools that let Python programs communicate with these models without having to build and train an AI model from scratch.</p>
<p>For our bot, we'll use Hugging Face's <strong>Inference Providers</strong> to send a user's message to a supported language model and receive its response.</p>
<p>We won't be training an AI model ourselves. Instead, we'll use an existing model and connect it to our Discord bot through Python.</p>
<p>Now that we know what Hugging Face is, let's connect it to our bot.</p>
<h3 id="heading-install-the-hugging-face-library">Install the Hugging Face Library</h3>
<p>First, install <code>huggingface_hub</code>:</p>
<pre><code class="language-bash">pip install -U huggingface_hub
</code></pre>
<p>We already installed <code>python-dotenv</code>, so we can use the same <code>.env</code> file from earlier to keep our Hugging Face token out of the source code.</p>
<p>Add your Hugging Face token to <code>.env</code>:</p>
<pre><code class="language-text">DISCORD_TOKEN=YOUR_BOT_TOKEN_HERE
HF_TOKEN=YOUR_HUGGING_FACE_TOKEN_HERE
</code></pre>
<p>Replace <code>YOUR_HUGGING_FACE_TOKEN_HERE</code> with your actual Hugging Face access token.</p>
<p>Just like your Discord bot token, <strong>don't share this token or upload it to GitHub</strong>.</p>
<h3 id="heading-create-the-hugging-face-client">Create the Hugging Face Client</h3>
<p>Now add this import near the top of <code>bot.py</code>:</p>
<pre><code class="language-python">from huggingface_hub import InferenceClient
</code></pre>
<p>Then load the token:</p>
<pre><code class="language-python">HF_TOKEN = os.getenv("HF_TOKEN")

if not HF_TOKEN:
    raise RuntimeError("HF_TOKEN is not set.")
</code></pre>
<p>The first line gets the token from our environment variables. The <code>if</code> statement checks whether the token actually exists. If it doesn't, Python stops and gives us a useful error instead of letting the program fail later in a confusing way.</p>
<p>Now create the Hugging Face client:</p>
<pre><code class="language-python">client = InferenceClient(
    api_key=HF_TOKEN
)
</code></pre>
<p>The <code>InferenceClient</code> is what our Python program will use to communicate with Hugging Face's inference service.</p>
<h3 id="heading-connect-the-ai-model-to-the-bot">Connect the AI Model to the Bot</h3>
<p>Now we can replace our previous keyword-based <code>!chat</code> command with one that sends the user's message to a language model.</p>
<pre><code class="language-python">@bot.command()
async def chat(ctx, *, message: str):
    try:
        response = client.chat_completion(
            model="YOUR_SUPPORTED_MODEL_ID",
            messages=[
                {
                    "role": "system",
                    "content": (
                        "You are a friendly Discord bot. "
                        "Keep responses helpful, concise, and conversational."
                    )
                },
                {
                    "role": "user",
                    "content": message
                }
            ],
            max_tokens=200
        )

        answer = response.choices[0].message.content

        await ctx.send(answer)

    except Exception as error:
        print(f"AI error: {error}")
        await ctx.send(
            "I couldn't generate a response right now. "
            "Please try again later."
        )
</code></pre>
<p>There's quite a bit happening here, so let's walk through it.</p>
<p>We start with the same command structure we've already used:</p>
<pre><code class="language-python">@bot.command()
async def chat(ctx, *, message: str):
</code></pre>
<p>This creates our <code>!chat</code> command and stores everything the user types after it in <code>message</code>.</p>
<p>For example:</p>
<pre><code class="language-text">!chat What is Python?
</code></pre>
<p>gives us:</p>
<pre><code class="language-python">message = "What is Python?"
</code></pre>
<p>Next, we use:</p>
<pre><code class="language-python">try:
</code></pre>
<p>This tells Python that we're about to run code that could potentially fail. Since we're communicating with an external service, things like an unavailable model, an invalid token, or a temporary connection problem can happen.</p>
<p>Now we call:</p>
<pre><code class="language-python">response = client.chat_completion(
</code></pre>
<p>This sends a chat-completion request to the model through Hugging Face. The <code>messages</code> parameter contains the conversation we want the model to respond to.</p>
<p>The first message has the role <code>"system"</code>:</p>
<pre><code class="language-python">{
    "role": "system",
    "content": (
        "You are a friendly Discord bot. "
        "Keep responses helpful, concise, and conversational."
    )
}
</code></pre>
<p>The system message gives the model instructions about how it should respond.</p>
<p>Then we provide the user's actual message:</p>
<pre><code class="language-python">{
    "role": "user",
    "content": message
}
</code></pre>
<p>If the user typed:</p>
<pre><code class="language-text">!chat What is Python?
</code></pre>
<p>then <code>message</code> contains:</p>
<pre><code class="language-text">What is Python?
</code></pre>
<p>So the model receives that as the user's input.</p>
<p>We also have:</p>
<pre><code class="language-python">max_tokens=200
</code></pre>
<p>This limits how much text the model can generate for one response. Keeping responses relatively short works well for Discord because huge blocks of text aren't always very pleasant to read in a chat channel.</p>
<p>You also need to replace:</p>
<pre><code class="language-python">model="YOUR_SUPPORTED_MODEL_ID"
</code></pre>
<p>with the ID of a model currently available through the Hugging Face Inference Providers you are using. Hugging Face's documentation shows that <code>InferenceClient</code> can use a model ID hosted on the Hugging Face Hub for chat completion.</p>
<p>Once the request is complete, we need to get the actual text from the response:</p>
<pre><code class="language-python">answer = response.choices[0].message.content
</code></pre>
<p>The response contains information about the model's output. <code>choices[0]</code> gets the first generated response, and <code>.message.content</code> gives us the actual text.</p>
<p>Then we send it to Discord:</p>
<pre><code class="language-python">await ctx.send(answer)
</code></pre>
<p>So the whole process looks like this:</p>
<pre><code class="language-text">User types !chat
        ↓
Discord sends the command to our bot
        ↓
Python gets the user's message
        ↓
Hugging Face receives the message
        ↓
The AI model generates a response
        ↓
Python gets the generated text
        ↓
The bot sends it back to Discord
</code></pre>
<h3 id="heading-handle-ai-errors">Handle AI Errors</h3>
<p>The last part of our command is:</p>
<pre><code class="language-python">except Exception as error:
    print(f"AI error: {error}")
    await ctx.send(
        "I couldn't generate a response right now. "
        "Please try again later."
    )
</code></pre>
<p>If something goes wrong inside the <code>try</code> block, Python jumps to the <code>except</code> block instead of crashing the entire bot.</p>
<p>The error is printed in the terminal so you can investigate what happened:</p>
<pre><code class="language-python">print(f"AI error: {error}")
</code></pre>
<p>Meanwhile, the Discord user gets a simple message:</p>
<pre><code class="language-text">I couldn't generate a response right now. Please try again later.
</code></pre>
<p>This is much better than letting an API error take down the whole bot.</p>
<p>At this point, you have a real AI-powered <code>!chat</code> command. You can type something like:</p>
<pre><code class="language-text">!chat Tell me an interesting fact about space.
</code></pre>
<p>and the model can generate a response instead of choosing from a small list of pre-written messages.</p>
<p>One thing to remember is that this bot is sending user messages to an external AI service. Don't automatically send private or sensitive conversations to an AI provider. If you make this bot available to other people, be clear about what information it processes and avoid storing or sending more data than the bot actually needs.</p>
<p>You can also combine this AI system with the SQLite database from earlier. For example, you could save a limited amount of conversation history and send relevant previous messages along with a new message. That would allow the bot to keep some context between messages instead of treating every message as a completely new conversation.</p>
<h2 id="heading-how-do-we-keep-the-bot-online">How Do We Keep the Bot Online?</h2>
<p>Here's where the phrase "online forever" needs a little clarification.</p>
<p>There are two different situations.</p>
<h3 id="heading-option-1-run-it-on-your-computer">Option 1: Run It on Your Computer</h3>
<p>When you run:</p>
<pre><code class="language-bash">python bot.py
</code></pre>
<p>the bot stays online while that program is running.</p>
<p>Close the terminal?</p>
<p>Bot goes offline.</p>
<p>Turn off the computer?</p>
<p>Bot goes offline.</p>
<p>Lose internet?</p>
<p>Bot goes offline.</p>
<p>This is perfect for development but it's not a 24/7 production setup.</p>
<h3 id="heading-option-2-host-it-on-a-server">Option 2: Host It on a Server</h3>
<p>For a bot that should stay online while your computer is off, you need a computer somewhere that stays available.</p>
<p>That computer can be a cloud server.</p>
<p>You upload your project, install the dependencies, add your environment variables, and start:</p>
<pre><code class="language-bash">python bot.py
</code></pre>
<p>Now the cloud machine runs the program instead of your laptop.</p>
<p>Services designed for continuously running workloads can be used for this kind of application. For example, Render currently provides a <strong>Background Worker</strong> service type for continuously running processes that don't need to receive incoming web traffic.</p>
<p>But you should check the provider's current pricing and service limitations before deploying. Free hosting tiers aren't necessarily designed for an always-on Discord bot, and a "free forever" 24/7 setup isn't something you should assume a hosting platform will provide.</p>
<h2 id="heading-what-forever-actually-means">What "Forever" Actually Means</h2>
<p>There isn't really a magical:</p>
<pre><code class="language-text">ONLINE_FOREVER = True
</code></pre>
<p>setting.</p>
<p>A bot can stay online continuously only as long as the computer or server running it continues operating.</p>
<p>Even a professionally hosted bot can go offline because of:</p>
<ul>
<li><p>Server maintenance</p>
</li>
<li><p>Deployments</p>
</li>
<li><p>Bugs</p>
</li>
<li><p>Network problems</p>
</li>
<li><p>Provider outages</p>
</li>
<li><p>Invalid credentials</p>
</li>
<li><p>API changes</p>
</li>
<li><p>Billing or account issues</p>
</li>
</ul>
<p>So the realistic goal is to keep the bot running automatically and restart it when something goes wrong.</p>
<p>That is what production hosting is designed to help with.</p>
<p>If your provider supports automatic restarts, enable them.</p>
<p>You can also make your Python code fail clearly when an important environment variable is missing:</p>
<pre><code class="language-python">if not TOKEN:
    raise RuntimeError("DISCORD_TOKEN is not set.")
</code></pre>
<p>A clear error is much easier to debug than a mysterious bot that simply doesn't appear online.</p>
<h2 id="heading-dont-try-to-keep-it-awake-with-random-tricks">Don't Try to "Keep It Awake" With Random Tricks</h2>
<p>You may find tutorials suggesting that you deploy a web server and repeatedly ping it from another service to prevent a free hosting instance from sleeping.</p>
<p>Be careful with that approach.</p>
<p>Hosting providers change their free-tier rules, and attempting to work around those limits can violate their terms.</p>
<p>If you need an actually persistent bot, use a hosting option that explicitly supports the workload.</p>
<p>For example, a background worker is designed for continuously running processes. That's much cleaner than trying to convince a web service that your Discord bot is secretly a website.</p>
<h2 id="heading-additional-features-and-where-to-go-next"><strong>Additional Featur</strong>es and Where to Go Next</h2>
<p>Now that you have a working Discord bot, there are plenty of directions you can take the project next.</p>
<p>You could turn the storytelling system into a more complete game by adding an inventory, multiple chapters, puzzles, or different endings. You could also replace text-based commands with Discord slash commands and buttons to make the bot easier to interact with.</p>
<p>If you're interested in AI, you could expand the chat system by giving the bot different personalities, adding carefully limited conversation context, or using AI to generate parts of the stories.</p>
<p>You could also add moderation features, daily story prompts, or other commands that fit the kind of Discord community you're building.</p>
<p>These are ideas for extending the project rather than features we'll build step by step in this tutorial. The important thing is that you now have the foundation to experiment with them yourself.</p>
<p>Start with one small feature, figure out how it works, and build from there. You don't need to turn the bot into a massive project all at once.</p>
<p>The more you experiment with the code, the more you'll start seeing how Python, Discord, databases, and AI can work together in a real application.</p>
<h2 id="heading-test-everything-locally-first">Test Everything Locally First</h2>
<p>Before deploying, test:</p>
<pre><code class="language-text">!hello
!story
!choose left
!choose right
!chat hello
!chat I want to learn Python
!support I'm stressed
!help
</code></pre>
<p>Then test weird inputs:</p>
<pre><code class="language-text">!choose banana
!chat
!support
!unknowncommand
</code></pre>
<p>You want to discover bugs while you're sitting in front of your computer, not three days later when someone tells you:</p>
<blockquote>
<p>"Your bot has been broken since Tuesday."</p>
</blockquote>
<h2 id="heading-deploying-the-bot">Deploying the Bot</h2>
<p>First, make sure your project contains:</p>
<pre><code class="language-text">discord-story-bot/
│
├── bot.py
├── requirements.txt
├── .gitignore
└── .python-version
</code></pre>
<p>A <code>.python-version</code> file can contain something like:</p>
<pre><code class="language-text">3.13
</code></pre>
<p>Using a version file makes your deployment environment more predictable. Render currently supports specifying a Python version through <code>.python-version</code> or an environment variable.</p>
<p>Your <code>requirements.txt</code> should contain your dependencies.</p>
<p>For example:</p>
<pre><code class="language-text">discord.py
python-dotenv
</code></pre>
<p>For deployment, you generally don't need the local <code>.env</code> file.</p>
<p>Instead, add:</p>
<pre><code class="language-text">DISCORD_TOKEN
</code></pre>
<p>as an environment variable in your hosting provider's dashboard.</p>
<p>That way the secret isn't stored inside your repository.</p>
<h3 id="heading-the-start-command">The Start Command</h3>
<p>Your deployment service needs to know what to run.</p>
<p>For this project, the start command is:</p>
<pre><code class="language-bash">python bot.py
</code></pre>
<p>The important thing is that the process doesn't immediately exit.</p>
<p>A Discord bot stays alive because <code>bot.run(TOKEN)</code> starts the Discord connection and keeps the program running.</p>
<p>If your hosting service supports background workers, that's a natural fit for a bot like this because the bot doesn't need to serve normal HTTP requests. Render specifically describes background workers as continuously running services that don't receive incoming network traffic.</p>
<h2 id="heading-remember-keep-your-secrets-secret">Remember: Keep Your Secrets Secret</h2>
<p>This is worth repeating because it causes a lot of beginner projects to get compromised.</p>
<p>Never commit this:</p>
<pre><code class="language-python">bot.run("YOUR_REAL_TOKEN")
</code></pre>
<p>Never upload:</p>
<pre><code class="language-text">.env
</code></pre>
<p>Never paste your actual token into a public GitHub issue.</p>
<p>If a token accidentally becomes public, treat it as compromised and regenerate it.</p>
<p>Environment variables are your friend.</p>
<h2 id="heading-what-you-learned">What You Learned</h2>
<p>You've now built a Discord bot that demonstrates several real programming concepts.</p>
<p>You learned how to:</p>
<ul>
<li><p>Create a Discord application</p>
</li>
<li><p>Connect Python to Discord</p>
</li>
<li><p>Use <code>discord.py</code></p>
</li>
<li><p>Configure Gateway Intents</p>
</li>
<li><p>Create commands</p>
</li>
<li><p>Use asynchronous functions</p>
</li>
<li><p>Read command arguments</p>
</li>
<li><p>Generate random stories</p>
</li>
<li><p>Store temporary user state</p>
</li>
<li><p>Create a basic chat system</p>
</li>
<li><p>Create a mental-wellness support feature</p>
</li>
<li><p>Handle command errors</p>
</li>
<li><p>Keep secrets out of source code</p>
</li>
<li><p>Prepare a project for deployment</p>
</li>
<li><p>Think about persistent hosting</p>
</li>
</ul>
<p>And underneath all those features, the architecture is still surprisingly simple:</p>
<pre><code class="language-text">User sends command
        ↓
Discord receives message
        ↓
discord.py receives event
        ↓
Python function runs
        ↓
Bot generates response
        ↓
Discord displays response
</code></pre>
<p>You don't need thousands of lines of code to get started.</p>
<p>You need a clear idea, a few Python concepts, and the willingness to keep debugging when something inevitably breaks.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>The coolest part of this project isn't really the Discord bot. It's what the project teaches you.</p>
<p>And once you understand the pieces, you can reuse the same ideas in countless projects.</p>
<p>A Discord bot can become a game, which could become a web application, which could also become a larger software project.</p>
<p>And suddenly you're not just learning Python syntax anymore. You're learning how software actually gets built, one command at a time.</p>
<p>Happy coding!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use Apple’s Foundation Models in a Web App with a macOS Companion ]]>
                </title>
                <description>
                    <![CDATA[ Not every AI feature needs a cloud model, with its per-token bills, network round-trips, and private data leaving your machine. If you're on a modern Mac, a capable language model is already on your d ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-apple-s-foundation-models-in-a-web-app-with-a-macos-companion/</link>
                <guid isPermaLink="false">6a5e92afe12aa31dae6e8a79</guid>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ macOS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Swift ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Balogun Wahab ]]>
                </dc:creator>
                <pubDate>Mon, 20 Jul 2026 21:27:11 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/7f0e2343-7394-46b5-a4c8-3ef0fecfa57a.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Not every AI feature needs a cloud model, with its per-token bills, network round-trips, and private data leaving your machine. If you're on a modern Mac, a capable language model is already on your disk.</p>
<p><strong>Foundation Models</strong> is Apple's Swift framework for working with large language models. It's the on-device model behind Apple Intelligence, Apple's Private Cloud Compute, or another provider's server model.</p>
<p>This tutorial targets the on-device model: you send it a prompt and it runs entirely on the Mac's own hardware locally, free-per-call, and offline-friendly.</p>
<p>Paired with Apple Vision for reading images on device, that's enough to build real AI features like summaries, classification, and structured extraction without the data ever leaving your machine.</p>
<h2 id="heading-table-of-contents">Table Of Contents</h2>
<ul>
<li><p><a href="#heading-what-you-will-build">What You Will Build</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-why-a-macos-companion-app">Why a macOS Companion App?</a></p>
</li>
<li><p><a href="#heading-foundation-models-cant-read-images-directly">Foundation Models Can't Read Images Directly</a></p>
</li>
<li><p><a href="#heading-project-structure">Project Structure</a></p>
</li>
<li><p><a href="#heading-build-the-react-app">Build the React App</a></p>
<ul>
<li><p><a href="#heading-check-companion-health">Check Companion Health</a></p>
</li>
<li><p><a href="#heading-convert-the-image-to-base64">Convert the Image to Base64</a></p>
</li>
<li><p><a href="#heading-analyze-immediately-after-upload">Analyze Immediately After Upload</a></p>
</li>
<li><p><a href="#heading-send-the-image-to-the-companion">Send the Image to the Companion</a></p>
</li>
<li><p><a href="#heading-render-the-json-output">Render the JSON Output</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-build-the-macos-companion-app">Build the macOS Companion App</a></p>
</li>
<li><p><a href="#heading-check-foundation-models-availability">Check Foundation Models Availability</a></p>
</li>
<li><p><a href="#heading-extract-text-with-apple-vision">Extract Text with Apple Vision</a></p>
</li>
<li><p><a href="#heading-ask-foundation-models-to-explain-the-vision-output">Ask Foundation Models to Explain the Vision Output</a></p>
</li>
<li><p><a href="#heading-return-json-to-the-browser">Return JSON to the Browser</a></p>
</li>
<li><p><a href="#heading-run-the-app">Run the App</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-resources">Resources</a></p>
</li>
</ul>
<h2 id="heading-what-you-will-build">What You Will Build</h2>
<p>You'll build <strong>Vision Bridge</strong>, a web app that sends an image to a local macOS companion. The companion reads the image with Apple Vision, reasons about it with Foundation Models, and returns structured JSON to the browser: private, on-device AI behind a plain web interface.</p>
<p>You can find the complete source code in this GitHub repository: <a href="http://github.com/03balogun/vision-bridge">github.com/03balogun/vision-bridge</a>.</p>
<p>The goal isn't to build a giant product but rather to understand the architecture behind how this works.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5db93b3da2342e8354088115/6d18db01-e921-4291-bb2e-26be2c02b304.png" alt="Screenshot of the Vision Bridge app, with image upload on the left and JSON output on the right" style="display:block;margin:0 auto" width="3024" height="1714" loading="lazy">

<p>Vision Bridge has two parts:</p>
<ul>
<li><p>A React app with a split-screen interface.</p>
</li>
<li><p>A macOS companion app that exposes a local API.</p>
</li>
</ul>
<p>The React app has:</p>
<ul>
<li><p>An image upload area</p>
</li>
<li><p>An image preview</p>
</li>
<li><p>Automatic analysis after upload</p>
</li>
<li><p>A JSON output viewer</p>
</li>
<li><p>A companion health status indicator</p>
</li>
</ul>
<p>The macOS companion app has:</p>
<ul>
<li><p><code>GET /v1/health</code></p>
</li>
<li><p><code>POST /v1/analyze-image</code></p>
</li>
<li><p>Apple Vision OCR</p>
</li>
<li><p>Foundation Models availability checks</p>
</li>
<li><p>Foundation Models reasoning over Vision output</p>
</li>
</ul>
<p>The final response looks like this:</p>
<pre><code class="language-json">{
  "support": {
    "visionAvailable": true,
    "foundationModelAvailable": true,
    "foundationModelStatus": "available"
  },
  "image": {
    "filename": "screenshot.png",
    "contentType": "image/png",
    "byteCount": 1048576,
    "width": 1440,
    "height": 900
  },
  "vision": {
    "detectedText": [
      {
        "text": "Build failed",
        "confidence": 0.96,
        "boundingBox": {
          "x": 0.12,
          "y": 0.31,
          "width": 0.45,
          "height": 0.08
        }
      }
    ]
  },
  "model": {
    "summary": "The image appears to show a software build failure.",
    "description": "A developer tool window is showing an error state with diagnostic text.",
    "suggestedTags": ["screenshot", "developer-tool", "error"],
    "possibleUses": [
      "Generate alt text",
      "Summarize screenshots",
      "Extract document data"
    ]
  }
}
</code></pre>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along, you need:</p>
<ul>
<li><p>macOS 26 or newer</p>
</li>
<li><p>Xcode with the macOS 26 SDK</p>
</li>
<li><p>Node.js 20 or newer</p>
</li>
<li><p>Basic React knowledge</p>
</li>
<li><p>Basic Swift knowledge</p>
</li>
<li><p>A Mac that supports Apple Intelligence</p>
</li>
</ul>
<p>Foundation Models availability depends on the Mac, the OS version, and Apple Intelligence settings. The companion checks this at runtime, which we'll cover below.</p>
<h2 id="heading-why-a-macos-companion-app">Why a macOS Companion App?</h2>
<p>You can't write this in a regular React app:</p>
<pre><code class="language-ts">import FoundationModels from "apple-frameworks";
</code></pre>
<p>That API doesn't exist in the browser. A native macOS app, however, can use any Apple framework, so the companion acts as a local bridge. The same pattern works for any native capability the web platform doesn't expose.</p>
<h2 id="heading-foundation-models-cant-read-images-directly">Foundation Models Can't Read Images Directly</h2>
<p>The public Foundation Models framework is a language model interface. It doesn't currently expose direct image input the way a multimodal cloud model might, so this tutorial never sends the image to the model. Instead, the companion feeds the Vision OCR observations and image metadata into the prompt. The model reasons over structured text, never the original pixels.</p>
<p>That split plays to each framework's strength: Vision is excellent at pulling machine-readable information out of images, and Foundation Models turns that information into summaries, labels, explanations, and structured output.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5db93b3da2342e8354088115/a5c11ad4-dcac-4690-bc6d-08b27fd6fed8.png" alt="Vision Bridge architecture: the browser sends the image over localhost to the Swift companion, which runs Apple Vision OCR, feeds the observations to Foundation Models, and returns structured JSON" style="display:block;margin:0 auto" width="2492" height="1572" loading="lazy">

<p>The above diagram shows the round trip that the rest of this tutorial builds. The browser sends the uploaded image as base64 JSON over localhost to the Swift companion. Inside the companion, Apple Vision runs OCR on the image and produces text observations: the recognized strings, their confidence scores, and their bounding boxes.</p>
<p>Those observations, not the image itself, are formatted into a prompt for Foundation Models, which generates a summary, description, and tags. The companion then bundles the Vision output and the model output into one JSON response and returns it to the browser.</p>
<h2 id="heading-project-structure">Project Structure</h2>
<p>Create a project with this structure:</p>
<pre><code class="language-text">vision-bridge/
  apps/
    web/
      src/
        main.tsx
        styles.css
      package.json
      vite.config.ts
    macos-companion/
      Package.swift
      Sources/
        VisionBridgeCompanion/
          main.swift
  package.json
  README.md
</code></pre>
<p>The root <code>package.json</code> gives us a few convenient commands:</p>
<pre><code class="language-json">{
  "scripts": {
    "dev": "npm --workspace apps/web run dev",
    "build": "npm --workspace apps/web run build",
    "companion": "swift run --package-path apps/macos-companion VisionBridgeCompanion"
  },
  "workspaces": ["apps/web"]
}
</code></pre>
<h2 id="heading-build-the-react-app">Build the React App</h2>
<p>The web app is intentionally simple. It has one job: let the user pick an image and show the JSON returned by the companion.</p>
<p>The web app uses Vite, React, Lucide icons, and a JSON viewer:</p>
<pre><code class="language-json">{
  "dependencies": {
    "@vitejs/plugin-react": "^6.0.3",
    "lucide-react": "^0.468.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-json-view-lite": "^2.5.0",
    "vite": "^8.1.3"
  }
}
</code></pre>
<p>After defining the dependencies, install them:</p>
<pre><code class="language-plaintext">npm install
</code></pre>
<p>The API base URL points to the local companion:</p>
<pre><code class="language-ts">const API_BASE_URL = "http://127.0.0.1:43119";
</code></pre>
<h3 id="heading-check-companion-health">Check Companion Health</h3>
<p>The web app pings the companion so the UI can show whether the native bridge is online:</p>
<pre><code class="language-ts">async function checkHealth() {
  setHealthError(null);

  try {
    const response = await fetch(`${API_BASE_URL}/v1/health`);
    if (!response.ok) {
      throw new Error(`Health check failed with ${response.status}`);
    }

    const payload = await response.json();
    setHealth(payload);
  } catch (error) {
    setHealth(null);
    setHealthError(error instanceof Error ? error.message : "Companion unavailable");
  }
}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/5db93b3da2342e8354088115/dc3c37eb-1d9c-4b44-82db-f38adada4f19.png" alt="Screenshot of the companion online status pill" style="display:block;margin:0 auto" width="732" height="212" loading="lazy">

<h3 id="heading-convert-the-image-to-base64">Convert the Image to Base64</h3>
<p>When the user selects a file, the app converts it to base64 so it can be sent as JSON:</p>
<pre><code class="language-ts">function readFileAsBase64(file: File) {
  return new Promise&lt;string&gt;((resolve, reject) =&gt; {
    const reader = new FileReader();
    reader.onload = () =&gt; {
      const result = String(reader.result);
      resolve(result.includes(",") ? result.split(",")[1] : result);
    };
    reader.onerror = () =&gt; reject(reader.error);
    reader.readAsDataURL(file);
  });
}
</code></pre>
<p>This isn't the only way to upload files. You could also use <code>multipart/form-data</code>, but JSON keeps the demo easy to inspect.</p>
<h3 id="heading-analyze-immediately-after-upload">Analyze Immediately After Upload</h3>
<p>The app starts analysis as soon as an image is uploaded:</p>
<pre><code class="language-ts">async function handleFile(file: File) {
  if (!file.type.startsWith("image/")) {
    setError("Choose a PNG, JPEG, HEIC, or another browser-readable image.");
    return;
  }

  const base64 = await readFileAsBase64(file);
  const nextImage = {
    file,
    previewUrl: URL.createObjectURL(file),
    base64,
  };

  setSelectedImage(nextImage);
  setAnalysis(null);
  setError(null);
  setCopied(false);

  analyzeImage(nextImage);
}
</code></pre>
<p><code>handleFile</code> does the preparation work for every new image. It rejects anything that isn't a browser-readable image, converts the file to base64, and builds a single object holding everything the rest of the flow needs: the original <code>File</code> (for its name and MIME type), an object URL for the preview, and the base64 payload for the API call.</p>
<p>It then clears out the previous run the old analysis, any error message, and the "copied" indicator so the UI never shows results from the last image next to a new one. Finally, it kicks off <code>analyzeImage(nextImage)</code> immediately.</p>
<p>Note that it passes the fresh object directly instead of relying on the <code>selectedImage</code> state: React state updates don't apply until the next render, so reading the state here would still give you the <em>previous</em> image.</p>
<p>The <code>Analyze</code> button still exists in the UI, but it works as a manual rerun button.</p>
<h3 id="heading-send-the-image-to-the-companion">Send the Image to the Companion</h3>
<p>Here's the core request:</p>
<pre><code class="language-ts">const analysisRequestId = useRef(0);

async function analyzeImage(image = selectedImage) {
  if (!image) {
    setError("Choose an image first.");
    return;
  }

  const requestId = analysisRequestId.current + 1;
  analysisRequestId.current = requestId;

  setRequestState("loading");
  setError(null);
  setCopied(false);

  try {
    const response = await fetch(`${API_BASE_URL}/v1/analyze-image`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        filename: image.file.name,
        mimeType: image.file.type || "application/octet-stream",
        base64: image.base64,
      }),
    });

    const payload = await response.json();

    if (requestId !== analysisRequestId.current) {
      return;
    }

    if (!response.ok) {
      throw new Error(payload.error?.message ?? `Analysis failed with ${response.status}`);
    }

    setAnalysis(payload);
    setRequestState("success");
  } catch (error) {
    if (requestId !== analysisRequestId.current) {
      return;
    }

    setRequestState("error");
    setError(error instanceof Error ? error.message : "Could not analyze image");
  }
}
</code></pre>
<p>This function is the entire client side of the bridge. It flips <code>requestState</code> to <code>loading</code> (which drives the spinner and disables the button), then sends a <code>POST</code> to <code>/v1/analyze-image</code> with a JSON body containing three fields: the filename, the MIME type, and the base64 image data. That body maps one-to-one onto the <code>AnalyzeImageRequest</code> struct the Swift companion decodes later.</p>
<p>Notice that the response is parsed as JSON <em>before</em> checking <code>response.ok</code>. That's deliberate: when the companion rejects a request (bad base64, oversized image), it still returns a JSON body with an <code>error.message</code> field, so the UI can show the companion's own explanation instead of a generic status code. On success, the payload goes straight into state, and the JSON viewer re-renders with the result.</p>
<p>The <code>requestId</code> bookkeeping guards against stale responses. If a user uploads a second image while the first is still analyzing, whichever request finishes <em>last</em> would win, and OCR plus model generation takes long enough that responses can genuinely arrive out of order. So every call increments a counter stored in a ref and remembers its own ID.</p>
<p>After the <code>await</code>, it checks whether it's still the newest request; if a newer upload started in the meantime, the older response is silently discarded instead of overwriting the latest image's result. The same check runs in the <code>catch</code> block, so an old failure can't clobber a newer success either. If you also want to cancel the in-flight HTTP request rather than just ignore its result, an <code>AbortController</code> is the natural next step.</p>
<h3 id="heading-render-the-json-output">Render the JSON Output</h3>
<p>The output pane uses <code>react-json-view-lite</code>:</p>
<pre><code class="language-tsx">&lt;JsonView
  data={jsonData}
  shouldExpandNode={allExpanded}
  style={jsonViewTheme}
/&gt;
</code></pre>
<h2 id="heading-build-the-macos-companion-app">Build the macOS Companion App</h2>
<p>The companion is a Swift command-line app. It exposes a small local HTTP API.</p>
<p>If you come from the web side, the mapping is simple: Swift Package Manager is Swift's npm, <code>Package.swift</code> is its <code>package.json</code>, and <code>swift run</code> is its <code>npm start</code>. It ships with Xcode, so there's nothing extra to install.</p>
<p>The <code>Package.swift</code> file looks like this:</p>
<pre><code class="language-swift">// swift-tools-version: 6.0

import PackageDescription

let package = Package(
    name: "VisionBridgeCompanion",
    platforms: [
        .macOS("26.0")
    ],
    products: [
        .executable(
            name: "VisionBridgeCompanion",
            targets: ["VisionBridgeCompanion"]
        )
    ],
    targets: [
        .executableTarget(
            name: "VisionBridgeCompanion"
        )
    ]
)
</code></pre>
<p>The companion imports the Apple frameworks it needs:</p>
<pre><code class="language-swift">import Foundation
import FoundationModels
import ImageIO
import Network
import Vision
</code></pre>
<p>It listens on <code>127.0.0.1:43119</code>:</p>
<pre><code class="language-swift">private let defaultPort: UInt16 = 43119
</code></pre>
<p>The app exposes two routes:</p>
<pre><code class="language-swift">switch (request.method, request.path) {
case ("GET", "/v1/health"):
    let health = HealthResponse(support: ModelSupport.current)
    return try json(health)

case ("POST", "/v1/analyze-image"):
    let payload = try JSONDecoder().decode(AnalyzeImageRequest.self, from: request.body)
    let response = try await service.analyze(payload)
    return try json(response)

default:
    return try json(
        ErrorResponse(error: APIErrorPayload(message: "Route not found")),
        status: .notFound
    )
}
</code></pre>
<p>This <code>switch</code> is the companion's entire routing layer — no web framework, just pattern matching on the method and path.</p>
<p>The two routes split the work cleanly:</p>
<ul>
<li><p><code>GET /v1/health</code> is the cheap, read-only route. It runs no analysis, it just reports whether Vision and Foundation Models are usable on this Mac via <code>ModelSupport.current</code> (covered in the next section). The React app calls it on load to render the online/offline status pill, so the user knows the bridge is up before they upload anything.</p>
</li>
<li><p><code>POST /v1/analyze-image</code> is where the real work happens. It decodes the request body into an <code>AnalyzeImageRequest</code> (with the same <code>filename</code>, <code>mimeType</code>, and <code>base64</code> fields the browser sent) and hands it to the analysis service. This validates the image, runs Vision OCR, prompts Foundation Models, and returns the combined result. The <code>try await</code> matters here: analysis is asynchronous, and the route simply waits for it before serializing the response.</p>
</li>
</ul>
<p>Anything else falls through to a JSON 404, so even unknown routes respond in the same format the browser already knows how to parse.</p>
<p>Errors work the same way: thrown errors are caught in one place and converted into JSON error responses with an appropriate status code, which is exactly what the web app's <code>payload.error?.message</code> check reads.</p>
<p>One practical detail: because the browser calls the companion from a different origin (the Vite dev server), every response also carries CORS headers, and the router answers preflight <code>OPTIONS</code> requests with an empty <code>204</code>. Without that, the browser would block the <code>fetch</code> before it ever reached these routes.</p>
<h2 id="heading-check-foundation-models-availability">Check Foundation Models Availability</h2>
<p>The companion shouldn't assume that the model is available. Check it first:</p>
<pre><code class="language-swift">private struct ModelSupport: Encodable {
    let visionAvailable: Bool
    let foundationModelAvailable: Bool
    let foundationModelStatus: String

    static var current: ModelSupport {
        let model = SystemLanguageModel.default

        switch model.availability {
        case .available:
            return ModelSupport(
                visionAvailable: true,
                foundationModelAvailable: true,
                foundationModelStatus: "available"
            )

        case .unavailable(let reason):
            return ModelSupport(
                visionAvailable: true,
                foundationModelAvailable: false,
                foundationModelStatus: "unavailable.\(reason.description)"
            )

        @unknown default:
            return ModelSupport(
                visionAvailable: true,
                foundationModelAvailable: false,
                foundationModelStatus: "unavailable.unknown"
            )
        }
    }
}
</code></pre>
<p>A user might have an unsupported Mac, Apple Intelligence might be disabled, or the model might not be ready yet. The response tells the browser which case it's dealing with.</p>
<h2 id="heading-extract-text-with-apple-vision">Extract Text with Apple Vision</h2>
<p>The companion decodes the base64 image, checks its metadata, then runs Vision OCR.</p>
<p>Here's the text recognition flow:</p>
<pre><code class="language-swift">private func recognizeText(in imageData: Data) async throws -&gt; [DetectedText] {
    var request = RecognizeTextRequest()
    request.recognitionLevel = .accurate
    request.automaticallyDetectsLanguage = true
    request.usesLanguageCorrection = true

    let observations = try await request.perform(on: imageData)

    var detectedText: [DetectedText] = []

    for observation in observations {
        guard let candidate = observation.topCandidates(1).first else {
            continue
        }

        let bounds = NormalizedBox.from(points: [
            observation.topLeft,
            observation.topRight,
            observation.bottomRight,
            observation.bottomLeft
        ])

        detectedText.append(DetectedText(
            text: candidate.string,
            confidence: Double(candidate.confidence),
            boundingBox: bounds
        ))
    }

    return detectedText
}
</code></pre>
<p>Vision gives us structured observations:</p>
<ul>
<li><p>recognized text</p>
</li>
<li><p>confidence scores</p>
</li>
<li><p>normalized bounding boxes</p>
</li>
</ul>
<p>Those observations become the model’s context.</p>
<h2 id="heading-ask-foundation-models-to-explain-the-vision-output">Ask Foundation Models to Explain the Vision Output</h2>
<p>Now the companion creates a prompt from the image metadata and OCR results.</p>
<p>Notice the instruction:</p>
<pre><code class="language-text">You cannot see the original image. Use only the metadata and OCR observations below.
</code></pre>
<p>That keeps the model honest. It shouldn't pretend to see pixels it never received.</p>
<p>Here's the prompt shape:</p>
<pre><code class="language-swift">let textPreview = detectedText
    .prefix(30)
    .map { "- \($0.text) (confidence: \(String(format: "%.2f", $0.confidence)))" }
    .joined(separator: "\n")

let prompt = """
You are summarizing Apple Vision OCR output for a developer tool named Vision Bridge.
You cannot see the original image. Use only the metadata and OCR observations below.

Image:
- filename: \(image.filename)
- content type: \(image.contentType)
- size: \(image.width ?? 0)x\(image.height ?? 0)

OCR observations:
\(textPreview.isEmpty ? "- No text detected." : textPreview)

Return a compact JSON object with these exact keys:
summary: one sentence
description: one short paragraph
suggestedTags: 3 to 6 short tags
possibleUses: 3 to 5 practical use cases for this kind of image analysis
"""
</code></pre>
<p>Then call the model:</p>
<pre><code class="language-swift">let session = LanguageModelSession(
    model: .default,
    instructions: "Return valid JSON only. Do not include Markdown fences."
)

let response = try await session.respond(to: prompt)
let raw = response.content.trimmingCharacters(in: .whitespacesAndNewlines)
</code></pre>
<p>Even when you ask for JSON, always validate the output. Models can still return Markdown fences or malformed text. The sample app strips simple Markdown code fences and falls back to a raw response if parsing fails.</p>
<h2 id="heading-return-json-to-the-browser">Return JSON to the Browser</h2>
<p>The companion combines the support state, image metadata, Vision results, and model output:</p>
<pre><code class="language-swift">return AnalyzeImageResponse(
    support: support,
    image: metadata,
    vision: VisionPayload(detectedText: detectedText),
    model: modelInsight
)
</code></pre>
<p>The browser doesn't need to know how Vision or Foundation Models work. It just receives JSON. The native app owns the native capabilities, while the web app owns the interface.</p>
<p>It's worth pausing on what each of the four blocks actually gives you, because they're not all the same kind of data:</p>
<ul>
<li><p><code>support</code> tells you what was possible on this Mac. If <code>foundationModelAvailable</code> is <code>false</code>, the <code>model</code> block still exists but contains a fallback message rather than real analysis, and the <code>foundationModelStatus</code> string (for example, <code>unavailable.appleIntelligenceNotEnabled</code>) tells the UI <em>why</em>, so it can explain rather than silently degrade.</p>
</li>
<li><p><code>image</code> echoes back the file's metadata plus the measured pixel dimensions. It's useful as a sanity check, and you need the width and height to do anything spatial with the Vision results.</p>
</li>
<li><p><code>vision</code> is the ground truth. Each entry in <code>detectedText</code> is a string Vision actually found, with a confidence score between 0 and 1 and a normalized bounding box: coordinates expressed as fractions of the image size, so <code>x: 0.12, width: 0.45</code> means "starts 12% from the left and spans 45% of the width." Because the boxes are normalized, you can draw highlight overlays on the preview at any display size by multiplying by the rendered dimensions. Low-confidence entries are worth filtering or flagging before you trust them.</p>
</li>
<li><p><code>model</code> is interpretation, not observation. The <code>summary</code>, <code>description</code>, <code>suggestedTags</code>, and <code>possibleUses</code> fields are generated by the language model from the OCR text. This is useful as alt text, captions, or tag suggestions, but they inherit whatever the OCR missed and should be treated as a draft, not a fact. When the model's output can't be parsed as JSON, <code>rawResponse</code> carries the unparsed text so nothing is lost.</p>
</li>
</ul>
<p>For a screenshot of a failed build, the model block might come back like this:</p>
<pre><code class="language-json">{
  "model": {
    "summary": "The image appears to show a software build failure.",
    "description": "A developer tool window is showing an error state with diagnostic text.",
    "suggestedTags": ["screenshot", "developer-tool", "error"],
    "possibleUses": [
      "Generate alt text",
      "Summarize screenshots",
      "Extract document data"
    ]
  }
}
</code></pre>
<p>That combination (exact text with positions from Vision, plus a human-readable interpretation from the model) is enough to build real features on top of a searchable screenshot library indexed by <code>detectedText</code> and <code>suggestedTags</code>, automatic alt text for uploaded images, or click-to-highlight overlays powered by the bounding boxes.</p>
<p>And because the prompt lives in the companion, changing what comes back (say, extracting line items from receipts instead of tagging screenshots) is a prompt edit, not an architecture change.</p>
<h2 id="heading-run-the-app">Run the App</h2>
<p>Start the companion:</p>
<pre><code class="language-sh">npm run companion
</code></pre>
<p>In another terminal, start the web app:</p>
<pre><code class="language-sh">npm run dev
</code></pre>
<p>Open the Vite URL:</p>
<pre><code class="language-text">http://127.0.0.1:5173
</code></pre>
<p>If that port is busy, Vite will choose another one.</p>
<p>The companion should be available at:</p>
<pre><code class="language-text">http://127.0.0.1:43119
</code></pre>
<p>You can test it directly:</p>
<pre><code class="language-sh">curl http://127.0.0.1:43119/v1/health
</code></pre>
<p>Expected response:</p>
<pre><code class="language-json">{
  "app": "Vision Bridge Companion",
  "ok": true,
  "support": {
    "foundationModelAvailable": true,
    "foundationModelStatus": "available",
    "visionAvailable": true
  },
  "version": "0.1.0"
}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/5db93b3da2342e8354088115/76a47c9a-934c-4133-ba7c-e2a9c6b6dad4.png" alt="Screenshot of terminal running companion" style="display:block;margin:0 auto" width="1448" height="556" loading="lazy">

<h2 id="heading-conclusion">Conclusion</h2>
<p>You now have a React interface that uploads an image, a Swift companion that analyzes it with Apple-native frameworks, and structured JSON flowing between them.</p>
<p>Vision Bridge is intentionally small, but the bridge itself is reusable. Once you have a trusted native companion, a web app can do more than send prompts to a remote model: it can ask the Mac to work with local context, use any Apple framework, and return structured data the browser can render, store, or sync.</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><a href="https://developer.apple.com/documentation/foundationmodels">Apple Foundation Models documentation</a></p>
</li>
<li><p><a href="https://developer.apple.com/documentation/vision">Apple Vision documentation</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Why "It Worked on My Machine" Still Happens in 2026 ]]>
                </title>
                <description>
                    <![CDATA[ Every engineering team has said it at least once: "It works on my machine." The phrase has become a running joke in software, but it's rarely funny when it happens in production. A feature passes ever ]]>
                </description>
                <link>https://www.freecodecamp.org/news/why-it-worked-on-my-machine-still-happens-in-2026/</link>
                <guid isPermaLink="false">6a57cee36dcb86f0029afe5f</guid>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ PaaS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Wed, 15 Jul 2026 18:18:11 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/8435d1c8-af34-4cff-8891-087ac2a3ad9d.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Every engineering team has said it at least once: "It works on my machine."</p>
<p>The phrase has become a running joke in software, but it's rarely funny when it happens in production.</p>
<p>A feature passes every local test, the pull request gets approved, the deployment finishes successfully, and then users start reporting failures.</p>
<p>On-call engineers get paged. Incident channels fill up. A fix that took ten minutes to write takes four hours to trace back to a missing environment variable or a runtime version mismatch nobody noticed.</p>
<p>The strange part is that this still happens in 2026.</p>
<p>Modern development has better tooling than ever. Containers, automated testing, cloud infrastructure, CI/CD pipelines, infrastructure as code, and AI coding assistants have all made building software considerably faster.</p>
<p>And yet engineering teams running customer-facing applications continue to lose significant time chasing bugs that only appear outside a developer's laptop.</p>
<p>One <a href="https://queue.acm.org/detail.cfm?id=3068754/">industry survey</a> found that developers spend roughly 40% of their time on tasks unrelated to writing features,&nbsp; and environment debugging is a leading culprit.</p>
<p>The reason isn't that engineers are careless. Most software doesn't fail because of bad code. It fails because code runs inside an environment, and those environments are rarely identical.</p>
<p>The gap between a developer's laptop and a production cluster is still one of the most consistent sources of engineering waste these days.</p>
<p>The real question is no longer why this problem exists. Every experienced engineering team understands environment drift. The better question is why so many product teams are still spending engineering time managing it.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-every-machine-tells-a-slightly-different-story">Every Machine Tells a Slightly Different Story</a></p>
</li>
<li><p><a href="#heading-dependencies-are-moving-targets">Dependencies Are Moving Targets</a></p>
</li>
<li><p><a href="#heading-configuration-causes-more-incidents-than-code-does">Configuration Causes More Incidents Than Code Does</a></p>
</li>
<li><p><a href="#heading-the-real-cost-of-managing-multiple-environments">The Real Cost of Managing Multiple Environments</a></p>
</li>
<li><p><a href="#heading-why-are-teams-still-managing-this-themselves-in-2026">Why Are Teams Still Managing This Themselves in 2026?</a></p>
</li>
<li><p><a href="#heading-local-success-doesnt-reflect-production-conditions">Local Success Doesn't Reflect Production Conditions</a></p>
</li>
<li><p><a href="#heading-why-are-more-engineering-teams-choosing-managed-platforms">Why Are More Engineering Teams Choosing Managed Platforms?</a></p>
</li>
<li><p><a href="#heading-what-a-basic-paas-setup-actually-looks-like">What a Basic PaaS Setup Actually Looks Like</a></p>
</li>
<li><p><a href="#heading-consistency-is-an-ownership-question-not-a-tooling-question">Consistency is an Ownership Question, Not a Tooling Question</a></p>
</li>
</ul>
<h2 id="heading-every-machine-tells-a-slightly-different-story"><strong>Every Machine Tells a Slightly Different Story</strong></h2>
<p>A production application depends on much more than source code.</p>
<p>It depends on the operating system, runtime versions, environment variables, databases, third-party services, networking rules, file permissions, installed libraries, and CPU architecture.</p>
<p>A developer running Node.js 24 LTS may be pairing with a teammate still on 22. One laptop has a newer OpenSSL version installed as a transitive dependency update. Another has a cached <a href="https://www.incredibuild.com/glossary/build-artifacts">build artefact</a> from three months ago that quietly changed behaviour after a library patch.</p>
<p>None of these differences looks significant on their own. Together, they create a local environment that behaves differently from every other environment in the pipeline.</p>
<p>This is how a test suite passes green on a developer's machine and fails in CI twenty minutes later. It's how an application boots cleanly on macOS but crashes on the Debian container your cloud provider runs.</p>
<p>It's why a microservice that handled 500 requests per second last Tuesday starts timing out this Monday after what appeared to be an unrelated dependency bump.</p>
<p>The code hasn't changed. The environment has.</p>
<h2 id="heading-dependencies-are-moving-targets"><strong>Dependencies Are Moving Targets</strong></h2>
<p>Package managers have made software development productive, but they've also dramatically increased the surface area of a running application.</p>
<p>A typical Node.js web application today has between 500 and 1,500 packages in its dependency tree, including indirect dependencies, even when a developer explicitly installs only a handful.</p>
<p>A Python service using common data processing and web frameworks can pull in 200 to 400 packages. Most engineers have no direct relationship with the vast majority of packages their application ships.</p>
<p>When dependency versions aren't locked correctly, two developers installing the same project on the same day can receive materially different software stacks. Lock files like package-lock.json, pnpm-lock.yaml, poetry.lock, Cargo.lock&nbsp;exist precisely to prevent this, and they help. But they're one layer of control in a much larger consistency problem.</p>
<p>Runtime versions still differ. System libraries still differ. Base OS images in containers drift across patch cycles. A Docker image built from node:22 today isn't the same image that gets built in six weeks when the upstream tag has been updated. Teams that don't pin their base images precisely are unknowingly accepting environment drift at the foundation of every deployment.</p>
<h2 id="heading-configuration-causes-more-incidents-than-code-does"><strong>Configuration Causes More Incidents Than Code Does</strong></h2>
<p>Many of the most disruptive production incidents on engineering teams have nothing to do with programming logic.</p>
<p>They come from configuration.</p>
<p>An environment variable is missing in the new deployment target. A database connection string points to staging instead of production. A feature flag is set to true in the developer's .env file but defaults to false in the deployed service, silently disabling a critical code path. An API key was rotated but the secret manager reference was updated in one environment and not the other.</p>
<p>These mistakes are common, and genuinely difficult to prevent,&nbsp;because configuration lives outside the application. It's managed separately, documented inconsistently, and almost never covered by standard test suites.</p>
<p>Post-incident reviews regularly surface configuration drift as the root cause of outages that took hours to diagnose because the application code looked completely correct.</p>
<p>The problem compounds across environments. A team running development, staging, pre-production, and production has four separate configuration states to keep aligned.</p>
<p>When an engineer adds a new environment variable, that change has to propagate through every environment reliably. In practice, it often doesn't. One environment gets missed. An old value lingers. The application behaves differently, and the investigation starts from scratch.</p>
<h2 id="heading-the-real-cost-of-managing-multiple-environments"><strong>The Real Cost of Managing Multiple Environments</strong></h2>
<p>Engineering leadership often underestimates how much time is consumed by environment management, not because it's hard to observe, but because it's distributed across dozens of small tasks that never appear as line items.</p>
<p>Someone updates the Node runtime in the base Docker image and spends an afternoon chasing a downstream test failure that turned out to be a transitive dependency incompatibility.</p>
<p>Someone provisions a new staging environment and spends a day replicating the production configuration by hand. Someone rotates credentials, misses one service, and triggers a silent failure that takes until the next deployment cycle to surface. Someone joins the team and spends the first two days getting a local environment running instead of shipping work.</p>
<p>Estimates from engineering productivity research suggest that infrastructure and environment-related tasks consume between 15 - 25% of total engineering capacity at companies that own their own deployment infrastructure. For a team of ten engineers, that's effectively two to three people running hard and producing no customer-facing output.</p>
<p>This is the cost that doesn't appear on sprint boards. It lives in Slack threads, in incident retrospectives, and in the quiet acknowledgement that the team is slower than it should be.</p>
<p>None of this work appears on a roadmap. Customers never ask for it. It doesn’t create differentiation. Yet product teams spend hundreds of engineering hours every year maintaining consistency between environments simply to keep software deployable. Environment drift isn't just a reliability problem. It's an engineering capacity problem.</p>
<h2 id="heading-why-are-teams-still-managing-this-themselves-in-2026"><strong>Why Are Teams Still Managing This Themselves in 2026?</strong></h2>
<p>Given all of this, the reasonable question is why so many engineering teams are still owning this complexity directly.</p>
<p>Part of the answer is inertia. Teams that built their infrastructure several years ago, when Kubernetes was the obvious answer to every scaling question and "we control our own stack" felt like a competitive advantage, now maintain that infrastructure because changing it has a cost.</p>
<p>The investment is already made. The tooling is already familiar. The pain is distributed and chronic rather than acute, which makes it easier to absorb than to address.</p>
<p>Part of the answer is organisational habit. Hiring a platform or DevOps engineer to manage infrastructure feels like the right response to environmental problems. But that engineer becomes responsible for maintaining the consistency layer indefinitely. Patching base images, updating runtime versions, managing certificate renewals, and debugging networking issues across environments, rather than delivering product leverage.</p>
<p>Part of the answer is a belief that more control produces better outcomes. Running your own infrastructure gives complete visibility into every configuration decision.</p>
<p>But complete control also means complete responsibility. Every decision the platform team makes is a decision the platform team must maintain, document, and revisit every time something upstream changes.</p>
<p>Most product engineering teams aren't in the infrastructure business. They're in the business of building software for customers, and every hour spent on environment consistency is an hour not spent on that.</p>
<p>The honest answer is that many teams are managing this complexity themselves because they haven't yet found a clear path to stopping.</p>
<h2 id="heading-local-success-doesnt-reflect-production-conditions"><strong>Local Success Doesn't Reflect Production Conditions</strong></h2>
<p>One consistent failure mode is treating a passing local test as a signal that a deployment is safe.</p>
<p>Production environments impose conditions that development machines never encounter. A service that starts cleanly on a laptop with no concurrent users will behave differently when handling 2,000 requests per second with three application instances competing for a shared database connection pool.</p>
<p>A background job that completes in milliseconds locally may time out in production when it runs simultaneously with twelve other jobs against a database under real write load.</p>
<p>Staging environments exist to surface these differences before they reach users. But staging only provides value when it actually resembles production, like the same infrastructure, the same runtime versions, the same configuration shape, same network topology.</p>
<p>Many teams treat staging as a best-effort approximation. Over time, configuration drift between staging and production means that staging stops catching the failures it was designed to catch. Teams end up discovering environment-related issues in production anyway, which is the worst place to find them.</p>
<p>Maintaining genuine parity across three or four environments is expensive and requires continuous attention. Infrastructure updates must be applied uniformly. Runtime versions must stay synchronised. Configuration must be propagated reliably.</p>
<p>Without active discipline, staging drifts away from production, and the safety net disappears.</p>
<h2 id="heading-why-are-more-engineering-teams-choosing-managed-platforms"><strong>Why Are More Engineering Teams Choosing Managed Platforms?</strong></h2>
<p>At some point, every engineering organisation has to ask a more fundamental question. Should we keep investing engineering time into maintaining environments, or should we move that responsibility to a platform built for it?</p>
<p>This is the context in which <a href="https://www.freecodecamp.org/news/my-team-s-experience-moving-from-aws-to-a-paas/">Platform as a Service</a> has become a more serious consideration for teams that previously managed their own infrastructure.</p>
<p>A well-designed PaaS doesn't remove engineering responsibility. It relocates it.</p>
<p>Developers still write code. They still define environment variables and build processes. They still decide what their application needs. The difference is that the platform provides a consistent, maintained runtime across every environment like development, staging, and production, without the team owning the underlying infrastructure.</p>
<p>The same application definition runs everywhere. Environment parity becomes a property of the platform rather than a discipline the team has to enforce continuously.</p>
<p>This matters most to engineering teams with real deployment velocity, the teams shipping multiple times per day, running several services, and operating with the expectation that deployments are predictable.</p>
<p>When the platform standardises the environment, deployments stop being experiments. Engineers stop discovering production-only failures at the worst possible time.</p>
<p>The operational tradeoff is real. Some organisations require control over their infrastructure for compliance, regulatory, or architectural reasons that a PaaS can't accommodate. But many teams that believe they need that control have never closely examined the cost of maintaining it.</p>
<p>The question isn't whether owning infrastructure gives you control. It's whether that control is producing outcomes that justify the engineering capacity it consumes.</p>
<h2 id="heading-what-a-basic-paas-setup-actually-looks-like"><strong>What a Basic PaaS Setup Actually Looks Like</strong></h2>
<p>The argument for a managed platform is easier to evaluate with a concrete picture of what adopting one involves. The details vary across providers like Render, Railway, Sevalla, etc, but the setup's shape is remarkably consistent and smaller than most teams expect.</p>
<p><strong>Step 1: Connect your repository.</strong> Every mainstream PaaS starts from your Git repository. You authorise the platform against GitHub or GitLab, point it at a repo, and choose a branch to deploy from. From that moment, the platform watches for pushes. There's no CI pipeline to write for the basic case, since build-and-deploy on push is the default behaviour.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66c6d8f04fa7fe6a6e337edd/46fdb46c-5b94-408c-ad69-bfca34992776.png" alt="Connect repository" style="display:block;margin:0 auto" width="1000" height="825" loading="lazy">

<p><strong>Step 2: Define the application, once, in a file.</strong> Instead of configuring servers, you describe what your application is: the runtime, the build command, the start command, and the services it needs. Most platforms let you do this through a dashboard, but the better practice is a declarative file that lives in the repo.</p>
<pre><code class="language-yaml">services:
  - type: web
    name: my-api
    runtime: node
    buildCommand: npm ci
    startCommand: npm run start
    envVars:
      - key: DATABASE_URL
        fromDatabase:
          name: my-api-db
          property: connectionString
      - key: NODE_ENV
        value: production

databases:
  - name: my-api-db
    plan: basic
</code></pre>
<p>This file is the payoff of the whole model. It's the single source of truth for how the application runs, it's version-controlled alongside the code, and, critically, it's the <em>same definition</em> in every environment.</p>
<p>The drift described earlier in this article, where staging quietly diverges from production, has nowhere to live, because there is no second copy of the environment to fall out of sync.</p>
<p><strong>Step 3: Set your environment variables in the platform, not in files.</strong> Secrets and configuration move out of scattered <code>.env</code> files and into the platform's environment settings, scoped per environment.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66c6d8f04fa7fe6a6e337edd/153f83a9-7aab-4f08-9f19-f6d73b7ccafa.png" alt="Environment variables" style="display:block;margin:0 auto" width="1000" height="293" loading="lazy">

<p>When an engineer adds a new variable, the platform surfaces it in one place rather than requiring a manual update across four deployment targets. Most platforms also support environment groups, so shared configuration is defined once and inherited.</p>
<p><strong>Step 4: Attach managed services.</strong> Databases, caches, and cron jobs are provisioned by the platform rather than installed and patched by your team.</p>
<p>In the example above, the database is declared in the same file as the application, and its connection string is injected automatically, so there's no connection string to copy incorrectly into staging.</p>
<p><strong>Step 5: Push, and let preview environments do the rest.</strong> This is where the parity argument becomes tangible. Most modern PaaS providers spin up a preview environment for every pull request: a full, disposable copy of the application, built from the same definition file, running on the same infrastructure as production.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66c6d8f04fa7fe6a6e337edd/2867152a-233a-4c12-91d3-3f05b56f79e2.png" alt="Deployment" style="display:block;margin:0 auto" width="1000" height="483" loading="lazy">

<p>"Works on my machine" stops being the standard of evidence, because every reviewer is looking at the code running in a production-shaped environment before it merges. When the PR closes, the environment is destroyed.</p>
<p>That's the whole setup. For a typical web service, going from repository to a deployed, auto-updating application with a managed database takes an afternoon, not a quarter.</p>
<p>For teams with existing infrastructure, the sensible starting point isn't a migration project. It's one service , ideally something low-risk and self-contained, like an internal tool or a background worker.</p>
<p>Run it on a platform for a month, compare the operational load against its Kubernetes-hosted siblings, and let the result inform the larger decision. Most teams that make this comparison discover the question isn't whether the platform can handle their workload. It's how much of their engineering week they'd been spending to get a worse version of the same guarantee.</p>
<h2 id="heading-consistency-is-an-ownership-question-not-a-tooling-question"><strong>Consistency is an Ownership Question, Not a Tooling Question</strong></h2>
<p>"It worked on my machine" gets framed as a process problem, or a testing problem, or occasionally a culture problem. The real framing is more useful: it's an ownership problem.</p>
<p>Every difference between environments like runtime versions, dependency trees, configuration values, and infrastructure state increases the probability that software behaves unexpectedly in production. The conventional response is to invest in better tooling: stricter lock files, more comprehensive CI, better container discipline, more thorough staging.</p>
<p>All of these reduce the problem. None of them eliminates the underlying dynamic, which is that the team is responsible for the consistency of every environment it owns.</p>
<p>The teams that have largely solved deployment reliability in 2026 aren't necessarily the ones with the most sophisticated infrastructure. Many of them are the ones that have reduced the number of environments they own and maintain.</p>
<p>They have moved infrastructure decisions to platforms designed to handle them, and redirected that engineering capacity toward problems that are actually differentiated: the product, the performance, the reliability of the application itself.</p>
<p>Environment consistency is a solvable problem. The remaining question is ownership. Every product team must decide whether maintaining infrastructure is part of its competitive advantage or simply an operational burden it has accepted over time. More engineering teams are concluding that their advantage comes from shipping product, not managing environments.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ The Heroku Nostalgia Trap: Why Easy Deploys Aren't the Only Answer ]]>
                </title>
                <description>
                    <![CDATA[ There's a sentiment I've seen in Slack threads, Hacker News comments, and late-night Discord vents more times than I can count: "I just want something like Heroku." I've said it myself. And I was wr ]]>
                </description>
                <link>https://www.freecodecamp.org/news/the-heroku-nostalgia-trap-why-easy-deploys-arent-the-only-answer/</link>
                <guid isPermaLink="false">6a4c4a3acf22af9f521e9536</guid>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Heroku ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Iroro Chadere ]]>
                </dc:creator>
                <pubDate>Tue, 07 Jul 2026 00:37:14 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/6bc939c6-c2b8-40f7-9238-101ebd57d39c.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>There's a sentiment I've seen in Slack threads, Hacker News comments, and late-night Discord vents more times than I can count:</p>
<blockquote>
<p><em>"I just want something like Heroku."</em></p>
</blockquote>
<p>I've said it myself. And I was wrong about what I meant.</p>
<p>Not wrong that Heroku was good. It was genuinely great, and the people who built it understood something most infrastructure companies still haven't internalized.</p>
<p>But when Salesforce killed the free tier in 2022 (and recently when it announced the <a href="https://www.heroku.com/blog/an-update-on-heroku/">end of sale for new enterprise customers</a>) and the migration scramble started, something interesting happened: almost everyone reached for the wrong lesson.</p>
<p>They thought the thing people loved about Heroku was easy deploys. So they built easier deploys.</p>
<p>But that wasn't it.</p>
<h2 id="heading-what-heroku-actually-did">What Heroku Actually Did</h2>
<p>When I was running a side project on Heroku sometime around 2019 or so, I didn't think about infrastructure at all. I pushed to Git, it deployed. My Postgres was there. If I needed a queue, I added a plugin. Everything lived in the same mental model, and that mental model took up approximately zero space in my head on a normal workday.</p>
<p>The thing Heroku sold wasn't a deployment pipeline. It was cognitive zero. The absence of infrastructure as a thing you had to think about.</p>
<p>That distinction matters more than it sounds. Because when you have to think about infrastructure, even a little, it bleeds into everything.</p>
<p>You make product decisions based on what's easy to deploy, not what's right. You delay shipping because you're not sure how the pieces connect. You spend a Friday afternoon debugging why your app can't reach its own database across two vendor networks instead of building the feature that would've landed three new customers.</p>
<p>Heroku removed all of that. One platform, everything pre-wired, one bill, one place to look when something breaks.</p>
<h2 id="heading-the-alternatives-got-one-thing-right-and-missed-the-rest-of-the-point">The Alternatives Got One Thing Right and Missed the Rest of the Point</h2>
<p>After Heroku's free tier died, Render, Railway, and Fly.io stepped up. All three are genuinely better than wrangling EC2 yourself. I've used all of them. I have no interest in being unfair to any of them.</p>
<p>But here's what actually happens when you use them.</p>
<p>You deploy your app to Render. Then you provision a Postgres database, also on Render, and paste the connection string into your environment variables.</p>
<p>Then you realize you need background jobs, so you sign up for Upstash or add a Redis instance somewhere. Then you want object storage, so you create an S3 bucket on AWS because Render doesn't have that.</p>
<p>Now you have three dashboards, three billing relationships, three sets of network rules to get right, and the mental overhead of three vendors stitched together with environment variables and prayer.</p>
<p>Railway is closer to the old vision, and it feels more integrated. But it runs on AWS underneath. You're paying Railway's margin on top of Amazon's margin, which means you're paying the hyperscaler tax twice: once to Amazon for the actual compute, and once to Railway for the privilege of not talking to Amazon directly.</p>
<p>Fly.io made the most interesting bet: they went bare metal. Real hardware, no AWS underneath, which structurally breaks the double-margin problem. But the integration story never quite closed. Your Postgres on Fly is still a thing you wire up separately. The "everything connected" feeling that made Heroku feel magical isn't there. You're still the one holding the wires.</p>
<h2 id="heading-the-thing-nobody-talks-about-egress-charges-between-your-own-services">The Thing Nobody Talks About: Egress Charges Between Your Own Services</h2>
<p>Here's something that took me longer to fully internalize than it should have.</p>
<p>When your app, your database, and your storage bucket live on different vendors, or even different services inside a cloud provider, data moving between them costs money. Not a lot, usually, until it is a lot.</p>
<p>An app that reads from a database 10,000 times a day, processes some results, and writes to S3 is moving data in three directions constantly. On a hyperscaler-backed platform, some of that movement crosses billing boundaries.</p>
<p>On a platform where compute, Postgres, storage, and queues all live on the same bare metal network? That movement is free, because it never leaves the building.</p>
<p>This isn't a hypothetical. It's a structural difference in how the platforms are architected, and it compounds over time in ways that don't show up clearly on any individual invoice.</p>
<h2 id="heading-opinionated-is-doing-real-work-here-not-marketing-work">"Opinionated" is Doing Real Work Here, Not Marketing Work</h2>
<p>I've worked with teams that spent actual engineering hours debating which queue system to use. Not implementing it. Debating it. SQS vs. BullMQ vs. RabbitMQ vs. something someone read about on a blog three years ago.</p>
<p>The argument for an opinionated platform isn't that you're incapable of making that decision. It's that the decision doesn't matter as much as you think, and the time you spend making it is time you're not spending on the thing that actually differentiates your product.</p>
<p>Postgres is the right database for almost every startup that exists. S3-compatible storage handles almost every file storage use case. A reliable queue is a reliable queue. These aren't interesting decisions. They stopped being interesting about a decade ago. The interesting decisions are in your product.</p>
<p>An opinionated platform forces you to stop relitigating settled questions. That's not a limitation. That's the point.</p>
<h2 id="heading-the-lock-in-question-answered-honestly">The Lock-in Question, Answered Honestly</h2>
<p>The most common pushback I hear when someone looks at a vertically integrated platform is: "What if I want to leave?"</p>
<p>It's a fair question and I used to ask it, too. Here's what I've realized: the lock-in concern is almost always theoretical, and it's usually raised by people who've never actually migrated off a platform.</p>
<p>Real lock-in requires something proprietary that your code depends on. A custom SDK that only works with that vendor. A query language that doesn't exist anywhere else. A deployment model that requires rewriting your app to leave.</p>
<p>If your app runs in a container, uses standard Postgres connection strings, speaks to S3 with an AWS SDK, and publishes jobs to a queue over a standard protocol, you're not locked in. You're just deployed somewhere. The migration path is a <code>pg_dump</code>, a bucket copy, and a new <code>docker push</code>. I've done migrations like that in a weekend.</p>
<p>The platforms that actually create lock-in are the ones that abstract everything into their own proprietary layer. Serverless functions with custom runtimes. Vendor-specific databases with proprietary query features. Edge compute that only exists on one network. Those are the things worth being suspicious of.</p>
<h2 id="heading-where-each-alternative-actually-breaks-down"><strong>Where Each Alternative Actually Breaks Down</strong></h2>
<p>Render is the easiest to recommend and the easiest to outgrow. Deploy a Next.js app, get a managed Postgres, done.</p>
<p>The problem shows up around month three when you need background jobs and object storage. Render doesn't have either natively.</p>
<p>So you reach for Upstash for queues and AWS S3 for storage. Now you have three dashboards, three billing relationships, and three networks that have to trust each other. The deployment step takes minutes. Everything around it takes the afternoon.</p>
<p>Railway feels more integrated than Render and the DX is genuinely good. But it runs on AWS. That's not a criticism of the team. It's a structural fact that has downstream consequences. As I mentioned before, you're paying Railway's margin on top of Amazon's margin, and data moving between your app and your database may cross billing boundaries depending on how Railway has provisioned things. The cost doesn't look alarming on any single invoice. But it compounds.</p>
<p>Fly.io made the most interesting architectural bet. Real hardware, no hyperscaler underneath, which structurally breaks the double-margin problem. \</p>
<p>I've deployed on Fly and the performance at the edge is real. But compute and Postgres are still separate things you wire together. Storage is still an external conversation. The "everything connected" feeling isn't there because the connections are still yours to make.</p>
<h2 id="heading-what-the-i-just-want-something-like-heroku-crowd-actually-needs">What the "I Just Want Something Like Heroku" Crowd Actually Needs</h2>
<p>I've thought about this a lot, and I think the nostalgia is real but misdirected.</p>
<p>People don't want Heroku specifically. They want the feeling that infrastructure is someone else's problem. Not because they can't handle it, but because handling it isn't why they got into building software. They want to push code and have things work. They want one place to look when things break. They want a bill they can understand.</p>
<p>The platforms that came after Heroku optimized for the wrong thing. They made the deployment step easier while leaving the integration work on you. They gave you a better on-ramp to the same fragmented landscape.</p>
<p>The more honest path is a platform that's made the architectural decisions Heroku never fully made: own the hardware, connect the services at the network level, charge one bill, and let the developer focus on code.</p>
<p>That's not nostalgia. That's just what the problem actually requires.\</p>
<h3 id="heading-another-alternative-to-heroku">Another Alternative to Heroku</h3>
<p>Here is what I've been watching: <a href="https://atlasflow.com">Atlasflow</a>. Bare metal, with compute, Postgres, S3-compatible storage, and queues on the same network before you ever touch them. And one bill.</p>
<p>I haven't run a production workload there yet, so I can't speak to reliability under pressure. That matters and I won't pretend it doesn't.</p>
<p>But the architectural argument is the most honest attempt I've seen at solving the actual problem rather than the symptom. Every other platform made deploy easier. Atlasflow is asking whether the integration should have been your problem in the first place.</p>
<p>That's not a small distinction. It's the whole thing.</p>
<p><em>I've been building production apps for close to a decade and belive me, you, the infrastructure landscape has gotten better in almost every measurable way. The integration problem is still mostly unsolved.</em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ From Flutter to Backend: How to Build and Ship Production REST APIs with Dart and Shelf ]]>
                </title>
                <description>
                    <![CDATA[ As a Flutter engineer, you already know Dart. You understand async/await, you work with models and repositories, you think in clean architecture, and you have shipped real applications. The gap betwee ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-and-ship-production-rest-apis-with-dart-and-shelf/</link>
                <guid isPermaLink="false">6a1d92fa080b80f11f574194</guid>
                
                    <category>
                        <![CDATA[ Dart ]]>
                    </category>
                
                    <category>
                        <![CDATA[ backend developments ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ REST API ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Oluwaseyi Fatunmole ]]>
                </dc:creator>
                <pubDate>Mon, 01 Jun 2026 14:11:06 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/8ba5ec9d-22ba-4313-9b34-ce1e0e7dce23.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>As a Flutter engineer, you already know Dart. You understand async/await, you work with models and repositories, you think in clean architecture, and you have shipped real applications.</p>
<p>The gap between where you are and being able to build and deploy a production backend is smaller than you think.</p>
<p>The missing piece is not a new language. It's not a new paradigm. It's understanding how Dart behaves when there's no widget tree, no BuildContext, no Flutter framework – just a running process handling HTTP requests, talking to a database, and sending responses back to clients.</p>
<p>That's exactly what this article covers.</p>
<p>We're going to build a full User and Profile Management REST API from scratch using Dart and Shelf, connect it to a PostgreSQL database running in Docker, secure it with JWT authentication, and deploy it to Fly.io.</p>
<p>By the end, you'll have a working production-grade backend written entirely in Dart, the same language you already know.</p>
<p>This article is part of a series (of standalone articles) where we'll build the same project using three different frameworks. We'll use Shelf here, Serverpod in the next article, and Dart Frog in the one after that. This will let you directly compare how each framework approaches the same problem.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-how-dart-works-on-the-server">How Dart Works on the Server</a></p>
</li>
<li><p><a href="#heading-what-is-shelf">What is Shelf?</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
<ul>
<li><p><a href="#heading-creating-the-project">Creating the Project</a></p>
</li>
<li><p><a href="#heading-project-structure">Project Structure</a></p>
</li>
<li><p><a href="#heading-database-setup-with-docker">Database Setup with Docker</a></p>
</li>
<li><p><a href="#heading-environment-configuration">Environment Configuration</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-shelf-core-concepts">Shelf Core Concepts</a></p>
<ul>
<li><p><a href="#heading-handlers">Handlers</a></p>
</li>
<li><p><a href="#heading-request-and-response">Request and Response</a></p>
</li>
<li><p><a href="#heading-router">Router</a></p>
</li>
<li><p><a href="#heading-pipeline-and-middleware">Pipeline and Middleware</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-connecting-to-postgresql">Connecting to PostgreSQL</a></p>
<ul>
<li><p><a href="#heading-the-database-connection-manager">The Database Connection Manager</a></p>
</li>
<li><p><a href="#heading-running-migrations">Running Migrations</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-building-the-api">Building the API</a></p>
<ul>
<li><p><a href="#heading-the-user-model">The User Model</a></p>
</li>
<li><p><a href="#heading-the-user-repository">The User Repository</a></p>
</li>
<li><p><a href="#heading-user-handlers">User Handlers</a></p>
</li>
<li><p><a href="#heading-the-profile-model">The Profile Model</a></p>
</li>
<li><p><a href="#heading-the-profile-repository">The Profile Repository</a></p>
</li>
<li><p><a href="#heading-profile-handlers">Profile Handlers</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-authentication">Authentication</a></p>
<ul>
<li><p><a href="#heading-password-hashing">Password Hashing</a></p>
</li>
<li><p><a href="#heading-jwt-token-generation-and-validation">JWT Token Generation and Validation</a></p>
</li>
<li><p><a href="#heading-auth-handlers">Auth Handlers</a></p>
</li>
<li><p><a href="#heading-auth-middleware">Auth Middleware</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-error-handling">Error Handling</a></p>
</li>
<li><p><a href="#heading-wiring-everything-together">Wiring Everything Together</a></p>
</li>
<li><p><a href="#heading-deployment">Deployment</a></p>
<ul>
<li><p><a href="#heading-dockerfile">Dockerfile</a></p>
</li>
<li><p><a href="#heading-docker-compose-for-local-production-testing">Docker Compose for Local Production Testing</a></p>
</li>
<li><p><a href="#heading-deploying-to-flyio">Deploying to Fly.io</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-testing-the-api">Testing the API</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before starting, you should have:</p>
<ul>
<li><p>Comfortable familiarity with Dart and Flutter development</p>
</li>
<li><p>Understanding of REST API concepts, endpoints, HTTP methods, status codes</p>
</li>
<li><p>Docker Desktop installed and running</p>
</li>
<li><p>A Fly.io account (free tier is sufficient, fly.io)</p>
</li>
<li><p>The Fly CLI installed (brew install flyctl on macOS, or the official installer on Windows/Linux)</p>
</li>
<li><p>A PostgreSQL client for inspecting the database, like TablePlus or DBeaver – both work well</p>
</li>
</ul>
<h2 id="heading-how-dart-works-on-the-server">How Dart Works on the Server</h2>
<p>When you run a Flutter app, the Flutter framework is doing an enormous amount of work, managing the widget tree, handling the render pipeline, coordinating state, and responding to platform events. Your Dart code sits on top of all of that.</p>
<p>On the server, none of that exists. There's no widget tree. There's no framework managing a UI lifecycle. There's just a Dart process running, listening on a port, receiving HTTP requests, doing work, and sending responses.</p>
<p>Dart's standard library, dart:io, has everything needed to do this at the lowest level:</p>
<pre><code class="language-dart">import 'dart:io';

void main() async {
  final server = await HttpServer.bind('0.0.0.0', 8080);
  print('Server running on port 8080');

  await for (final request in server) {
    request.response
      ..statusCode = 200
      ..write('Hello from Dart')
      ..close();
  }
}
</code></pre>
<p>This is a working HTTP server in raw Dart. No packages, no framework. Every request comes in through the HttpServer stream, and you write directly to the response.</p>
<p>This works, but it scales poorly. As soon as you need routing, middleware, authentication, and structured error handling, raw dart:io becomes difficult to manage. That is the problem Shelf solves.</p>
<h2 id="heading-what-is-shelf">What is Shelf?</h2>
<p>Shelf is a composable web server middleware library for Dart, maintained by the Dart team. It doesn't try to be a full framework – instead, it gives you the primitives to build one, or to assemble exactly what you need.</p>
<p>The Shelf mental model is built on four concepts:</p>
<ul>
<li><p><strong>Handler:</strong> a function that takes a Request and returns a Response. Everything in Shelf is ultimately a handler.</p>
</li>
<li><p><strong>Middleware:</strong> a function that wraps a handler, adding behaviour before or after it runs. Logging, authentication, and error handling are all middleware.</p>
</li>
<li><p><strong>Pipeline:</strong> a chain of middleware with a handler at the end. Requests flow through the middleware chain before reaching the handler.</p>
</li>
<li><p><strong>Router:</strong> maps URL patterns and HTTP methods to specific handlers.</p>
</li>
</ul>
<p>If you've used Flutter's Navigator or provider middleware concepts, the composition model will feel familiar. Small, single-responsibility pieces assembled into a working whole.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<h3 id="heading-creating-the-project">Creating the Project</h3>
<p>Dart includes a server-side project template that gives us a clean starting point:</p>
<pre><code class="language-bash">dart create -t server-shelf user_profile_api
cd user_profile_api
</code></pre>
<p>Add the dependencies we need to pubspec.yaml:</p>
<pre><code class="language-yaml">name: user_profile_api
description: User and Profile Management REST API built with Dart and Shelf
version: 1.0.0

environment:
  sdk: '&gt;=3.0.0 &lt;4.0.0'

dependencies:
  shelf: ^1.4.1
  shelf_router: ^1.1.4
  postgres: ^3.3.0
  dart_jsonwebtoken: ^2.12.0
  bcrypt: ^1.1.3
  dotenv: ^4.1.0
  crypto: ^3.0.3

dev_dependencies:
  lints: ^3.0.0
  test: ^1.24.0
</code></pre>
<p>Run:</p>
<pre><code class="language-bash">dart pub get
</code></pre>
<h3 id="heading-project-structure">Project Structure</h3>
<p>Now we'll build a backend project structure that Flutter engineers will find intuitive, that's familiar enough to navigate immediately, and that's correct enough for backend conventions:</p>
<pre><code class="language-plaintext">user_profile_api/
  bin/
    server.dart              ← entry point
  lib/
    config/
      database.dart          ← connection manager
      env.dart               ← environment config
    handlers/
      auth_handler.dart      ← auth endpoints
      user_handler.dart      ← user endpoints
      profile_handler.dart   ← profile endpoints
    middleware/
      auth_middleware.dart   ← JWT validation
      error_middleware.dart  ← global error handling
      logger_middleware.dart ← request logging
    models/
      user.dart
      profile.dart
    repositories/
      user_repository.dart
      profile_repository.dart
    services/
      auth_service.dart      ← JWT + password logic
    router.dart              ← route definitions
  migrations/
    001_create_users.sql
    002_create_profiles.sql
  docker-compose.yml
  Dockerfile
  .env
  .env.example
</code></pre>
<p>This separation of concerns maps directly to what you'll already know if you're a Flutter engineer: models, repositories, and services are the same concepts. Handlers replace ViewModels or Controllers. Middleware replaces interceptors.</p>
<h3 id="heading-database-setup-with-docker">Database Setup with Docker</h3>
<p>Create docker-compose.yml in the project root:</p>
<pre><code class="language-yaml">version: '3.8'

services:
  postgres:
    image: postgres:16-alpine
    container_name: user_profile_db
    environment:
      POSTGRES_DB: user_profile_api
      POSTGRES_USER: dart_user
      POSTGRES_PASSWORD: dart_password
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:
</code></pre>
<p>Start the database:</p>
<pre><code class="language-bash">docker compose up -d
</code></pre>
<p>Verify that it's running:</p>
<pre><code class="language-bash">docker compose ps
# user_profile_db   running   0.0.0.0:5432-&gt;5432/tcp
</code></pre>
<h3 id="heading-environment-configuration">Environment Configuration</h3>
<p>Create .env in the project root:</p>
<pre><code class="language-plaintext">DB_HOST=localhost
DB_PORT=5432
DB_NAME=user_profile_api
DB_USER=dart_user
DB_PASSWORD=dart_password
JWT_SECRET=your_super_secret_key_change_this_in_production
JWT_EXPIRY_HOURS=24
PORT=8080
</code></pre>
<p>Create .env.example with the same keys but no values. This is what you commit to Git:</p>
<pre><code class="language-plaintext">DB_HOST=
DB_PORT=
DB_NAME=
DB_USER=
DB_PASSWORD=
JWT_SECRET=
JWT_EXPIRY_HOURS=
PORT=
</code></pre>
<p>Add .env to .gitignore:</p>
<pre><code class="language-plaintext">.env
</code></pre>
<p>Create lib/config/env.dart:</p>
<pre><code class="language-dart">import 'package:dotenv/dotenv.dart';

class Env {
  static late final DotEnv _env;

  static void load() {
    _env = DotEnv(includePlatformEnvironment: true)..load();
  }

  static String get dbHost =&gt; _env['DB_HOST'] ?? 'localhost';
  static int get dbPort =&gt; int.parse(_env['DB_PORT'] ?? '5432');
  static String get dbName =&gt; _env['DB_NAME'] ?? 'user_profile_api';
  static String get dbUser =&gt; _env['DB_USER'] ?? 'dart_user';
  static String get dbPassword =&gt; _env['DB_PASSWORD'] ?? '';
  static String get jwtSecret =&gt; _env['JWT_SECRET'] ?? '';
  static int get jwtExpiryHours =&gt; int.parse(_env['JWT_EXPIRY_HOURS'] ?? '24');
  static int get port =&gt; int.parse(_env['PORT'] ?? '8080');
}
</code></pre>
<p>includePlatformEnvironment: true means the Env class reads from both the .env file and real system environment variables, so the same code works locally with a .env file and in production with injected environment variables.</p>
<h2 id="heading-shelf-core-concepts">Shelf Core Concepts</h2>
<p>Before building the API, it's worth understanding each Shelf concept properly – not just what it does, but why it's designed the way it is.</p>
<h3 id="heading-handlers">Handlers</h3>
<p>A handler is the most fundamental unit in Shelf. It's simply a function:</p>
<pre><code class="language-dart">import 'package:shelf/shelf.dart';

Response helloHandler(Request request) {
  return Response.ok('Hello, Dart backend!');
}
</code></pre>
<p>Request in, Response out. That's the entire contract. Every endpoint you write is a handler. Every piece of middleware is a function that takes a handler and returns a handler.</p>
<p>Handlers can be async:</p>
<pre><code class="language-dart">Future&lt;Response&gt; getUserHandler(Request request) async {
  final users = await userRepository.findAll();
  return Response.ok(jsonEncode(users));
}
</code></pre>
<h3 id="heading-request-and-response">Request and Response</h3>
<p>Request gives you everything about the incoming HTTP call:</p>
<pre><code class="language-dart">Future&lt;Response&gt; handler(Request request) async {
  // URL and path
  print(request.url);           // the full URL
  print(request.url.path);      // just the path

  // Path parameters (when using shelf_router)
  final id = request.params['id'];

  // Query parameters
  final page = request.url.queryParameters['page'];

  // Headers
  final auth = request.headers['authorization'];

  // Body
  final body = await request.readAsString();
  final json = jsonDecode(body) as Map&lt;String, dynamic&gt;;

  return Response.ok('handled');
}
</code></pre>
<p>Response has named constructors for common status codes:</p>
<pre><code class="language-dart">Response.ok(body)           // 200
Response.notFound(body)     // 404
Response(201, body: body)   // any status code
Response(400, body: body)   // bad request
Response(401, body: body)   // unauthorized
Response(500, body: body)   // server error
</code></pre>
<p>Always set the Content-Type header when returning JSON:</p>
<pre><code class="language-dart">Response.ok(
  jsonEncode({'message': 'success'}),
  headers: {'Content-Type': 'application/json'},
)
</code></pre>
<h3 id="heading-router">Router</h3>
<p>shelf_router maps URL patterns and HTTP methods to handlers:</p>
<pre><code class="language-dart">import 'package:shelf_router/shelf_router.dart';

final router = Router();

router.get('/users', getAllUsersHandler);
router.get('/users/&lt;id&gt;', getUserHandler);
router.post('/users', createUserHandler);
router.put('/users/&lt;id&gt;', updateUserHandler);
router.delete('/users/&lt;id&gt;', deleteUserHandler);
</code></pre>
<p>The syntax defines a path parameter. Access it inside the handler via request.params['id'].</p>
<h3 id="heading-pipeline-and-middleware">Pipeline and Middleware</h3>
<p>A Pipeline chains middleware together with a handler at the end:</p>
<pre><code class="language-dart">import 'package:shelf/shelf.dart';

final handler = Pipeline()
    .addMiddleware(loggerMiddleware())
    .addMiddleware(errorMiddleware())
    .addMiddleware(authMiddleware())
    .addHandler(router.call);
</code></pre>
<p>Middleware is a function with this signature:</p>
<pre><code class="language-dart">Middleware myMiddleware() {
  return (Handler innerHandler) {
    return (Request request) async {
      // Before the handler runs
      print('Request received: \({request.method} \){request.url}');

      final response = await innerHandler(request);

      // After the handler runs
      print('Response sent: ${response.statusCode}');

      return response;
    };
  };
}
</code></pre>
<p>The outer function returns a Middleware. That Middleware is a function that takes the next Handler in the chain and returns a new Handler. This nesting is what allows middleware to run code both before and after the inner handler.</p>
<h2 id="heading-connecting-to-postgresql">Connecting to PostgreSQL</h2>
<h3 id="heading-the-database-connection-manager">The Database Connection Manager</h3>
<p>Create lib/config/database.dart:</p>
<pre><code class="language-dart">import 'package:postgres/postgres.dart';
import 'env.dart';

class Database {
  static Connection? _connection;

  static Future&lt;Connection&gt; get connection async {
    if (_connection != null) return _connection!;
    _connection = await _connect();
    return _connection!;
  }

  static Future&lt;Connection&gt; _connect() async {
    final conn = await Connection.open(
      Endpoint(
        host: Env.dbHost,
        port: Env.dbPort,
        database: Env.dbName,
        username: Env.dbUser,
        password: Env.dbPassword,
      ),
      settings: const ConnectionSettings(
        sslMode: SslMode.disable,
      ),
    );

    print('✅ Database connected: \({Env.dbHost}:\){Env.dbPort}/${Env.dbName}');
    return conn;
  }

  static Future&lt;void&gt; close() async {
    await _connection?.close();
    _connection = null;
  }
}
</code></pre>
<p>This is a singleton connection manager – the same pattern Flutter engineers use for shared services. The connection is created once on first access and reused for every subsequent database call.</p>
<h3 id="heading-running-migrations">Running Migrations</h3>
<p>Create the migrations folder and SQL files:</p>
<p>migrations/001_create_users.sql:</p>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email VARCHAR(255) UNIQUE NOT NULL,
  password_hash VARCHAR(255) NOT NULL,
  first_name VARCHAR(100) NOT NULL,
  last_name VARCHAR(100) NOT NULL,
  is_active BOOLEAN DEFAULT TRUE,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
</code></pre>
<p>migrations/002_create_profiles.sql:</p>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS profiles (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  bio TEXT,
  avatar_url VARCHAR(500),
  phone VARCHAR(20),
  location VARCHAR(255),
  website VARCHAR(500),
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  UNIQUE(user_id)
);

CREATE INDEX IF NOT EXISTS idx_profiles_user_id ON profiles(user_id);
</code></pre>
<p>Create a migration runner in lib/config/database.dart:</p>
<pre><code class="language-dart">static Future&lt;void&gt; runMigrations() async {
  final conn = await connection;
  final migrationsDir = Directory('migrations');

  final files = migrationsDir
      .listSync()
      .whereType&lt;File&gt;()
      .where((f) =&gt; f.path.endsWith('.sql'))
      .toList()
    ..sort((a, b) =&gt; a.path.compareTo(b.path));

  for (final file in files) {
    final sql = await file.readAsString();
    await conn.execute(sql);
    print('✅ Migration applied: ${file.path}');
  }
}
</code></pre>
<h2 id="heading-building-the-api">Building the API</h2>
<p>With the database connected and migrations in place, we can now build the actual API layer.</p>
<p>This section covers the models, repositories, and handlers for both users and profiles. Models define the shape of the data, repositories handle all database interactions, and handlers translate HTTP requests into repository calls and send responses back to the client. We'll build the user layer first, then the profile layer on top of it.</p>
<h3 id="heading-the-user-model">The User Model</h3>
<p>The User model represents a single user record in the database. It maps directly to the users table created in the migration and handles two-way conversion between database rows and Dart objects.</p>
<p>Create lib/models/user.dart:</p>
<pre><code class="language-dart">class User {
  final String id;
  final String email;
  final String passwordHash;
  final String firstName;
  final String lastName;
  final bool isActive;
  final DateTime createdAt;
  final DateTime updatedAt;

  const User({
    required this.id,
    required this.email,
    required this.passwordHash,
    required this.firstName,
    required this.lastName,
    required this.isActive,
    required this.createdAt,
    required this.updatedAt,
  });

  factory User.fromRow(Map&lt;String, dynamic&gt; row) =&gt; User(
        id: row['id'] as String,
        email: row['email'] as String,
        passwordHash: row['password_hash'] as String,
        firstName: row['first_name'] as String,
        lastName: row['last_name'] as String,
        isActive: row['is_active'] as bool,
        createdAt: row['created_at'] as DateTime,
        updatedAt: row['updated_at'] as DateTime,
      );

  // Never include passwordHash in JSON responses
  Map&lt;String, dynamic&gt; toJson() =&gt; {
        'id': id,
        'email': email,
        'firstName': firstName,
        'lastName': lastName,
        'isActive': isActive,
        'createdAt': createdAt.toIso8601String(),
        'updatedAt': updatedAt.toIso8601String(),
      };
}
</code></pre>
<p>fromRow maps a PostgreSQL result row to a User. toJson deliberately excludes passwordHash – you should never return password data in API responses.</p>
<h3 id="heading-the-user-repository">The User Repository</h3>
<p>The UserRepository is the single point of contact between the application and the users table. Every database operation for users goes through here, keeping the SQL contained and the handlers clean.</p>
<p>Create lib/repositories/user_repository.dart:</p>
<pre><code class="language-dart">import 'dart:async';
import 'package:postgres/postgres.dart';
import '../config/database.dart';
import '../models/user.dart';

class UserRepository {
  Future&lt;Connection&gt; get _conn =&gt; Database.connection;

  Future&lt;List&lt;User&gt;&gt; findAll() async {
    final conn = await _conn;
    final results = await conn.execute(
      'SELECT * FROM users WHERE is_active = TRUE ORDER BY created_at DESC',
    );

    return results.map((row) =&gt; User.fromRow(row.toColumnMap())).toList();
  }

  Future&lt;User?&gt; findById(String id) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('SELECT * FROM users WHERE id = @id AND is_active = TRUE'),
      parameters: {'id': id},
    );

    if (results.isEmpty) return null;
    return User.fromRow(results.first.toColumnMap());
  }

  Future&lt;User?&gt; findByEmail(String email) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('SELECT * FROM users WHERE email = @email'),
      parameters: {'email': email},
    );

    if (results.isEmpty) return null;
    return User.fromRow(results.first.toColumnMap());
  }

  Future&lt;User&gt; create({
    required String email,
    required String passwordHash,
    required String firstName,
    required String lastName,
  }) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('''
        INSERT INTO users (email, password_hash, first_name, last_name)
        VALUES (@email, @passwordHash, @firstName, @lastName)
        RETURNING *
      '''),
      parameters: {
        'email': email,
        'passwordHash': passwordHash,
        'firstName': firstName,
        'lastName': lastName,
      },
    );

    return User.fromRow(results.first.toColumnMap());
  }

  Future&lt;User?&gt; update({
    required String id,
    String? firstName,
    String? lastName,
  }) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('''
        UPDATE users
        SET
          first_name = COALESCE(@firstName, first_name),
          last_name  = COALESCE(@lastName, last_name),
          updated_at = NOW()
        WHERE id = @id AND is_active = TRUE
        RETURNING *
      '''),
      parameters: {
        'id': id,
        'firstName': firstName,
        'lastName': lastName,
      },
    );

    if (results.isEmpty) return null;
    return User.fromRow(results.first.toColumnMap());
  }

  Future&lt;bool&gt; delete(String id) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('''
        UPDATE users SET is_active = FALSE, updated_at = NOW()
        WHERE id = @id AND is_active = TRUE
        RETURNING id
      '''),
      parameters: {'id': id},
    );

    return results.isNotEmpty;
  }
}
</code></pre>
<p>A few things worth noting here. Sql.named uses named parameters (@paramName) instead of positional parameters. This prevents SQL injection and makes queries readable.</p>
<p>Also, the delete operation is a soft delete. It sets is_active = FALSE rather than removing the row. This is the standard production approach: data is never truly deleted, it's deactivated.</p>
<p>COALESCE(@firstName, first_name) on the update means: use the new value if provided, otherwise keep the existing value. This handles partial updates cleanly without requiring all fields every time.</p>
<h3 id="heading-user-handlers">User Handlers</h3>
<p>The UserHandler class exposes the repository operations as HTTP endpoints. It owns a Router instance internally and maps each route to a private method, keeping the routing logic and the handler logic together in one place.</p>
<p>Create lib/handlers/user_handler.dart:</p>
<pre><code class="language-dart">import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import '../repositories/user_repository.dart';

class UserHandler {
  final UserRepository _repository;

  UserHandler(this._repository);

  Router get router {
    final router = Router();
    router.get('/', _getAll);
    router.get('/&lt;id&gt;', _getOne);
    router.put('/&lt;id&gt;', _update);
    router.delete('/&lt;id&gt;', _delete);
    return router;
  }

  Future&lt;Response&gt; _getAll(Request request) async {
    final users = await _repository.findAll();
    return Response.ok(
      jsonEncode(users.map((u) =&gt; u.toJson()).toList()),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Future&lt;Response&gt; _getOne(Request request, String id) async {
    final user = await _repository.findById(id);

    if (user == null) {
      return Response.notFound(
        jsonEncode({'error': 'User not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    return Response.ok(
      jsonEncode(user.toJson()),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Future&lt;Response&gt; _update(Request request, String id) async {
    final body = jsonDecode(await request.readAsString()) as Map&lt;String, dynamic&gt;;

    final user = await _repository.update(
      id: id,
      firstName: body['firstName'] as String?,
      lastName: body['lastName'] as String?,
    );

    if (user == null) {
      return Response.notFound(
        jsonEncode({'error': 'User not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    return Response.ok(
      jsonEncode(user.toJson()),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Future&lt;Response&gt; _delete(Request request, String id) async {
    final deleted = await _repository.delete(id);

    if (!deleted) {
      return Response.notFound(
        jsonEncode({'error': 'User not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    return Response(
      204,
      headers: {'Content-Type': 'application/json'},
    );
  }
}
</code></pre>
<h3 id="heading-the-profile-model">The Profile Model</h3>
<p>The Profile model represents a user's extended information, stored separately from the core user record. The one-to-one relationship is enforced by the unique index on user_id in the profiles table. All fields except userId are nullable since a profile can be created with partial information and filled in over time.</p>
<p>Create lib/models/profile.dart:</p>
<pre><code class="language-dart">class Profile {
  final String id;
  final String userId;
  final String? bio;
  final String? avatarUrl;
  final String? phone;
  final String? location;
  final String? website;
  final DateTime createdAt;
  final DateTime updatedAt;

  const Profile({
    required this.id,
    required this.userId,
    this.bio,
    this.avatarUrl,
    this.phone,
    this.location,
    this.website,
    required this.createdAt,
    required this.updatedAt,
  });

  factory Profile.fromRow(Map&lt;String, dynamic&gt; row) =&gt; Profile(
        id: row['id'] as String,
        userId: row['user_id'] as String,
        bio: row['bio'] as String?,
        avatarUrl: row['avatar_url'] as String?,
        phone: row['phone'] as String?,
        location: row['location'] as String?,
        website: row['website'] as String?,
        createdAt: row['created_at'] as DateTime,
        updatedAt: row['updated_at'] as DateTime,
      );

  Map&lt;String, dynamic&gt; toJson() =&gt; {
        'id': id,
        'userId': userId,
        'bio': bio,
        'avatarUrl': avatarUrl,
        'phone': phone,
        'location': location,
        'website': website,
        'createdAt': createdAt.toIso8601String(),
        'updatedAt': updatedAt.toIso8601String(),
      };
}
</code></pre>
<h3 id="heading-the-profile-repository">The Profile Repository</h3>
<p>The ProfileRepository handles all database operations for the profiles table. Unlike the user repository which looks up by id, most profile operations use userId as the lookup key since that is how the client references a profile — by whose it belongs to, not by its own internal ID.</p>
<p>Create lib/repositories/profile_repository.dart:</p>
<pre><code class="language-dart">import 'package:postgres/postgres.dart';
import '../config/database.dart';
import '../models/profile.dart';

class ProfileRepository {
  Future&lt;Connection&gt; get _conn =&gt; Database.connection;

  Future&lt;Profile?&gt; findByUserId(String userId) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('SELECT * FROM profiles WHERE user_id = @userId'),
      parameters: {'userId': userId},
    );

    if (results.isEmpty) return null;
    return Profile.fromRow(results.first.toColumnMap());
  }

  Future&lt;Profile&gt; create({
    required String userId,
    String? bio,
    String? avatarUrl,
    String? phone,
    String? location,
    String? website,
  }) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('''
        INSERT INTO profiles (user_id, bio, avatar_url, phone, location, website)
        VALUES (@userId, @bio, @avatarUrl, @phone, @location, @website)
        RETURNING *
      '''),
      parameters: {
        'userId': userId,
        'bio': bio,
        'avatarUrl': avatarUrl,
        'phone': phone,
        'location': location,
        'website': website,
      },
    );

    return Profile.fromRow(results.first.toColumnMap());
  }

  Future&lt;Profile?&gt; update({
    required String userId,
    String? bio,
    String? avatarUrl,
    String? phone,
    String? location,
    String? website,
  }) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('''
        UPDATE profiles
        SET
          bio        = COALESCE(@bio, bio),
          avatar_url = COALESCE(@avatarUrl, avatar_url),
          phone      = COALESCE(@phone, phone),
          location   = COALESCE(@location, location),
          website    = COALESCE(@website, website),
          updated_at = NOW()
        WHERE user_id = @userId
        RETURNING *
      '''),
      parameters: {
        'userId': userId,
        'bio': bio,
        'avatarUrl': avatarUrl,
        'phone': phone,
        'location': location,
        'website': website,
      },
    );

    if (results.isEmpty) return null;
    return Profile.fromRow(results.first.toColumnMap());
  }
}
</code></pre>
<h3 id="heading-profile-handlers">Profile Handlers</h3>
<p>The ProfileHandler manages the profile endpoints nested under a user's ID. Before every operation, it verifies the parent user exists — a profile can't be created, fetched, or updated for a user that doesn't exist. It also prevents duplicate profiles by checking for an existing record before allowing a create.</p>
<p>Create lib/handlers/profile_handler.dart:</p>
<pre><code class="language-dart">import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import '../repositories/profile_repository.dart';
import '../repositories/user_repository.dart';

class ProfileHandler {
  final ProfileRepository _profileRepository;
  final UserRepository _userRepository;

  ProfileHandler(this._profileRepository, this._userRepository);

  Router get router {
    final router = Router();
    router.get('/&lt;userId&gt;/profile', _getProfile);
    router.post('/&lt;userId&gt;/profile', _createProfile);
    router.put('/&lt;userId&gt;/profile', _updateProfile);
    return router;
  }

  Future&lt;Response&gt; _getProfile(Request request, String userId) async {
    final user = await _userRepository.findById(userId);
    if (user == null) {
      return Response.notFound(
        jsonEncode({'error': 'User not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final profile = await _profileRepository.findByUserId(userId);
    if (profile == null) {
      return Response.notFound(
        jsonEncode({'error': 'Profile not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    return Response.ok(
      jsonEncode(profile.toJson()),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Future&lt;Response&gt; _createProfile(Request request, String userId) async {
    final user = await _userRepository.findById(userId);
    if (user == null) {
      return Response.notFound(
        jsonEncode({'error': 'User not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final existing = await _profileRepository.findByUserId(userId);
    if (existing != null) {
      return Response(
        409,
        body: jsonEncode({'error': 'Profile already exists for this user'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final body = jsonDecode(await request.readAsString()) as Map&lt;String, dynamic&gt;;

    final profile = await _profileRepository.create(
      userId: userId,
      bio: body['bio'] as String?,
      avatarUrl: body['avatarUrl'] as String?,
      phone: body['phone'] as String?,
      location: body['location'] as String?,
      website: body['website'] as String?,
    );

    return Response(
      201,
      body: jsonEncode(profile.toJson()),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Future&lt;Response&gt; _updateProfile(Request request, String userId) async {
    final body = jsonDecode(await request.readAsString()) as Map&lt;String, dynamic&gt;;

    final profile = await _profileRepository.update(
      userId: userId,
      bio: body['bio'] as String?,
      avatarUrl: body['avatarUrl'] as String?,
      phone: body['phone'] as String?,
      location: body['location'] as String?,
      website: body['website'] as String?,
    );

    if (profile == null) {
      return Response.notFound(
        jsonEncode({'error': 'Profile not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    return Response.ok(
      jsonEncode(profile.toJson()),
      headers: {'Content-Type': 'application/json'},
    );
  }
}
</code></pre>
<h2 id="heading-authentication">Authentication</h2>
<p>With the core user and profile CRUD in place, the next step is securing the API.</p>
<p>Authentication in this project works in two parts: an AuthService handles the cryptographic operations — password hashing and JWT generation and verification — and an AuthHandler exposes the register and login endpoints that clients call to get a token. Once a token is issued, the AuthMiddleware validates it on every protected request before it reaches a handler.</p>
<h3 id="heading-password-hashing">Password Hashing</h3>
<p>Create lib/services/auth_service.dart:</p>
<pre><code class="language-dart">import 'package:bcrypt/bcrypt.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import '../config/env.dart';
import '../models/user.dart';

class AuthService {
  String hashPassword(String password) {
    return BCrypt.hashpw(password, BCrypt.gensalt());
  }

  bool verifyPassword(String password, String hash) {
    return BCrypt.checkpw(password, hash);
  }

  String generateToken(User user) {
    final jwt = JWT(
      {
        'sub': user.id,
        'email': user.email,
        'iat': DateTime.now().millisecondsSinceEpoch ~/ 1000,
      },
    );

    return jwt.sign(
      SecretKey(Env.jwtSecret),
      expiresIn: Duration(hours: Env.jwtExpiryHours),
    );
  }

  JWT? verifyToken(String token) {
    try {
      return JWT.verify(token, SecretKey(Env.jwtSecret));
    } catch (_) {
      return null;
    }
  }
}
</code></pre>
<p>BCrypt.hashpw generates a salted hash. BCrypt.checkpw verifies a plain password against a stored hash. The salt is embedded in the hash itself – you don't store it separately.</p>
<p>verifyToken returns null on any failure, expired token, invalid signature, or malformed token rather than throwing. This keeps the auth middleware clean.</p>
<h3 id="heading-auth-handlers">Auth Handlers</h3>
<p>Create lib/handlers/auth_handler.dart:</p>
<pre><code class="language-dart">import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import '../repositories/user_repository.dart';
import '../services/auth_service.dart';

class AuthHandler {
  final UserRepository _userRepository;
  final AuthService _authService;

  AuthHandler(this._userRepository, this._authService);

  Router get router {
    final router = Router();
    router.post('/register', _register);
    router.post('/login', _login);
    return router;
  }

  Future&lt;Response&gt; _register(Request request) async {
    final body = jsonDecode(await request.readAsString()) as Map&lt;String, dynamic&gt;;

    final email = body['email'] as String?;
    final password = body['password'] as String?;
    final firstName = body['firstName'] as String?;
    final lastName = body['lastName'] as String?;

    if (email == null || password == null || firstName == null || lastName == null) {
      return Response(
        400,
        body: jsonEncode({'error': 'email, password, firstName, and lastName are required'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    if (password.length &lt; 8) {
      return Response(
        400,
        body: jsonEncode({'error': 'Password must be at least 8 characters'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final existing = await _userRepository.findByEmail(email);
    if (existing != null) {
      return Response(
        409,
        body: jsonEncode({'error': 'An account with this email already exists'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final passwordHash = _authService.hashPassword(password);

    final user = await _userRepository.create(
      email: email,
      passwordHash: passwordHash,
      firstName: firstName,
      lastName: lastName,
    );

    final token = _authService.generateToken(user);

    return Response(
      201,
      body: jsonEncode({
        'user': user.toJson(),
        'token': token,
      }),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Future&lt;Response&gt; _login(Request request) async {
    final body = jsonDecode(await request.readAsString()) as Map&lt;String, dynamic&gt;;

    final email = body['email'] as String?;
    final password = body['password'] as String?;

    if (email == null || password == null) {
      return Response(
        400,
        body: jsonEncode({'error': 'email and password are required'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final user = await _userRepository.findByEmail(email);

    // Deliberately vague error, never confirm whether an email exists
    if (user == null || !_authService.verifyPassword(password, user.passwordHash)) {
      return Response(
        401,
        body: jsonEncode({'error': 'Invalid email or password'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final token = _authService.generateToken(user);

    return Response.ok(
      jsonEncode({
        'user': user.toJson(),
        'token': token,
      }),
      headers: {'Content-Type': 'application/json'},
    );
  }
}
</code></pre>
<p>The login error message is deliberately vague: "Invalid email or password" rather than "Email not found" or "Wrong password." Confirming which part is wrong helps attackers enumerate valid accounts.</p>
<h3 id="heading-auth-middleware">Auth Middleware</h3>
<p>Create lib/middleware/auth_middleware.dart:</p>
<pre><code class="language-dart">import 'dart:convert';
import 'package:shelf/shelf.dart';
import '../services/auth_service.dart';

Middleware authMiddleware(AuthService authService) {
  return (Handler innerHandler) {
    return (Request request) async {
      final authHeader = request.headers['authorization'];

      if (authHeader == null || !authHeader.startsWith('Bearer ')) {
        return Response(
          401,
          body: jsonEncode({'error': 'Authorization header missing or malformed'}),
          headers: {'Content-Type': 'application/json'},
        );
      }

      final token = authHeader.substring(7); // Remove 'Bearer '
      final jwt = authService.verifyToken(token);

      if (jwt == null) {
        return Response(
          401,
          body: jsonEncode({'error': 'Invalid or expired token'}),
          headers: {'Content-Type': 'application/json'},
        );
      }

      // Attach the user ID to the request context for downstream handlers
      final updatedRequest = request.change(
        context: {
          ...request.context,
          'userId': jwt.payload['sub'] as String,
          'userEmail': jwt.payload['email'] as String,
        },
      );

      return innerHandler(updatedRequest);
    };
  };
}
</code></pre>
<p>request.change(context: {...}) is how Shelf passes data from middleware to handlers, the equivalent of attaching data to a request in Express or ASP.NET middleware. Any handler downstream can read request.context['userId'] to know which user is authenticated.</p>
<h2 id="heading-error-handling">Error Handling</h2>
<p>No matter how carefully you write your handlers, unexpected failures will happen in production — malformed request bodies, database timeouts, unhandled edge cases.</p>
<p>Rather than letting each handler manage its own error responses individually, we'll centralise error handling in a single middleware that wraps the entire pipeline. This guarantees a consistent error response shape across every endpoint and prevents internal error details from leaking to the client.</p>
<p>Create lib/middleware/error_middleware.dart:</p>
<pre><code class="language-dart">import 'dart:convert';
import 'package:shelf/shelf.dart';

Middleware errorMiddleware() {
  return (Handler innerHandler) {
    return (Request request) async {
      try {
        return await innerHandler(request);
      } on FormatException catch (e) {
        return Response(
          400,
          body: jsonEncode({'error': 'Invalid request body: ${e.message}'}),
          headers: {'Content-Type': 'application/json'},
        );
      } catch (e, stackTrace) {
        // Log the full error and stack trace server-side
        print('Unhandled error: $e');
        print(stackTrace);

        // Never expose internal error details to the client
        return Response(
          500,
          body: jsonEncode({'error': 'An internal server error occurred'}),
          headers: {'Content-Type': 'application/json'},
        );
      }
    };
  };
}
</code></pre>
<p>Create lib/middleware/logger_middleware.dart:</p>
<pre><code class="language-dart">import 'package:shelf/shelf.dart';

Middleware loggerMiddleware() {
  return (Handler innerHandler) {
    return (Request request) async {
      final start = DateTime.now();

      final response = await innerHandler(request);

      final duration = DateTime.now().difference(start).inMilliseconds;
      print(
        '[${DateTime.now().toIso8601String()}] '
        '\({request.method} \){request.url.path} '
        '→ \({response.statusCode} (\){duration}ms)',
      );

      return response;
    };
  };
}
</code></pre>
<h2 id="heading-wiring-everything-together">Wiring Everything Together</h2>
<p>With the handlers, repositories, and middleware all in place, the final step is connecting them into a single running server. The router maps URL prefixes to their handler, the pipeline stacks the middleware in the correct order, and the entry point boots everything up in sequence — loading environment variables, running migrations, and starting the server.</p>
<p>Create lib/router.dart:</p>
<pre><code class="language-dart">import 'package:shelf_router/shelf_router.dart';
import 'handlers/auth_handler.dart';
import 'handlers/user_handler.dart';
import 'handlers/profile_handler.dart';
import 'middleware/auth_middleware.dart';
import 'repositories/user_repository.dart';
import 'repositories/profile_repository.dart';
import 'services/auth_service.dart';

Router createRouter() {
  final userRepository = UserRepository();
  final profileRepository = ProfileRepository();
  final authService = AuthService();

  final authHandler = AuthHandler(userRepository, authService);
  final userHandler = UserHandler(userRepository);
  final profileHandler = ProfileHandler(profileRepository, userRepository);

  final router = Router();

  // Public routes, no auth required
  router.mount('/auth', authHandler.router.call);

  // Protected routes, auth middleware applied
  router.mount(
    '/users',
    Pipeline()
        .addMiddleware(authMiddleware(authService))
        .addHandler(userHandler.router.call),
  );

  router.mount(
    '/users',
    Pipeline()
        .addMiddleware(authMiddleware(authService))
        .addHandler(profileHandler.router.call),
  );

  return router;
}
</code></pre>
<p>Create the entry point bin/server.dart:</p>
<pre><code class="language-dart">import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import '../lib/config/database.dart';
import '../lib/config/env.dart';
import '../lib/middleware/error_middleware.dart';
import '../lib/middleware/logger_middleware.dart';
import '../lib/router.dart';

void main() async {
  // Load environment variables
  Env.load();

  // Run database migrations
  await Database.runMigrations();

  // Build the handler pipeline
  final router = createRouter();

  final handler = Pipeline()
      .addMiddleware(errorMiddleware())
      .addMiddleware(loggerMiddleware())
      .addHandler(router.call);

  // Start the server
  final server = await shelf_io.serve(
    handler,
    InternetAddress.anyIPv4,
    Env.port,
  );

  print('🚀 Server running on port ${server.port}');
}
</code></pre>
<p>Run the server:</p>
<pre><code class="language-bash">dart run bin/server.dart
# ✅ Database connected: localhost:5432/user_profile_api
# ✅ Migration applied: migrations/001_create_users.sql
# ✅ Migration applied: migrations/002_create_profiles.sql
# 🚀 Server running on port 8080
</code></pre>
<h2 id="heading-deployment">Deployment</h2>
<p>The server is running locally and all endpoints are working. Now it's time to ship it.</p>
<p>We'll cover two deployment paths: first packaging the app and database together with Docker Compose for local production testing, then deploying to Fly.io where your API will be accessible over the internet with a managed PostgreSQL database and automatic TLS.</p>
<h3 id="heading-dockerfile">Dockerfile</h3>
<p>Create Dockerfile in the project root:</p>
<pre><code class="language-dockerfile">FROM dart:stable AS build

WORKDIR /app
COPY pubspec.* ./
RUN dart pub get

COPY . .
RUN dart compile exe bin/server.dart -o bin/server

FROM debian:stable-slim

RUN apt-get update &amp;&amp; apt-get install -y ca-certificates &amp;&amp; rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY --from=build /app/bin/server bin/server
COPY --from=build /app/migrations migrations/

EXPOSE 8080

CMD ["bin/server"]
</code></pre>
<p>This is a multi-stage build. The first stage uses the full Dart SDK image to compile the server to a native binary. The second stage copies only the compiled binary and migrations into a minimal Debian image – no Dart SDK, no source code, no build tools. The final image is lean and production-ready.</p>
<h3 id="heading-docker-compose-for-local-production-testing">Docker Compose for Local Production Testing</h3>
<p>Update docker-compose.yml to include the app alongside the database:</p>
<pre><code class="language-yaml">version: '3.8'

services:
  postgres:
    image: postgres:16-alpine
    container_name: user_profile_db
    environment:
      POSTGRES_DB: user_profile_api
      POSTGRES_USER: dart_user
      POSTGRES_PASSWORD: dart_password
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dart_user -d user_profile_api"]
      interval: 5s
      timeout: 5s
      retries: 5

  api:
    build: .
    container_name: user_profile_api
    ports:
      - "8080:8080"
    environment:
      DB_HOST: postgres
      DB_PORT: 5432
      DB_NAME: user_profile_api
      DB_USER: dart_user
      DB_PASSWORD: dart_password
      JWT_SECRET: local_test_secret_replace_in_production
      JWT_EXPIRY_HOURS: 24
      PORT: 8080
    depends_on:
      postgres:
        condition: service_healthy

volumes:
  postgres_data:
</code></pre>
<p>The healthcheck on the Postgres service ensures that the API container only starts once the database is ready to accept connections (a common production problem when services start simultaneously).</p>
<p>Build and run everything:</p>
<pre><code class="language-bash">docker compose up --build
</code></pre>
<h3 id="heading-deploying-to-flyio">Deploying to Fly.io</h3>
<p>Fly.io is one of the cleanest deployment targets for containerized backend services. It handles global distribution, automatic TLS, and managed PostgreSQL databases.</p>
<p><strong>Step 1 – Install and authenticate:</strong></p>
<pre><code class="language-bash"># macOS
brew install flyctl

# Authenticate
fly auth login
</code></pre>
<p><strong>Step 2 – Launch the app:</strong></p>
<pre><code class="language-bash">fly launch
</code></pre>
<p>Fly detects the Dockerfile automatically and asks a few questions: app name, region, and whether to create a PostgreSQL database. Answer yes to the PostgreSQL prompt, and Fly will provision a managed database and inject the connection string automatically.</p>
<p><strong>Step 3 – Set environment variables:</strong></p>
<pre><code class="language-bash">fly secrets set JWT_SECRET="your_production_secret_here"
fly secrets set JWT_EXPIRY_HOURS="24"
</code></pre>
<p>Database connection variables are set automatically by Fly when it provisions the PostgreSQL cluster.</p>
<p><strong>Step 4 – Deploy:</strong></p>
<pre><code class="language-bash">fly deploy
</code></pre>
<p>Fly builds the Docker image, pushes it to their registry, and deploys it to your chosen region. Once complete:</p>
<pre><code class="language-bash">fly status
# Your app is running at https://your-app-name.fly.dev
</code></pre>
<p><strong>Step 5 – Verify the deployment:</strong></p>
<pre><code class="language-bash">curl https://your-app-name.fly.dev/auth/register \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"password123","firstName":"Seyi","lastName":"Dev"}'
</code></pre>
<h2 id="heading-testing-the-api">Testing the API</h2>
<p>With the server running locally on port 8080, here's the full flow to verify that everything works end to end.</p>
<p>Register a user:</p>
<pre><code class="language-bash">curl http://localhost:8080/auth/register \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "email": "seyi@example.com",
    "password": "securepassword",
    "firstName": "Seyi",
    "lastName": "Dev"
  }'
</code></pre>
<p>Response:</p>
<pre><code class="language-json">{
  "user": {
    "id": "uuid-here",
    "email": "seyi@example.com",
    "firstName": "Seyi",
    "lastName": "Dev",
    "isActive": true,
    "createdAt": "2025-01-01T00:00:00.000Z",
    "updatedAt": "2025-01-01T00:00:00.000Z"
  },
  "token": "eyJhbGci..."
}
</code></pre>
<p>Login:</p>
<pre><code class="language-bash">curl http://localhost:8080/auth/login \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"email": "seyi@example.com", "password": "securepassword"}'
</code></pre>
<p>Get all users (authenticated):</p>
<pre><code class="language-bash">curl http://localhost:8080/users \
  -H "Authorization: Bearer eyJhbGci..."
</code></pre>
<p>Create a profile:</p>
<pre><code class="language-bash">curl http://localhost:8080/users/{userId}/profile \
  -X POST \
  -H "Authorization: Bearer eyJhbGci..." \
  -H "Content-Type: application/json" \
  -d '{
    "bio": "Flutter engineer turned backend developer",
    "location": "Lagos, Nigeria",
    "website": "https://example.com"
  }'
</code></pre>
<p>Update a user:</p>
<pre><code class="language-bash">curl http://localhost:8080/users/{userId} \
  -X PUT \
  -H "Authorization: Bearer eyJhbGci..." \
  -H "Content-Type: application/json" \
  -d '{"firstName": "Oluwaseyi"}'
</code></pre>
<p>Delete a user:</p>
<pre><code class="language-bash">curl http://localhost:8080/users/{userId} \
  -X DELETE \
  -H "Authorization: Bearer eyJhbGci..."
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You just built and deployed a production-grade REST API in Dart – the same language you already know from Flutter. No new language, no new paradigm. Just Dart running in a different context.</p>
<p>The Shelf mental model (Handlers, Middleware, Pipelines, Routers) is deliberately minimal. It doesn't make decisions for you. It gives you composable primitives and lets you assemble them into exactly the architecture your project needs. That philosophy will feel familiar to Flutter engineers who build their own clean architecture rather than relying on a prescriptive framework.</p>
<p>What you built here – models, repositories, services, handlers, and middleware – is the same separation of concerns you apply in Flutter, applied to the backend. The concepts transfer. The Dart skills transfer. The architecture discipline transfers.</p>
<p>With this, you'll understand that Dart is a powerful language that cuts across both frontend and backend ecosystems. Aside from Shelf, we have Dartfrog and Serverpod which still functions well on the backend side of things. More on those in upcoming articles.</p>
<p>So yeah, try this out and thank me later!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ The Tradeoff That Slows Production Teams Down: Flexibility vs Actually Shipping ]]>
                </title>
                <description>
                    <![CDATA[ Every company says it wants speed. Roadmaps talk about velocity. Leadership meetings talk about reducing cycle time. Quarterly goals talk about faster execution and quicker releases. Every business wa ]]>
                </description>
                <link>https://www.freecodecamp.org/news/the-tradeoff-that-slows-production-teams-down-flexibility-vs-actually-shipping/</link>
                <guid isPermaLink="false">6a19ccc19e433f18f384364b</guid>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ production ]]>
                    </category>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                    <category>
                        <![CDATA[ PaaS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Fri, 29 May 2026 17:28:33 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/495a017a-0f6f-4e3b-8d55-6c3854917c51.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Every company says it wants speed.</p>
<p>Roadmaps talk about velocity. Leadership meetings talk about reducing cycle time. Quarterly goals talk about faster execution and quicker releases.</p>
<p>Every business wants teams moving faster.</p>
<p>Then many of those same companies make a decision that quietly slows everything down. They optimise for infrastructure flexibility instead of product delivery.</p>
<p>It sounds reasonable in the beginning. Teams want control. Engineers want options. Platform architects want systems that can support every future scenario.</p>
<p>So production teams start building infrastructure ecosystems around themselves.</p>
<p>Deployment pipelines get built from scratch. Cloud resources become heavily customised. Internal platforms gain endless knobs, switches, and configuration layers. New projects begin with architecture discussions instead of customer problems.</p>
<p>Months later, software delivery slows down.</p>
<p>Product teams miss timelines. Releases move out by quarters. Customer feedback arrives later. Competitors keep shipping.</p>
<p>The tradeoff hiding underneath all of this is simple. Teams choose flexibility over actually shipping.</p>
<p>And beyond a certain point, flexibility becomes one of the most expensive forms of organisational drag a company can create.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-the-myth-that-more-flexibility-creates-better-production-systems">The Myth That More Flexibility Creates Better Production Systems</a></p>
</li>
<li><p><a href="#heading-infrastructure-ownership-quietly-becomes-a-second-business">Infrastructure Ownership Quietly Becomes a Second Business</a></p>
</li>
<li><p><a href="#heading-the-real-cost-is-delayed-customer-learning">The Real Cost Is Delayed Customer Learning</a></p>
</li>
<li><p><a href="#heading-paas-changes-the-optimisation-function">PaaS Changes the Optimisation Function</a></p>
</li>
<li><p><a href="#heading-the-best-production-teams-remove-decisions">The Best Production Teams Remove Decisions</a></p>
</li>
<li><p><a href="#heading-custom-infrastructure-usually-solves-problems-nobody-has-yet">Custom Infrastructure Usually Solves Problems Nobody Has Yet</a></p>
</li>
<li><p><a href="#heading-the-real-competitive-advantage-is-shipping-faster">The Real Competitive Advantage Is Shipping Faster</a></p>
</li>
<li><p><a href="#heading-when-paas-might-not-be-the-right-choice">When PaaS Might Not Be the Right Choice</a></p>
</li>
<li><p><a href="#heading-stop-building-infrastructure-businesses-by-accident">Stop Building Infrastructure Businesses By Accident</a></p>
</li>
</ul>
<h2 id="heading-the-myth-that-more-flexibility-creates-better-production-systems">The Myth That More Flexibility Creates Better Production Systems</h2>
<p>Engineering teams love optionality. The logic sounds convincing.</p>
<p>If infrastructure is fully customizable, teams can adapt to future requirements. If deployment systems are built internally, every use case can be supported. If every layer is configurable, engineers can optimise for unique situations.</p>
<p>This feels like responsible engineering. But it often becomes expensive business behaviour.</p>
<p>Most production teams massively overestimate how often they need deep infrastructure flexibility.</p>
<p>What actually happens becomes predictable.</p>
<p>A product team starts a new initiative. Instead of shipping an early version and learning from customers, discussions begin.</p>
<ul>
<li><p>Should Kubernetes clusters be organised by team or service?</p>
</li>
<li><p>Should CI/CD use GitHub Actions or Jenkins?</p>
</li>
<li><p>Should secrets management use Vault or cloud-native tooling?</p>
</li>
<li><p>Should observability use Prometheus or Datadog?</p>
</li>
<li><p>Should deployment strategies use canary releases, <a href="https://www.redhat.com/en/topics/devops/what-is-blue-green-deployment">blue-green deployments</a>, or something custom?</p>
</li>
</ul>
<p>Weeks disappear. No customer sees anything. No assumptions get tested. No learning happens.</p>
<p>Meanwhile, product managers wait. Leadership waits. Customers wait.</p>
<p>Even with <a href="https://sevalla.com/blog/building-apps-with-sevalla-and-claude-code/">agentic coding tools</a> like Claude generating code, scaffolding systems and accelerating implementation, teams still lose speed when every output collides with infrastructure decisions and deployment debates.</p>
<p>The problem isn't technology. The problem is optimising around theoretical future flexibility instead of present business outcomes.</p>
<p>Software creates value when customers use it. Everything else is support work.</p>
<h2 id="heading-infrastructure-ownership-quietly-becomes-a-second-business">Infrastructure Ownership Quietly Becomes a Second Business</h2>
<p>Traditional deployment models accidentally create a dangerous pattern: companies think they are building products. Slowly, they start building infrastructure organisations.</p>
<p>Production teams provision servers. Then networking. Then IAM systems. Then deployment pipelines. Then, observability layers. Then secrets management. Then autoscaling. Then rollback systems.</p>
<p>Every decision feels reasonable in isolation. But collectively, teams create an operational machine they now own forever.</p>
<p>And ownership is where the hidden cost appears.</p>
<p>Because infrastructure work doesn't end after launch. It expands. Pipelines need maintenance. Security policies change. Monitoring systems require tuning. Platform dependencies break. Internal tooling needs upgrades.</p>
<p>Production teams gradually spend more time maintaining systems around software than improving software itself.</p>
<p>This creates a strange situation: highly paid engineers become caretakers for infrastructure instead of builders of customer value.</p>
<p>No customer purchases a product because deployment pipelines have become elegant. No customer upgrades because IAM policies are beautifully designed. No competitor loses market share because Kubernetes YAML looks sophisticated.</p>
<p>Customers care about products solving problems. Infrastructure only matters when it slows product delivery.</p>
<p>And infrastructure ownership creates endless opportunities for that to happen.</p>
<h2 id="heading-the-real-cost-is-delayed-customer-learning">The Real Cost Is Delayed Customer Learning</h2>
<p>The biggest cost of infrastructure complexity isn't engineering effort. It's delayed learning.</p>
<p>Software companies win through feedback loops. Teams ship something. Customers react. Teams learn. Products improve.</p>
<p>The faster this cycle operates, the stronger the company becomes.</p>
<p>Infrastructure work interrupts that loop. Every month spent building deployment systems is a month where customers aren't using new features. Every quarter spent designing internal platforms delays customer feedback. Every architecture discussion delays real market signals.</p>
<p>This is where many organisations misunderstand velocity.</p>
<p>They look at sprint metrics. They measure tickets completed. They count engineering output.</p>
<p>But business speed isn't measured through internal activity. Business speed measures how quickly ideas become customer reality.</p>
<p>Infrastructure ownership slows that process dramatically. And slower learning creates slower companies.</p>
<h2 id="heading-paas-changes-the-optimisation-function">PaaS Changes the Optimisation Function</h2>
<p>This is where <a href="https://www.freecodecamp.org/news/from-metrics-to-meaning-how-paas-helps-developers-understand-production/">Platform as a Service</a> changes the equation.</p>
<p>PaaS forces organisations to optimise around shipping rather than infrastructure ownership. That shift matters more than most teams realise.</p>
<p>Instead of spending weeks designing deployment architecture, production teams connect repositories and deploy.</p>
<p>Instead of building pipelines manually, pipelines already exist.</p>
<p>Instead of designing scaling systems, scaling becomes infrastructure behaviour rather than engineering work.</p>
<p>Instead of repeatedly building foundations, infrastructure becomes a utility.</p>
<p>That sounds simple. It should be simple. Deployment should feel boring. But the fact that deployment often becomes a major organisational project is usually evidence of unnecessary complexity rather than unavoidable complexity.</p>
<p>PaaS providers remove entire categories of decisions. And while many engineers see that as a compromise, it's often the opposite.</p>
<p>Constraints create speed. Speed creates learning. Learning creates better products.</p>
<h2 id="heading-the-best-production-teams-remove-decisions">The Best Production Teams Remove Decisions</h2>
<p>There's a common misconception that elite engineering organisations maximise options. The opposite is often true.</p>
<p>High-performing production teams aggressively eliminate decisions. They standardise. They create defaults. They remove unnecessary choices.</p>
<p>Because every decision carries a cost.</p>
<p>Cognitive load grows. Coordination increases. Meetings multiply. Dependencies expand. Eventually, the workaround software becomes larger than the software itself.</p>
<p>PaaS systems follow a different philosophy. They intentionally reduce optionality.</p>
<p>That reduction creates focus. And focus creates product velocity. Product velocity creates business outcomes.</p>
<p>The chain is straightforward. Too many organisations break it by introducing infrastructure ownership far too early.</p>
<h2 id="heading-custom-infrastructure-usually-solves-problems-nobody-has-yet">Custom Infrastructure Usually Solves Problems Nobody Has Yet</h2>
<p>One of the most expensive habits in software companies is solving future problems before current ones exist.</p>
<p>Teams build for scale before scale exists. They create multi-region architectures before international users arrive. They build deployment frameworks before deployment pain appears.</p>
<p>This usually comes from good intentions. Engineers want to avoid future rewrites. But the irony is that premature flexibility creates an immediate business slowdown.</p>
<p>A startup with twenty engineers shouldn't operate like a company with ten thousand engineers. Yet many production teams copy infrastructure patterns from giant technology firms.</p>
<p>What gets ignored is context. Large technology companies have entire platform teams maintaining internal systems. They have thousands of engineers supporting infrastructure investments.</p>
<p>Most companies do not.</p>
<p>Copying technical architecture without copying organisational scale creates enormous inefficiency.</p>
<p>PaaS acts as protection against this behaviour. It prevents teams from accidentally becoming infrastructure companies before they become successful product companies.</p>
<h2 id="heading-the-real-competitive-advantage-is-shipping-faster">The Real Competitive Advantage Is Shipping Faster</h2>
<p>Companies rarely lose because infrastructure flexibility was insufficient. They lost because competitors learned faster.</p>
<p>Speed matters. Not speed in sprint or <a href="https://linear.app/">linear dashboards</a>. Not speed in story points.</p>
<p>Actual speed. The ability to move ideas into production quickly. The ability to test assumptions rapidly. The ability to learn continuously.</p>
<p>Shipping creates learning. Learning creates improvement. Improvement creates advantage.</p>
<p>Infrastructure complexity interrupts this loop. PaaS strengthens it.</p>
<p>This is why deployment decisions should never be treated as purely technical discussions. They are business decisions.</p>
<p>Infrastructure ownership affects company velocity. Velocity affects market outcomes.</p>
<p>The argument isn't about servers. The argument is about competitive speed.</p>
<h2 id="heading-when-paas-might-not-be-the-right-choice">When PaaS Might Not Be the Right Choice</h2>
<p>There are situations where PaaS can become limiting.</p>
<p>Organisations with highly specialised infrastructure requirements may require direct control over networking, security layers, hardware optimisation, or deployment behaviour.</p>
<p>Some industries have regulatory requirements that create unusually specific infrastructure needs.</p>
<p>Large organisations with mature platform engineering teams may also justify custom infrastructure investments.</p>
<p>There are also cases where platform costs become meaningful at very large scale.</p>
<p>These scenarios exist. But many companies use edge cases as justification years before they become relevant. They prepare for infrastructure problems they may never have while struggling to ship ordinary product releases today.</p>
<p>That sequence creates unnecessary friction.</p>
<h2 id="heading-stop-building-infrastructure-businesses-by-accident">Stop Building Infrastructure Businesses By Accident</h2>
<p>Engineering culture often celebrates flexibility.</p>
<p>Flexibility sounds sophisticated. It sounds future-proof. It sounds like good systems thinking.</p>
<p>But flexibility carries a cost. Every additional option creates complexity. Every additional decision slows movement. Every additional layer creates maintenance work.</p>
<p>Production teams should ask a simpler question. Does this help us ship customer-facing software faster? If the answer is no, it deserves scrutiny.</p>
<p>Too many companies accidentally build infrastructure ecosystems that optimise for hypothetical future needs.</p>
<p>Meanwhile, competitors deploy products, learn from customers and improve faster.</p>
<p>Shipping beats flexibility. And for many production teams, choosing a PaaS is one of the clearest ways to prove it.</p>
<p>Hope you enjoyed this article. You can <a href="https://linkedin.com/in/manishmshiva">connect with me on LinkedIn</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Advanced Error Handling in Dart: Records, Result Types, Monads, and Freezed Exceptions ]]>
                </title>
                <description>
                    <![CDATA[ Every Dart developer has written this at some point: try {   final user = await repository.getUser(id);   // do something with user } catch (e) {   // what is e? who knows.   print(e.toString()); } I ]]>
                </description>
                <link>https://www.freecodecamp.org/news/advanced-error-handling-in-dart-records-result-types-monads-and-freezed-exceptions/</link>
                <guid isPermaLink="false">6a17657ebadcd8afcb2bcdb4</guid>
                
                    <category>
                        <![CDATA[ Dart ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ error handling ]]>
                    </category>
                
                    <category>
                        <![CDATA[ exception ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Software Engineering ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Oluwaseyi Fatunmole ]]>
                </dc:creator>
                <pubDate>Wed, 27 May 2026 21:43:26 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/21795781-af21-4c57-9457-6c58f22af656.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Every Dart developer has written this at some point:</p>
<pre><code class="language-dart">try {
  final user = await repository.getUser(id);
  // do something with user
} catch (e) {
  // what is e? who knows.
  print(e.toString());
}
</code></pre>
<p>It works. It compiles. It ships. And then six months later, a bug report lands in your inbox from a user who got a blank screen instead of an error message, and you spend three hours tracing it back to a <code>catch (e)</code> block that swallowed the failure silently.</p>
<p>This is the fundamental problem with exception-based error handling in Dart. Exceptions are invisible in function signatures. They carry no type information at the call site. The compiler can't help you because it doesn't know a function can fail.</p>
<p>Every failure path is a social contract between the author and the caller — and social contracts break under pressure, in large teams, and at 2am during an incident.</p>
<p>Production applications deserve better than that.</p>
<p>In this article, we're going to walk through a complete, modern approach to error handling in Dart — the kind used in real production Flutter codebases. We'll start with Dart Records as lightweight result containers, build a proper sealed Result type, extend it into the Monad pattern, integrate the <code>dartz</code> package for functional Either types, and finally cap it off with typed, exhaustive exceptions using Freezed.</p>
<p>By the end, failures in your codebase will be typed, visible, compiler-enforced, and impossible to ignore.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-the-problem-with-exceptions-in-dart">The Problem with Exceptions in Dart</a></p>
</li>
<li><p><a href="#heading-part-1-record-types-as-lightweight-result-containers">Part 1: Record Types as Lightweight Result Containers</a></p>
<ul>
<li><p><a href="#heading-what-are-dart-records">What are Dart Records?</a></p>
</li>
<li><p><a href="#heading-records-as-result-types">Records as Result Types</a></p>
</li>
<li><p><a href="#heading-sealed-classes-as-namespaced-constructors">Sealed Classes as Namespaced Constructors</a></p>
</li>
<li><p><a href="#heading-domain-specific-record-types">Domain-Specific Record Types</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-part-2-building-a-proper-sealed-result-type">Part 2: Building a Proper Sealed Result Type</a></p>
<ul>
<li><p><a href="#heading-the-appresult-sealed-class">The AppResult Sealed Class</a></p>
</li>
<li><p><a href="#heading-consuming-results-with-when">Consuming Results with when()</a></p>
</li>
<li><p><a href="#heading-why-this-is-better">Why This is Better</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-part-3-extending-to-the-monad-pattern">Part 3: Extending to the Monad Pattern</a></p>
<ul>
<li><p><a href="#heading-what-makes-something-a-monad">What Makes Something a Monad?</a></p>
</li>
<li><p><a href="#heading-adding-map-and-flatmap">Adding map and flatMap</a></p>
</li>
<li><p><a href="#heading-chaining-operations">Chaining Operations</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-part-4-either-with-dartz">Part 4: Either with dartz</a></p>
<ul>
<li><p><a href="#heading-what-is-either">What is Either?</a></p>
</li>
<li><p><a href="#heading-using-either-in-practice">Using Either in Practice</a></p>
</li>
<li><p><a href="#heading-bridging-records-and-either">Bridging Records and Either</a></p>
</li>
<li><p><a href="#heading-folding-an-either">Folding an Either</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-part-5-typed-exceptions-with-freezed">Part 5: Typed Exceptions with Freezed</a></p>
<ul>
<li><p><a href="#heading-why-freezed-for-exceptions">Why Freezed for Exceptions?</a></p>
</li>
<li><p><a href="#heading-building-iexception">Building iException</a></p>
</li>
<li><p><a href="#heading-pattern-matching-on-exception-types">Pattern Matching on Exception Types</a></p>
</li>
<li><p><a href="#heading-a-cleaner-base-getter-pattern">A Cleaner Base Getter Pattern</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-part-6-putting-it-all-together">Part 6: Putting It All Together</a></p>
<ul>
<li><p><a href="#heading-the-full-architecture">The Full Architecture</a></p>
</li>
<li><p><a href="#heading-repository-layer">Repository Layer</a></p>
</li>
<li><p><a href="#heading-domain-layer">Domain Layer</a></p>
</li>
<li><p><a href="#heading-presentation-layer">Presentation Layer</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before starting, you should have:</p>
<ul>
<li><p>A working Flutter project with Dart 3.0 or later</p>
</li>
<li><p>Basic familiarity with Dart generics and async/await</p>
</li>
<li><p>Basic understanding of sealed classes in Dart</p>
</li>
<li><p>The <code>freezed</code>, <code>freezed_annotation</code>, and <code>build_runner</code> packages available</p>
</li>
<li><p>The <code>dartz</code> package available</p>
</li>
<li><p><code>flutter pub run build_runner build</code> working in your project</p>
</li>
</ul>
<h2 id="heading-the-problem-with-exceptions-in-dart">The Problem with Exceptions in Dart</h2>
<p>Let's look at what typical exception-based error handling actually looks like across a full stack:</p>
<pre><code class="language-dart">// Repository
Future&lt;User&gt; getUser(String id) async {
  final response = await dio.get('/users/$id');
  return User.fromJson(response.data);
}

// Use case
Future&lt;User&gt; execute(String id) async {
  return await repository.getUser(id);
}

// ViewModel
Future&lt;void&gt; loadUser(String id) async {
  try {
    final user = await useCase.execute(id);
    state = UserState.loaded(user);
  } catch (e) {
    state = UserState.error(e.toString());
  }
}
</code></pre>
<p>This looks reasonable. But there are serious hidden problems here.</p>
<p><strong>The failure is invisible in the signature:</strong> <code>Future&lt;User&gt;</code> tells the caller "you will get a User." It says nothing about what happens when the network fails, when the token expires, or when the JSON is malformed. The caller has to know — by reading the implementation — that this function can fail.</p>
<p><strong>The compiler can't help you:</strong> If you forget the <code>try/catch</code> in the ViewModel, the app compiles fine. The crash happens at runtime, in production, in front of a real user.</p>
<p><code>catch (e)</code> <strong>catches everything:</strong> A typo in a variable name, a null dereference, a real network failure — they all land in the same catch block. You can't distinguish between them without inspecting the error string, which is fragile.</p>
<p><strong>Errors lose their type across layers:</strong> By the time an <code>UnauthorizedException</code> from the API layer reaches the ViewModel, it's just an <code>Object</code>. All structural information is gone.</p>
<p>The solution is to make failures a first-class part of your function signatures, your type system, and your compiler checks. That is exactly what the patterns in this article do.</p>
<h2 id="heading-part-1-record-types-as-lightweight-result-containers">Part 1: Record Types as Lightweight Result Containers</h2>
<h3 id="heading-what-are-dart-records">What are Dart Records?</h3>
<p>Dart 3.0 introduced Records — anonymous, immutable value types that group multiple fields together without needing a full class definition.</p>
<pre><code class="language-dart">// A record with two named fields
({String name, int age}) person = (name: 'Seyi', age: 28);

print(person.name); // Seyi
print(person.age);  // 28
</code></pre>
<p>Records are structurally typed — two records with the same field names and types are the same type, regardless of where they were defined. They're also immutable and compare by value, not by reference.</p>
<h3 id="heading-records-as-result-types">Records as Result Types</h3>
<p>The simplest application of records in error handling is encoding success and failure as a single return type with nullable fields:</p>
<pre><code class="language-dart">typedef Result&lt;E, T&gt; = ({E? e, T? data});
</code></pre>
<p>This defines a record type with two nullable fields — <code>e</code> for the error and <code>data</code> for the success value. The contract is simple: exactly one of them will be non-null.</p>
<pre><code class="language-dart">// On success — data is present, e is null
Result&lt;String, User&gt; result = (e: null, data: user);

// On failure — e is present, data is null
Result&lt;String, User&gt; result = (e: 'User not found', data: null);
</code></pre>
<p>This is already a significant improvement over exceptions. The return type now tells the caller that this function can produce either data or an error. The failure is part of the signature.</p>
<p>You can define more specific typedefs for different layers of your application:</p>
<pre><code class="language-dart">typedef ApiResult&lt;T, E&gt;      = ({T? data, E? exception});
typedef SecurityResponse     = ({bool? isSecured, String? error});
typedef Repository&lt;T&gt;        = ApiResult&lt;T, iException&gt;;
</code></pre>
<p>Each typedef gives a meaningful name to a record shape, making the intent clear at every call site.</p>
<h3 id="heading-sealed-classes-as-namespaced-constructors">Sealed Classes as Namespaced Constructors</h3>
<p>Creating result records manually every time is repetitive and error-prone. The cleanest solution is to use a sealed class purely as a namespace for static factory methods:</p>
<pre><code class="language-dart">sealed class Res&lt;E, T&gt; {
  static Result&lt;E, T&gt; success&lt;E, T&gt;(T data) =&gt; (e: null, data: data);
  static Result&lt;E, T&gt; failure&lt;E, T&gt;(E e) =&gt; (e: e, data: null);
}
</code></pre>
<p>Notice what <code>sealed</code> is doing here: it's not being used for polymorphism. It can't be instantiated. It exists purely to group two related static methods under a meaningful, non-extendable name.</p>
<p>The call site becomes clean and intentional:</p>
<pre><code class="language-dart">// In a repository
Future&lt;Result&lt;iException, User&gt;&gt; getUser(String id) async {
  try {
    final user = await _api.fetchUser(id);
    return Res.success(user);
  } on NetworkException catch (e) {
    return Res.failure(iException.internet(message: e.message));
  }
}
</code></pre>
<p>The same pattern applies for Dio-specific responses:</p>
<pre><code class="language-dart">sealed class DioResult&lt;T, E&gt; {
  static ApiResult&lt;T, E&gt; success&lt;T, E&gt;(T data) =&gt; (data: data, exception: null);
  static ApiResult&lt;T, E&gt; failure&lt;T, E&gt;(E exception) =&gt; (data: null, exception: exception);
}
</code></pre>
<p>And for repository-level results with a simplified type alias:</p>
<pre><code class="language-dart">// GET&lt;E, T&gt; is just ({E? e, T? res})
typedef New&lt;T&gt; = GET&lt;iException, T&gt;;

sealed class R&lt;E, T&gt; {
  static New&lt;T&gt; success&lt;T&gt;(T data) =&gt; (e: null, res: data);
  static New&lt;T&gt; failed&lt;T&gt;(iException error) =&gt; (e: error, res: null);
}
</code></pre>
<p>Each sealed class namespace has a single responsibility and maps to a single layer of the application.</p>
<h3 id="heading-domain-specific-record-types">Domain-Specific Record Types</h3>
<p>Records also work beautifully for domain-specific result shapes that don't fit a generic success/failure pattern:</p>
<pre><code class="language-dart">typedef SecurityResponse = ({bool? isSecured, String? error});

sealed class Check {
  static SecurityResponse isSecured() =&gt; (isSecured: true, error: null);
  static SecurityResponse isInsecured(String error) =&gt; (isSecured: false, error: error);
}
</code></pre>
<p>Using it:</p>
<pre><code class="language-dart">final check = Check.isSecured();
if (check.isSecured == true) {
  // proceed
}

final check = Check.isInsecured('Certificate validation failed');
print(check.error); // Certificate validation failed
</code></pre>
<p>Clean, readable, and self-documenting. The record shape tells you exactly what the function can return.</p>
<p><strong>The limitation to keep in mind:</strong> Record-based result types require you to manually check which field is non-null. There is no compiler enforcement that you handle both cases, and no built-in way to transform the result without unwrapping it manually. That's where a proper sealed Result type becomes necessary.</p>
<h2 id="heading-part-2-building-a-proper-sealed-result-type">Part 2: Building a Proper Sealed Result Type</h2>
<h3 id="heading-the-appresult-sealed-class">The AppResult Sealed Class</h3>
<p>A sealed Result type goes further than a record — it uses Dart's type system to make the two possible states structurally distinct, and provides a <code>when()</code> method that forces the caller to handle both cases at compile time.</p>
<pre><code class="language-dart">import 'app_failure.dart';

sealed class AppResult&lt;T&gt; {
  const AppResult();

  R when&lt;R&gt;({
    required R Function(T value) success,
    required R Function(AppFailure failure) failure,
  });
}

class AppSuccess&lt;T&gt; extends AppResult&lt;T&gt; {
  const AppSuccess(this.value);

  final T value;

  @override
  R when&lt;R&gt;({
    required R Function(T value) success,
    required R Function(AppFailure failure) failure,
  }) {
    return success(value);
  }
}

class AppFailureResult&lt;T&gt; extends AppResult&lt;T&gt; {
  const AppFailureResult(this.error);

  final AppFailure error;

  @override
  R when&lt;R&gt;({
    required R Function(T value) success,
    required R Function(AppFailure failure) failure,
  }) {
    return failure(error);
  }
}
</code></pre>
<p>Let's walk through the design decisions carefully.</p>
<p><code>sealed class AppResult&lt;T&gt;</code>: <code>sealed</code> means all subtypes must live in the same file and the compiler knows every possible subtype. This is what enables exhaustive pattern matching. <code>&lt;T&gt;</code> is the type of data you get on success.</p>
<p><code>AppSuccess&lt;T&gt;</code>: holds the actual data. When <code>when()</code> is called on an <code>AppSuccess</code>, it always calls the <code>success</code> callback and passes the value through.</p>
<p><code>AppFailureResult&lt;T&gt;</code>: holds an <code>AppFailure</code> (your error model). When <code>when()</code> is called on an <code>AppFailureResult</code>, it always calls the <code>failure</code> callback. Notice it still carries <code>&lt;T&gt;</code> even though there is no value — this makes both subtypes compatible with the same <code>AppResult&lt;T&gt;</code> type.</p>
<p><strong>The</strong> <code>when()</code> <strong>method</strong>: this is the key mechanism. Both callbacks are <code>required</code>. The compiler won't let you call <code>when()</code> without handling both cases. You can't forget the error path. You can't forget the success path. The object itself decides which branch runs — not an if/else in the calling code.</p>
<pre><code class="language-dart">// Repository returning AppResult
Future&lt;AppResult&lt;User&gt;&gt; login(String email, String password) async {
  try {
    final user = await _api.login(email, password);
    return AppSuccess(user);
  } on UnauthorizedException {
    return AppFailureResult(AppFailure.unauthorized());
  } on NetworkException {
    return AppFailureResult(AppFailure.network());
  } catch (e) {
    return AppFailureResult(AppFailure.unknown(e.toString()));
  }
}
</code></pre>
<h3 id="heading-consuming-results-with-when">Consuming Results with <code>when()</code></h3>
<pre><code class="language-dart">final result = await _repository.login(email, password);

result.when(
  success: (user) =&gt; emit(AuthState.authenticated(user)),
  failure: (error) =&gt; emit(AuthState.error(error.message)),
);
</code></pre>
<p>You can also use it to return values:</p>
<pre><code class="language-dart">// Returning a Widget
final widget = result.when(
  success: (user) =&gt; UserProfileCard(user: user),
  failure: (error) =&gt; ErrorView(message: error.message),
);

// Returning a String
final message = result.when(
  success: (data) =&gt; 'Welcome back, ${data.name}',
  failure: (error) =&gt; 'Something went wrong: ${error.message}',
);
</code></pre>
<p>The return type <code>R</code> is inferred — whatever both callbacks return, <code>when()</code> returns. If they return a <code>Widget</code>, you get a <code>Widget</code>. If they return a <code>String</code>, you get a <code>String</code>.</p>
<h3 id="heading-why-this-is-better">Why This is Better</h3>
<table>
<thead>
<tr>
<th></th>
<th>Exceptions</th>
<th>AppResult</th>
</tr>
</thead>
<tbody><tr>
<td>Failure visible in signature</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td>Compiler enforces handling</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td>Both paths required at call site</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td>Type safe across all layers</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td>Readable and self-documenting</td>
<td>❌</td>
<td>✅</td>
</tr>
</tbody></table>
<h2 id="heading-part-3-extending-to-the-monad-pattern">Part 3: Extending to the Monad Pattern</h2>
<h3 id="heading-what-makes-something-a-monad">What Makes Something a Monad?</h3>
<p>A monad is a pattern from functional programming. In practical terms, a type is monadic when it satisfies three things:</p>
<p><strong>Wrap</strong> — you can put a value into the context.</p>
<pre><code class="language-dart">AppSuccess(user) // wrapping a User into AppResult
</code></pre>
<p><strong>Transform (map)</strong> — you can apply a function to the wrapped value without manually unwrapping it. If the result is a failure, the transformation is skipped and the failure propagates.</p>
<p><strong>Chain (flatMap)</strong> — you can sequence multiple operations that each return the same wrapper type, without nesting. The first failure short-circuits the entire chain.</p>
<p><code>AppResult</code> as defined above satisfies the first rule and the <em>spirit</em> of the second through <code>when()</code>. But without <code>map</code> and <code>flatMap</code>, it's not mechanically monadic. Let's fix that.</p>
<h3 id="heading-adding-map-and-flatmap">Adding <code>map</code> and <code>flatMap</code></h3>
<pre><code class="language-dart">sealed class AppResult&lt;T&gt; {
  const AppResult();

  /// Transform the success value, propagate failure untouched
  AppResult&lt;R&gt; map&lt;R&gt;(R Function(T value) transform) {
    return when(
      success: (value) =&gt; AppSuccess(transform(value)),
      failure: (error) =&gt; AppFailureResult(error),
    );
  }

  /// Chain an operation that itself returns an AppResult
  AppResult&lt;R&gt; flatMap&lt;R&gt;(AppResult&lt;R&gt; Function(T value) transform) {
    return when(
      success: (value) =&gt; transform(value),
      failure: (error) =&gt; AppFailureResult(error),
    );
  }

  R when&lt;R&gt;({
    required R Function(T value) success,
    required R Function(AppFailure failure) failure,
  });
}
</code></pre>
<p><code>map</code> transforms the success value using a regular function. If the result is already a failure, <code>map</code> skips the transformation entirely and passes the failure through unchanged. This is called "failure propagation" — errors flow through the chain automatically.</p>
<p><code>flatMap</code> chains an operation that itself returns an <code>AppResult</code>. This is what allows sequencing — when each step in a process can independently succeed or fail, <code>flatMap</code> connects them so the first failure stops the chain.</p>
<h3 id="heading-chaining-operations">Chaining Operations</h3>
<p>Without monadic chaining, sequential operations that can each fail look like this:</p>
<pre><code class="language-dart">final loginResult = await login(email, password);

loginResult.when(
  success: (user) async {
    final profileResult = await getProfile(user.id);
    profileResult.when(
      success: (profile) async {
        final settingsResult = await loadSettings(profile.settingsId);
        settingsResult.when(
          success: (settings) =&gt; emit(AppState.ready(settings)),
          failure: (error) =&gt; emit(AppState.error(error)),
        );
      },
      failure: (error) =&gt; emit(AppState.error(error)),
    );
  },
  failure: (error) =&gt; emit(AppState.error(error)),
);
</code></pre>
<p>Deeply nested, repetitive error handling on every single step. With <code>flatMap</code>:</p>
<pre><code class="language-dart">final result = (await login(email, password))
    .flatMap((user) =&gt; getProfile(user.id))
    .flatMap((profile) =&gt; loadSettings(profile.settingsId))
    .map((settings) =&gt; settings.theme);

result.when(
  success: (theme) =&gt; emit(AppState.ready(theme)),
  failure: (error) =&gt; emit(AppState.error(error)),
);
</code></pre>
<p>Each step only runs if the previous one succeeded. The first failure short-circuits the entire chain. Error handling happens once at the end, not at every step. This is the full power of the monad pattern applied to real application code.</p>
<h2 id="heading-part-4-either-with-dartz">Part 4: Either with dartz</h2>
<h3 id="heading-what-is-either">What is Either?</h3>
<p><code>Either&lt;L, R&gt;</code> is a type from functional programming that represents one of two possible values — a <code>Left</code> or a <code>Right</code>. By convention:</p>
<ul>
<li><p><code>Left</code> — the failure case</p>
</li>
<li><p><code>Right</code> — the success case</p>
</li>
</ul>
<p>The <code>dartz</code> package brings this and many other functional programming primitives to Dart. Add it to your project:</p>
<pre><code class="language-yaml">dependencies:
  dartz: ^0.10.1
</code></pre>
<p>In the codebase we are building from, <code>Either</code> is used with a type alias that makes the intent explicit:</p>
<pre><code class="language-dart">import 'package:dartz/dartz.dart';

typedef API&lt;T&gt; = Either&lt;T, iException&gt;;
</code></pre>
<p>Note the convention here: <code>Left</code> holds the success value <code>T</code>, and <code>Right</code> holds the failure <code>iException</code>. This is intentionally flipped from the functional programming norm. Both conventions exist in real codebases — what matters is that you're consistent.</p>
<h3 id="heading-using-either-in-practice">Using Either in Practice</h3>
<p>Creating Either values:</p>
<pre><code class="language-dart">// Success — Left holds the data
Either&lt;User, iException&gt; result = Left(user);

// Failure — Right holds the exception
Either&lt;User, iException&gt; result = Right(iException.internet(message: 'No connection'));
</code></pre>
<p>Checking which side you're on:</p>
<pre><code class="language-dart">if (result.isLeft()) {
  final user = result.fold((user) =&gt; user, (_) =&gt; null);
}
</code></pre>
<h3 id="heading-bridging-records-and-either">Bridging Records and Either</h3>
<p>The real power of the <code>API</code> typedef comes from <code>ApiRes</code> — a utility class that converts between the record-based world of your data layer and the Either-based world of your domain layer:</p>
<pre><code class="language-dart">class ApiRes {
  static Future&lt;API&lt;T&gt;&gt; deserialize&lt;T&gt;(ApiResult&lt;T, iException&gt; res) async {
    return (res.data != null)
        ? Left(res.data as T)
        : Right(res.exception!);
  }

  static Future&lt;API&gt; deserializeDynamic(
    ApiResult&lt;dynamic, iException&gt; res,
  ) async {
    return (res.data != null) ? Left(res.data) : Right(res.exception!);
  }
}
</code></pre>
<p><code>ApiResult&lt;T, iException&gt;</code> is your record type from the data layer — a Dio response wrapped with nullable fields. <code>ApiRes.deserialize</code> takes that record and converts it into a proper <code>Either</code>, ready to be used in the domain layer.</p>
<p>In practice, a repository method looks like this:</p>
<pre><code class="language-dart">Future&lt;API&lt;User&gt;&gt; getUser(String id) async {
  // Data layer returns a record
  final res = await _dataSource.fetchUser(id);

  // Convert to Either at the boundary
  return ApiRes.deserialize&lt;User&gt;(res);
}
</code></pre>
<p>The boundary between layers is the conversion point. Inside the data layer, you work with records. At the boundary, you convert. In the domain layer, you work with Either. Each layer has the type that suits it best.</p>
<h3 id="heading-folding-an-either">Folding an Either</h3>
<p><code>dartz</code> provides a <code>fold</code> method on Either that works similarly to <code>when()</code> on <code>AppResult</code>:</p>
<pre><code class="language-dart">final result = await repository.getUser(id);

result.fold(
  (user) =&gt; emit(UserState.loaded(user)),       // Left — success
  (exception) =&gt; emit(UserState.error(exception.message)), // Right — failure
);
</code></pre>
<p><code>dartz</code> also gives you monadic operations out of the box:</p>
<pre><code class="language-dart">// map — transform the Left value
final nameResult = result.map((user) =&gt; user.name);

// flatMap / bind — chain Either-returning operations
final profileResult = result.flatMap(
  (user) =&gt; getProfile(user.id),
);
</code></pre>
<p>The full functional toolkit, ready to use without building it yourself.</p>
<h2 id="heading-part-5-typed-exceptions-with-freezed">Part 5: Typed Exceptions with Freezed</h2>
<h3 id="heading-why-freezed-for-exceptions">Why Freezed for Exceptions?</h3>
<p>Standard Dart exceptions carry almost no useful information:</p>
<pre><code class="language-dart">throw Exception('Something went wrong');
// At the catch site: what went wrong? what type? what code? who knows.
</code></pre>
<p>Even custom exception classes require significant boilerplate to implement properly — <code>==</code>, <code>hashCode</code>, <code>toString</code>, immutability, copyWith. Freezed generates all of that automatically, and adds exhaustive pattern matching on top.</p>
<p>Add the required packages:</p>
<pre><code class="language-yaml">dependencies:
  freezed_annotation: ^2.4.1

dev_dependencies:
  freezed: ^2.4.5
  build_runner: ^2.4.6
</code></pre>
<h3 id="heading-building-iexception">Building iException</h3>
<pre><code class="language-dart">import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'exception.freezed.dart';

@freezed
class iException with _$iException {
  const factory iException.internet({
    required String message,
    int? code,
  }) = InternetException;

  const factory iException.mapper({
    required String message,
    int? code,
  }) = MapperException;

  const factory iException.validation({
    required String message,
    int? code,
  }) = ValidationException;

  const factory iException.unauthorized({
    required String message,
    int? code,
  }) = UnauthorizedException;

  const factory iException.unknown({
    required String message,
    int? code,
  }) = UnknownException;

  const iException._();
}
</code></pre>
<p>Run code generation:</p>
<pre><code class="language-bash">flutter pub run build_runner build --delete-conflicting-outputs
</code></pre>
<p>What Freezed generates from this:</p>
<pre><code class="language-plaintext">iException (sealed base)
├── InternetException    — network failures, no connectivity
├── MapperException      — JSON parsing and deserialization failures
├── ValidationException  — input validation failures
├── UnauthorizedException — auth failures, expired tokens
└── UnknownException     — catch-all for unexpected errors
</code></pre>
<p>Each subclass is fully immutable, has <code>==</code> and <code>hashCode</code> based on its fields, and a proper <code>toString</code>. Creating exceptions is clean and explicit:</p>
<pre><code class="language-dart">iException.internet(message: 'No internet connection')
iException.unauthorized(message: 'Session expired', code: 401)
iException.validation(message: 'Email format is invalid')
iException.mapper(message: 'Failed to parse UserResponse', code: 500)
iException.unknown(message: e.toString())
</code></pre>
<p>The private constructor <code>const iException._()</code> is a Freezed requirement when you add any instance method or getter to the base class — it allows Freezed's generated subclasses to call <code>super._()</code> without exposing a public constructor on the base.</p>
<h3 id="heading-pattern-matching-on-exception-types">Pattern Matching on Exception Types</h3>
<p>Because <code>iException</code> is a Freezed sealed class, you get <code>when</code>, <code>maybeWhen</code>, <code>map</code>, and <code>maybeMap</code> for free from code generation:</p>
<pre><code class="language-dart">exception.when(
  internet: (message, code) =&gt; 'No internet: $message',
  mapper: (message, code) =&gt; 'Parse error: $message',
  validation: (message, code) =&gt; 'Invalid input: $message',
  unauthorized: (message, code) =&gt; 'Unauthorised — please log in again',
  unknown: (message, code) =&gt; 'Unexpected error: $message',
);
</code></pre>
<p>Every case is required. The compiler rejects incomplete matches. You can't accidentally handle only some exception types and silently miss others.</p>
<p>For cases where you only care about specific types:</p>
<pre><code class="language-dart">exception.maybeWhen(
  unauthorized: (message, code) =&gt; _redirectToLogin(),
  orElse: () =&gt; _showGenericError(exception),
);
</code></pre>
<h3 id="heading-a-cleaner-base-getter-pattern">A Cleaner Base Getter Pattern</h3>
<p>One thing worth improving in the base <code>iException</code> is providing a safe <code>message</code> getter that works across all subtypes without throwing <code>UnimplementedError</code>:</p>
<pre><code class="language-dart">const iException._();

String get displayMessage =&gt; when(
  internet: (message, _) =&gt; message,
  mapper: (message, _) =&gt; message,
  validation: (message, _) =&gt; message,
  unauthorized: (message, _) =&gt; message,
  unknown: (message, _) =&gt; message,
);
</code></pre>
<p>Now any code holding an <code>iException</code> — regardless of which subtype — can call <code>.displayMessage</code> safely:</p>
<pre><code class="language-dart">// In a ViewModel or BLoC — no need to pattern match just for the message
emit(ErrorState(message: exception.displayMessage));
</code></pre>
<p>This is significantly cleaner than a base getter that throws <code>UnimplementedError</code> at runtime.</p>
<h2 id="heading-part-6-putting-it-all-together">Part 6: Putting It All Together</h2>
<h3 id="heading-the-full-architecture">The Full Architecture</h3>
<p>Here's how all four patterns connect across a real clean architecture Flutter application:</p>
<pre><code class="language-plaintext">Data Layer
  Dio/HTTP call returns raw response
    └── Wrapped in ApiResult&lt;T, iException&gt; (record type)
          │
          ▼
Repository Layer
  ApiRes.deserialize() converts record → Either&lt;T, iException&gt;
    └── Returns API&lt;T&gt; = Either&lt;T, iException&gt;
          │
          ▼
Domain / Use Case Layer
  AppResult&lt;T&gt; is the standard return type
    └── Sealed class with AppSuccess and AppFailureResult
          │
          ▼
Presentation Layer
  result.when() handles both paths
    └── exception.when() handles all failure types
</code></pre>
<p>Each layer has the result type that suits its responsibility. Conversion happens at the boundaries. The presentation layer always deals with <code>AppResult&lt;T&gt;</code> — it doesn't need to know about Either or records.</p>
<h3 id="heading-repository-layer">Repository Layer</h3>
<pre><code class="language-dart">class AuthRepository {
  final AuthDataSource _dataSource;

  AuthRepository(this._dataSource);

  Future&lt;AppResult&lt;User&gt;&gt; login(String email, String password) async {
    // Data source returns a record
    final res = await _dataSource.login(email, password);

    // Convert to Either at the data/domain boundary
    final either = await ApiRes.deserialize&lt;User&gt;(res);

    // Convert Either to AppResult for the domain layer
    return either.fold(
      (user) =&gt; AppSuccess(user),
      (exception) =&gt; AppFailureResult(exception),
    );
  }

  Future&lt;AppResult&lt;List&lt;User&gt;&gt;&gt; getUsers() async {
    final res = await _dataSource.fetchUsers();
    final either = await ApiRes.deserialize&lt;List&lt;User&gt;&gt;(res);

    return either.fold(
      (users) =&gt; AppSuccess(users),
      (exception) =&gt; AppFailureResult(exception),
    );
  }
}
</code></pre>
<h3 id="heading-domain-layer">Domain Layer</h3>
<pre><code class="language-dart">class LoginUseCase {
  final AuthRepository _repository;

  LoginUseCase(this._repository);

  Future&lt;AppResult&lt;User&gt;&gt; execute(String email, String password) async {
    if (email.isEmpty || password.isEmpty) {
      return AppFailureResult(
        iException.validation(message: 'Email and password are required'),
      );
    }

    return _repository.login(email, password);
  }
}
</code></pre>
<p>The use case adds its own validation layer — returning a <code>ValidationException</code> before even hitting the repository. All failures flow through the same <code>AppResult&lt;T&gt;</code> type regardless of where they originated.</p>
<h3 id="heading-presentation-layer">Presentation Layer</h3>
<pre><code class="language-dart">class AuthViewModel extends ChangeNotifier {
  final LoginUseCase _loginUseCase;

  AuthViewModel(this._loginUseCase);

  AuthState _state = const AuthState.idle();
  AuthState get state =&gt; _state;

  Future&lt;void&gt; login(String email, String password) async {
    _state = const AuthState.loading();
    notifyListeners();

    final result = await _loginUseCase.execute(email, password);

    result.when(
      success: (user) {
        _state = AuthState.authenticated(user);
      },
      failure: (exception) {
        // Pattern match on the exception type for specific handling
        final message = exception.when(
          internet: (msg, _) =&gt; 'No internet connection. Please check your network.',
          unauthorized: (msg, _) =&gt; 'Your session has expired. Please log in again.',
          validation: (msg, _) =&gt; msg,
          mapper: (msg, _) =&gt; 'Something went wrong. Please try again.',
          unknown: (msg, _) =&gt; 'An unexpected error occurred.',
        );

        _state = AuthState.error(message);
      },
    );

    notifyListeners();
  }
}
</code></pre>
<p>Two levels of exhaustive pattern matching — one for the result, one for the exception type. Every possible failure has a specific, user-friendly message. The compiler guarantees nothing is missed.</p>
<p>And using the monadic chain from Part 3 for a multi-step flow:</p>
<pre><code class="language-java">Future&lt;void&gt; loadDashboard(String userId) async {
  _state = const DashboardState.loading();
  notifyListeners();

  final result = (await _userRepo.getUser(userId))
      .flatMap((user) =&gt; _profileRepo.getProfile(user.profileId))
      .flatMap((profile) =&gt; _settingsRepo.loadSettings(profile.settingsId))
      .map((settings) =&gt; DashboardData(settings: settings));

  result.when(
    success: (data) =&gt; _state = DashboardState.loaded(data),
    failure: (exception) =&gt; _state = DashboardState.error(
      exception.displayMessage,
    ),
  );

  notifyListeners();
}
</code></pre>
<p>Three sequential async operations, each of which can independently fail, handled in a clean chain with a single error handler at the end. This is what production-grade error handling looks like.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Error handling is one of those things that every codebase has, but few codebases have done well. The default in Dart , throwing and catching exceptions, is convenient for small projects and becomes a liability at scale. Failures become invisible, type information is lost across layers, and the compiler can't help you when something goes wrong.</p>
<p>The patterns in this article change that entirely.</p>
<p>Records give you lightweight result containers with zero boilerplate — perfect for layer-specific result types and domain-specific responses. Sealed Result types bring compiler enforcement — both paths are required, no failure can be silently ignored. The Monad pattern adds the ability to chain sequential operations cleanly, with automatic failure propagation through the chain. Either with <code>dartz</code> brings the full functional toolkit and a clean boundary type between your data and domain layers. And Freezed exceptions give your failure states structure, immutability, and exhaustive pattern matching, so every error type is handled explicitly and nothing slips through.</p>
<p>None of these patterns are complicated once you understand the problem they solve. And the problem they solve – invisible, unenforceable, type-unsafe error handling – is one of the most common sources of production bugs in Flutter applications.</p>
<p>The next step is taking one of these patterns into a real project. Using these will totally transform the error handling story and processes of your entire code base.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Learn Command Line Interface (CLI) Development with Dart: From Zero to a Fully Published Developer Tool ]]>
                </title>
                <description>
                    <![CDATA[ Most developers spend a significant portion of their day in the terminal. They run flutter build, push with git, manage packages with dart pub, and orchestrate pipelines from the command line. Every o ]]>
                </description>
                <link>https://www.freecodecamp.org/news/learn-command-line-interface-cli-development-with-dart-from-zero-to-a-fully-published-developer-tool/</link>
                <guid isPermaLink="false">69fe3149f239332df4fdfd46</guid>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Dart ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cli ]]>
                    </category>
                
                    <category>
                        <![CDATA[ command line ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Mobile Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Software Engineering ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Oluwaseyi Fatunmole ]]>
                </dc:creator>
                <pubDate>Fri, 08 May 2026 18:54:01 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/a4c564c2-f5f3-4824-b4e7-d103b5fc488e.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Most developers spend a significant portion of their day in the terminal. They run <code>flutter build</code>, push with <code>git</code>, manage packages with <code>dart pub</code>, and orchestrate pipelines from the command line. Every one of those tools is a CLI, or command line interface: a program that lives in the terminal and responds to text commands.</p>
<p>Yet most developers have never built one.</p>
<p>That's a missed opportunity. CLI tools are one of the most practical things a developer can ship. They automate repetitive workflows, standardise processes across teams, and, when published, become tangible artifacts that the developer community can discover, install, and use.</p>
<p>In this handbook, you'll go from zero to building a fully distributed Dart CLI tool. We'll start with the fundamentals – how CLIs work, how Dart receives and processes terminal input, and the core syntax you need to know. Then we'll build three progressively complex CLIs, starting with the basics and finishing with a real-world API request runner. Finally, we will cover every distribution path available, from <code>pub.dev</code> to compiled binaries, Homebrew taps, Docker, and local team activation.</p>
<p>By the end of the guide, you'll understand both how to build a CLI tool in Dart as well as how to ship it so other developers can actually use it.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-is-a-cli-and-why-should-you-build-one">What is a CLI and Why Should You Build One?</a></p>
</li>
<li><p><a href="#heading-cli-syntax-anatomy">CLI Syntax Anatomy</a></p>
</li>
<li><p><a href="#heading-how-dart-receives-terminal-input">How Dart Receives Terminal Input</a></p>
</li>
<li><p><a href="#heading-core-cli-concepts-in-dart">Core CLI Concepts in Dart</a></p>
<ul>
<li><p><a href="#heading-stdout-stderr-and-stdin">stdout, stderr, and stdin</a></p>
</li>
<li><p><a href="#heading-exit-codes">Exit Codes</a></p>
</li>
<li><p><a href="#heading-environment-variables">Environment Variables</a></p>
</li>
<li><p><a href="#heading-file-and-directory-operations">File and Directory Operations</a></p>
</li>
<li><p><a href="#heading-running-external-processes">Running External Processes</a></p>
</li>
<li><p><a href="#heading-platform-detection">Platform Detection</a></p>
</li>
<li><p><a href="#heading-async-in-cli">Async in CLI</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-setting-up-your-dart-cli-project">Setting Up Your Dart CLI Project</a></p>
</li>
<li><p><a href="#heading-cli-1-hello-cli-the-fundamentals">CLI 1 — Hello CLI: The Fundamentals</a></p>
</li>
<li><p><a href="#heading-cli-2-darttodo-a-terminal-task-manager">CLI 2 — dart_todo: A Terminal Task Manager</a></p>
<ul>
<li><p><a href="#heading-introducing-the-args-package">Introducing the args Package</a></p>
</li>
<li><p><a href="#heading-building-darttodo">Building dart_todo</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-cli-3-darthttp-a-lightweight-api-request-runner">CLI 3 — dart_http: A Lightweight API Request Runner</a></p>
<ul>
<li><a href="#heading-building-darthttp">Building dart_http</a></li>
</ul>
</li>
<li><p><a href="#heading-adding-color-and-polish-to-your-cli">Adding Color and Polish to Your CLI</a></p>
</li>
<li><p><a href="#heading-testing-your-cli-tool">Testing Your CLI Tool</a></p>
</li>
<li><p><a href="#heading-deploying-and-distributing-your-cli">Deploying and Distributing Your CLI</a></p>
<ul>
<li><p><a href="#heading-mode-1-pubdev-public-package-distribution">Mode 1: pub.dev — Public Package Distribution</a></p>
</li>
<li><p><a href="#heading-mode-2-local-path-activation">Mode 2: Local Path Activation</a></p>
</li>
<li><p><a href="#heading-mode-3-compiled-binary-via-github-releases">Mode 3: Compiled Binary via GitHub Releases</a></p>
</li>
<li><p><a href="#heading-mode-4-homebrew-tap">Mode 4: Homebrew Tap</a></p>
</li>
<li><p><a href="#heading-mode-5-docker">Mode 5: Docker</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-choosing-the-right-distribution-mode">Choosing the Right Distribution Mode</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before starting, you should have:</p>
<ul>
<li><p>Dart SDK installed (<code>dart --version</code> should work in your terminal)</p>
</li>
<li><p>Basic familiarity with Dart syntax</p>
</li>
<li><p>Comfort with the terminal and running commands</p>
</li>
<li><p>A pub.dev account (for the publishing section)</p>
</li>
<li><p>A GitHub account (for the binary distribution section)</p>
</li>
</ul>
<h2 id="heading-what-is-a-cli-and-why-should-you-build-one">What is a CLI and Why Should You Build One?</h2>
<p>A CLI (or <strong>Command Line Interface</strong>) is a program you interact with entirely through text commands in a terminal, rather than through buttons and screens in a graphical interface.</p>
<p>Many of the tools you likely already rely on as a developer are CLI tools:</p>
<pre><code class="language-yaml">flutter build apk
git commit -m "fix: auth flow"
dart pub get
npm install
</code></pre>
<p>Flutter, Git, Dart, npm – all CLIs. You are already a CLI user every single day. This article is about becoming a CLI builder.</p>
<p>There are three strong reasons to build CLI tools as a developer:</p>
<ol>
<li><p><strong>Automating repetitive work:</strong> Anything you type more than twice a week is a candidate for automation. Generating boilerplate folder structures, running sequences of commands, scaffolding files, checking environments before a build a CLI turns a seven-step manual process into a single command.</p>
</li>
<li><p><strong>Standardising team workflows:</strong> Instead of a README that says "run these commands in this order," you ship one command that does all of it – consistently, every time, with no room for human error or a missed step.</p>
</li>
<li><p><strong>Building and publishing tooling.</strong> A published Dart CLI package is a tangible artifact. It shows up on pub.dev, gets installed and used by other developers, and communicates real engineering depth in a way that a portfolio or resume cannot.</p>
</li>
</ol>
<h2 id="heading-cli-syntax-anatomy">CLI Syntax Anatomy</h2>
<p>Before writing a single line of code, it helps to understand the structure of a CLI command. Every command follows a consistent pattern:</p>
<pre><code class="language-bash">tool [subcommand] [arguments] [options/flags]
</code></pre>
<p>Breaking down a real example:</p>
<pre><code class="language-bash">flutter build apk --release --obfuscate
│       │     │   │
tool    sub   arg  flags
</code></pre>
<ul>
<li><p><strong>Tool</strong> — the program itself (<code>flutter</code>, <code>dart</code>, <code>git</code>)</p>
</li>
<li><p><strong>Subcommand</strong> — the action being performed (<code>build</code>, <code>run</code>, <code>pub</code>)</p>
</li>
<li><p><strong>Arguments</strong> — what the action operates on (<code>apk</code>, <code>main.dart</code>, a filename)</p>
</li>
<li><p><strong>Flags and Options</strong> — modifiers that change behaviour</p>
</li>
</ul>
<p>There are two types of options:</p>
<pre><code class="language-plaintext">--release              # Boolean flag — either present or absent

--output=build/app     # Key-value option — name and a value
-v                     # Short flag — single hyphen, single character
</code></pre>
<p>This is the anatomy your CLIs will follow. Understanding it before writing any code means you will design your commands intentionally rather than stumbling into structure by accident.</p>
<h2 id="heading-how-dart-receives-terminal-input">How Dart Receives Terminal Input</h2>
<p>In Dart, everything the user types after your tool name is passed into your program through the <code>main</code> function:</p>
<pre><code class="language-dart">void main(List&lt;String&gt; args) {
  print(args);
}
</code></pre>
<p>Run it:</p>
<pre><code class="language-bash">dart run bin/mytool.dart hello world --name=Seyi
# [hello, world, --name=Seyi]
</code></pre>
<p>That <code>List&lt;String&gt; args</code> is just a list of strings. Each word or flag the user typed becomes an element in that list. Everything else you build on top of a CLI subcommands, flags, validation — is ultimately just processing this list.</p>
<h2 id="heading-core-cli-concepts-in-dart">Core CLI Concepts in Dart</h2>
<p>Before building anything, there's a set of foundational concepts that every CLI developer needs to understand. These are the building blocks that everything else sits on top of.</p>
<h3 id="heading-stdout-stderr-and-stdin">stdout, stderr, and stdin</h3>
<p>Most developers use <code>print()</code> for all output when they start building CLIs. That works for learning but it's incorrect in production.</p>
<p>There are two separate output streams in a terminal program:</p>
<ul>
<li><p><code>stdout</code> — regular output, meant for the user</p>
</li>
<li><p><code>stderr</code> — error output, meant for diagnostic messages and failures</p>
</li>
</ul>
<pre><code class="language-dart">import 'dart:io';

void main(List&lt;String&gt; args) {
  if (args.isEmpty) {
    stderr.writeln('Error: no arguments provided');
    exit(1);
  }

  stdout.writeln('Processing: ${args[0]}');
}
</code></pre>
<p>Keeping these separate matters because users can redirect stdout to a file without errors polluting it:</p>
<pre><code class="language-bash">dart run bin/tool.dart &gt; output.txt
# Errors still appear in the terminal
# Normal output goes cleanly to the file
</code></pre>
<p>Tools like <code>git</code>, <code>flutter</code>, and <code>curl</code> all do this correctly. Your CLI should too.</p>
<p><code>stdin</code> is the third stream — reading input from the user interactively at runtime:</p>
<pre><code class="language-dart">import 'dart:io';

void main() {
  stdout.write('Enter your name: ');
  final name = stdin.readLineSync();

  if (name == null || name.trim().isEmpty) {
    stderr.writeln('Error: no name provided');
    exit(1);
  }

  stdout.writeln('Hello, $name!');
}
</code></pre>
<p><code>stdout.write</code> (without <code>ln</code>) keeps the cursor on the same line so the user types right after the prompt. <code>stdin.readLineSync()</code> blocks until the user presses Enter and returns the typed string, or <code>null</code> if the stream closes unexpectedly. Always handle the null case.</p>
<h3 id="heading-exit-codes">Exit Codes</h3>
<p>Every program returns an exit code when it finishes. This is how the shell – and any script or CI system calling your tool – knows whether it succeeded or failed.</p>
<pre><code class="language-dart">import 'dart:io';

void main(List&lt;String&gt; args) {
  if (args.isEmpty) {
    stderr.writeln('Error: please provide an argument');
    exit(1); // failure
  }

  stdout.writeln('Done');
  exit(0); // success — also the default if you don't call exit()
}
</code></pre>
<p>The conventions are:</p>
<ul>
<li><p><code>0</code> — success</p>
</li>
<li><p><code>1</code> — general failure</p>
</li>
<li><p><code>2</code> — incorrect usage (wrong arguments, missing flags)</p>
</li>
</ul>
<p>Exit codes are critical when your CLI is called inside shell scripts or GitHub Actions workflows. A non-zero exit code stops a pipeline immediately. That's exactly the behaviour you want from a quality gate or a validation step.</p>
<h3 id="heading-environment-variables">Environment Variables</h3>
<p>Your CLI can read environment variables set in the user's shell:</p>
<pre><code class="language-dart">import 'dart:io';

void main() {
  final token = Platform.environment['API_TOKEN'];

  if (token == null) {
    stderr.writeln('Error: API_TOKEN environment variable is not set');
    exit(1);
  }

  stdout.writeln('Token found — proceeding...');
}
</code></pre>
<p>Set it in the terminal and run:</p>
<pre><code class="language-bash">export API_TOKEN=mytoken123
dart run bin/tool.dart
# Token found — proceeding...
</code></pre>
<p>This pattern is essential for CLI tools that interact with APIs, cloud services, or CI environments where credentials should never be hardcoded.</p>
<h3 id="heading-file-and-directory-operations">File and Directory Operations</h3>
<p>Many CLI tools read from or write to the file system. Dart's <code>dart:io</code> library covers everything you need:</p>
<pre><code class="language-dart">import 'dart:io';

void main(List&lt;String&gt; args) {
  if (args.isEmpty) {
    stderr.writeln('Usage: tool &lt;filename&gt;');
    exit(2);
  }

  final file = File(args[0]);

  if (!file.existsSync()) {
    stderr.writeln('Error: "${args[0]}" not found');
    exit(1);
  }

  final contents = file.readAsStringSync();
  stdout.writeln(contents);

  final output = File('output.txt');
  output.writeAsStringSync('Processed:\n$contents');
  stdout.writeln('Written to output.txt');
}
</code></pre>
<p>Working with directories:</p>
<pre><code class="language-dart">import 'dart:io';

void main() {
  // Where the command was run from
  final cwd = Directory.current.path;
  stdout.writeln('Working directory: $cwd');

  // Create a directory relative to current location
  final dir = Directory('$cwd/generated');

  if (!dir.existsSync()) {
    dir.createSync(recursive: true);
    stdout.writeln('Created: ${dir.path}');
  } else {
    stdout.writeln('Already exists: ${dir.path}');
  }
}
</code></pre>
<p>The <code>recursive: true</code> flag on <code>createSync</code> means it creates all intermediate directories — equivalent to <code>mkdir -p</code> in bash.</p>
<h3 id="heading-running-external-processes">Running External Processes</h3>
<p>One of the most powerful things a CLI can do is call other programs. Your Dart CLI can run <code>git</code>, <code>flutter</code>, <code>dart</code>, or any shell command programmatically:</p>
<pre><code class="language-dart">import 'dart:io';

void main() async {
  // Run a command and wait for it to finish
  final result = await Process.run('dart', ['pub', 'get']);

  stdout.write(result.stdout);

  if (result.exitCode != 0) {
    stderr.write(result.stderr);
    exit(result.exitCode);
  }

  stdout.writeln('Dependencies installed successfully');
}
</code></pre>
<p>For long-running commands where you want output to stream live as it happens:</p>
<pre><code class="language-dart">import 'dart:io';

void main() async {
  final process = await Process.start('flutter', ['build', 'apk']);

  // Pipe output directly to the terminal in real time
  process.stdout.pipe(stdout);
  process.stderr.pipe(stderr);

  final exitCode = await process.exitCode;
  exit(exitCode);
}
</code></pre>
<p><code>Process.run</code> — waits for completion, returns all output at once. Use for short commands.</p>
<p><code>Process.start</code> — streams output live as it arrives. Use for long-running commands where the user needs to see progress.</p>
<h3 id="heading-platform-detection">Platform Detection</h3>
<p>Sometimes your CLI needs to behave differently depending on the operating system it is running on:</p>
<pre><code class="language-dart">import 'dart:io';

void main() {
  if (Platform.isWindows) {
    stdout.writeln('Running on Windows');
  } else if (Platform.isMacOS) {
    stdout.writeln('Running on macOS');
  } else if (Platform.isLinux) {
    stdout.writeln('Running on Linux');
  }

  // Useful for path handling across operating systems
  stdout.writeln(Platform.pathSeparator); // \ on Windows, / elsewhere
  stdout.writeln(Platform.operatingSystem); // 'macos', 'linux', 'windows'
}
</code></pre>
<p>This matters when your CLI creates files, resolves paths, or calls shell commands that differ between operating systems.</p>
<h3 id="heading-async-in-cli">Async in CLI</h3>
<p>Dart CLIs support <code>async/await</code> natively. Any <code>main</code> function can be made async:</p>
<pre><code class="language-dart">import 'dart:io';

void main() async {
  stdout.writeln('Starting...');

  await Future.delayed(const Duration(seconds: 1)); // simulating async work

  stdout.writeln('Done');
}
</code></pre>
<p>Any operation involving file I/O, HTTP requests, or spawning processes will be asynchronous. Get comfortable with async <code>main</code> functions early — you'll use them constantly.</p>
<h2 id="heading-setting-up-your-dart-cli-project">Setting Up Your Dart CLI Project</h2>
<p>Create a new Dart console project:</p>
<pre><code class="language-bash">dart create -t console my_cli_tool
cd my_cli_tool
</code></pre>
<p>This generates a clean structure:</p>
<pre><code class="language-plaintext">my_cli_tool/
  bin/
    my_cli_tool.dart    ← entry point
  lib/                  ← shared library code
  test/                 ← tests
  pubspec.yaml
  README.md
</code></pre>
<p>The <code>bin/</code> directory is where your executable entry point lives. The <code>lib/</code> directory is where you put everything else — commands, utilities, models — that <code>bin/</code> imports and uses.</p>
<p>Open <code>pubspec.yaml</code>. You'll need to add an <code>executables</code> block before publishing:</p>
<pre><code class="language-yaml">name: my_cli_tool
description: A sample CLI tool built with Dart
version: 1.0.0

environment:
  sdk: '&gt;=3.0.0 &lt;4.0.0'

executables:
  my_cli_tool: my_cli_tool  # executable name: bin file name

dependencies:
  args: ^2.4.2

dev_dependencies:
  lints: ^3.0.0
  test: ^1.24.0
</code></pre>
<p>The <code>executables</code> block is what makes <code>dart pub global activate my_cli_tool</code> work. It tells Dart which script in <code>bin/</code> to expose as a runnable command after installation.</p>
<h2 id="heading-cli-1-hello-cli-the-fundamentals">CLI 1 — Hello CLI: The Fundamentals</h2>
<p>This first CLI uses pure Dart — no packages. The goal is to get comfortable with args, subcommands, input validation, and exit codes before introducing any external dependencies.</p>
<p>Replace the contents of <code>bin/my_cli_tool.dart</code>:</p>
<pre><code class="language-dart">import 'dart:io';

void main(List&lt;String&gt; args) {
  if (args.isEmpty) {
    printHelp();
    exit(0);
  }

  final command = args[0];

  switch (command) {
    case 'greet':
      handleGreet(args.sublist(1));
    case 'time':
      handleTime();
    case 'echo':
      handleEcho(args.sublist(1));
    case 'help':
      printHelp();
    default:
      stderr.writeln('Unknown command: "$command"');
      stderr.writeln('Run "mytool help" to see available commands.');
      exit(1);
  }
}

void handleGreet(List&lt;String&gt; args) {
  if (args.isEmpty) {
    stderr.writeln('Usage: mytool greet &lt;name&gt;');
    exit(2);
  }

  final name = args[0];
  stdout.writeln('Hello, $name! Welcome to your first Dart CLI.');
}

void handleTime() {
  final now = DateTime.now();
  stdout.writeln(
    'Current time: ${now.hour.toString().padLeft(2, '0')}:'
    '${now.minute.toString().padLeft(2, '0')}:'
    '${now.second.toString().padLeft(2, '0')}',
  );
}

void handleEcho(List&lt;String&gt; args) {
  if (args.isEmpty) {
    stderr.writeln('Usage: mytool echo &lt;message&gt;');
    exit(2);
  }

  stdout.writeln(args.join(' '));
}

void printHelp() {
  stdout.writeln('''
mytool — a simple Dart CLI

Usage:
  mytool &lt;command&gt; [arguments]

Commands:
  greet &lt;name&gt;      Greet someone by name
  time              Show the current time
  echo &lt;message&gt;    Echo a message back to the terminal
  help              Show this help message

Examples:
  mytool greet Seyi
  mytool echo "Hello from the terminal"
  mytool time
  ''');
}
</code></pre>
<p>Run it:</p>
<pre><code class="language-bash">dart run bin/my_cli_tool.dart help

dart run bin/my_cli_tool.dart greet Seyi
# Hello, Seyi! Welcome to your first Dart CLI.

dart run bin/my_cli_tool.dart time
# Current time: 14:32:10

dart run bin/my_cli_tool.dart echo "Dart CLIs are powerful"
# Dart CLIs are powerful

dart run bin/my_cli_tool.dart unknown
# Unknown command: "unknown"
# Run "mytool help" to see available commands.
</code></pre>
<p>Three things this CLI demonstrates that are worth internalising:</p>
<ol>
<li><p><strong>Subcommands are just a switch on</strong> <code>args[0]</code><strong>.</strong> The pattern is simple and scalable — add a new <code>case</code> to add a new command.</p>
</li>
<li><p><code>args.sublist(1)</code> <strong>passes remaining args to the handler.</strong> When <code>greet</code> receives <code>['greet', 'Seyi']</code>, it calls <code>handleGreet(['Seyi'])</code> — clean and isolated.</p>
</li>
<li><p><strong>Every error path has a message and a non-zero exit code.</strong> The user always knows what went wrong and what to do next.</p>
</li>
</ol>
<h2 id="heading-cli-2-darttodo-a-terminal-task-manager">CLI 2 — dart_todo: A Terminal Task Manager</h2>
<p>This CLI introduces the <code>args</code> package, JSON file persistence, and structured terminal output. It's meaningfully more complex than CLI 1 and reflects real patterns you will use in production tools.</p>
<h3 id="heading-introducing-the-args-package">Introducing the args Package</h3>
<p>Manually parsing <code>List&lt;String&gt; args</code> works for simple cases, but breaks down quickly when you add flags like <code>--priority=high</code>, boolean options like <code>--done</code>, or commands with multiple optional arguments.</p>
<p>The <code>args</code> package handles all of that cleanly.</p>
<p>Add it to your <code>pubspec.yaml</code>:</p>
<pre><code class="language-yaml">dependencies:
  args: ^2.4.2
</code></pre>
<p>Run:</p>
<pre><code class="language-bash">dart pub get
</code></pre>
<p>The core concept in <code>args</code> is the <code>ArgParser</code>. You define what your CLI accepts, and <code>args</code> handles parsing, validation, and generating help text automatically:</p>
<pre><code class="language-dart">import 'package:args/args.dart';

void main(List&lt;String&gt; arguments) {
  final parser = ArgParser()
    ..addCommand('add')
    ..addCommand('list')
    ..addFlag('help', abbr: 'h', negatable: false);

  final results = parser.parse(arguments);

  if (results['help'] as bool) {
    print(parser.usage);
    return;
  }
}
</code></pre>
<p>For more complex CLIs with subcommands that each have their own flags, use <code>ArgParser</code> per command:</p>
<pre><code class="language-dart">final parser = ArgParser();

final addCommand = ArgParser()
  ..addOption('priority', abbr: 'p', defaultsTo: 'normal');

parser.addCommand('add', addCommand);
</code></pre>
<h3 id="heading-building-darttodo">Building dart_todo</h3>
<p>Create a fresh project:</p>
<pre><code class="language-bash">dart create -t console dart_todo
cd dart_todo
</code></pre>
<p>Update <code>pubspec.yaml</code>:</p>
<pre><code class="language-yaml">name: dart_todo
description: A terminal task manager built with Dart
version: 1.0.0

environment:
  sdk: '&gt;=3.0.0 &lt;4.0.0'

executables:
  dart_todo: dart_todo

dependencies:
  args: ^2.4.2

dev_dependencies:
  lints: ^3.0.0
  test: ^1.24.0
</code></pre>
<p>Run <code>dart pub get</code>.</p>
<p>Create the folder structure:</p>
<pre><code class="language-plaintext">dart_todo/
  bin/
    dart_todo.dart
  lib/
    models/
      task.dart
    storage/
      task_storage.dart
    commands/
      add_command.dart
      list_command.dart
      complete_command.dart
      delete_command.dart
      clear_command.dart
  pubspec.yaml
</code></pre>
<h4 id="heading-step-1-the-task-model-libmodelstaskdart">Step 1 — The Task Model (<code>lib/models/task.dart</code>)</h4>
<pre><code class="language-dart">class Task {
  final int id;
  final String title;
  final String priority;
  final bool isComplete;
  final DateTime createdAt;

  Task({
    required this.id,
    required this.title,
    required this.priority,
    this.isComplete = false,
    required this.createdAt,
  });

  Task copyWith({bool? isComplete}) {
    return Task(
      id: id,
      title: title,
      priority: priority,
      isComplete: isComplete ?? this.isComplete,
      createdAt: createdAt,
    );
  }

  Map&lt;String, dynamic&gt; toJson() =&gt; {
        'id': id,
        'title': title,
        'priority': priority,
        'isComplete': isComplete,
        'createdAt': createdAt.toIso8601String(),
      };

  factory Task.fromJson(Map&lt;String, dynamic&gt; json) =&gt; Task(
        id: json['id'] as int,
        title: json['title'] as String,
        priority: json['priority'] as String,
        isComplete: json['isComplete'] as bool,
        createdAt: DateTime.parse(json['createdAt'] as String),
      );
}
</code></pre>
<h4 id="heading-step-2-storage-libstoragetaskstoragedart">Step 2 — Storage (<code>lib/storage/task_storage.dart</code>)</h4>
<p>This class handles reading and writing tasks to a local JSON file so they persist between CLI runs:</p>
<pre><code class="language-dart">import 'dart:convert';
import 'dart:io';

import '../models/task.dart';

class TaskStorage {
  static final _file = File(
    '${Platform.environment['HOME'] ?? Directory.current.path}/.dart_todo.json',
  );

  static List&lt;Task&gt; loadAll() {
    if (!_file.existsSync()) return [];

    try {
      final content = _file.readAsStringSync();
      final List&lt;dynamic&gt; json = jsonDecode(content) as List&lt;dynamic&gt;;
      return json
          .map((e) =&gt; Task.fromJson(e as Map&lt;String, dynamic&gt;))
          .toList();
    } catch (_) {
      return [];
    }
  }

  static void saveAll(List&lt;Task&gt; tasks) {
    final json = jsonEncode(tasks.map((t) =&gt; t.toJson()).toList());
    _file.writeAsStringSync(json);
  }
}
</code></pre>
<p>Tasks are stored in a hidden JSON file in the user's home directory — a common pattern for CLI tools that need lightweight local persistence.</p>
<h4 id="heading-step-3-commands">Step 3 — Commands</h4>
<p><code>lib/commands/add_command.dart</code>:</p>
<pre><code class="language-dart">import 'dart:io';

import '../models/task.dart';
import '../storage/task_storage.dart';

void runAdd(List&lt;String&gt; args, String priority) {
  if (args.isEmpty) {
    stderr.writeln('Usage: dart_todo add &lt;title&gt; [--priority=high|normal|low]');
    exit(2);
  }

  final title = args.join(' ');
  final tasks = TaskStorage.loadAll();

  final newTask = Task(
    id: tasks.isEmpty ? 1 : tasks.last.id + 1,
    title: title,
    priority: priority,
    createdAt: DateTime.now(),
  );

  tasks.add(newTask);
  TaskStorage.saveAll(tasks);

  stdout.writeln('Added task #\({newTask.id}: "\)title" [$priority]');
}
</code></pre>
<p><code>lib/commands/list_command.dart</code>:</p>
<pre><code class="language-cpp">import 'dart:io';

import '../storage/task_storage.dart';

void runList() {
  final tasks = TaskStorage.loadAll();

  if (tasks.isEmpty) {
    stdout.writeln('No tasks yet. Add one with: dart_todo add &lt;title&gt;');
    return;
  }

  stdout.writeln('');
  stdout.writeln('  ID   Status      Priority   Title');
  stdout.writeln('  ───  ──────────  ─────────  ────────────────────────');

  for (final task in tasks) {
    final status = task.isComplete ? 'done  ' : 'pending';
    final id = task.id.toString().padRight(4);
    final priority = task.priority.padRight(9);
    stdout.writeln('  \(id \)status  \(priority  \){task.title}');
  }

  stdout.writeln('');
}
</code></pre>
<p><code>lib/commands/complete_command.dart</code>:</p>
<pre><code class="language-dart">import 'dart:io';

import '../storage/task_storage.dart';

void runComplete(List&lt;String&gt; args) {
  if (args.isEmpty) {
    stderr.writeln('Usage: dart_todo complete &lt;id&gt;');
    exit(2);
  }

  final id = int.tryParse(args[0]);
  if (id == null) {
    stderr.writeln('Error: "${args[0]}" is not a valid task ID');
    exit(1);
  }

  final tasks = TaskStorage.loadAll();
  final index = tasks.indexWhere((t) =&gt; t.id == id);

  if (index == -1) {
    stderr.writeln('Error: No task found with ID $id');
    exit(1);
  }

  if (tasks[index].isComplete) {
    stdout.writeln('Task #$id is already complete.');
    return;
  }

  tasks[index] = tasks[index].copyWith(isComplete: true);
  TaskStorage.saveAll(tasks);

  stdout.writeln('Task #\(id marked as complete: "\){tasks[index].title}"');
}
</code></pre>
<p><code>lib/commands/delete_command.dart</code>:</p>
<pre><code class="language-dart">import 'dart:io';

import '../storage/task_storage.dart';

void runDelete(List&lt;String&gt; args) {
  if (args.isEmpty) {
    stderr.writeln('Usage: dart_todo delete &lt;id&gt;');
    exit(2);
  }

  final id = int.tryParse(args[0]);
  if (id == null) {
    stderr.writeln('Error: "${args[0]}" is not a valid task ID');
    exit(1);
  }

  final tasks = TaskStorage.loadAll();
  final index = tasks.indexWhere((t) =&gt; t.id == id);

  if (index == -1) {
    stderr.writeln('Error: No task found with ID $id');
    exit(1);
  }

  final title = tasks[index].title;
  tasks.removeAt(index);
  TaskStorage.saveAll(tasks);

  stdout.writeln('Deleted task #\(id: "\)title"');
}
</code></pre>
<p><code>lib/commands/clear_command.dart</code>:</p>
<pre><code class="language-dart">import 'dart:io';

import '../storage/task_storage.dart';

void runClear() {
  stdout.write('Are you sure you want to delete all tasks? (y/N): ');
  final input = stdin.readLineSync()?.trim().toLowerCase();

  if (input != 'y') {
    stdout.writeln('Cancelled.');
    return;
  }

  TaskStorage.saveAll([]);
  stdout.writeln('All tasks cleared.');
}
</code></pre>
<h4 id="heading-step-4-entry-point-bindarttododart">Step 4 — Entry Point (<code>bin/dart_todo.dart</code>)</h4>
<pre><code class="language-dart">import 'dart:io';

import 'package:args/args.dart';

import '../lib/commands/add_command.dart';
import '../lib/commands/clear_command.dart';
import '../lib/commands/complete_command.dart';
import '../lib/commands/delete_command.dart';
import '../lib/commands/list_command.dart';

void main(List&lt;String&gt; arguments) {
  final parser = ArgParser();

  // Add subcommand parsers
  final addParser = ArgParser()
    ..addOption(
      'priority',
      abbr: 'p',
      defaultsTo: 'normal',
      allowed: ['high', 'normal', 'low'],
      help: 'Task priority level',
    );

  parser
    ..addCommand('add', addParser)
    ..addCommand('list')
    ..addCommand('complete')
    ..addCommand('delete')
    ..addCommand('clear')
    ..addFlag('help', abbr: 'h', negatable: false, help: 'Show help');

  ArgResults results;

  try {
    results = parser.parse(arguments);
  } catch (e) {
    stderr.writeln('Error: $e');
    stderr.writeln(parser.usage);
    exit(2);
  }

  if (results['help'] as bool || results.command == null) {
    printHelp(parser);
    exit(0);
  }

  final command = results.command!;

  switch (command.name) {
    case 'add':
      runAdd(command.rest, command['priority'] as String);
    case 'list':
      runList();
    case 'complete':
      runComplete(command.rest);
    case 'delete':
      runDelete(command.rest);
    case 'clear':
      runClear();
    default:
      stderr.writeln('Unknown command: "${command.name}"');
      exit(1);
  }
}

void printHelp(ArgParser parser) {
  stdout.writeln('''
dart_todo — a terminal task manager

Usage:
  dart_todo &lt;command&gt; [arguments]

Commands:
  add &lt;title&gt;        Add a new task
    -p, --priority   Priority: high, normal, low (default: normal)
  list               List all tasks
  complete &lt;id&gt;      Mark a task as complete
  delete &lt;id&gt;        Delete a task
  clear              Delete all tasks

Examples:
  dart_todo add "Write the CLI article" --priority=high
  dart_todo list
  dart_todo complete 1
  dart_todo delete 2
  dart_todo clear
  ''');
}
</code></pre>
<p>Run it:</p>
<pre><code class="language-bash">dart run bin/dart_todo.dart add "Write the CLI article" --priority=high
# Added task #1: "Write the CLI article" [high]

dart run bin/dart_todo.dart add "Review PR comments"
# Added task #2: "Review PR comments" [normal]

dart run bin/dart_todo.dart list
#   ID   Status      Priority   Title
#   ───  ──────────  ─────────  ────────────────────────
#   1    ⬜ pending  high       Write the CLI article
#   2    ⬜ pending  normal     Review PR comments

dart run bin/dart_todo.dart complete 1
# Task #1 marked as complete: "Write the CLI article"

dart run bin/dart_todo.dart delete 2
# Deleted task #2: "Review PR comments"
</code></pre>
<p><code>dart_todo</code> demonstrates the patterns that form the backbone of almost every real CLI tool — argument parsing with <code>args</code>, JSON persistence, interactive prompts, structured output, and clean error handling across every command.</p>
<h2 id="heading-cli-3-darthttp-a-lightweight-api-request-runner">CLI 3 — dart_http: A Lightweight API Request Runner</h2>
<p>This is the most complex CLI in this article – and the most immediately useful. <code>dart_http</code> lets developers make HTTP requests directly from the terminal, with pretty-printed JSON responses, response metadata, header support, and the ability to save responses to a file.</p>
<pre><code class="language-bash">dart_http get https://jsonplaceholder.typicode.com/users/1
dart_http post https://jsonplaceholder.typicode.com/posts --body='{"title":"Hello"}'
dart_http get https://jsonplaceholder.typicode.com/users --save=users.json
dart_http get https://api.example.com/me --header="Authorization: Bearer mytoken"
</code></pre>
<h3 id="heading-building-darthttp">Building dart_http</h3>
<p>Create the project:</p>
<pre><code class="language-bash">dart create -t console dart_http
cd dart_http
</code></pre>
<p>Update <code>pubspec.yaml</code>:</p>
<pre><code class="language-yaml">name: dart_http
description: A lightweight API request runner for the terminal
version: 1.0.0

environment:
  sdk: '&gt;=3.0.0 &lt;4.0.0'

executables:
  dart_http: dart_http

dependencies:
  args: ^2.4.2
  http: ^1.2.1

dev_dependencies:
  lints: ^3.0.0
  test: ^1.24.0
</code></pre>
<p>Run <code>dart pub get</code>.</p>
<p>Project structure:</p>
<pre><code class="language-plaintext">dart_http/
  bin/
    dart_http.dart
  lib/
    runner/
      request_runner.dart
    printer/
      response_printer.dart
    utils/
      headers_parser.dart
  pubspec.yaml
</code></pre>
<h4 id="heading-step-1-headers-parser-libutilsheadersparserdart">Step 1 — Headers Parser (<code>lib/utils/headers_parser.dart</code>)</h4>
<pre><code class="language-dart">Map&lt;String, String&gt; parseHeaders(List&lt;String&gt; rawHeaders) {
  final headers = &lt;String, String&gt;{};

  for (final header in rawHeaders) {
    final index = header.indexOf(':');
    if (index == -1) continue;

    final key = header.substring(0, index).trim();
    final value = header.substring(index + 1).trim();
    headers[key] = value;
  }

  return headers;
}
</code></pre>
<h4 id="heading-step-2-response-printer-libprinterresponseprinterdart">Step 2 — Response Printer (<code>lib/printer/response_printer.dart</code>)</h4>
<pre><code class="language-dart">import 'dart:convert';
import 'dart:io';

void printResponse({
  required int statusCode,
  required String body,
  required int durationMs,
  required int bodyBytes,
}) {
  final statusLabel = _statusLabel(statusCode);
  final size = _formatSize(bodyBytes);

  stdout.writeln('');
  stdout.writeln('\(statusLabel | \){durationMs}ms | $size');
  stdout.writeln('─' * 50);

  try {
    final decoded = jsonDecode(body);
    const encoder = JsonEncoder.withIndent('  ');
    stdout.writeln(encoder.convert(decoded));
  } catch (_) {
    // Not JSON — print as plain text
    stdout.writeln(body);
  }

  stdout.writeln('');
}

String _statusLabel(int code) {
  if (code &gt;= 200 &amp;&amp; code &lt; 300) return '✅ $code';
  if (code &gt;= 300 &amp;&amp; code &lt; 400) return '↪️  $code';
  if (code &gt;= 400 &amp;&amp; code &lt; 500) return '❌ $code';
  return '$code';
}

String _formatSize(int bytes) {
  if (bytes &lt; 1024) return '${bytes}b';
  if (bytes &lt; 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)}kb';
  return '${(bytes / (1024 * 1024)).toStringAsFixed(1)}mb';
}
</code></pre>
<h4 id="heading-step-3-request-runner-librunnerrequestrunnerdart">Step 3 — Request Runner (<code>lib/runner/request_runner.dart</code>)</h4>
<pre><code class="language-dart">import 'dart:io';

import 'package:http/http.dart' as http;

import '../printer/response_printer.dart';

Future&lt;void&gt; runRequest({
  required String method,
  required String url,
  required Map&lt;String, String&gt; headers,
  String? body,
  String? saveToFile,
}) async {
  final uri = Uri.tryParse(url);

  if (uri == null) {
    stderr.writeln('Error: "$url" is not a valid URL');
    exit(1);
  }

  stdout.writeln('→ \({method.toUpperCase()} \)url');

  http.Response response;
  final stopwatch = Stopwatch()..start();

  try {
    switch (method.toLowerCase()) {
      case 'get':
        response = await http.get(uri, headers: headers);
      case 'post':
        response = await http.post(uri, headers: headers, body: body);
      case 'put':
        response = await http.put(uri, headers: headers, body: body);
      case 'patch':
        response = await http.patch(uri, headers: headers, body: body);
      case 'delete':
        response = await http.delete(uri, headers: headers);
      default:
        stderr.writeln('Error: unsupported method "$method"');
        exit(2);
    }
  } catch (e) {
    stderr.writeln('Error: request failed — $e');
    exit(1);
  }

  stopwatch.stop();

  printResponse(
    statusCode: response.statusCode,
    body: response.body,
    durationMs: stopwatch.elapsedMilliseconds,
    bodyBytes: response.bodyBytes.length,
  );

  if (saveToFile != null) {
    final file = File(saveToFile);
    file.writeAsStringSync(response.body);
    stdout.writeln('Response saved to $saveToFile');
  }
}
</code></pre>
<h4 id="heading-step-4-entry-point-bindarthttpdart">Step 4 — Entry Point (<code>bin/dart_http.dart</code>)</h4>
<pre><code class="language-dart">import 'dart:io';

import 'package:args/args.dart';

import '../lib/runner/request_runner.dart';
import '../lib/utils/headers_parser.dart';

void main(List&lt;String&gt; arguments) async {
  final parser = ArgParser();

  for (final method in ['get', 'post', 'put', 'patch', 'delete']) {
    final commandParser = ArgParser()
      ..addMultiOption('header', abbr: 'H', help: 'Request header (repeatable)')
      ..addOption('body', abbr: 'b', help: 'Request body (for POST/PUT/PATCH)')
      ..addOption('save', abbr: 's', help: 'Save response body to a file');

    parser.addCommand(method, commandParser);
  }

  parser.addFlag('help', abbr: 'h', negatable: false, help: 'Show help');

  ArgResults results;

  try {
    results = parser.parse(arguments);
  } catch (e) {
    stderr.writeln('Error: $e');
    printHelp();
    exit(2);
  }

  if (results['help'] as bool || results.command == null) {
    printHelp();
    exit(0);
  }

  final command = results.command!;
  final method = command.name!;
  final rest = command.rest;

  if (rest.isEmpty) {
    stderr.writeln('Error: please provide a URL');
    stderr.writeln('Usage: dart_http $method &lt;url&gt;');
    exit(2);
  }

  final url = rest[0];
  final rawHeaders = command['header'] as List&lt;String&gt;;
  final body = command['body'] as String?;
  final saveToFile = command['save'] as String?;

  final headers = parseHeaders(rawHeaders);

  // Default Content-Type for requests with a body
  if (body != null &amp;&amp; !headers.containsKey('Content-Type')) {
    headers['Content-Type'] = 'application/json';
  }

  await runRequest(
    method: method,
    url: url,
    headers: headers,
    body: body,
    saveToFile: saveToFile,
  );
}

void printHelp() {
  stdout.writeln('''
dart_http — a lightweight API request runner

Usage:
  dart_http &lt;method&gt; &lt;url&gt; [options]

Methods:
  get       Send a GET request
  post      Send a POST request
  put       Send a PUT request
  patch     Send a PATCH request
  delete    Send a DELETE request

Options:
  -H, --header    Add a request header (repeatable)
  -b, --body      Request body (JSON string)
  -s, --save      Save response body to a file
  -h, --help      Show this help message

Examples:
  dart_http get https://jsonplaceholder.typicode.com/users
  dart_http get https://api.example.com/me --header="Authorization: Bearer token"
  dart_http post https://api.example.com/posts --body=\'{"title":"Hello"}\'
  dart_http get https://api.example.com/users --save=users.json
  ''');
}
</code></pre>
<p>Run it:</p>
<pre><code class="language-bash">dart run bin/dart_http.dart get https://jsonplaceholder.typicode.com/users/1

# → GET https://jsonplaceholder.typicode.com/users/1
# 200 | 87ms | 510b
# ──────────────────────────────────────────────────
# {
#   "id": 1,
#   "name": "Leanne Graham",
#   "username": "Bret",
#   "email": "Sincere@april.biz"
# }

dart run bin/dart_http.dart get https://jsonplaceholder.typicode.com/users --save=users.json
# → GET https://jsonplaceholder.typicode.com/users
# 200 | 143ms | 5.3kb
# ──────────────────────────────────────────────────
# [ ... ]
# Response saved to users.json

dart run bin/dart_http.dart post https://jsonplaceholder.typicode.com/posts \
  --body='{"title":"Hello from dart_http","userId":1}'
# → POST https://jsonplaceholder.typicode.com/posts
# 201 | 312ms | 72b
</code></pre>
<h2 id="heading-adding-color-and-polish-to-your-cli">Adding Color and Polish to Your CLI</h2>
<p>The CLIs above are functional, but terminal output can be made significantly more readable with color. The <code>ansi_styles</code> package provides ANSI escape code support for coloring text in the terminal.</p>
<p>Add it to <code>pubspec.yaml</code>:</p>
<pre><code class="language-yaml">dependencies:
  ansi_styles: ^0.3.0
</code></pre>
<p>Using it:</p>
<pre><code class="language-dart">import 'package:ansi_styles/ansi_styles.dart';

stdout.writeln(AnsiStyles.green('✅ Success'));
stdout.writeln(AnsiStyles.red('❌ Error: something went wrong'));
stdout.writeln(AnsiStyles.yellow('⚠️  Warning: check your config'));
stdout.writeln(AnsiStyles.bold('dart_http — API request runner'));
stdout.writeln(AnsiStyles.cyan('→ GET https://api.example.com/users'));
</code></pre>
<p>Apply color intentionally and consistently:</p>
<ul>
<li><p><strong>Green</strong> — success states, completed operations</p>
</li>
<li><p><strong>Red</strong> — errors and failures</p>
</li>
<li><p><strong>Yellow</strong> — warnings and non-blocking issues</p>
</li>
<li><p><strong>Cyan</strong> — informational output, URLs, paths</p>
</li>
<li><p><strong>Bold</strong> — headers, tool names, important values</p>
</li>
</ul>
<p>Avoid coloring everything. Color loses meaning when it is everywhere. Use it to draw the user's eye to what actually matters.</p>
<h2 id="heading-testing-your-cli-tool">Testing Your CLI Tool</h2>
<p>CLI tools are testable, and they should be tested. The most reliable approach is to test the logic inside your commands directly — not the terminal output formatting, but the behaviour.</p>
<p>Add <code>test</code> to your dev dependencies if it's not already there:</p>
<pre><code class="language-yaml">dev_dependencies:
  test: ^1.24.0
</code></pre>
<p><strong>Testing command logic:</strong></p>
<pre><code class="language-dart">import 'package:test/test.dart';

import '../lib/models/task.dart';

void main() {
  group('Task model', () {
    test('copyWith updates isComplete correctly', () {
      final task = Task(
        id: 1,
        title: 'Write tests',
        priority: 'high',
        createdAt: DateTime.now(),
      );

      final completed = task.copyWith(isComplete: true);

      expect(completed.isComplete, isTrue);
      expect(completed.title, equals('Write tests'));
      expect(completed.id, equals(1));
    });

    test('toJson and fromJson round-trips correctly', () {
      final task = Task(
        id: 2,
        title: 'Ship the tool',
        priority: 'normal',
        createdAt: DateTime.parse('2025-01-01T00:00:00.000'),
      );

      final json = task.toJson();
      final restored = Task.fromJson(json);

      expect(restored.id, equals(task.id));
      expect(restored.title, equals(task.title));
      expect(restored.priority, equals(task.priority));
    });
  });
}
</code></pre>
<p><strong>Testing the headers parser:</strong></p>
<pre><code class="language-dart">import 'package:test/test.dart';

import '../lib/utils/headers_parser.dart';

void main() {
  group('parseHeaders', () {
    test('parses a single header correctly', () {
      final result = parseHeaders(['Authorization: Bearer mytoken']);
      expect(result['Authorization'], equals('Bearer mytoken'));
    });

    test('parses multiple headers', () {
      final result = parseHeaders([
        'Authorization: Bearer token',
        'Accept: application/json',
      ]);
      expect(result.length, equals(2));
      expect(result['Accept'], equals('application/json'));
    });

    test('ignores malformed headers without a colon', () {
      final result = parseHeaders(['malformed-header']);
      expect(result.isEmpty, isTrue);
    });
  });
}
</code></pre>
<p>Run your tests:</p>
<pre><code class="language-bash">dart test
</code></pre>
<h2 id="heading-deploying-and-distributing-your-cli">Deploying and Distributing Your CLI</h2>
<p>Building a CLI tool is half the work. Getting it into the hands of developers is the other half. There are five distribution paths available, each suited to a different use case.</p>
<h3 id="heading-mode-1-pubdev-public-package-distribution">Mode 1: pub.dev — Public Package Distribution</h3>
<p>Publishing to pub.dev makes your tool installable by anyone in the Dart and Flutter community with a single command.</p>
<h4 id="heading-prepare-your-package">Prepare your package:</h4>
<p>Your <code>pubspec.yaml</code> needs to be complete:</p>
<pre><code class="language-yaml">name: dart_http
description: A lightweight API request runner for Dart developers.
version: 1.0.0
homepage: https://github.com/yourname/dart_http

environment:
  sdk: '&gt;=3.0.0 &lt;4.0.0'

executables:
  dart_http: dart_http
</code></pre>
<p>The <code>executables</code> block is critical. It tells pub.dev which script in <code>bin/</code> to expose as a runnable command.</p>
<p>You also need:</p>
<ul>
<li><p><code>README.md</code> — what the tool does, how to install it, usage examples</p>
</li>
<li><p><code>CHANGELOG.md</code> — version history</p>
</li>
<li><p><code>LICENSE</code> — an open source license (MIT is standard)</p>
</li>
</ul>
<h4 id="heading-validate-before-publishing">Validate before publishing:</h4>
<pre><code class="language-bash">dart pub publish --dry-run
</code></pre>
<p>This runs all validation checks without actually publishing. Fix any warnings before proceeding.</p>
<h4 id="heading-publish">Publish:</h4>
<pre><code class="language-bash">dart pub publish
</code></pre>
<p>You will be prompted to authenticate with your pub.dev account. Once published, your tool is available globally:</p>
<pre><code class="language-bash">dart pub global activate dart_http
dart_http get https://api.example.com/users
</code></pre>
<h3 id="heading-mode-2-local-path-activation">Mode 2: Local Path Activation</h3>
<p>For internal team tools that you don't want to publish publicly, activate directly from a local or cloned repository:</p>
<pre><code class="language-bash">dart pub global activate --source path /path/to/dart_http
</code></pre>
<p>Any developer on the team clones the repo and runs this command once. The tool is then available globally in their terminal without needing a pub.dev publish.</p>
<p>This is the right distribution mode for:</p>
<ul>
<li><p>Internal company tooling</p>
</li>
<li><p>Tools that depend on private packages</p>
</li>
<li><p>Work-in-progress tools shared within a team before a public release</p>
</li>
</ul>
<h3 id="heading-mode-3-compiled-binary-via-github-releases">Mode 3: Compiled Binary via GitHub Releases</h3>
<p>Dart can compile to a self-contained native executable — no Dart SDK required on the target machine. This makes your tool accessible to developers outside the Dart ecosystem.</p>
<h4 id="heading-compile">Compile:</h4>
<pre><code class="language-bash"># macOS
dart compile exe bin/dart_http.dart -o dist/dart_http-macos

# Linux
dart compile exe bin/dart_http.dart -o dist/dart_http-linux

# Windows
dart compile exe bin/dart_http.dart -o dist/dart_http-windows.exe
</code></pre>
<p>The compiled binary is fully self-contained. Copy it to any machine and run it — no Dart installation needed.</p>
<h4 id="heading-automate-with-github-actions">Automate with GitHub Actions:</h4>
<p>Create <code>.github/workflows/release.yml</code>:</p>
<pre><code class="language-yaml">name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v3

      - uses: dart-lang/setup-dart@v1
        with:
          sdk: stable

      - name: Install dependencies
        run: dart pub get

      - name: Compile binary
        run: |
          mkdir -p dist
          dart compile exe bin/dart_http.dart -o dist/dart_http-${{ runner.os }}

      - name: Upload binary to release
        uses: softprops/action-gh-release@v1
        with:
          files: dist/dart_http-${{ runner.os }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
</code></pre>
<p>Every time you push a version tag (<code>v1.0.0</code>), GitHub Actions compiles binaries for all three platforms and attaches them to the GitHub Release automatically.</p>
<h4 id="heading-write-an-install-script">Write an install script:</h4>
<pre><code class="language-bash">#!/usr/bin/env bash
set -euo pipefail

VERSION="1.0.0"
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
BINARY="dart_http-$OS"
INSTALL_DIR="/usr/local/bin"

curl -L "https://github.com/yourname/dart_http/releases/download/v\(VERSION/\)BINARY" \
  -o "$INSTALL_DIR/dart_http"

chmod +x "$INSTALL_DIR/dart_http"
echo "dart_http installed successfully"
</code></pre>
<p>Developers install it with:</p>
<pre><code class="language-bash">curl -fsSL https://raw.githubusercontent.com/yourname/dart_http/main/install.sh | bash
</code></pre>
<h3 id="heading-mode-4-homebrew-tap">Mode 4: Homebrew Tap</h3>
<p>Homebrew is the standard package manager for macOS and is widely used on Linux. A Homebrew tap makes your tool installable with <code>brew install</code> — the most familiar installation pattern for macOS developers.</p>
<h4 id="heading-create-your-tap-repository">Create your tap repository:</h4>
<p>Create a new GitHub repository named <code>homebrew-tools</code> (the <code>homebrew-</code> prefix is required by Homebrew's naming convention).</p>
<h4 id="heading-write-the-formula">Write the formula:</h4>
<p>Create <code>Formula/dart_http.rb</code> in that repository:</p>
<pre><code class="language-ruby">class DartHttp &lt; Formula
  desc "A lightweight API request runner for the terminal"
  homepage "https://github.com/yourname/dart_http"
  version "1.0.0"

  on_macos do
    url "https://github.com/yourname/dart_http/releases/download/v1.0.0/dart_http-macOS"
    sha256 "YOUR_SHA256_HASH_HERE"
  end

  on_linux do
    url "https://github.com/yourname/dart_http/releases/download/v1.0.0/dart_http-Linux"
    sha256 "YOUR_SHA256_HASH_HERE"
  end

  def install
    bin.install "dart_http-#{OS.mac? ? 'macOS' : 'Linux'}" =&gt; "dart_http"
  end

  test do
    system "#{bin}/dart_http", "--help"
  end
end
</code></pre>
<p>Generate the SHA256 hash for each binary:</p>
<pre><code class="language-bash">shasum -a 256 dist/dart_http-macOS
</code></pre>
<h4 id="heading-install-from-the-tap">Install from the tap:</h4>
<pre><code class="language-bash">brew tap yourname/tools
brew install dart_http
</code></pre>
<p>When you release a new version, update the <code>url</code> and <code>sha256</code> values in the formula and push the change. Users run <code>brew upgrade dart_http</code> to update.</p>
<h3 id="heading-mode-5-docker">Mode 5: Docker</h3>
<p>Docker distribution is best suited for CI environments, teams that standardise on containers, or tools with complex dependencies.</p>
<h4 id="heading-write-a-dockerfile">Write a Dockerfile:</h4>
<pre><code class="language-dockerfile">FROM dart:stable AS build

WORKDIR /app
COPY pubspec.* ./
RUN dart pub get

COPY . .
RUN dart compile exe bin/dart_http.dart -o /app/dart_http

FROM debian:stable-slim
COPY --from=build /app/dart_http /usr/local/bin/dart_http

ENTRYPOINT ["dart_http"]
</code></pre>
<p>This uses a multi-stage build: the first stage compiles the binary using the Dart SDK image, and the second stage copies only the binary into a minimal Debian image. The final image has no Dart SDK — just the compiled binary.</p>
<h4 id="heading-build-and-run">Build and run:</h4>
<pre><code class="language-bash">docker build -t dart_http .
docker run dart_http get https://jsonplaceholder.typicode.com/users/1
</code></pre>
<h4 id="heading-publish-to-docker-hub">Publish to Docker Hub:</h4>
<pre><code class="language-bash">docker tag dart_http yourname/dart_http:1.0.0
docker push yourname/dart_http:1.0.0
</code></pre>
<p>Users can then run your tool without installing anything locally:</p>
<pre><code class="language-bash">docker run yourname/dart_http get https://api.example.com/users
</code></pre>
<h2 id="heading-choosing-the-right-distribution-mode">Choosing the Right Distribution Mode</h2>
<table>
<thead>
<tr>
<th>Mode</th>
<th>Best for</th>
<th>Dart SDK required</th>
</tr>
</thead>
<tbody><tr>
<td>pub.dev</td>
<td>Public Dart/Flutter developer tools</td>
<td>Yes</td>
</tr>
<tr>
<td>Local path activation</td>
<td>Internal team tools, pre-release builds</td>
<td>Yes</td>
</tr>
<tr>
<td>Compiled binary</td>
<td>Language-agnostic tools, broad adoption</td>
<td>No</td>
</tr>
<tr>
<td>Homebrew tap</td>
<td>macOS/Linux developer tools</td>
<td>No</td>
</tr>
<tr>
<td>Docker</td>
<td>CI environments, complex dependencies</td>
<td>No</td>
</tr>
</tbody></table>
<p>For most tools, the practical recommendation is:</p>
<ul>
<li><p>Start with <strong>pub.dev</strong> if your audience is Dart developers</p>
</li>
<li><p>Add <strong>compiled binary + GitHub Releases</strong> once you want broader adoption</p>
</li>
<li><p>Add a <strong>Homebrew tap</strong> when macOS developers start asking for it</p>
</li>
<li><p>Use <strong>Docker</strong> only when it is already part of your team's workflow</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You've gone from understanding what a CLI is to building three progressively complex tools and distributing them across five different channels.</p>
<p>The foundational skills – <code>args</code>, <code>stdin</code>, <code>stdout</code>, <code>stderr</code>, exit codes, file I/O, and process spawning – are the same building blocks that tools like <code>flutter</code>, <code>git</code>, and <code>dart</code> themselves are built on. Everything else is composition.</p>
<p>The three CLIs we built (Hello CLI, <code>dart_todo</code>, and <code>dart_http</code>) each introduced a new layer: raw Dart fundamentals, the <code>args</code> package with JSON persistence, and real-world HTTP interaction. The distribution section ensures that whatever you build next, you have a clear path to getting it in front of the developers who will use it.</p>
<p>Dart is a powerful language for CLI development. Its strong typing, async support, native compilation, and pub.dev ecosystem make it a serious choice for building developer tooling, not just mobile apps.</p>
<p>The next step is building something that solves a real problem for you or your team, and shipping it.</p>
<p>Happy coding!!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Unblock Your AI PR Review Bottleneck: A Tech Lead’s Guide to Building a Codebase-Aware Reviewer ]]>
                </title>
                <description>
                    <![CDATA[ A few months ago, I was reviewing a pull request that added three new API endpoints. The diff was clean. Tests passed. The agent that generated it had even written sensible authorisation checks. By ev ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-unblock-ai-pr-review-bottleneck-handbook/</link>
                <guid isPermaLink="false">69f906a346610fd60629a300</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ code review ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Productivity ]]>
                    </category>
                
                    <category>
                        <![CDATA[ leadership ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Qudrat Ullah ]]>
                </dc:creator>
                <pubDate>Mon, 04 May 2026 20:50:43 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/c94dff21-66d0-4256-bf3e-25c1978364d9.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>A few months ago, I was reviewing a pull request that added three new API endpoints. The diff was clean. Tests passed. The agent that generated it had even written sensible authorisation checks. By every signal I usually rely on, it was ready to merge.</p>
<p>The problem only showed up when I checked which authentication middleware the agent had imported.</p>
<p>Our codebase had two: a v1 middleware backed by MongoDB and a v2 middleware backed by MySQL, which we had spent the previous quarter migrating.</p>
<p>New endpoints were supposed to use v2. The agent had used v1 for all three. Tests passed because user records still existed in both databases (that was the point of the migration), and the v1 middleware happily authenticated them. The code worked. But every new endpoint we shipped was reinforcing the legacy auth path we had just spent a quarter trying to retire.</p>
<p>I caught it on the second read. Twenty minutes after the comments, the engineer fixed it and reopened the PR. The third reviewer probably wouldn't have caught it. The migration timeline lived in a Slack thread from six months earlier. The rule that "new endpoints use v2" lived in my head.</p>
<p>This kind of catch is the slow-burn version of why AI changed my job as a tech lead. Code generation got faster. My review queue got longer. The hardest reviews were the ones where everything looked right, and the only thing wrong was something that lived in the team's collective memory rather than in the diff.</p>
<p>This handbook is about what we did to fix that. It's the story of how we went from drowning in clean-looking PRs to running a custom AI PR reviewer that catches a meaningful share of these mistakes before any human is pulled in. The fix turned out to be less about buying a better tool and more about moving the team's memory into a place the AI could actually read.</p>
<p>The lessons should transfer whether your team uses Claude Code, Cursor, Cline, GitHub Copilot, or any combination. The structure matters more than the tool.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-the-old-bottleneck-and-the-one-ai-created">The Old Bottleneck, and the One AI Created</a></p>
</li>
<li><p><a href="#heading-what-the-new-review-work-actually-looks-like">What the New Review Work Actually Looks Like</a></p>
</li>
<li><p><a href="#heading-why-i-did-not-just-buy-a-tool">Why I Did Not Just Buy a Tool</a></p>
</li>
<li><p><a href="#heading-the-realisation-move-the-rules-into-the-codebase">The Realisation: Move the Rules Into the Codebase</a></p>
</li>
<li><p><a href="#heading-two-files-that-changed-everything-agentsmd-and-claudemd">Two Files That Changed Everything: AGENTS.md and CLAUDE.md</a></p>
</li>
<li><p><a href="#heading-where-per-service-memory-files-earn-their-keep">Where Per-Service Memory Files Earn Their Keep</a></p>
</li>
<li><p><a href="#heading-what-this-looks-like-on-disk">What This Looks Like on Disk</a></p>
</li>
<li><p><a href="#heading-generated-documentation-as-a-side-effect">Generated Documentation as a Side Effect</a></p>
</li>
<li><p><a href="#heading-building-the-pr-review-command">Building the PR Review Command</a></p>
</li>
<li><p><a href="#heading-guardrails-read-only-by-default">Guardrails: Read-Only by Default</a></p>
</li>
<li><p><a href="#heading-the-compounding-loop-that-made-the-real-difference">The Compounding Loop That Made the Real Difference</a></p>
</li>
<li><p><a href="#heading-starting-from-zero-on-an-existing-project">Starting From Zero on an Existing Project</a></p>
</li>
<li><p><a href="#heading-what-still-needs-human-review">What Still Needs Human Review</a></p>
</li>
<li><p><a href="#heading-a-two-week-setup-plan">A Two-Week Setup Plan</a></p>
</li>
<li><p><a href="#heading-what-is-working-what-i-am-still-improving">What Is Working, What I Am Still Improving</a></p>
</li>
<li><p><a href="#heading-sources">Sources</a></p>
</li>
</ul>
<h2 id="heading-the-old-bottleneck-and-the-one-ai-created">The Old Bottleneck, and the One AI Created</h2>
<p>To understand why this fix was needed, it helps to remember what reviewing code looked like a couple of years ago.</p>
<p>Back then, the slow part was upstream of the PR. A ticket would land, and before anyone could open a branch, there was a long preamble of context-gathering.</p>
<p>Junior engineers needed time to understand what the change was for. Senior engineers had to explain business rules and architectural decisions. Tickets sat in "ready" columns for days while someone with the right context made themselves available. Then the writing itself took time, because typing real code is slower than typing comments about it.</p>
<p>That bottleneck mostly dissolved when the team got serious about AI-assisted development. Engineers used the agent to read the codebase, ask clarifying questions, draft an implementation plan, and produce a working branch in hours instead of days. Tickets moved through the queue faster. Junior engineers shipped more without blocking on senior availability. From the outside, this looked like an unambiguous win.</p>
<p>But the bottleneck didn't disappear. It moved.</p>
<p>Within a few weeks of widespread AI adoption, my review queue had doubled. Then tripled. Engineers were opening PRs faster than I could read them.</p>
<p>The PRs themselves looked clean: well-formatted, with sensible variable names, passing tests, and AI-generated descriptions that read better than most human-written ones.</p>
<p>On the surface, this was great. In practice, it was creating a different kind of pain. I was the senior engineer who knew which patterns mattered and which paths through the codebase were the right ones, and I was the bottleneck. The team's velocity was now capped by my reading speed.</p>
<p>The CircleCI 2026 State of Software Delivery report confirmed I was not alone. Drawing on more than 28 million CI workflow runs across over 22,000 organisations, the report showed feature branch throughput had grown 59% year over year, the largest jump CircleCI had ever measured. Main branch throughput, where code actually gets promoted to production, fell by 7% for the median team in the same period. Build success rates dropped to 70.8%, the lowest in five years.</p>
<p>The pattern was consistent across the industry. AI accelerated writing. The rest of the system absorbed the cost.</p>
<p>So the question for me, as a tech lead, became concrete: how do I unblock myself without lowering the bar?</p>
<h2 id="heading-what-the-new-review-work-actually-looks-like">What the New Review Work Actually Looks Like</h2>
<p>Before I explain the fix, it helps to know what kinds of issues were actually piling up. They weren't the dramatic kind. None of them would crash production. They were small, recurring, and looked plausible at a glance.</p>
<p>Take the simplest case I kept catching. An engineer would ask the agent to add a delete button on a new screen. The button needed to call our existing backend delete endpoint. Instead of reusing the hook the team already had for that endpoint, the agent would write the fetch call inline.</p>
<p>The code worked. The tests passed. But a week later, when someone changed the backend response shape, only one of the two call sites got updated.</p>
<p>That kind of duplication doesn't show up in a code review unless the reviewer happens to remember that a hook exists.</p>
<p>Another example I saw constantly: the agent comparing a status field against the literal string <code>"completed"</code> instead of using the <code>Status.Completed</code> enum that the rest of the services used. The code ran. The tests ran. The next refactor of the enum quietly skipped the file. After a few days, someone would spend half a day debugging a state machine that was working fine until the agent's literal silently fell out of sync.</p>
<p>These were two-minute fixes once spotted, but spotting them took me a reasonable time per PR. The friction wasn't the difficulty. It was the repetition.</p>
<p>The pattern repeated across larger problems, too.</p>
<p>I once asked an agent to build an event creation wizard. The wizard needed several dropdowns and one new component.</p>
<p>We have a design system folder where shared UI components live, and the rule on the team is simple: check there first, and if you build something new, register it there.</p>
<p>The agent had no way to know that. It only loaded the wizard's own files, so it never opened the design system folder. It generated brand new dropdowns inline, with APIs that were almost identical to the ones we already had. The new component went straight into the wizard rather than into the design system. CI passed. The wizard worked. We caught the duplication in human review, but it was the kind of catch that depended entirely on a reviewer who happened to know the design system existed.</p>
<p>The same pattern hit in one of the repos I was looking at for backend architecture. Backend follows a strict four-layer pattern: route, controller, app, repo. Controllers must never call repository functions directly. That rule keeps authorisation centralised, business logic testable, and database concerns isolated.</p>
<p>One PR I reviewed had the agent calling repo functions straight from a controller, skipping the app layer entirely. The code worked. The tests passed because the agent had also written tests against the new shape. But it broke a discipline the team had spent years building. If that PR had landed, the next AI-assisted PR could have used it as a template, and the layering would have eroded one diff at a time.</p>
<p>The common thread is that all of these mistakes had something written down somewhere, in code, in a Slack thread, in a senior engineer's head, that would have prevented them. The information existed. The agent just couldn't see it.</p>
<h2 id="heading-why-i-did-not-just-buy-a-tool">Why I Did Not Just Buy a Tool</h2>
<p>The obvious next move was to install one of the AI PR reviewers that flooded the market in 2026.</p>
<p>I evaluated several. Anthropic launched Claude Code Review in March 2026, billed on token usage and averaging \(15 to \)25 per review. CodeRabbit Pro charges \(24 per developer per month on annual billing, or \)30 per developer per month on monthly billing, with seats counted against developers who actually open PRs. Greptile in March 2026 moved to a base-plus-usage model at $30 per seat per month, including 50 reviews, after which each additional review costs a dollar. GitHub announced that all Copilot plans will transition to usage-based billing on June 1, 2026, with code reviews consuming both AI Credits and GitHub Actions minutes from that date.</p>
<p>For a small team with low PR volume, none of these is a dealbreaker. For a larger team running heavy AI-assisted development, the costs compound fast. A 10-person team running five PRs each per day blows through Greptile's included reviews in a single week. CodeRabbit Pro at \(24 per seat scales linearly with developers. The premium Claude Code Review at \)15 to $25 per PR is the most expensive option per review by an order of magnitude.</p>
<p>I looked at the cost numbers, but cost wasn't actually the deciding factor. The deciding factor was that none of these tools would have caught the problems I just listed.</p>
<p>A generic reviewer wouldn't have caught the v1/v2 middleware. It had no way to know v2 was the canonical path. A generic reviewer wouldn't have caught the duplicate dropdowns. It had no way to know our design system existed. A generic reviewer wouldn't have caught the bypassed architecture. It had no way to know that controllers must not call repositories.</p>
<p>The information that lets a reviewer flag any of these is exactly the information that lives in the team's head, not in any tool's default prompt.</p>
<p>The better-rated tools support custom rules, and that's where I started to see the real shape of the problem. Once you are configuring custom rules, you've already accepted that the value is in the rules. The tool is just whatever runs them.</p>
<p>This raised a different question: if the rules are the product, why pay per seat or per review for someone else's wrapper around them?</p>
<p>This is what made me change direction.</p>
<h2 id="heading-the-realisation-move-the-rules-into-the-codebase">The Realisation: Move the Rules Into the Codebase</h2>
<p>Once I started thinking of the rules as the product, the path forward got clearer.</p>
<p>I asked myself a simple question: what was I actually doing in code review that the AI was not? The answer turned out to be the same thing, over and over. I was typing review comments that captured a piece of the team's memory.</p>
<p>"Use the Status enum, not a string literal." "There is already a hook for this in <code>/hooks/useDeleteItem</code>." "Controllers must not import from the repo layer; route this through the app layer." "Check the design system folder before creating new components."</p>
<p>Each of those comments was knowledge that lived in my head and arrived in the codebase one PR comment at a time. None of it was available to the agent the next time it generated a similar PR.</p>
<p>So the fix was not to buy a smarter reviewer. The fix was to write the rules down in a place every agent on the team would read before any review happened.</p>
<p>If I had typed "use the enum, not a literal" three times in three different PRs, that was a rule the agent should know about from now on. If I had pointed at the design system folder for the fourth time, that was a rule. If I had explained the four-layer architecture twice in PR comments, that was a rule.</p>
<p>I needed somewhere to put these rules. That turned out to be a less obvious decision than I expected.</p>
<h2 id="heading-two-files-that-changed-everything-agentsmd-and-claudemd">Two Files That Changed Everything: AGENTS.md and CLAUDE.md</h2>
<p>If you start looking into how to give an AI agent a persistent project context, you run into two competing conventions almost immediately.</p>
<p>The first is <strong>AGENTS.md</strong>, an open standard that has gathered real momentum. According to InfoQ, by mid-2025, the format had already been adopted by more than 20,000 GitHub repositories and was being positioned as a complement to traditional documentation: machine-readable context that lives alongside human-facing files like README.md.</p>
<p>The standard's own site reports it is now used by more than 60,000 open-source projects and has moved to stewardship under the Agentic AI Foundation, which sits inside the Linux Foundation. The format is supported by OpenAI Codex, GitHub Copilot, Google Gemini, Cursor, and Windsurf, among others.</p>
<p>The second is <strong>CLAUDE.md</strong>, which is Anthropic's convention for Claude Code. The Claude Code documentation describes two complementary memory systems: CLAUDE.md, where you write the persistent context yourself, and an auto-memory mechanism that lets Claude save its own notes from corrections and observed patterns. By default, Claude Code reads CLAUDE.md, not AGENTS.md.</p>
<p>This split mattered for us because half the team uses Claude Code and the other half uses Cursor. We had two practical options: maintain both files with the same content (and accept the duplication), or symlink one filename to the other so both ecosystems read the same source of truth. We went with the symlink. It's one less thing to drift.</p>
<p>The next question was what to actually put in the file. After a few iterations, here's the shape that worked. Think of it as a briefing document for a new engineer who has read no code and seen no Slack threads. The minimum content was:</p>
<ul>
<li><p>The tech stack (languages, frameworks, package manager)</p>
</li>
<li><p>The project structure, especially important for our monorepo</p>
</li>
<li><p>Where shared utilities, components, and helpers live, and the rule that new code should reuse them before creating new versions</p>
</li>
<li><p>Architectural patterns the project follows, with file path examples</p>
</li>
<li><p>Anti-patterns and what to do instead</p>
</li>
<li><p>Test conventions and where good examples live</p>
</li>
<li><p>Pointers to deeper documentation when more detail is needed</p>
</li>
</ul>
<p>Two practical rules emerged from the first month of using these files.</p>
<p><strong>Keep them lean:</strong> There is a counterintuitive failure mode with long instruction lists: the agent doesn't just skip the new ones at the bottom. The average compliance across all of them drops. A bloated memory file becomes a memory file that the agent skims. If a section runs more than a paragraph or two, move it to a separate document and link to it.</p>
<p><strong>Phrase rules as imperatives, not aspirations:</strong> "Controllers must not call repositories. Route through the app layer." beats "Try to keep controllers thin." The first is testable. The second is decorative.</p>
<p>That was the entry point. But a single root-level file was not enough for a monorepo with multiple services and frontends, which led to the next decision.</p>
<h2 id="heading-where-per-service-memory-files-earn-their-keep">Where Per-Service Memory Files Earn Their Keep</h2>
<p>A single <code>AGENTS.md</code> at the root of a monorepo collapses under its own weight pretty quickly. Each service in our codebase has its own architecture, conventions, and business rules. Trying to fit all of that into one file produced a long document that the agent treated as background noise, and we were back to the bloat problem from the previous section.</p>
<p>The pattern that worked: every service or app gets its own <code>AGENTS.md</code> at its root, and the project-level <code>AGENTS.md</code> becomes an index that points to them.</p>
<p>A per-service <code>AGENTS.md</code> covers things like:</p>
<ul>
<li><p>The architecture for this service (the four-layer pattern, the directory layout)</p>
</li>
<li><p>Naming conventions specific to this service</p>
</li>
<li><p>Test patterns and where good examples live</p>
</li>
<li><p>Business rules that this service is responsible for</p>
</li>
<li><p>Inter-service contracts and what other services consume from this one</p>
</li>
<li><p>Pointers to deeper docs in <code>docs/</code></p>
</li>
<li><p>A "Lessons learned" section, which I'll come back to in the section on the compounding loop</p>
</li>
</ul>
<p>The same lean rule applies. Keep it short, point at examples, and phrase guidance as imperatives.</p>
<p>The reason this works mechanically is that the agent loads the right files for the work at hand. When an engineer asks the agent to change something in <code>backend/</code>, the agent reads the project-level <code>AGENTS.md</code>, sees that work in <code>backend/</code> should be guided by <code>backend/AGENTS.md</code>, and loads that file. It doesn't load the frontend's <code>AGENTS.md</code>, because that work is somewhere else. The context window stays focused on what's relevant.</p>
<p>Without this split, you have two bad options. Either you put everything in the root file, where the agent ignores most of it, or you put nothing in the root file, where the agent has no team context at all. The per-service split gives you both depth and signal.</p>
<p>But these files only work if the deeper docs they point to actually exist, which is where the next piece of the system came in.</p>
<h2 id="heading-what-this-looks-like-on-disk">What This Looks Like on Disk</h2>
<p>Before going further, it helps to see the whole structure laid out. Here's the shape we settled on for our monorepo. The exact folder names follow Claude Code's conventions. If you use Cursor, it would be <code>.cursor/</code>, and if you use Cline, it would be <code>.clinerules</code> – but the shape transfers directly.</p>
<pre><code class="language-plaintext">project-root/
├── AGENTS.md                       # symlink to CLAUDE.md
├── CLAUDE.md                       # root memory file
├── README.md                       # human-facing project readme
│
├── .claude/                        # tool-specific config folder
│   ├── README.md                   # explains the .claude/ layout
│   ├── settings.json               # permissions and guardrails
│   ├── agents/                     # specialised subagents (optional)
│   ├── commands/                   # slash commands engineers run
│   │   ├── review-pr.md            # the PR review command
│   │   └── plan-feature.md         # implementation plan command
│   ├── hooks/                      # lifecycle hooks (optional)
│   ├── pr-rules/                   # rule files for PR review
│   │   ├── common.md               # rules that apply to every PR
│   │   ├── frontend.md             # rules for frontend changes
│   │   ├── backend.md              # rules for backend changes
│   │   ├── service-a.md            # rules for service-a
│   │   └── service-b.md            # rules for service-b
│   └── skills/                     # reusable workflows
│
├── frontend/
│   ├── AGENTS.md                   # frontend conventions
│   ├── docs/
│   │   ├── overview.md
│   │   ├── architecture.md         # routing, state, data layer
│   │   ├── design-system.md        # design system reference
│   │   └── testing.md              # test conventions
│   └── src/
│
├── backend/
│   ├── AGENTS.md                   # the four-layer pattern
│   ├── docs/
│   │   ├── overview.md
│   │   ├── architecture.md         # route -&gt; controller -&gt; app -&gt; repo
│   │   ├── auth.md                 # v1 vs v2 middleware
│   │   ├── business-rules.md
│   │   └── integrations.md
│   └── src/
│
├── service-a/
│   ├── AGENTS.md
│   ├── docs/
│   │   ├── overview.md
│   │   ├── business-rules.md
│   │   └── integrations.md
│   └── src/
│
└── service-b/
    ├── AGENTS.md
    ├── docs/
    │   ├── overview.md
    │   ├── business-rules.md
    │   └── integrations.md
    └── src/
</code></pre>
<p>A few things worth pointing out:</p>
<p>The <code>.claude/</code> folder uses standard subfolder names: <code>commands</code>, <code>agents</code>, <code>hooks</code>, <code>skills</code>. These follow Claude Code's plugin model, but most modern AI coding tools have similar slots. Following the conventions makes the structure recognisable to anyone on the team and lowers the cost of switching tools later.</p>
<p>The <code>pr-rules/</code> folder isn't a standard convention. It's a folder we created to hold per-area review rules that the PR review command loads selectively. You don't have to call it <code>pr-rules</code> – the name matters less than having one place where review rules live.</p>
<p>Each service has its own <code>AGENTS.md</code> plus a <code>docs/</code> folder. The root <code>AGENTS.md</code> is short and acts as an index. It tells the agent things like "if you touch files in <code>backend/</code>, also read <code>backend/AGENTS.md</code> first." The per-service file then points at the deeper docs as needed.</p>
<h2 id="heading-generated-documentation-as-a-side-effect">Generated Documentation as a Side Effect</h2>
<p>Setting up per-service <code>AGENTS.md</code> files surfaced a problem I had been quietly avoiding. Most of our services didn't have decent documentation. Not API reference material, which lives in code, but the higher-level "what does this service do, what business rules does it enforce, what does it consume and produce" information that lives in nobody's head except the original author's.</p>
<p>The honest reason was that writing this kind of documentation by hand had never paid back the time it took. By the time the doc was finished, half of it was already stale.</p>
<p>So I tried something I wouldn't have considered earlier. I used the AI itself to generate a first draft for each service. I pointed the agent at each service's code and asked it to produce a <code>docs/</code> folder with a specific structure: an overview, a list of business rules, an integrations document, a domain model, and any quirks worth knowing. The agent read the code, traced the call paths, and wrote a draft.</p>
<p>I then reviewed the output by hand, corrected the things it got wrong, and committed the result. The first drafts were 70-80% correct. The remaining 20-30% was where the agent had made plausible but wrong inferences, and those were exactly the cases where human review mattered.</p>
<p>The generated docs ended up serving two audiences. The agent uses them when reasoning about changes, which means it has real context for the service it's touching rather than guessing from local files. And new engineers use them on their first day, which has cut our onboarding time noticeably.</p>
<p>We used to write onboarding documents that drifted out of date within months. These docs stay closer to current because the agent reads them on every PR, and any drift gets surfaced when the agent gives wrong advice based on stale information.</p>
<p>The pattern that works is to keep the per-service <code>AGENTS.md</code> short and pointing at the docs, rather than duplicating their content. <code>AGENTS.md</code> is the always-loaded index. <code>docs/</code> holds the details. The agent loads the relevant doc on demand when the task calls for it.</p>
<p>With the rules in place and the docs in place, I had everything I needed to build the actual reviewer.</p>
<h2 id="heading-building-the-pr-review-command">Building the PR Review Command</h2>
<p>This is the piece that most directly unblocked my queue.</p>
<p>This command didn't appear out of nowhere. It started as the checklist I was running through in my head every time I opened a PR. I was reviewing every change manually, leaving the same comments, flagging the same patterns. So I wrote that checklist down, expanded it with references to the per-service docs for the harder rules, and turned it into a command anyone on the team could run.</p>
<p>Then I handed it to the engineers and changed the rule: run this on your own branch before marking the PR ready for review. That single shift moved the work from after the PR was opened to before. Engineers now catch 90-95% of the blockers, improvements, and nice-to-haves on their own machine, fix them locally, and only then push the change.</p>
<p>The PR description includes the AI's summary, so when anyone opens the PR, they can see the reviewer's green signal at the top before even reading the diff.</p>
<p>GitHub stays clean. The conversation on the PR becomes about the things that actually need a human, not the recurring stuff the team already knows how to fix.</p>
<p>The command lives in <code>.claude/commands/review-pr.md</code>. Here's a generalised version. Your tool's command structure may differ, but the shape is what matters.</p>
<pre><code class="language-markdown"># Review PR

Review the current branch's PR. Be direct. Cite `file:line`. Surface real issues,
no padding.

## 1. Scope the diff

Run, in order:

    gh pr view --json number,title,body,headRefName 2&gt;/dev/null || true
    git fetch origin main
    git log --no-merges origin/main..HEAD --oneline
    git diff origin/main...HEAD --stat
    git diff origin/main...HEAD

Read the PR body. Note the stated intent. Every change should trace to it. Flag
anything that does not.

Use `...` (three dots) for the diff. It compares against the merge base and
excludes commits brought in by merging main.

## 2. Load rules

Always read `.claude/pr-rules/common.md`.

Then read the per-area file for each workspace touched in the diff:

| Workspace path | Rules file                      |
| -------------- | ------------------------------- |
| `frontend/**`  | `.claude/pr-rules/frontend.md`  |
| `backend/**`   | `.claude/pr-rules/backend.md`   |
| `service-a/**` | `.claude/pr-rules/service-a.md` |
| `service-b/**` | `.claude/pr-rules/service-b.md` |

For non-trivial changes, follow doc pointers inside the rules files (for
example, `backend/AGENTS.md`, `backend/docs/architecture.md`).

Apply every entry under each file's "Lessons learned" section as a check.

## 3. Output

Use exactly this format.

    ## Summary
    &lt;one paragraph: what the PR does, whether it matches the stated intent&gt;

    ## Blocking
    - [file:line] issue, why it blocks

    ## Should fix
    - [file:line] issue

    ## Nice to have
    - issue

    ## Verified
    - what was checked and looks good

If nothing blocks, say so. Do not manufacture concerns.

If you find an issue worth remembering for future PRs, suggest the bullet to
add to the relevant rules file's "Lessons learned" section. Do not edit the
rules file yourself, leave that to the human.
</code></pre>
<p>A few of the design choices in this command turned out to matter more than I expected.</p>
<p>The structured output format (Summary, Blocking, Should fix, Nice to have, Verified) keeps the review easy to scan and easy to paste into a PR description. The "Verified" section is the most underrated of the five: it tells the human reviewer what the AI already checked, so they can spend their attention elsewhere. Without it, the human reviewer ends up doing the same checks twice.</p>
<p>The instruction to be direct and stop padding does real work. Without it, AI reviewers tend to manufacture concerns to look thorough, which trains engineers to skim past the bot. Telling it explicitly to say "nothing blocks" when nothing blocks made the signal-to-noise ratio of the output much better.</p>
<p>The "suggest a bullet for the rules file" instruction at the end is the heart of the whole system, and I'll explain why in the section on the compounding loop. The key constraint here is that the agent suggests the bullet but doesn't commit to it. A human evaluates whether it's general enough to be a rule, and only then adds it to the file. That manual step is what keeps the rules sharp instead of bloated.</p>
<p>With each PR, if humans fix something or the AI suggests something, you keep adding those to your MD files and keep improving your agents for the future. The result compounds quickly.</p>
<p>One more thing here: the diff-scoping commands are all read-only. The command shouldn't be able to push, edit PRs, or close anything. Which is the next piece of the system.</p>
<h2 id="heading-guardrails-read-only-by-default">Guardrails: Read-Only by Default</h2>
<p>Giving an AI agent broad permissions on your codebase is a security incident waiting to happen. Even if you trust the model to behave, an LLM occasionally does unexpected things, and a fast-moving agent on an unrestricted shell can cause damage in seconds.</p>
<p>The fix is a <code>settings.json</code> (in Claude Code – other tools have their own equivalents) at the root of <code>.claude/</code> that explicitly declares what the agent can and can't do. The deny list matters more than the allow list, and a good one is organised around four categories of risk.</p>
<p>The first is <strong>secrets and configuration</strong>. Any read against anything that appears to be a credential is blocked. That covers <code>.env</code> files of every variant (<code>.env</code>, <code>.env.local</code>, <code>.env.production</code>, <code>.env.test</code>, and so on), <code>.npmrc</code>, <code>.netrc</code>, <code>.pgpass</code>, <code>id_rsa</code>, <code>id_ed25519</code>, <code>*.pem</code>, <code>*.key</code>, <code>*.p12</code>, <code>**/credentials.json</code>, <code>**/secrets.json</code>, <code>**/.aws/**</code>, <code>**/.ssh/**</code>, <code>**/.gcloud/**</code>, and <code>**/.kube/**</code>. Environment dumps are blocked too: <code>env</code>, <code>printenv</code>, <code>set</code>, <code>export</code>. The agent has no legitimate reason to read or echo any of these, ever.</p>
<p>The second is <strong>destructive Git operations</strong>. The agent can read Git history but can't rewrite or push it. Blocked: <code>git push</code>, <code>git commit</code>, <code>git revert</code>, <code>git cherry-pick</code>, <code>git merge</code>, <code>git rebase</code>, <code>git reset --hard</code>, <code>git tag</code>. Allowed: <code>git fetch</code>, <code>git status</code>, <code>git log</code>, <code>git diff</code>, <code>git show</code>, <code>git branch</code>, <code>git rev-parse</code>, <code>git merge-base</code>, <code>git config --get</code>.</p>
<p>The third is <strong>write operations on PRs and issues</strong>. The agent can read your GitHub state but can't act on it. Blocked: <code>gh pr create</code>, <code>gh pr edit</code>, <code>gh pr merge</code>, <code>gh pr close</code>, <code>gh pr comment</code>, <code>gh pr review</code>, <code>gh issue create</code>, <code>gh issue edit</code>, <code>gh issue close</code>, <code>gh issue comment</code>, <code>gh release create</code>, <code>gh repo create</code>, <code>gh repo edit</code>, <code>gh repo delete</code>. Allowed: <code>gh pr view</code>, <code>gh pr list</code>, <code>gh pr diff</code>, <code>gh pr checks</code>, <code>gh issue view</code>, <code>gh issue list</code>, <code>gh release view</code>.</p>
<p>The fourth is <strong>workflow and automation control</strong>. These are the surfaces where a compromised or misled agent could do the most damage. Blocked: <code>gh workflow run</code>, <code>gh run rerun</code>, <code>gh run cancel</code>, <code>gh secret</code>, <code>gh variable</code>, <code>gh auth</code>, <code>gh ssh-key</code>, <code>gh gpg-key</code>, and the unrestricted <code>gh api</code>.</p>
<p>For shell commands the agent legitimately needs to run, like build and test commands, allowlist specific patterns: <code>pnpm test</code>, <code>pnpm lint</code>, <code>pnpm format:check</code>, <code>pnpm build</code>, <code>pnpm vitest</code>. Anything outside the allowed list requires human confirmation. These are your own settings&nbsp;– I've just mentioned what I prefer.</p>
<p>The pattern is simple: read-only by default, write-allowed only for the specific commands you have explicitly approved. The agent can investigate, plan, and recommend. It can't ship.</p>
<p>With the structure in place and the guardrails set, the system started doing its job. What I didn't expect was how much better it would get over the months that followed.</p>
<h2 id="heading-the-compounding-loop-that-made-the-real-difference">The Compounding Loop That Made the Real Difference</h2>
<p>When we started, the AI reviewer was useful but not transformative. It caught some obvious issues, missed plenty of subtle ones, and produced a fair amount of noise.</p>
<p>The first month, my review burden dropped by 35%. The time I was spending on PR checking was reduced to 1/3, almost. Decent, not life-changing.</p>
<p>What changed over time wasn't the tool. It was the rules.</p>
<p>Every time a PR creator and reviewer caught something the AI had missed, we were adding bullets to the relevant rules file. Every time the AI flagged something useful that turned out to be a recurring pattern, the agent's own suggestion at the end of the review went into the file.</p>
<p>After a few days, the rules files had grown into something that captured a meaningful fraction of the team's collective review knowledge, written down in a place every agent on the team would read.</p>
<p>The catch rate went up. The noise went down because the rules also said what was acceptable and what we already considered solved. New engineers stopped getting the same comments on their first three PRs because the AI caught the comments first. Engineers joining the team didn't have to absorb the conventions through six months of review feedback. They installed the project, opened it in their editor, and the agent already knew.</p>
<p>This is the part most teams miss when they evaluate AI PR review tools. They look at the catch rate today and decide whether the tool is worth the price. The catch rate today isn't the right number. The right number is what the catch rate looks like in six months, after the rules file has absorbed every recurring mistake your team has made.</p>
<p>A single rule written down today saves a small amount of review time. Over a hundred PRs, it saves more. After a year, the rules file is a written-down version of a tech lead's accumulated taste. We've switched between Claude Code, the GitHub Copilot CLI, and Cursor for various tasks during this period. The AI tool changes, but the rules file in the repo stays the same.</p>
<p>The discipline that makes this work is treating the rules file as living documentation. Every recurring review comment is a candidate for promotion into the file. If you catch yourself typing the same feedback in two different PRs, that's a rule that belongs in <code>pr-rules/</code>. The "suggest a bullet" instruction in the review command is what makes this practical: the AI does the typing, the human does the deciding.</p>
<p>This is also what made me realise the system was worth the time it took to set up. The PR review command, on its own, is useful but unremarkable. The compounding loop is what turns it into infrastructure.</p>
<h2 id="heading-starting-from-zero-on-an-existing-project">Starting From Zero on an Existing Project</h2>
<p>If you've read this far and feel like the gap between your project and what I just described is a sprint of work, that's the most common reaction. It's also not correct.</p>
<p>The blank <code>AGENTS.md</code> is intimidating, especially on an existing codebase. You know your team has a thousand conventions, and writing a thousand rules sounds like a project that takes weeks before it produces any value.</p>
<p>The honest answer is that you can't write all the rules up front, and you shouldn't try. The first version of any of these files should take an afternoon, not a sprint.</p>
<p>Here's how I would actually start.</p>
<p>Run <code>/init</code> (or your tool's equivalent). In Claude Code, <code>/init</code> scans the project, infers the obvious shape (language, framework, entry points, build commands), and writes an initial <code>CLAUDE.md</code>. The output is a starting point, not a finished file. Read it, delete most of what it generates, and keep the bones.</p>
<p>Then add three things, each one bullet long.</p>
<p>First, an architecture rule. Pick the single most important convention your team enforces. For us, that was the four-layer pattern. The bullet was: "Controllers must not call repository functions directly. They must go through the app layer."</p>
<p>Second, a discoverability rule. Pick the single most important shared resource the team has, the one new code is most likely to duplicate. For us, that was the design system. The bullet was: "Before creating a new UI component, check <code>/src/design-system/</code> first."</p>
<p>Third, a "do not touch" rule. Pick the single most dangerous file or area in the codebase. Auth, billing, or migrations whichever has the most production risk. The bullet was: "Do not modify files in <code>/auth/</code> without human approval."</p>
<p>That's enough to start. Three rules, ten minutes of writing, and most of your team's recurring AI mistakes start to drop.</p>
<p>If even three rules feels like too much, start with one. Pick a single line that matters in your codebase and write it down.</p>
<p>"No <code>any</code> types in TypeScript." "Always use the enum, never compare against the string literal." "Run the linter before opening a PR." It doesn't have to be sophisticated. It doesn't have to cover edge cases. It just has to capture one piece of judgement that lives in your head today and would otherwise stay there.</p>
<p>Tomorrow, add another. The first week, you might catch 5% of the recurring mistakes. By 20 or 30 PRs in, you might catch 20-30%. The rules file doesn't need to be impressive on day one. It needs to exist and keep growing.</p>
<p>This is the compounding effect I'll come back to soon, and it's the reason this approach works on real projects rather than just in theory.</p>
<p>From there, the file grows the same way it would grow for any team. Every review catch becomes a candidate rule. After a few weeks, you have ten or fifteen rules. After a few months, you have a real review system.</p>
<p>The mistake is trying to write the perfect file on day one. The right file is the one you start with and keep editing.</p>
<h2 id="heading-what-still-needs-human-review">What Still Needs Human Review</h2>
<p>This system doesn't replace human review, and it shouldn't be allowed to.</p>
<p>The AI reviewer catches what the rules describe, plus a fair number of obvious things it would have spotted anyway. It doesn't catch problems that depend on context the rules don't capture. It doesn't catch product judgement. It doesn't catch the question of whether the change should have been built at all.</p>
<p>It also has an important blind spot when reviewing AI-authored code. The reviewer shares the same training data and reasoning patterns as the agent that wrote the code. If the original agent missed the v1/v2 distinction because it had no way to see the migration timeline, an AI reviewer reading the same diff has the same problem. Two AIs in a review loop are not two independent reviewers. They share blind spots.</p>
<p>That is why the AI reviewer in this setup never approves a PR. It produces a structured review that goes into the PR description. A human still reads the change and approves it. The AI is the first pass, not the gate.</p>
<p>Accountability also has to live with a human. When something the AI approved breaks production, someone has to own the post-mortem and decide what changes are needed for next time. The AI can't be that person. What it can do, well, is reduce the stack of small mistakes a human reviewer has to find before they get to the harder questions.</p>
<h2 id="heading-a-two-week-setup-plan">A Two-Week Setup Plan</h2>
<p>If you want to set this up for your own team, here's a concrete plan that fits in a couple of weeks. None of this needs to happen in a single push.</p>
<h3 id="heading-day-1-bootstrap-the-memory-file">Day 1: Bootstrap the memory file.</h3>
<p>Run <code>/init</code> (or your tool's equivalent) at the root of the project. Read the generated <code>CLAUDE.md</code> (or <code>AGENTS.md</code>). Delete most of it. Keep the tech stack and project structure sections.</p>
<p>Add the three rules from the previous section: one architecture rule, one discoverability rule, and one "do not touch" rule. Decide whether you want both files or a symlink.</p>
<h3 id="heading-day-2-add-per-service-files-for-your-highest-risk-areas">Day 2: Add per-service files for your highest-risk areas</h3>
<p>Pick the two or three areas of the codebase that change most often or carry the most risk. Add an <code>AGENTS.md</code> to each, following the same lean pattern. Include the architectural pattern for that area, the naming conventions, where to find good test examples, and pointers to any existing docs. Skip anything that doesn't need to be there yet.</p>
<h3 id="heading-day-3-set-up-the-directory-structure-and-guardrails">Day 3: Set up the directory structure and guardrails</h3>
<p>Create a <code>.claude/</code> folder (or your tool's equivalent) at the root, with <code>commands/</code> and <code>pr-rules/</code> subfolders. Add a <code>settings.json</code> with the deny list categories from the guardrails section. Test that the agent can't read a <code>.env</code> file, run <code>git push</code>, or create a PR. If any of those work, fix the settings before doing anything else.</p>
<h3 id="heading-day-4-write-the-pr-review-command">Day 4: Write the PR review command</h3>
<p>Adapt the command in this article to your structure. Include the diff scoping, the rule loading, the output format, and the "suggest a new rule" instruction at the end. Run it on a branch you've already merged, and tune the output until it's useful.</p>
<h3 id="heading-day-5-run-it-on-real-prs">Day 5: Run it on real PRs</h3>
<p>Have one or two engineers run the command on their next PRs before opening them. Read the output. Note what it caught, what it missed, and what was noise. Add the missing catches to the rules files. The first week is mostly tuning.</p>
<h3 id="heading-week-2-roll-out-and-document">Week 2: Roll out and document</h3>
<p>Once the command produces useful output reliably, ask the whole team to run it before opening PRs and paste the output into the PR description. Add a short section to your contributing guide explaining the workflow. Set a recurring item in your team's rituals to review the rules files monthly and trim anything that has gone stale.</p>
<p>That gets you to a working system. From there, the maintenance is incremental. Every recurring review comment becomes a candidate rule. Every architectural decision becomes a candidate update to the relevant <code>AGENTS.md</code>. The system improves as a side effect of the work the team is already doing.</p>
<h2 id="heading-what-is-working-what-i-am-still-improving">What Is Working, What I Am Still Improving</h2>
<p>Here's my honest assessment after a few months of running this:</p>
<h3 id="heading-whats-working">What's Working</h3>
<p>My review burden is meaningfully smaller. Engineers fix most of the easy mistakes before I see the PR. The "Verified" section of the AI's output tells me what to skip past. New engineers ramp faster because the conventions live in a place their tooling reads. The rules files have grown into something I would actually use to onboard someone new.</p>
<h3 id="heading-what-isnt-finished">What Isn't Finished</h3>
<p>The AI still misses problems that depend on context, and the rules don't capture them. The rules files grow, but they also need pruning, and we haven't been disciplined about that.</p>
<p>We're still figuring out how to handle rules that apply only conditionally. Docs are helping in that case, but we need to keep those up to date. And no system survives a determined engineer who skips the workflow or docs when they're in a rush.</p>
<p>There's no shortcut here. The work is real, ongoing, and mostly about discipline. The discipline is treating your codebase as something the AI needs to learn, and treating every recurring review comment as something that should be written down once instead of typed thirty times. If you're willing to do that, the tools take care of the rest.</p>
<p>If you take three things from this article, take these.</p>
<ol>
<li><p>First, don't pay for a generic reviewer to do a job your codebase needs to inform. Generic reviewers catch generic problems. Most of your real review work is specific to your team.</p>
</li>
<li><p>Second, put the rules in a file the AI reads, not in your head. <code>AGENTS.md</code>, <code>CLAUDE.md</code>, per-service files, per-area rules files. Pick a structure and stick to it.</p>
</li>
<li><p>Third, treat every human review catch as a chance to update the rules. The compounding effect over months is the entire point. A review system that improves itself is worth more than any single tool.</p>
</li>
</ol>
<p>That's the system. It took a couple of weeks to build the foundation and a few months for the rules to mature. It costs very little to run, and it has done more for our PR throughput than any tool I evaluated.</p>
<h2 id="heading-sources">Sources</h2>
<ul>
<li><p>CircleCI's 2026 State of Software Delivery report, analysing more than 28 million CI workflows from over 22,000 organisations: <a href="https://circleci.com/resources/2026-state-of-software-delivery/">https://circleci.com/resources/2026-state-of-software-delivery/</a></p>
</li>
<li><p>CircleCI's blog post detailing the year-over-year throughput numbers, including the 59% feature branch growth and the main branch decline: <a href="https://circleci.com/blog/five-takeaways-2026-software-delivery-report/">https://circleci.com/blog/five-takeaways-2026-software-delivery-report/</a></p>
</li>
<li><p>GitHub announcement of Copilot's transition to usage-based billing on June 1, 2026: <a href="https://github.blog/news-insights/company-news/github-copilot-is-moving-to-usage-based-billing/">https://github.blog/news-insights/company-news/github-copilot-is-moving-to-usage-based-billing/</a></p>
</li>
<li><p>GitHub changelog confirming Copilot code review will start consuming GitHub Actions minutes on June 1, 2026: <a href="https://github.blog/changelog/2026-04-27-github-copilot-code-review-will-start-consuming-github-actions-minutes-on-june-1-2026/">https://github.blog/changelog/2026-04-27-github-copilot-code-review-will-start-consuming-github-actions-minutes-on-june-1-2026/</a></p>
</li>
<li><p>AGENTS.md, the open standard's official site, including its stewardship under the Agentic AI Foundation and the Linux Foundation: <a href="https://agents.md/">https://agents.md/</a></p>
</li>
<li><p>Anthropic's Claude Code documentation on the memory system, including CLAUDE.md, auto memory, and the /init command: <a href="https://code.claude.com/docs/en/memory">https://code.claude.com/docs/en/memory</a></p>
</li>
<li><p>Anthropic's Claude Code GitHub Actions documentation, including notes on token-based billing and recommended cost controls: <a href="https://code.claude.com/docs/en/github-actions">https://code.claude.com/docs/en/github-actions</a></p>
</li>
<li><p>CodeRabbit's pricing documentation, confirming the per-developer-per-month seat model: <a href="https://docs.coderabbit.ai/management/plans">https://docs.coderabbit.ai/management/plans</a></p>
</li>
<li><p>Greptile's March 2026 pricing announcement, introducing the base-plus-usage model at $30 per seat per month with 50 included reviews: <a href="https://www.greptile.com/blog/greptile-v4">https://www.greptile.com/blog/greptile-v4</a></p>
</li>
<li><p>HumanLayer's write-up on writing a good CLAUDE.md, including data on instruction-following degradation: <a href="https://www.humanlayer.dev/blog/writing-a-good-claude-md">https://www.humanlayer.dev/blog/writing-a-good-claude-md</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How AI Changed the Economics of Writing Clean Code ]]>
                </title>
                <description>
                    <![CDATA[ If you've ever wanted to add an interface to a codebase and gotten pushback, you already know the argument: "That's twice the code for the same thing." And honestly? It was a fair point. You'd write t ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-ai-changed-the-economics-of-writing-clean-code/</link>
                <guid isPermaLink="false">69f0bce210a70b3335bf635a</guid>
                
                    <category>
                        <![CDATA[ Software Engineering ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Code Quality ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ best practices ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Aaron Yong ]]>
                </dc:creator>
                <pubDate>Tue, 28 Apr 2026 13:57:54 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/ecb13bda-70dd-437a-8d9a-4ef8b18ccc05.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you've ever wanted to add an interface to a codebase and gotten pushback, you already know the argument: "That's twice the code for the same thing."</p>
<p>And honestly? It was a fair point. You'd write the contract — the interface, the abstract class, the protocol — and then write the implementation. Two files where one would do. That's more surface area, more indirection, and more to maintain.</p>
<p>The Ruby and Rails communities built an entire philosophy around this: convention over configuration, less ceremony, fewer keystrokes. If the framework could infer your intent, why spell it out?</p>
<p>Then AI happened.</p>
<p>I was recently chatting with a CEO about what current-generation software engineers get wrong, and he put it cleanly:</p>
<blockquote>
<p>"Abstract interfaces were challenging a few months ago just because it required twice as much code. But with AI, lines of code are free. The reason we still need such constructs is because at some point a human still needs to look at the code. Interfaces reduce the cognitive load."</p>
</blockquote>
<p>That framing stuck with me. The cost of writing code has collapsed. The cost of reading it hasn't moved. And that asymmetry changes everything about how you should think about abstraction.</p>
<p>Here's what I mean.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-your-brain-is-the-bottleneck">Your Brain Is the Bottleneck</a></p>
</li>
<li><p><a href="#heading-the-greats-already-knew-this">The Greats Already Knew This</a></p>
</li>
<li><p><a href="#heading-the-economics-have-flipped">The Economics Have Flipped</a></p>
</li>
<li><p><a href="#heading-the-data-backs-it-up">The Data Backs It Up</a></p>
</li>
<li><p><a href="#heading-the-contrarian-case-and-why-it-actually-agrees">The Contrarian Case (And Why It Actually Agrees)</a></p>
</li>
<li><p><a href="#heading-what-this-means-for-you">What This Means for You</a></p>
</li>
<li><p><a href="#heading-references">References</a></p>
</li>
</ul>
<h2 id="heading-your-brain-is-the-bottleneck">Your Brain Is the Bottleneck</h2>
<p>This isn't a vibes argument. There's actual neuroscience behind why interfaces help.</p>
<p>In 1988, educational psychologist John Sweller introduced Cognitive Load Theory. A <a href="https://dl.acm.org/doi/full/10.1145/3483843">2022 ACM review</a> covers how it's been applied to computing education since.</p>
<p>The short version: your brain juggles three types of load when processing information. <em>Intrinsic</em> load is the inherent difficulty of the problem itself. <em>Extraneous</em> load is the noise — poorly organized information, unnecessary details, bad naming. <em>Germane</em> load is the good stuff — the mental effort you spend building useful mental models.</p>
<p>Here's the kicker: your working memory can only hold a handful of chunks of information at a time — cognitive scientists typically estimate somewhere between 2 and 6. Not 2 to 6 files, or 2 to 6 classes — 2 to 6 <em>things</em>.</p>
<p>Felienne Hermans explores this in <em>The Programmer's Brain</em> (2021), arguing that design patterns act as chunking aids. When you recognize a Strategy pattern, your brain collapses an entire class hierarchy into a single cognitive unit. The word "Strategy" replaces five classes and their relationships. That's not hand-waving about clean code — that's how human memory actually works.</p>
<p>And we can literally see it on brain scans. In 2021, a team led by Norman Peitek and Janet Siegmund published <a href="https://dl.acm.org/doi/10.1109/ICSE43902.2021.00056">an fMRI study on program comprehension</a> that won the ACM SIGSOFT Distinguished Paper Award at ICSE.</p>
<p>They put developers in brain scanners and watched what happened when they read code. The finding: semantic-level comprehension — understanding <em>what</em> code does — required measurably less neural activation than bottom-up syntactic parsing — tracing <em>how</em> it does it.</p>
<p>An interface lets you comprehend at the semantic level. <code>UserRepository.findById(id)</code> tells you everything you need to know without opening the implementation. Your brain doesn't need to hold the SQL query, the connection pool logic, the error handling, and the result mapping in working memory simultaneously. The interface compresses all of that into one chunk.</p>
<p>That's not elegance. That's neuroscience.</p>
<h2 id="heading-the-greats-already-knew-this">The Greats Already Knew This</h2>
<p>The case for abstraction isn't new. The people who built the foundations of computer science were making this argument before most of us were born.</p>
<p>Dijkstra said it with precision:</p>
<blockquote>
<p><em>"The purpose of abstracting is not to be vague, but to create a new semantic level in which one can be absolutely precise."</em></p>
</blockquote>
<p>Abstraction isn't about hiding things from people who can't handle complexity. It's about creating a level of discourse where you can reason clearly.</p>
<p>David Parnas formalized information hiding in his <a href="https://dl.acm.org/doi/10.1145/361598.361623">1972 ACM paper</a>: <em>"Every module is characterized by its knowledge of a design decision which it hides from all others."</em> He proved that decomposing systems by design decisions (rather than processing steps) produced modules that were both more flexible <em>and</em> easier to understand. Comprehensibility wasn't a bonus — it was the design criterion.</p>
<p>Tony Hoare argued that abstraction is the most powerful tool available to the human intellect — a way to manage complexity by focusing on what matters and ignoring what doesn't. Martin Fowler brought it down to earth:</p>
<blockquote>
<p><em>"Any fool can write code that a computer can understand. Good programmers write code that humans can understand."</em></p>
</blockquote>
<p>And then there's John Ousterhout, whose book <em>A Philosophy of Software Design</em> (2018) makes the connection to cognitive load explicit. His central argument: more lines of code can actually be <em>simpler</em> if they reduce cognitive load.</p>
<p>His concept of <em>deep modules</em> — simple interfaces hiding complex implementations — is essentially the argument that interfaces are worth their weight in code. The Unix file system API (<code>open</code>, <code>close</code>, <code>read</code>, <code>write</code>, <code>lseek</code>) is five functions hiding an enormous amount of complexity. That's a deep module. That's the goal.</p>
<p>The Gang of Four put it first in their book for a reason. Page one: <em>"Program to an interface, not an implementation."</em></p>
<p>None of this is controversial. But it's easy to forget when your AI tool just generated 200 lines of perfectly functional inline code in three seconds.</p>
<h2 id="heading-the-economics-have-flipped">The Economics Have Flipped</h2>
<p>Here's where the CEO's insight becomes an economic argument.</p>
<p>The historical case against interfaces was always about <em>writing cost</em>. Interfaces meant more code to write, more files to create, more boilerplate to maintain. The entire dynamic typing movement — Python, Ruby, JavaScript — was partly a reaction to the ceremony that languages like Java imposed. Convention over configuration. Don't Repeat Yourself. Less is more.</p>
<p>But ask yourself: what exactly is the cost of writing boilerplate now?</p>
<p>GitHub's <a href="https://arxiv.org/abs/2302.06590">2022 controlled study</a> found that developers using Copilot completed tasks 55% faster. The boilerplate that used to justify skipping interfaces — the extra file, the type definitions, the method signatures — takes seconds to generate. The writing cost of an interface has effectively collapsed to zero.</p>
<p>But again, the reading cost hasn't budged.</p>
<p>Robert C. Martin argued in <em>Clean Code</em> (2008) that developers spend far more time reading code than writing it — an observation he framed as a ratio of 10 to 1.</p>
<p>You can quibble with the exact number (it's anecdotal), but the direction is consistent across studies. A <a href="https://ieeexplore.ieee.org/document/7997917/">large-scale field study</a> tracking 78 professional developers across 3,148 working hours found they spend roughly 58% of their time on program comprehension alone. New developer onboarding averages six weeks — most of which is spent understanding existing systems, not producing new ones.</p>
<p>Addy Osmani named this asymmetry perfectly. In a <a href="https://addyosmani.com/blog/comprehension-debt/">March 2026 piece</a>, he described <em>comprehension debt</em>:</p>
<blockquote>
<p>"When a developer on your team writes code, the human review process has always been a bottleneck — but a productive and educational one. Reading their PR forces comprehension. AI-generated code breaks that feedback loop. The volume is too high."</p>
</blockquote>
<p>The output looks clean, passes linting, follows conventions — precisely the signals that historically triggered merge confidence. But comprehension debt is distinct from technical debt because it accumulates invisibly — your velocity metrics, your DORA scores, your PR counts all look fine while your team's actual understanding of the codebase quietly erodes.</p>
<p>So here's the math: AI reduced the cost of writing abstractions to near zero. The cost of <em>not</em> having them — in human reading time, onboarding friction, and comprehension debt — hasn't changed at all. The break-even point for "is this interface worth it?" just shifted massively in favor of "yes."</p>
<h2 id="heading-the-data-backs-it-up">The Data Backs It Up</h2>
<p>This isn't theoretical. We have data on what happens when AI generates code without good abstractions.</p>
<p><a href="https://www.gitclear.com/ai_assistant_code_quality_2025_research">GitClear analyzed 211 million changed lines of code</a> between 2020 and 2024. Their findings: code churn — lines reverted or updated within two weeks — doubled compared to the pre-AI baseline. Copy-pasted code blocks rose from 8.3% to 12.3%. And refactoring-associated changes dropped from 25% to under 10%.</p>
<p>AI-generated code, as they put it, "resembles an itinerant contributor, prone to violate the DRY-ness of the repos visited."</p>
<p>The <a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/">METR study</a> (2025) found something even more striking. Experienced open-source developers <em>predicted</em> AI would make them 24% faster. They <em>perceived</em> being 20% faster while using it. They were actually 19% slower. The perception gap is the story — you <em>feel</em> productive while generating code that creates more work downstream.</p>
<p>And then there's a study from Anthropic (yes, the company that makes Claude — full disclosure). They observed 52 software engineers learning a new library. The AI-assisted group completed tasks at the same speed, but scored <a href="https://arxiv.org/abs/2601.20245">17% lower on comprehension quizzes</a> afterward — 50% versus 67%. The biggest declines were in debugging ability. You can ship code you don't understand. You can't debug code you don't understand.</p>
<p>Kent Beck <a href="https://tidyfirst.substack.com/p/90-of-my-skills-are-now-worth-0">put it bluntly</a>: "The value of 90% of my skills just dropped to $0. The leverage for the remaining 10% went up 1000x." What that remaining 10% is, he leaves deliberately open — but it's hard to read that and not think about system design.</p>
<h2 id="heading-the-contrarian-case-and-why-it-actually-agrees">The Contrarian Case (And Why It Actually Agrees)</h2>
<p>I'd be dishonest if I didn't address the people who argue against abstraction. And some of them are very smart.</p>
<p>Casey Muratori's <a href="https://www.computerenhance.com/p/clean-code-horrible-performance">"Clean Code, Horrible Performance"</a> demonstrated that polymorphism and virtual dispatch can make code 10 to 15 times slower than straightforward procedural alternatives.</p>
<p>His benchmark is real. If you're writing a game engine or a high-frequency trading system, abstract interfaces on your hot path will cost you.</p>
<p>Dan Abramov wrote <a href="https://overreacted.io/goodbye-clean-code/">"Goodbye, Clean Code"</a> after watching a premature abstraction make his codebase harder to modify:</p>
<blockquote>
<p><em>"My code traded the ability to change requirements for reduced duplication, and it was not a good trade."</em></p>
</blockquote>
<p>Sandi Metz <a href="https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction">put it more sharply</a>: <em>"Duplication is far cheaper than the wrong abstraction."</em></p>
<p>And Rich Hickey, in his talk <a href="https://www.infoq.com/presentations/Simple-Made-Easy/">"Simple Made Easy"</a>, draws the critical distinction: <em>simple</em> (not intertwined) is not the same as <em>easy</em> (familiar). Wrong abstractions <em>complect</em> — they braid concerns together rather than separating them.</p>
<p>Here's the thing: none of these are arguments against abstraction. They're arguments against <em>bad</em> abstraction.</p>
<p>Muratori's performance argument applies to hot paths in performance-critical systems — not to your REST API's service layer. Abramov and Metz argue against <em>premature</em> abstraction — pulling patterns out before you understand the domain. And Hickey's entire talk is a case <em>for</em> the right abstractions, the ones that genuinely decompose rather than complect.</p>
<p>The irony is that in an AI-assisted world, these arguments are <em>easier</em> to address. You can generate the explicit, unabstracted version first. Let it stabilize. Watch the patterns emerge. Then extract the abstraction — with AI handling the mechanical refactoring. The cost of the "duplicate first, abstract later" approach just dropped to near zero.</p>
<h2 id="heading-what-this-means-for-you">What This Means for You</h2>
<p>If you're writing code with AI tools — and at this point, <a href="https://survey.stackoverflow.co/2024/ai">most of us are</a> — the temptation is to let the AI produce whatever it produces and move on. It works. It passes the tests. Ship it.</p>
<p>But "it works" is table stakes. The harder question is: can the next person who opens this code understand it in under five minutes? Can <em>you</em> understand it in six months?</p>
<p>Interfaces aren't about making code prettier or satisfying some abstract (pun intended) design principle. They're compression algorithms for human cognition. They let your brain operate at the semantic level instead of the syntactic level. And now that AI has eliminated the only real cost of creating them — the boilerplate — there's no economic argument left for skipping them.</p>
<p>The rules haven't changed. The excuse has just expired.</p>
<h2 id="heading-references">References</h2>
<h3 id="heading-academic-papers">Academic Papers</h3>
<ul>
<li><p>Duran, R., Zavgorodniaia, A., &amp; Sorva, J. (2022). <a href="https://dl.acm.org/doi/full/10.1145/3483843">"Cognitive Load Theory in Computing Education Research: A Review."</a> <em>ACM Transactions on Computing Education, 22</em>(4), Article 40.</p>
</li>
<li><p>Parnas, D.L. (1972). <a href="https://dl.acm.org/doi/10.1145/361598.361623">"On the Criteria To Be Used in Decomposing Systems into Modules."</a> <em>Communications of the ACM, 15</em>(12), 1053–1058.</p>
</li>
<li><p>Peitek, N., Apel, S., Parnin, C., Brechmann, A., &amp; Siegmund, J. (2021). <a href="https://dl.acm.org/doi/10.1109/ICSE43902.2021.00056">"Program Comprehension and Code Complexity Metrics: An fMRI Study."</a> <em>ICSE 2021</em>. ACM SIGSOFT Distinguished Paper Award.</p>
</li>
<li><p>Peng, S., Kalliamvakou, E., Cihon, P., &amp; Demirer, M. (2023). <a href="https://arxiv.org/abs/2302.06590">"The Impact of AI on Developer Productivity: Evidence from GitHub Copilot."</a> <em>arXiv:2302.06590</em>.</p>
</li>
<li><p>Shen, J.H. &amp; Tamkin, A. (2026). <a href="https://arxiv.org/abs/2601.20245">"How AI Impacts Skill Formation."</a> <em>arXiv:2601.20245</em>.</p>
</li>
<li><p>Xia, X., Bao, L., Lo, D., Xing, Z., Hassan, A.E., &amp; Li, S. (2018). <a href="https://ieeexplore.ieee.org/document/7997917/">"Measuring Program Comprehension: A Large-Scale Field Study with Professionals."</a> <em>IEEE Transactions on Software Engineering, 44</em>(10), 951–976.</p>
</li>
<li><p>METR. (2025). <a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/">"Measuring the Impact of Early 2025 AI on Experienced Open Source Developer Productivity."</a> <em>metr.org</em>.</p>
</li>
</ul>
<h3 id="heading-talks-and-blog-posts">Talks and Blog Posts</h3>
<ul>
<li><p>Hickey, R. (2011). <a href="https://www.infoq.com/presentations/Simple-Made-Easy/">"Simple Made Easy."</a> <em>Strange Loop Conference</em>.</p>
</li>
<li><p>Beck, K. (2023). <a href="https://tidyfirst.substack.com/p/90-of-my-skills-are-now-worth-0">"90% of My Skills Are Now Worth $0."</a> <em>Tidy First? Substack</em>.</p>
</li>
<li><p>Osmani, A. (2026). <a href="https://addyosmani.com/blog/comprehension-debt/">"Comprehension Debt: The Hidden Cost of AI-Generated Code."</a> <em>addyosmani.com</em>.</p>
</li>
<li><p>Muratori, C. (2023). <a href="https://www.computerenhance.com/p/clean-code-horrible-performance">"Clean Code, Horrible Performance."</a> <em>Computer Enhance</em>.</p>
</li>
<li><p>Abramov, D. (2020). <a href="https://overreacted.io/goodbye-clean-code/">"Goodbye, Clean Code."</a> <em>overreacted.io</em>.</p>
</li>
<li><p>Metz, S. (2016). <a href="https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction">"The Wrong Abstraction."</a> <em>sandimetz.com</em>.</p>
</li>
<li><p>GitClear. (2025). <a href="https://www.gitclear.com/ai_assistant_code_quality_2025_research">"AI Assistant Code Quality in 2025."</a> <em>gitclear.com</em>.</p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ From Symptoms to Root Cause: How to Use the 5 Whys Technique ]]>
                </title>
                <description>
                    <![CDATA[ Most teams don't struggle because they can't fix problems. They struggle because they fix the wrong thing. An API fails in production. You restart the service, errors go away, and it feels resolved. U ]]>
                </description>
                <link>https://www.freecodecamp.org/news/from-symptoms-to-root-cause-how-to-use-the-5-whys-technique/</link>
                <guid isPermaLink="false">69ea4d69904b915438990f19</guid>
                
                    <category>
                        <![CDATA[ problem solving skills ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ debugging ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ashutosh Krishna ]]>
                </dc:creator>
                <pubDate>Thu, 23 Apr 2026 16:48:41 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/b5dbd964-9a03-448d-92a5-92e3b4a47fef.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Most teams don't struggle because they can't fix problems. They struggle because they fix the wrong thing.</p>
<p>An API fails in production. You restart the service, errors go away, and it feels resolved. Until it happens again. And again. What's happening here is simple: you're treating symptoms, not the underlying cause.</p>
<p>The <strong>5 Whys technique</strong> is a straightforward way to deal with this. It comes from the Toyota Production System and was designed to help teams dig deeper into problems instead of settling for quick fixes.</p>
<p>The idea is simple. Ask "why" repeatedly until you reach the real cause.</p>
<p>But in practice, this is where things go wrong.</p>
<p>Teams often:</p>
<ul>
<li><p>Stop too early</p>
</li>
<li><p>Assume answers without checking data</p>
</li>
<li><p>Focus on people instead of systems</p>
</li>
<li><p>Treat "five" as a rule instead of a guideline</p>
</li>
</ul>
<p>So even though the process looks structured, the outcome is still shallow.</p>
<p>In this article, we'll focus on how to actually use the 5 Whys in real situations. Not just the theory, but what it looks like when you apply it to an engineering problem.</p>
<h3 id="heading-heres-what-well-cover">Here's What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-what-is-the-5-whys-technique">What is the 5 Whys Technique?</a></p>
</li>
<li><p><a href="#heading-origins-of-the-5-whys-method">Origins of the 5 Whys Method</a></p>
</li>
<li><p><a href="#heading-how-to-conduct-an-effective-5-whys-analysis">How to Conduct an Effective 5 Whys Analysis</a></p>
</li>
<li><p><a href="#heading-real-world-example-applying-5-whys-in-an-engineering-scenario">Real-World Example: Applying 5 Whys in an Engineering Scenario</a></p>
</li>
<li><p><a href="#heading-when-to-use-and-when-not-to-use-5-whys">When to Use (and When Not to Use) 5 Whys</a></p>
</li>
<li><p><a href="#heading-benefits-of-the-5-whys-technique">Benefits of the 5 Whys Technique</a></p>
</li>
<li><p><a href="#heading-common-pitfalls-and-limitations">Common Pitfalls and Limitations</a></p>
</li>
<li><p><a href="#heading-tips-for-using-5-whys-effectively">Tips for Using 5 Whys Effectively</a></p>
</li>
<li><p><a href="#heading-summary">Summary</a></p>
</li>
</ul>
<h2 id="heading-what-is-the-5-whys-technique">What is the 5 Whys Technique?</h2>
<p>The 5 Whys technique is a way to break down a problem by repeatedly asking why it happened, with the goal of reaching a cause that actually explains the issue and can be addressed.</p>
<p>At its core, it's not about the number five. The name can be misleading. What matters is the process of following a chain of cause and effect until the explanation stops being superficial and starts becoming useful.</p>
<p>Each answer you uncover should move you one level deeper. You start with what went wrong, then explore what led to it, and continue until you reach something that is both believable and actionable. In most real situations, that final answer is not a single event but a gap in a system, a missing check, or an assumption that was never validated.</p>
<p>The technique became widely known through the Toyota Production System, where it was used to improve processes by focusing on causes rather than quick fixes.</p>
<p>That context is important because it highlights the original intent. The goal was not just to explain problems, but to prevent them from happening again.</p>
<p>A simple example makes this clearer. Imagine a mobile app suddenly starts crashing after a release. Asking "Why?" might look like this:</p>
<ol>
<li><p>Why is the app crashing? → Because a null value is being accessed in the code.</p>
</li>
<li><p>Why is there a null value? → Because the API response is missing a required field</p>
</li>
<li><p>Why is the field missing? → Because a recent backend change made the field optional.</p>
</li>
<li><p>Why was this change not handled in the app? → Because the app assumes the field is always present.</p>
</li>
<li><p>Why was this assumption not caught earlier? → Because there are no contract tests validating API responses.</p>
</li>
</ol>
<p>At this point, the issue is no longer just "fix the null check". The deeper problem is the lack of validation between systems, which allows breaking changes to slip through.</p>
<p>A useful way to think about the 5 Whys is that it forces you to stay with the problem a little longer than you normally would. Most of the time, the first explanation feels sufficient, so it's easy to stop there. This method pushes you to go one step further, and then another, until the explanation holds up under scrutiny.</p>
<p>At the same time, it's not a rigid formula. You might reach a solid root cause in three steps, or it might take more than five. The quality of the reasoning matters more than the count.</p>
<h2 id="heading-origins-of-the-5-whys-method">Origins of the 5 Whys Method</h2>
<p>The 5 Whys method comes from the Toyota Production System, a manufacturing approach focused on continuous improvement and problem solving at the source.</p>
<p>It's often associated with Sakichi Toyoda, whose philosophy was simple: don’t just fix a problem. Understand why it happened so it doesn't happen again.</p>
<p>Inside Toyota, this wasn't treated as a formal tool or checklist. It was part of the day-to-day way of working. When something went wrong on the production line, the goal wasn't to get things running quickly and move on. The goal was to stop, investigate, and make sure the same issue wouldn't repeat.</p>
<p>That mindset is important to understand. The 5 Whys was never meant to be a rigid exercise where you ask five questions and stop. It was a way to encourage deeper thinking and accountability in processes.</p>
<p>Another key idea in the Toyota system is that problems are usually caused by processes, not people. Instead of asking "who made the mistake", the focus is on "what allowed this mistake to happen". The 5 Whys fits naturally into this approach because it pushes you toward system level causes rather than individual blame.</p>
<p>Over time, the method spread beyond manufacturing and is now used in software engineering, product teams, operations, and many other fields. The context has changed, but the core idea remains the same: if you don't understand the cause, you're likely to see the same problem again.</p>
<p>This origin story is useful not just as background, but as a reminder of intent. The value of the 5 Whys doesn't come from the questions themselves. It comes from the discipline of not settling for the first answer.</p>
<h2 id="heading-how-to-conduct-an-effective-5-whys-analysis">How to Conduct an Effective 5 Whys Analysis</h2>
<p>A 5 Whys analysis works best when it is treated as a structured way of thinking, not a checklist to rush through. The quality of the outcome depends less on how many times you ask "why" and more on how carefully you reason through each step.</p>
<p>It helps to approach it in stages, each with a clear purpose.</p>
<h3 id="heading-step-1-define-the-problem-clearly">Step 1: Define the Problem Clearly</h3>
<p>Start with a problem statement that is specific and observable. Avoid vague descriptions like "the system is slow" or "things are failing". Instead, describe what actually happened in a way that can be verified.</p>
<p>For example, "API response time exceeded 5 seconds for 30 percent of requests between 2 PM and 3 PM" is much more useful than "API is slow".</p>
<p>A clear problem statement keeps the analysis grounded. If the starting point is fuzzy, the entire chain of reasoning will drift.</p>
<h3 id="heading-step-2-ask-why-iteratively">Step 2: Ask "Why" Iteratively</h3>
<p>Once the problem is defined, begin asking why it happened. Each answer should directly address the question before it and naturally lead to the next one.</p>
<p>The key here is continuity. Every step should feel like a logical extension of the previous one. If you find yourself jumping topics or introducing unrelated explanations, it's a sign that the chain is breaking.</p>
<p>Keep going until the answers stop being immediate symptoms and start pointing toward underlying conditions or decisions.</p>
<p>Also, don't force the process to stop at five. Some problems may need fewer steps, while others may need more. What matters is reaching a point where the explanation is meaningful and actionable.</p>
<h3 id="heading-step-3-validate-each-answer-with-evidence">Step 3: Validate Each Answer with Evidence</h3>
<p>This is where many analyses go wrong. It's easy to come up with plausible answers, but plausibility is not enough.</p>
<p>Each "why" should be backed by some form of evidence. This could be logs, metrics, recent changes, or direct observation. If an answer can't be verified, treat it as a hypothesis and confirm it before moving forward.</p>
<p>Without validation, the entire analysis becomes a chain of assumptions. Even if the final answer sounds reasonable, it may not reflect reality.</p>
<h3 id="heading-step-4-identify-the-root-cause">Step 4: Identify the Root Cause</h3>
<p>A good root cause is one that explains the sequence of events and can be acted upon to prevent the issue in the future.</p>
<p>In many cases, this turns out to be a gap in a process rather than a single technical failure. It could be a missing validation step, an incomplete test, or an assumption that was never challenged.</p>
<p>If the final answer still feels like a symptom, you probably need to go one level deeper. On the other hand, if the answer points to something you can change in your system or workflow, you are likely in the right place.</p>
<h3 id="heading-step-5-define-corrective-actions">Step 5: Define Corrective Actions</h3>
<p>The analysis is only useful if it leads to meaningful action.</p>
<p>Once you've identified the root cause, the next step is to define changes that prevent the problem from happening again. These should go beyond quick fixes and address the underlying issue.</p>
<p>For example, instead of just fixing a bug, you might introduce better testing, add monitoring, or improve review processes.</p>
<p>Good corrective actions share a few traits: they're specific, practical to implement, and they directly address the root cause identified in the analysis.</p>
<h2 id="heading-real-world-example-applying-5-whys-in-an-engineering-scenario">Real-World Example: Applying 5 Whys in an Engineering Scenario</h2>
<p>To see how this works in practice, let’s walk through a realistic backend issue. The goal here is not just to reach an answer, but to show how each step builds on evidence and leads to something actionable.</p>
<h3 id="heading-the-problem">The Problem:</h3>
<p>Users report intermittent failures while fetching order details:</p>
<pre><code class="language-bash">GET /api/orders/{id}
→ HTTP 500 Internal Server Error
</code></pre>
<p>Application logs show:</p>
<pre><code class="language-plaintext">// Java 21 example (Spring Boot style logging)
logger.error("Database connection timeout while fetching order", ex);
</code></pre>
<p>At this point, it's tempting to conclude that the database is the problem. But that's only what we can see on the surface.</p>
<h3 id="heading-applying-the-5-whys">Applying the 5 Whys</h3>
<h4 id="heading-1-why-did-the-api-return-a-500-error">1. Why did the API return a 500 error?</h4>
<p>Because the database query timed out.</p>
<p>This is directly supported by the error logs, so we can treat it as a confirmed fact.</p>
<h4 id="heading-2-why-did-the-query-time-out">2. Why did the query time out?</h4>
<p>Because the database connection pool was exhausted.</p>
<p>Metrics show that all available connections were in use during peak traffic.</p>
<h4 id="heading-3-why-was-the-connection-pool-exhausted">3. Why was the connection pool exhausted?</h4>
<p>Because some requests were holding database connections for too long.</p>
<p>Slow query logs confirm that a subset of queries had unusually high execution times.</p>
<h4 id="heading-4-why-were-some-queries-slow">4. Why were some queries slow?</h4>
<p>Because a recently introduced feature added a query on a non-indexed column.</p>
<p>Looking at recent deployments reveals a change that introduced filtering without proper indexing.</p>
<h4 id="heading-5-why-was-an-unoptimized-query-deployed-to-production">5. Why was an unoptimized query deployed to production?</h4>
<p>Because there is no performance validation step in the development or release process.</p>
<p>There are no checks in code review or CI/CD to catch inefficient database queries before deployment.</p>
<h3 id="heading-root-cause">Root Cause</h3>
<p>The issue is not the timeout itself.</p>
<p>It's this:</p>
<blockquote>
<p>The system allows inefficient database queries to reach production without any safeguards.</p>
</blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/f93fb121-d5ac-45bc-8b3b-cc4f915c48a3.png" alt="f93fb121-d5ac-45bc-8b3b-cc4f915c48a3" style="display:block;margin:0 auto" width="423" height="544" loading="lazy">

<h3 id="heading-what-a-shallow-fix-would-look-like">What a Shallow Fix Would Look Like</h3>
<p>If we stopped early, we might:</p>
<ul>
<li><p>Increase the database timeout</p>
</li>
<li><p>Increase the connection pool size</p>
</li>
</ul>
<p>These might reduce the frequency of failures, but they don't solve the underlying problem.</p>
<h3 id="heading-what-a-strong-fix-looks-like">What a Strong Fix Looks Like</h3>
<p>A proper 5 Whys analysis leads to changes that improve the system:</p>
<ul>
<li><p>Add appropriate indexing for frequently queried fields</p>
</li>
<li><p>Introduce query performance checks in CI/CD pipelines</p>
</li>
<li><p>Add monitoring and alerts for slow queries</p>
</li>
<li><p>Include database considerations in code reviews</p>
</li>
</ul>
<h3 id="heading-why-this-example-matters">Why This Example Matters</h3>
<p>The difference between a shallow fix and a real solution is depth.</p>
<p>The first explanation often feels sufficient, especially under pressure. But stopping there means the issue is likely to return in a different form.</p>
<p>The value of the 5 Whys comes from following the chain all the way to something you can change in your system.</p>
<h2 id="heading-when-to-use-and-when-not-to-use-5-whys">When to Use (and When Not to Use) 5 Whys</h2>
<p>Like any problem-solving method, the 5 Whys is useful in the right context and less effective in others. Knowing when to apply it is just as important as knowing how to use it.</p>
<p>If used appropriately, it can uncover meaningful insights. If used in the wrong situation, it can lead to oversimplified or misleading conclusions</p>
<h3 id="heading-when-to-use-5-whys">When to Use 5 Whys</h3>
<p>The 5 Whys is most useful when your goal is to understand <strong>why something happened</strong>, not just to fix it and move on.</p>
<p>It works well in situations where problems are recurring or not fully explained by the first answer. For example, production incidents, repeated bugs, or issues that reappear after a quick fix are strong signals that you need deeper analysis. In these cases, the technique helps uncover what is happening beneath the surface.</p>
<p>It's also effective during retrospectives and postmortems. When a release doesn't go as expected or a sprint runs into issues, the 5 Whys helps teams move beyond observations like "this failed" and get to "why did this fail in the first place".</p>
<p>In general, use it when:</p>
<ul>
<li><p>The problem is not obvious</p>
</li>
<li><p>The issue has occurred more than once</p>
</li>
<li><p>You want to prevent recurrence, not just resolve the current instance</p>
</li>
</ul>
<h3 id="heading-when-not-to-use-5-whys">When Not to Use 5 Whys</h3>
<p>The 5 Whys has its limits, and using it in the wrong context can lead to oversimplified conclusions.</p>
<p>If a problem involves multiple interacting factors, a single chain of "why" questions may not capture the full picture. Complex systems often have several contributing causes, and forcing them into one linear explanation can hide important details. In such cases, the 5 Whys should be combined with other approaches.</p>
<p>It's also less effective when there's not enough data. If each answer is based on assumptions rather than evidence, the analysis quickly becomes unreliable. The method depends on validation at every step.</p>
<p>Another limitation is in time-critical situations. During an active incident, the priority is to restore the system. The deeper analysis should happen later, once things are stable.</p>
<p>Finally, if your goal is quantitative analysis or optimization, the 5 Whys alone isn't enough. You'll need more data-driven methods to support decision making.</p>
<p>A simple rule of thumb is this. If you are trying to <strong>learn from a problem</strong>, use the 5 Whys. If you are trying to <strong>fix something immediately or analyze complex data</strong>, use it carefully or alongside other techniques.</p>
<h2 id="heading-benefits-of-the-5-whys-technique">Benefits of the 5 Whys Technique</h2>
<p>The 5 Whys technique is simple, but it offers several powerful benefits that can help you solve problems more effectively and make lasting improvements. Here are the key advantages:</p>
<h3 id="heading-simple-and-easy-to-apply">Simple and Easy to Apply</h3>
<p>One of the biggest strengths of the 5 Whys is how easy it is to start using. You don't need special tools, training, or complex frameworks. It can be applied in a quick discussion, during debugging, or as part of a formal postmortem.</p>
<p>This low barrier makes it accessible across teams, regardless of experience level.</p>
<h3 id="heading-encourages-deeper-thinking">Encourages Deeper Thinking</h3>
<p>The method naturally pushes you to go beyond the first explanation. Instead of reacting to what's visible, it encourages you to question why the problem occurred in the first place.</p>
<p>This shift from surface-level fixes to deeper understanding often leads to better decisions.</p>
<h3 id="heading-promotes-system-level-improvements">Promotes System-Level Improvements</h3>
<p>When used correctly, the focus moves away from individual people and toward systems. Instead of asking who made a mistake, the analysis asks what allowed the mistake to happen.</p>
<p>This leads to improvements in processes, safeguards, and overall system design rather than one-off fixes.</p>
<h3 id="heading-works-well-in-team-settings">Works Well in Team Settings</h3>
<p>Because the approach is simple, it's easy for multiple people to contribute. Different perspectives help uncover gaps that might otherwise be missed.</p>
<p>It also creates a shared understanding of the problem, which is valuable during retrospectives and incident reviews.</p>
<h3 id="heading-helps-prevent-recurring-issues">Helps Prevent Recurring Issues</h3>
<p>Quick fixes often solve the immediate problem but don't stop it from happening again. The 5 Whys helps identify underlying causes, which makes it easier to prevent similar issues in the future.</p>
<p>Over time, this leads to more stable systems and fewer repeated incidents.</p>
<h2 id="heading-common-pitfalls-and-limitations">Common Pitfalls and Limitations</h2>
<p>While the 5 Whys technique is useful, it’s not always perfect. There are some limitations to keep in mind, so you can use it effectively and know when it might not be enough.</p>
<h3 id="heading-stopping-too-early">Stopping Too Early</h3>
<p>One of the most common mistakes is ending the analysis after the first or second answer. These early answers usually describe symptoms, not causes.</p>
<p>Stopping too soon leads to fixes that address the surface but leave the underlying issue unresolved.</p>
<h3 id="heading-treating-assumptions-as-facts">Treating Assumptions as Facts</h3>
<p>It's easy to come up with explanations that sound reasonable. But without evidence, they're just assumptions.</p>
<p>If each step isn't validated with logs, metrics, or observations, the entire analysis can drift away from reality.</p>
<h3 id="heading-focusing-on-individuals-instead-of-systems">Focusing on Individuals Instead of Systems</h3>
<p>Answers like "someone made a mistake" don't add much value. While they may be true, they don't explain why the system allowed that mistake to have an impact.</p>
<p>Focusing on processes and safeguards leads to more meaningful improvements.</p>
<h3 id="heading-oversimplifying-complex-problems">Oversimplifying Complex Problems</h3>
<p>The 5 Whys follows a linear chain of reasoning, but real-world systems often have multiple contributing factors.</p>
<p>Relying on a single chain can hide important interactions. In such cases, the method should be combined with other approaches.</p>
<h3 id="heading-treating-it-as-a-rigid-formula">Treating It as a Rigid Formula</h3>
<p>The name suggests asking "why" five times, but this shouldn't be taken literally. Some problems require fewer steps, while others need more.</p>
<p>Forcing the structure can lead to artificial or weak conclusions.</p>
<h3 id="heading-not-a-replacement-for-deeper-analysis">Not a Replacement for Deeper Analysis</h3>
<p>The 5 Whys isn't designed for every type of problem. For complex system failures, performance optimization, or data-heavy investigations, additional tools and methods are often required.</p>
<p>It works best as a starting point or a complement to other techniques, not a complete solution on its own.</p>
<h2 id="heading-tips-for-using-5-whys-effectively">Tips for Using 5 Whys Effectively</h2>
<p>To get the most out of the 5 Whys technique, there are a few tips that can help you use it effectively. These will guide you to ask the right questions and reach useful, actionable insights.</p>
<h3 id="heading-start-with-a-clear-specific-problem">Start with a Clear, Specific Problem</h3>
<p>A vague problem leads to vague answers. Spend a little extra time making sure the problem statement is precise and based on observable facts. This keeps the analysis grounded and avoids unnecessary detours.</p>
<h3 id="heading-base-every-step-on-evidence">Base Every Step on Evidence</h3>
<p>Treat each answer as something that needs to be verified. Use logs, metrics, recent changes, or direct observations to support your reasoning. If something can't be validated, call it out as a hypothesis and confirm it before moving forward.</p>
<h3 id="heading-keep-the-chain-logical-and-connected">Keep the Chain Logical and Connected</h3>
<p>Each "why" should naturally follow from the previous answer. If the reasoning starts to jump between unrelated ideas, pause and re-evaluate. A clean, logical chain is a strong indicator that you're on the right track.</p>
<h3 id="heading-focus-on-systems-not-individuals">Focus on Systems, Not Individuals</h3>
<p>Avoid stopping at explanations that point to human error. Instead, ask what allowed that error to have an impact. This shift in thinking leads to improvements that actually reduce the chances of similar issues in the future.</p>
<h3 id="heading-do-not-force-exactly-five-steps">Do Not Force Exactly Five Steps</h3>
<p>The number five is a guideline, not a rule. Some problems become clear in three steps, while others need more exploration. Stop when you reach a cause that's both convincing and actionable.</p>
<h3 id="heading-involve-the-right-people">Involve the Right People</h3>
<p>If possible, do the analysis as a group. People from different parts of the system bring different perspectives, which helps uncover details that might otherwise be missed. It also creates shared ownership of both the problem and the solution.</p>
<h3 id="heading-turn-insights-into-actions">Turn Insights into Actions</h3>
<p>The analysis only matters if it leads to change. Make sure the final outcome includes clear, practical steps that address the root cause. Without this, even a well-done analysis has limited impact.</p>
<h2 id="heading-summary">Summary</h2>
<p>The 5 Whys is a simple technique, but using it well takes some discipline.</p>
<p>At its core, it's about resisting the urge to stop at the first explanation. By following the chain of cause and effect, you move from symptoms to something you can actually fix. In many cases, that turns out to be a gap in a process rather than a one-off failure.</p>
<p>When applied thoughtfully, it helps teams learn from problems instead of just reacting to them. Over time, this leads to better systems, fewer recurring issues, and more confidence in how problems are handled.</p>
<p>The key is to treat it as a way of thinking, not just a set of steps.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Storyteller: A Medium For Guiding Others Through Code ]]>
                </title>
                <description>
                    <![CDATA[ As a computer science instructor, I have long wished that there was a better way to guide others through my code. When I was first learning to program, I was a big fan of traditional programming books ]]>
                </description>
                <link>https://www.freecodecamp.org/news/storyteller-a-medium-for-guiding-others-through-code/</link>
                <guid isPermaLink="false">69a23fd4d4053a09f35c3d3e</guid>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ coding ]]>
                    </category>
                
                    <category>
                        <![CDATA[ General Programming ]]>
                    </category>
                
                    <category>
                        <![CDATA[ code playbacks ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Mark Mahoney ]]>
                </dc:creator>
                <pubDate>Sat, 28 Feb 2026 01:07:32 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5fc16e412cae9c5b190b6cdd/902c2299-ea98-4136-8ee8-36668f0c08ee.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>As a computer science instructor, I have long wished that there was a better way to guide others through my code. When I was first learning to program, I was a big fan of traditional programming books. I have shelves and shelves of 800+ page books covering different programming languages and technologies.</p>
<p>I have known for a while now that most learners today don't share my love of big thick books, and to be honest, I rarely read those books in their entirety. Those big books often had a lot more exposition about the code than was probably needed. As a book buyer I wanted to make sure that I was getting my money's worth so the thicker they were, the better. It is much more common these days for learners to consume blog based tutorials and videos.</p>
<p>If you're learning to code right now, you've probably experienced the frustration of these formats too. I want to share something I've been working on that might help.</p>
<h2 id="heading-blogs-and-videos"><strong>Blogs and Videos</strong></h2>
<p>Blog-style tutorials mix code and the explanation of it in a top-to-bottom fashion. Scrolling through these web-based explanations feels familiar and one can copy and paste with ease. However, linking the explanation of the code and the code itself has always been less than ideal. Often I find myself jumping around the blog post wishing I could see the entire code example while working through the explanation. Instead, I am only able to see small parts of the code and it is challenging to see how those parts relate to other parts.</p>
<p>Video tutorials are very popular these days. They solve some of the problems associated with blog-style tutorials. Videos are great because you get two streams of information: the author's audible narrative and the code being written. A viewer can focus on the two streams simultaneously. However, videos have some problems too.</p>
<h3 id="heading-viewing-videos"><strong>Viewing Videos</strong></h3>
<p>From the perspective of the viewer, videos are hard to search through and are not useful as a copy and paste source or a code reference. More importantly, though, they discourage the viewer from taking their time and reflecting on the material. Often, when I am viewing a video tutorial I don't pause and let concepts sink in before the video moves on. Yes, I could be more disciplined and pause and rewind more often but usually I don't.</p>
<h3 id="heading-making-videos"><strong>Making videos</strong></h3>
<p>From the perspective of the video creator, it is clear that not all code being developed is interesting to watch. Some of it is not really worth showing the viewer. Not all video creators can keep the narrative interesting the whole time.</p>
<p>I know I struggle with the 'performance' aspect of making videos (you won't find me coding on Twitch anytime soon). Many times after I am done making a video, as I review it, I wish I had mentioned something that I forgot. It is hard to go back and edit the video without scrapping it and starting over.</p>
<h2 id="heading-storyteller"><strong>Storyteller</strong></h2>
<p>I have created a new medium to guide viewers through code examples. It combines the best of books, blog posts, and videos. This new medium allows a developer to write code using a top-notch editor (Visual Studio Code) and then replay the development of that code in the browser.</p>
<p>The author can add comments at important points in the evolution of the code. The comments can include text, hand drawn pictures, screenshots, and audio and video recordings. This allows the author to add visualizations that we have in our heads but don't make it into the code itself. The tool is called <a href="https://github.com/markm208/storyteller">Storyteller</a>.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/67df75cfc82238bba0f330b3/82dcb5c8-999f-432f-bd60-adcb3d8b9889.png" alt="82dcb5c8-999f-432f-bd60-adcb3d8b9889" style="display:block;margin:0 auto" width="3022" height="1638" loading="lazy">

<p>Here are a few examples of a 'playback':</p>
<ul>
<li><p><a href="https://playbackpress.com/books/pybook/chapter/2/10">Enlarging a Picture (Python)</a></p>
</li>
<li><p><a href="https://playbackpress.com/books/cppbook/chapter/8/8">Dynamic Variables and Pointers (C++)</a></p>
</li>
</ul>
<p>These work best on a big screen. If you are viewing a playback on a small screen you can view it in 'blog' mode (there is button in the top right to switch from 'code' mode to 'blog' mode).</p>
<p>I have created groups of these guided code walk-throughs to help me teach different topics to my students. These are all free and hosted on a website I created called <a href="https://playbackpress.com/books">Playback Press</a>. Here are some of the 'books' I have created so far:</p>
<ul>
<li><p><a href="https://playbackpress.com/books/cppbook/">An Animated Introduction to Programming in C++</a></p>
</li>
<li><p><a href="https://playbackpress.com/books/pybook/">An Animated Introduction to Programming with Python</a></p>
</li>
<li><p><a href="https://playbackpress.com/books/webdevbook/">An Introduction to Web Development from Back to Front</a></p>
</li>
<li><p><a href="https://playbackpress.com/books/cljbook/">An Animated Introduction to Clojure</a></p>
</li>
<li><p><a href="https://playbackpress.com/books/exbook/">An Animated Introduction to Elixir</a></p>
</li>
<li><p><a href="https://playbackpress.com/books/sqlbook/">Database Design and SQL for Beginners</a></p>
</li>
<li><p><a href="https://playbackpress.com/books/flutterbook/">Mobile App Development with Dart and Flutter</a></p>
</li>
<li><p><a href="https://playbackpress.com/books/patternsbook/">OO Design Patterns with Java</a></p>
</li>
</ul>
<p>I usually assign these as readings in my classes instead of using expensive textbooks. It is a lot easier for me to write several programs than it is to find a perfect textbook.</p>
<p>I also use them for in-class demos instead of writing code live. This makes code demos flow much faster and smoother. If I make an interesting mistake while preparing the code I can still highlight it with a comment. If I make an uninteresting or embarrassing mistake I can just ignore it and the students won't focus on it.</p>
<h3 id="heading-the-advantages-of-code-playbacks"><strong>The Advantages of Code Playbacks:</strong></h3>
<ul>
<li><p>The primary focus is on the code. It is always visible and easy to search and navigate.</p>
</li>
<li><p>Since the code is so accessible, the explanation of it tends to be short and concise.</p>
</li>
<li><p>The narrative can include whiteboard style drawings, screenshots, or videos of running code in addition to a text explanation.</p>
</li>
<li><p>As an author, I can review the code several times and add/edit comments each time I go through it. I don't have to give a perfect performance like I do with a video.</p>
</li>
<li><p>Comment points highlight when the author wants the viewer to take a moment to really think about the code and reflect on it. The playback only moves forward when the viewer is ready.</p>
</li>
<li><p>The code mentioned in a comment can be highlighted so the viewer knows exactly where they should be looking.</p>
</li>
<li><p>The code can be downloaded at any point in the playback. Then a viewer can run it, change it, and add to it.</p>
</li>
<li><p>The tool is a language independent editor plug-in and can be used to describe programs in any language.</p>
</li>
<li><p>Viewers only need a web browser to go through a playback.</p>
</li>
</ul>
<p>Recently, I've been exploring how to make playbacks even more useful for learners.</p>
<h2 id="heading-ai-as-an-infinitely-patient-tutor"><strong>AI as an Infinitely Patient Tutor</strong></h2>
<p>I have extended code playbacks to include an AI tutor. One thing I've learned in my years of teaching is that students often hesitate to ask questions. They worry about looking foolish, or they don't want to slow down the class, or they simply can't articulate what's confusing them.</p>
<p>What if every student had access to a patient tutor who never got frustrated with repeated questions and could explain concepts in multiple ways until something clicked?</p>
<p>I've integrated AI directly into the playback experience. As students work through a playback, they can ask questions about anything they're seeing. This might be a specific line of code, a concept I mentioned in a comment, or how something connects to material from earlier in the playback. The AI has full context. It can see the code, it understands where the student is in the playback, and it can provide explanations tailored to that exact moment. The AI is right there <em>with</em> the student, looking at the same code, understanding the same context.</p>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/WAPql5KZFR4?si=jFnCqidSTtfaZA4e" frameborder="0" allowfullscreen="" title="Embedded content" loading="lazy"></iframe></div>

<p>The AI can also generate self-grading multiple choice questions based on the code and comments in a playback. These low-stakes quizzes make the learning experience more engaging and help learners check their understanding as they go.</p>
<p>Let me be clear: the AI doesn't replace me as an instructor. I still create the playbacks. I still decide what concepts to cover, what order to present them, and what examples best illustrate the ideas. The AI is an extension of my teaching, not a replacement for it.</p>
<p>Note: The AI features are available to registered users on <a href="https://playbackpress.com/books">Playback Press</a>. Registration is free but logging in is required to access the AI tutor. If you want to see what this feels like, try one of the playbacks linked above and ask the AI a question about what you're seeing.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>My goal has always been to help people learn to code. Books gave us depth but demanded commitment. Blogs gave us accessibility but fragmented the code. Videos gave us narrative but took away control. Playbacks keep the code front and center while letting learners move at their own pace and reflect when they need to. Adding AI doesn't change that philosophy, it just means there's always someone available to answer questions. Together, they get closer to the experience of having an expert sit beside you and walk you through a program. That's what I've been trying to build, and I think we're getting there.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
