[{"content":" ok so before we start, quick heads up: this is not the kind of post you read and close. open a code editor and a browser right next to it. every section has working code, run it as you go. if you just read through without touching the keyboard, by tomorrow you\u0026rsquo;ll remember nothing. i genuinely promise.\nWhat is canvas, actually So there\u0026rsquo;s this \u0026lt;canvas\u0026gt; element in HTML that almost nobody really talks about. Most tutorials skip straight to Three.js or p5.js. Which is fine, libraries are great. But there\u0026rsquo;s something really satisfying about understanding what\u0026rsquo;s happening underneath all of that. Canvas is basically a blank rectangle in your browser where you get full pixel-level control using JavaScript. No DOM, no CSS, just you drawing things manually.\nGames, data visualizations, image editors, generative art, physics simulations \u0026ndash; a huge chunk of the coolest things you see on the web are built with canvas. And it\u0026rsquo;s not even that complicated once you have the right mental model.\nBy the time we\u0026rsquo;re done with this series, you\u0026rsquo;ll have built Snake, Flappy Bird, Pac-Man, a fake 3D rotating cube and an ASCII art converter. None of it uses a game engine. Just math and canvas API.\nBut right now we\u0026rsquo;re starting from zero. Let\u0026rsquo;s go.\nThe setup The HTML part is pretty much nothing:\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34; /\u0026gt; \u0026lt;title\u0026gt;Canvas\u0026lt;/title\u0026gt; \u0026lt;style\u0026gt; body { margin: 0; background: #111; display: flex; justify-content: center; align-items: center; height: 100vh; } canvas { background: #000; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;canvas id=\u0026#34;gameCanvas\u0026#34; width=\u0026#34;600\u0026#34; height=\u0026#34;400\u0026#34;\u0026gt;\u0026lt;/canvas\u0026gt; \u0026lt;script\u0026gt; const canvas = document.getElementById(\u0026#34;gameCanvas\u0026#34;); const ctx = canvas.getContext(\u0026#34;2d\u0026#34;); // everything we draw happens here ctx.fillStyle = \u0026#34;#86efac\u0026#34;; ctx.fillRect(50, 50, 200, 100); \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Save that, open it in a browser. You should see a green rectangle. That\u0026rsquo;s it, you\u0026rsquo;re drawing on canvas.\nTwo things to notice here. First, the width and height on the \u0026lt;canvas\u0026gt; element are not CSS properties. Don\u0026rsquo;t set canvas dimensions with CSS \u0026ndash; that just stretches the canvas and makes everything blurry. Always set them as HTML attributes.\nSecond, getContext('2d') is where all the real stuff lives. That ctx object is your drawing tool. Everything in this post and the next four is a method on that object.\n// worth knowing: there's also getContext('webgl') for 3D rendering, but that's a completely different world. We're sticking with '2d' for this whole series. Honestly you can do a lot more with it than most people think. The coordinate system (the actual lie) This is the most important thing to understand early, because it\u0026rsquo;s different from everything you learned in math class.\nIn math, the Y axis goes up. (0, 0) is the bottom left, positive Y means higher.\nOn canvas, Y goes down. (0, 0) is the top left, and increasing Y moves things downward.\n(0,0) ──────────────────────→ X increases │ │ │ ↓ Y increases So if you want to draw something near the bottom of a 400px tall canvas, you give it a Y of around 350. If you want it near the top, Y is small.\nThis trips people up constantly. I spent like half an hour confused about why my shapes were mirrored when I first started. Now you know, so you won\u0026rsquo;t waste that time.\nThe other thing: all coordinates you pass to canvas functions are in pixels. No percentages, no em, just plain numbers.\nDrawing rectangles Rectangles are the simplest thing to draw, and canvas has three methods for them:\n// filled rectangle ctx.fillStyle = \u0026#34;#86efac\u0026#34;; ctx.fillRect(x, y, width, height); // outlined rectangle (no fill) ctx.strokeStyle = \u0026#34;#5eead4\u0026#34;; ctx.lineWidth = 2; ctx.strokeRect(x, y, width, height); // erase a rectangle (makes it transparent) ctx.clearRect(x, y, width, height); All four arguments follow the same pattern: x position, y position, width, height. The x and y are the top-left corner of the rectangle.\nTry this:\n// dark background ctx.fillStyle = \u0026#34;#111\u0026#34;; ctx.fillRect(0, 0, canvas.width, canvas.height); // a few boxes ctx.fillStyle = \u0026#34;#86efac\u0026#34;; ctx.fillRect(50, 50, 100, 60); ctx.strokeStyle = \u0026#34;#5eead4\u0026#34;; ctx.lineWidth = 2; ctx.strokeRect(200, 50, 100, 60); ctx.fillStyle = \u0026#34;#fca5a5\u0026#34;; ctx.fillRect(350, 50, 100, 60); ctx.clearRect(380, 70, 40, 20); // punch a hole in the red rectangle clearRect is especially useful later when we do animation \u0026ndash; it\u0026rsquo;s how you erase the previous frame before drawing the next one.\nPaths \u0026ndash; the real way to draw anything Rectangles are convenient but limited. To draw anything more interesting \u0026ndash; lines, triangles, custom shapes, circles \u0026ndash; you use paths.\nThe idea is simple: you tell canvas to \u0026ldquo;start a new path\u0026rdquo;, then describe a sequence of points/curves, then fill or stroke that shape. Think of it like lifting a pen, moving to a starting point, then drawing.\nctx.beginPath(); // start fresh, clear any previous path ctx.moveTo(100, 100); // pick up the pen, move to (100, 100) ctx.lineTo(200, 100); // draw a line to (200, 100) ctx.lineTo(150, 180); // draw a line to (150, 180) ctx.closePath(); // draw a line back to the starting point ctx.fillStyle = \u0026#34;#fbbf24\u0026#34;; ctx.fill(); // fill the shape ctx.strokeStyle = \u0026#34;#fff\u0026#34;; ctx.lineWidth = 2; ctx.stroke(); // also draw the outline Run that and you\u0026rsquo;ll get a triangle. The closePath() call connects the last point back to the first \u0026ndash; without it you\u0026rsquo;d have an open shape and filling would still work but the outline would have a gap.\nOne thing that catches people: beginPath() is really important. If you forget it, canvas keeps adding to the same path you were drawing before, and things get weird fast. Get in the habit of always starting with beginPath().\nLet\u0026rsquo;s draw something more useful \u0026ndash; a cross/plus shape using lines:\nfunction drawCross(ctx, x, y, size) { const half = size / 2; const third = size / 3; ctx.beginPath(); // horizontal bar ctx.moveTo(x - half, y - third); ctx.lineTo(x + half, y - third); ctx.lineTo(x + half, y + third); ctx.lineTo(x - half, y + third); ctx.closePath(); ctx.beginPath(); // vertical bar ctx.moveTo(x - third, y - half); ctx.lineTo(x + third, y - half); ctx.lineTo(x + third, y + half); ctx.lineTo(x - third, y + half); ctx.closePath(); ctx.fillStyle = \u0026#34;#fca5a5\u0026#34;; ctx.fill(); } drawCross(ctx, 300, 200, 100); Wait, that\u0026rsquo;s two beginPath() calls. When you call fill() after the second path, does it fill both? Actually no \u0026ndash; calling beginPath() the second time clears the first one. Each fill/stroke call only applies to the current path.\nIf you want to draw two separate shapes and fill them differently, you always call beginPath() before each one.\nArcs and circles Circles are just a special case of an arc. The method is:\nctx.arc(x, y, radius, startAngle, endAngle, counterclockwise); Angles are in radians, not degrees. Math.PI is 180 degrees, so a full circle goes from 0 to Math.PI * 2. This is one of those things you just accept and move on.\n// a full circle ctx.beginPath(); ctx.arc(300, 200, 50, 0, Math.PI * 2); ctx.fillStyle = \u0026#34;#86efac\u0026#34;; ctx.fill(); // a half circle (top half) ctx.beginPath(); ctx.arc(300, 200, 80, Math.PI, Math.PI * 2); ctx.fillStyle = \u0026#34;#5eead4\u0026#34;; ctx.fill(); For a Pac-Man shape, you want a circle with a wedge cut out for the mouth:\nctx.beginPath(); ctx.moveTo(300, 200); // center point ctx.arc(300, 200, 50, 0.3, Math.PI * 2 - 0.3); // arc with gap ctx.closePath(); // connect back to center ctx.fillStyle = \u0026#34;#fbbf24\u0026#34;; ctx.fill(); The 0.3 and Math.PI * 2 - 0.3 are the start and end angles \u0026ndash; the gap is where the mouth is. Mess with that number to make the mouth bigger or smaller.\nA quick degrees to radians helper to keep your sanity:\nconst toRad = (deg) =\u0026gt; (deg * Math.PI) / 180; // now you can write: ctx.arc(300, 200, 50, toRad(30), toRad(330)); Colors and styling fillStyle and strokeStyle accept pretty much anything CSS does:\nctx.fillStyle = \u0026#34;#86efac\u0026#34;; // hex ctx.fillStyle = \u0026#34;rgb(134, 239, 172)\u0026#34;; // rgb ctx.fillStyle = \u0026#34;rgba(134, 239, 172, 0.5)\u0026#34;; // with transparency ctx.fillStyle = \u0026#34;hsl(145, 70%, 74%)\u0026#34;; // hsl For line work, a few properties you\u0026rsquo;ll use all the time:\nctx.lineWidth = 3; // thickness in pixels ctx.lineCap = \u0026#34;round\u0026#34;; // end cap style: \u0026#39;butt\u0026#39;, \u0026#39;round\u0026#39;, \u0026#39;square\u0026#39; ctx.lineJoin = \u0026#34;round\u0026#34;; // corner style: \u0026#39;miter\u0026#39;, \u0026#39;round\u0026#39;, \u0026#39;bevel\u0026#39; Gradients take a tiny bit more setup but they look good:\n// linear gradient const grad = ctx.createLinearGradient(0, 0, canvas.width, 0); // left to right grad.addColorStop(0, \u0026#34;#86efac\u0026#34;); grad.addColorStop(1, \u0026#34;#5eead4\u0026#34;); ctx.fillStyle = grad; ctx.fillRect(0, 0, canvas.width, canvas.height); createLinearGradient takes four args: the start x/y and end x/y. The color stops are 0 to 1, representing the position along that gradient line.\nThere\u0026rsquo;s also createRadialGradient for circular gradients, but we\u0026rsquo;ll come back to that when we actually need it.\nText Drawing text is straightforward:\nctx.font = \u0026#34;24px Space Grotesk\u0026#34;; // CSS font string ctx.fillStyle = \u0026#34;#c8d1c1\u0026#34;; ctx.fillText(\u0026#34;hello canvas\u0026#34;, 100, 100); // or with a stroke: ctx.strokeStyle = \u0026#34;#86efac\u0026#34;; ctx.lineWidth = 1; ctx.strokeText(\u0026#34;outlined text\u0026#34;, 100, 150); One thing to know: the y coordinate for text is the baseline, not the top of the text. So if you put text at y=0, most of it will be above the canvas and invisible. Give it a little room.\nAlignment properties:\nctx.textAlign = \u0026#34;center\u0026#34;; // \u0026#39;left\u0026#39;, \u0026#39;right\u0026#39;, \u0026#39;center\u0026#39;, \u0026#39;start\u0026#39;, \u0026#39;end\u0026#39; ctx.textBaseline = \u0026#34;middle\u0026#34;; // \u0026#39;top\u0026#39;, \u0026#39;middle\u0026#39;, \u0026#39;bottom\u0026#39;, \u0026#39;alphabetic\u0026#39; These are really useful when you want to center text inside a shape. Set both to 'center'/'middle' and use the center coordinates of the shape, and it\u0026rsquo;ll be perfectly centered.\n// puzzle 01 =\u003e before you continue Try to draw a clock face before reading the project section. Don't overthink it -- just a circle for the face, 12 tick marks around it at equal angles, and some numbers at the main positions (12, 3, 6, 9). You have all the tools for this now.\nThe rotation math for equally spaced points around a circle is:\nx = cx + radius * Math.cos(angle)\ny = cy + radius * Math.sin(angle)\nWhere angle goes from 0 to Math.PI * 2 in 12 equal steps.\nTake 15 minutes and genuinely try it yourself. Then come back and keep reading.\nMini-project: Static Pac-Man Frame // project 01 of 05 — static pac-man frame We're not building the full game yet. Not even close. Right now the goal is to use everything from this post to draw a Pac-Man scene that looks right. No movement, no logic, just drawing. By the end of Part 2, we'll start animating it. By Part 4, it'll be a real game.\nBuild this step by step, don\u0026rsquo;t paste the whole thing at once. You want to understand what each addition does.\nStep 1: The canvas and background \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34; /\u0026gt; \u0026lt;title\u0026gt;Pac-Man\u0026lt;/title\u0026gt; \u0026lt;style\u0026gt; body { margin: 0; background: #111; display: flex; justify-content: center; align-items: center; height: 100vh; } canvas { border: 2px solid #1a3a5c; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;canvas id=\u0026#34;c\u0026#34; width=\u0026#34;560\u0026#34; height=\u0026#34;620\u0026#34;\u0026gt;\u0026lt;/canvas\u0026gt; \u0026lt;script\u0026gt; const canvas = document.getElementById(\u0026#34;c\u0026#34;); const ctx = canvas.getContext(\u0026#34;2d\u0026#34;); const W = canvas.width; const H = canvas.height; // black background ctx.fillStyle = \u0026#34;#000\u0026#34;; ctx.fillRect(0, 0, W, H); \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; W and H as shortcuts is a habit worth building now. You\u0026rsquo;ll reference canvas dimensions constantly.\nStep 2: The maze walls A proper Pac-Man maze is complex. We\u0026rsquo;ll fake a simplified version using rectangles for the outer border and a few inner walls. Later when we do the real game in Part 4, we\u0026rsquo;ll use a tile map. For now, keep it simple:\nfunction drawMaze() { ctx.strokeStyle = \u0026#34;#1a6fa8\u0026#34;; ctx.lineWidth = 4; // outer border ctx.strokeRect(20, 20, W - 40, H - 60); // a few inner walls to make it feel like a maze // top section dividers ctx.strokeRect(20, 20, W / 2 - 50, 80); ctx.strokeRect(W / 2 + 50, 20, W / 2 - 70, 80); // ghost house in the center const ghX = W / 2 - 60; const ghY = H / 2 - 40; ctx.strokeRect(ghX, ghY, 120, 80); // ghost house door (drawn over the top edge to make a gap) ctx.fillStyle = \u0026#34;#fca5a5\u0026#34;; ctx.fillRect(W / 2 - 25, ghY - 2, 50, 4); } drawMaze(); Step 3: The dots Pac-Man\u0026rsquo;s dots are just small filled circles in a grid pattern, placed inside the maze. We skip the center ghost area and the edges where walls are:\nfunction drawDots() { const dotRadius = 3; const spacing = 28; const startX = 46; const startY = 46; for (let row = 0; row \u0026lt; 20; row++) { for (let col = 0; col \u0026lt; 18; col++) { const x = startX + col * spacing; const y = startY + row * spacing; // skip the ghost house area const inGhostHouse = x \u0026gt; W / 2 - 80 \u0026amp;\u0026amp; x \u0026lt; W / 2 + 80 \u0026amp;\u0026amp; y \u0026gt; H / 2 - 60 \u0026amp;\u0026amp; y \u0026lt; H / 2 + 100; // skip the edges const tooClose = x \u0026lt; 30 || x \u0026gt; W - 30 || y \u0026lt; 30 || y \u0026gt; H - 70; if (inGhostHouse || tooClose) continue; ctx.beginPath(); ctx.arc(x, y, dotRadius, 0, Math.PI * 2); ctx.fillStyle = \u0026#34;#e8d5a3\u0026#34;; ctx.fill(); } } } drawDots(); Step 4: Power pellets The four big dots at the corners \u0026ndash; these make ghosts vulnerable:\nfunction drawPowerPellets() { const pellets = [ { x: 60, y: 80 }, { x: W - 60, y: 80 }, { x: 60, y: H - 100 }, { x: W - 60, y: H - 100 }, ]; pellets.forEach((p) =\u0026gt; { ctx.beginPath(); ctx.arc(p.x, p.y, 8, 0, Math.PI * 2); ctx.fillStyle = \u0026#34;#e8d5a3\u0026#34;; ctx.fill(); }); } drawPowerPellets(); Step 5: Pac-Man Put him in the bottom-left area of the maze, mouth open:\nfunction drawPacman(x, y, radius, mouthAngle) { ctx.beginPath(); ctx.moveTo(x, y); ctx.arc(x, y, radius, mouthAngle, Math.PI * 2 - mouthAngle); ctx.closePath(); ctx.fillStyle = \u0026#34;#fbbf24\u0026#34;; ctx.fill(); } drawPacman(90, H - 120, 20, 0.25); Step 6: A ghost (drawing it freehand) Ghosts have that classic rounded top and wavy bottom. You can fake it with an arc for the top half and some curves for the body:\nfunction drawGhost(x, y, color) { const r = 18; const h = 36; ctx.beginPath(); // semi-circle top ctx.arc(x, y, r, Math.PI, 0); // right side ctx.lineTo(x + r, y + h); // wavy bottom using quadratic curves ctx.quadraticCurveTo(x + r * 0.66, y + h - 10, x + r * 0.33, y + h); ctx.quadraticCurveTo(x, y + h - 10, x - r * 0.33, y + h); ctx.quadraticCurveTo(x - r * 0.66, y + h - 10, x - r, y + h); // left side ctx.lineTo(x - r, y); ctx.closePath(); ctx.fillStyle = color; ctx.fill(); // eyes const eyeColors = [\u0026#34;#fff\u0026#34;, \u0026#34;#1a6fa8\u0026#34;]; const eyes = [ { ex: x - 7, ey: y - 4 }, { ex: x + 7, ey: y - 4 }, ]; eyes.forEach(({ ex, ey }) =\u0026gt; { ctx.beginPath(); ctx.arc(ex, ey, 5, 0, Math.PI * 2); ctx.fillStyle = \u0026#34;#fff\u0026#34;; ctx.fill(); ctx.beginPath(); ctx.arc(ex + 1, ey + 1, 2.5, 0, Math.PI * 2); ctx.fillStyle = \u0026#34;#1a6fa8\u0026#34;; ctx.fill(); }); } // draw four ghosts in the ghost house drawGhost(W / 2 - 40, H / 2 - 20, \u0026#34;#fca5a5\u0026#34;); // Blinky drawGhost(W / 2, H / 2 - 20, \u0026#34;#f9a8d4\u0026#34;); // Pinky drawGhost(W / 2 + 40, H / 2 - 20, \u0026#34;#5eead4\u0026#34;); // Inky Notice we used quadraticCurveTo there. It takes a control point and an end point \u0026ndash; the control point \u0026ldquo;pulls\u0026rdquo; the curve toward it. That\u0026rsquo;s how you get curves between points that aren\u0026rsquo;t arcs.\nStep 7: Score display function drawHUD() { ctx.fillStyle = \u0026#34;#fff\u0026#34;; ctx.font = \u0026#34;16px JetBrains Mono\u0026#34;; ctx.textAlign = \u0026#34;left\u0026#34;; ctx.fillText(\u0026#34;SCORE\u0026#34;, 20, H - 20); ctx.fillStyle = \u0026#34;#fbbf24\u0026#34;; ctx.fillText(\u0026#34;0\u0026#34;, 90, H - 20); ctx.fillStyle = \u0026#34;#fff\u0026#34;; ctx.textAlign = \u0026#34;center\u0026#34;; ctx.fillText(\u0026#34;HIGH SCORE\u0026#34;, W / 2, H - 20); ctx.fillStyle = \u0026#34;#fbbf24\u0026#34;; ctx.fillText(\u0026#34;0\u0026#34;, W / 2 + 80, H - 20); } drawHUD(); The complete file Here\u0026rsquo;s everything together in the right order:\nconst canvas = document.getElementById(\u0026#34;c\u0026#34;); const ctx = canvas.getContext(\u0026#34;2d\u0026#34;); const W = canvas.width; const H = canvas.height; // draw order matters -- later calls go on top of earlier ones ctx.fillStyle = \u0026#34;#000\u0026#34;; ctx.fillRect(0, 0, W, H); drawMaze(); drawDots(); drawPowerPellets(); drawPacman(90, H - 120, 20, 0.25); drawGhost(W / 2 - 40, H / 2 - 20, \u0026#34;#fca5a5\u0026#34;); drawGhost(W / 2, H / 2 - 20, \u0026#34;#f9a8d4\u0026#34;); drawGhost(W / 2 + 40, H / 2 - 20, \u0026#34;#5eead4\u0026#34;); drawHUD(); It won\u0026rsquo;t look perfect, the maze is simplified. But you should see a recognizable Pac-Man scene. And importantly, you built it yourself using the raw API, no library, no tutorial copying.\n// on draw order: canvas is a painter's model. whatever you draw last is on top. if you draw the dots before the background, the background will cover the dots. always draw backgrounds first. // checkpoint -- part 01 I know how to set up a canvas element and get the 2d context I understand the coordinate system (Y goes down, origin is top-left) I can draw rectangles using fillRect, strokeRect, clearRect I understand the path model: beginPath, moveTo, lineTo, closePath, fill/stroke I can draw circles and arcs with ctx.arc() I can style shapes with colors, gradients and line properties I can draw and align text on the canvas I built the static Pac-Man frame A small thing to try on your own Before Part 2, take the clock face puzzle from earlier (if you didn\u0026rsquo;t do it, do it now) and add the hour and minute hands. Use new Date() to get the actual current time, convert the hours and minutes into angles, then draw lines from the center outward.\nThis is a fully working clock, on canvas, with real time, without any library. It\u0026rsquo;s a good thing to have built.\n// up next — Part 02: Making Things Move\nRight now everything we drew is static. One call, one frame, done. In Part 2 we get into requestAnimationFrame -- the loop that makes everything on canvas feel alive. We'll build the animation engine that powers every game in this series, and we'll use it to make Snake. By the end you'll have a fully playable game.\n","permalink":"/canvas/canvas-01-pixels-paths/","summary":"\u003c!--\n  NOTE FOR HUGO SETUP:\n  This post uses inline HTML. Make sure your hugo.yaml has:\n\n  markup:\n    goldmark:\n      renderer:\n        unsafe: true\n\n  Without this, Hugo strips the HTML blocks and the styling breaks.\n--\u003e\n\u003cstyle\u003e\n/* ── CANVAS POST SCOPED VARIABLES ──────────────── */\n.cv-post {\n  --cv-green:   #86efac;\n  --cv-cyan:    #5eead4;\n  --cv-amber:   #fbbf24;\n  --cv-red:     #fca5a5;\n  --cv-bg:      #0d110c;\n  --cv-bg2:     #111810;\n  --cv-border:  rgba(134, 239, 172, 0.15);\n  --cv-border2: rgba(134, 239, 172, 0.35);\n  --cv-muted:   #788571;\n  --cv-text:    #c8d1c1;\n}\n\n/* ── PUZZLE BLOCK ──────────────────────────────── */\n.cv-puzzle {\n  background: rgba(251, 191, 36, 0.04);\n  border: 1px solid rgba(251, 191, 36, 0.2);\n  border-left: 3px solid #fbbf24;\n  border-radius: 6px;\n  padding: 20px 24px;\n  margin: 32px 0;\n}\n.cv-puzzle-label {\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 0.68rem;\n  letter-spacing: 0.14em;\n  text-transform: uppercase;\n  color: #fbbf24;\n  margin-bottom: 12px;\n}\n.cv-puzzle p,\n.cv-puzzle li { color: #c4a855; font-size: 0.94rem; }\n.cv-puzzle strong { color: #fbbf24; }\n.cv-puzzle code {\n  background: rgba(251, 191, 36, 0.08);\n  border: 1px solid rgba(251, 191, 36, 0.2);\n  padding: 1px 6px;\n  border-radius: 3px;\n  font-size: 0.85em;\n}\n\n/* ── CHECKPOINT BLOCK ──────────────────────────── */\n.cv-checkpoint {\n  background: rgba(134, 239, 172, 0.03);\n  border: 1px solid rgba(134, 239, 172, 0.18);\n  border-left: 3px solid #86efac;\n  border-radius: 6px;\n  padding: 20px 24px;\n  margin: 32px 0;\n}\n.cv-cp-label {\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 0.68rem;\n  letter-spacing: 0.14em;\n  text-transform: uppercase;\n  color: #86efac;\n  margin-bottom: 14px;\n}\n.cv-checkpoint ul {\n  list-style: none;\n  padding: 0;\n  margin: 0;\n}\n.cv-checkpoint ul li {\n  display: flex;\n  align-items: flex-start;\n  gap: 10px;\n  font-size: 0.9rem;\n  color: #8aad8e;\n  margin-bottom: 8px;\n  cursor: pointer;\n}\n.cv-cb {\n  width: 15px; height: 15px;\n  border: 1px solid #2a4a2e;\n  border-radius: 2px;\n  flex-shrink: 0;\n  margin-top: 2px;\n  background: #0d110c;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  transition: all 0.15s;\n  font-size: 9px;\n  font-weight: bold;\n  color: transparent;\n}\n.cv-cb.done {\n  background: #86efac;\n  border-color: #86efac;\n  color: #0d110c;\n}\n\n/* ── NOTE / CALLOUT BLOCK ──────────────────────── */\n.cv-note {\n  background: rgba(94, 234, 212, 0.04);\n  border: 1px solid rgba(94, 234, 212, 0.18);\n  border-left: 3px solid #5eead4;\n  border-radius: 6px;\n  padding: 16px 22px;\n  margin: 24px 0;\n  font-size: 0.93rem;\n  color: #78b8b0;\n}\n.cv-note strong { color: #5eead4; }\n.cv-note code {\n  background: rgba(94, 234, 212, 0.08);\n  padding: 1px 6px;\n  border-radius: 3px;\n  font-size: 0.85em;\n}\n\n/* ── PROJECT BLOCK ─────────────────────────────── */\n.cv-project {\n  background: rgba(134, 239, 172, 0.02);\n  border: 1px solid rgba(134, 239, 172, 0.12);\n  border-radius: 8px;\n  padding: 24px 28px;\n  margin: 36px 0;\n}\n.cv-project-header {\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 0.7rem;\n  letter-spacing: 0.16em;\n  text-transform: uppercase;\n  color: #86efac;\n  margin-bottom: 16px;\n  padding-bottom: 12px;\n  border-bottom: 1px solid rgba(134, 239, 172, 0.1);\n}\n.cv-project p { color: #9ab89e; font-size: 0.94rem; }\n\n/* ── NEXT TEASER ───────────────────────────────── */\n.cv-next {\n  background: rgba(18, 22, 16, 0.7);\n  border: 1px solid rgba(134, 239, 172, 0.12);\n  border-radius: 8px;\n  padding: 22px 26px;\n  margin: 40px 0 0 0;\n  text-align: center;\n}\n.cv-next p { color: #788571; font-size: 0.9rem; margin: 0; }\n.cv-next strong { color: #86efac; }\n\u003c/style\u003e\n\u003cdiv class=\"cv-post\"\u003e\n\u003cp\u003eok so before we start, quick heads up: this is not the kind of post you read and close. open a code editor and a browser right next to it. every section has working code, run it as you go. if you just read through without touching the keyboard, by tomorrow you\u0026rsquo;ll remember nothing. i genuinely promise.\u003c/p\u003e","title":"Canvas 01 : Pixels, Paths and the Coordinate Lie"},{"content":" Blog Summary: The bedrock of the course. Covers why JS exists, how it runs, and the async programming model that makes JS unique and powerful for web development.\n1. Why Programming Languages? [SOURCE — COURSE MATERIAL]\nComputers understand only binary (0s and 1s). Languages exist so humans can write readable instructions that compilers/interpreters then convert to binary.\nHuman-written code → Compiler → 01010101 → CPU executes The flow:\nDeveloper writes high-level code (JS, Python, C++) A compiler/runtime converts it to machine code CPU executes machine code from RAM SSD holds the program at rest; RAM holds it while running 2. Compiled vs Interpreted (Scripting) Languages [SOURCE — COURSE MATERIAL]\nType Compiled Interpreted / Scripting Examples C, C++, Go, Rust JavaScript, Python Execution Must compile first, then run Runs line by line at runtime Speed Faster at runtime Slower but flexible Dev cycle Compile → Run Just run C++ compile flow:\n# Step 1: Write code # Step 2: Compile g++ main.cpp -o main # Step 3: Run ./main JavaScript — no compile step needed:\nnode index.js # Just run it [ADDED — EXPLANATION] JS was originally a scripting language for browsers. The JS engine (V8 in Chrome) compiles JS to machine code just-in-time (JIT) at runtime. This is why JS can be fast even though it\u0026rsquo;s \u0026ldquo;interpreted.\u0026rdquo;\n3. Why JavaScript Over Other Languages? [SOURCE — COURSE MATERIAL]\nJS dominates web because:\nIt\u0026rsquo;s the only language browsers understand natively Works on frontend AND backend (Node.js) Huge ecosystem (npm) Async model is perfect for I/O-heavy web servers [ADDED — IMPORTANT BACKGROUND] JS was not designed to be a backend language. It was created in 1995 by Brendan Eich for Netscape to make web pages interactive. Node.js (2009) changed everything — someone took the V8 engine out of Chrome and added file system/network APIs on top, making JS capable on the backend.\n4. Static vs Dynamic Typing [SOURCE — COURSE MATERIAL]\n// Dynamic (JS) — types are inferred at runtime let x = 5; // number x = \u0026#34;hello\u0026#34;; // now a string — totally fine // Static (TypeScript / Java) — type declared, enforced let x: number = 5; x = \u0026#34;hello\u0026#34;; // ERROR at compile time Implication: JS is flexible but error-prone. TypeScript adds types on top (covered later).\n5. Single-Threaded Nature of JS [SOURCE — COURSE MATERIAL]\nJS has one call stack — it can only do one thing at a time.\nMental model: Your brain is single-threaded — it can truly focus on only one task. But you delegate background tasks (boiling water, washing machine running) and context-switch between quick tasks.\nJS does the same:\nDelegate long operations (file reads, network) to the browser/OS Context switch using the event loop JS Thread: executes code → hits async call → delegates → continues → callback fires later 6. JavaScript Primitives [SOURCE — COURSE MATERIAL]\nSimple Primitives:\n// Number let age = 25; let price = 9.99; // String let name = \u0026#34;Harkirat\u0026#34;; let greeting = `Hello, ${name}!`; // template literal // Boolean let isLoggedIn = true; let hasPaid = false; // Null / Undefined let nothing = null; // explicitly nothing let unknown; // undefined — declared but no value Complex Primitives (Reference Types):\n// Array let fruits = [\u0026#34;apple\u0026#34;, \u0026#34;banana\u0026#34;, \u0026#34;cherry\u0026#34;]; fruits[0]; // \u0026#34;apple\u0026#34; fruits.push(\u0026#34;mango\u0026#34;); // adds to end fruits.length; // 4 // Object let user = { name: \u0026#34;Harkirat\u0026#34;, age: 25, isAdmin: false, }; user.name; // \u0026#34;Harkirat\u0026#34; user[\u0026#34;age\u0026#34;]; // 25 — bracket notation 7. Functions [SOURCE — COURSE MATERIAL]\nA function takes input, does something, returns output.\n// Function declaration function add(a, b) { return a + b; } // Function expression (stored in variable) const multiply = function (a, b) { return a * b; }; // Arrow function (modern, concise) const square = (n) =\u0026gt; n * n; // Calling add(3, 4); // 7 square(5); // 25 Why functions? DRY principle (Don\u0026rsquo;t Repeat Yourself):\n// BAD — repeated logic console.log(1 * 1); console.log(2 * 2); console.log(3 * 3); // GOOD — reusable function function printSquare(n) { console.log(n * n); } printSquare(1); printSquare(2); printSquare(3); 8. Loops [SOURCE — COURSE MATERIAL]\n// For loop — sum from 1 to 100 let sum = 0; for (let i = 1; i \u0026lt;= 100; i++) { sum += i; } console.log(sum); // 5050 // While loop let count = 0; while (count \u0026lt; 5) { console.log(count); count++; } // Array iteration const nums = [1, 2, 3, 4, 5]; for (let i = 0; i \u0026lt; nums.length; i++) { console.log(nums[i]); } // Modern style: nums.forEach((n) =\u0026gt; console.log(n)); 9. Callback Functions [SOURCE — COURSE MATERIAL]\nA callback is a function passed as an argument to another function, to be called later.\n// Basic callback concept function doMath(a, b, operation) { return operation(a, b); } function add(x, y) { return x + y; } function multiply(x, y) { return x * y; } doMath(3, 4, add); // 7 doMath(3, 4, multiply); // 12 // Anonymous callback (no separate function) doMath(3, 4, function (x, y) { return x - y; }); // -1 doMath(3, 4, (x, y) =\u0026gt; x ** y); // 81 (arrow function) [ADDED — EXPLANATION] Callbacks are the foundation of async programming in JS. When you say \u0026ldquo;call this function when you\u0026rsquo;re done,\u0026rdquo; that\u0026rsquo;s a callback.\n10. Asynchronous Programming [SOURCE — COURSE MATERIAL]\nProblem: Synchronous blocking // Everything stops while waiting const data = readFile(\u0026#34;large-file.txt\u0026#34;); // blocks for 5 seconds console.log(\u0026#34;This prints after 5 seconds\u0026#34;); // bad! Async solution — delegate and continue: // setTimeout — basic async function console.log(\u0026#34;1: Start\u0026#34;); setTimeout(function () { console.log(\u0026#34;3: Callback fires after 1 second\u0026#34;); }, 1000); console.log(\u0026#34;2: This runs immediately\u0026#34;); // Output: // 1: Start // 2: This runs immediately // 3: Callback fires after 1 second Real async functions: const fs = require(\u0026#34;fs\u0026#34;); // Non-blocking file read fs.readFile(\u0026#34;data.txt\u0026#34;, \u0026#34;utf8\u0026#34;, function (error, content) { if (error) { console.log(\u0026#34;Error reading file:\u0026#34;, error); return; } console.log(\u0026#34;File contents:\u0026#34;, content); }); console.log(\u0026#34;This runs before file is read!\u0026#34;); // prints first 11. The Event Loop [SOURCE — COURSE MATERIAL]\n[ADDED — IMPORTANT BACKGROUND] The event loop is JavaScript\u0026rsquo;s mechanism for handling async operations despite being single-threaded.\nCall Stack Web APIs/OS Callback Queue ───────── ────────── ────────────── main() ──→ setTimeout (1s timer) fs.readFile fetch When complete: callback is pushed to Callback Queue Event loop checks: if Call Stack is empty, push from Queue → Stack Visualization: http://latentflip.com/loupe — highly recommended\nKey rule: A callback only executes when the call stack is completely empty.\n12. Callback Hell [SOURCE — COURSE MATERIAL]\n// The \u0026#34;Pyramid of Doom\u0026#34; — deeply nested callbacks setTimeout(function () { console.log(\u0026#34;After 1 second\u0026#34;); setTimeout(function () { console.log(\u0026#34;After 2 more seconds\u0026#34;); setTimeout(function () { console.log(\u0026#34;After 3 more seconds\u0026#34;); // imagine 10 more levels... }, 3000); }, 2000); }, 1000); 13. Promises [SOURCE — COURSE MATERIAL]\nPromises are syntactic sugar over callbacks. They make async code readable.\n// Creating a Promise function wait(ms) { return new Promise(function (resolve, reject) { setTimeout(function () { resolve(\u0026#34;Done waiting!\u0026#34;); }, ms); }); } // Using .then() chaining — NO nesting! wait(1000) .then(function (result) { console.log(result); // \u0026#34;Done waiting!\u0026#34; return wait(2000); }) .then(function (result) { console.log(\u0026#34;Two more seconds passed\u0026#34;); }) .catch(function (error) { console.log(\u0026#34;Something went wrong:\u0026#34;, error); }); Promise states:\npending — initial, waiting fulfilled — resolved successfully rejected — failed 14. Async/Await [SOURCE — COURSE MATERIAL]\nAsync/await is syntactic sugar over Promises. Reads like synchronous code.\n// Same logic as above, much cleaner async function main() { try { const result = await wait(1000); console.log(result); // \u0026#34;Done waiting!\u0026#34; await wait(2000); console.log(\u0026#34;Two more seconds passed\u0026#34;); } catch (error) { console.log(\u0026#34;Error:\u0026#34;, error); } } main(); Rules:\nasync keyword makes a function return a Promise await pauses execution until Promise resolves (only inside async functions) Always wrap in try/catch 15. Array Methods: map \u0026amp; filter [SOURCE — COURSE MATERIAL]\nconst numbers = [1, 2, 3, 4, 5, 6]; // map — transform each element const doubled = numbers.map((n) =\u0026gt; n * 2); // [2, 4, 6, 8, 10, 12] // filter — keep elements that pass a test const evens = numbers.filter((n) =\u0026gt; n % 2 === 0); // [2, 4, 6] // Chain them const doubledEvens = numbers.filter((n) =\u0026gt; n % 2 === 0).map((n) =\u0026gt; n * 2); // [4, 8, 12] Exercises Quick (10–15 min) Write a function sumArray(arr) that returns the sum of all elements using a for loop. Then rewrite it using reduce.\nHint 1: reduce takes a callback and an initial value.\nHint 2: arr.reduce((acc, curr) =\u0026gt; acc + curr, 0)\nCommon mistakes: Forgetting the initial value 0 in reduce.\nIntermediate (30–60 min) Create a function fetchAndLog(url) that:\nFetches data from a URL (use Node\u0026rsquo;s https module or browser fetch) Logs the result after 2 seconds (use setTimeout) Handles errors gracefully Expected behavior: Data prints after delay. If URL is bad, logs an error.\nHint 1: Chain .then() before setTimeout, or use await.\nHint 2: Wrap in async/await with try/catch.\nChallenge (1–2 hours) Implement your own Promise class from scratch (simplified):\nConstructor takes an executor function with resolve and reject .then() method registers success handler .catch() registers error handler Common Confusions Confusion Reality \u0026ldquo;Async means parallel\u0026rdquo; No. JS is still single-threaded. Async means non-blocking delegation. \u0026ldquo;await pauses JS entirely\u0026rdquo; No. It pauses the current async function only. Other code can run. \u0026ldquo;Promises and callbacks are different things\u0026rdquo; Promises are built ON callbacks. They\u0026rsquo;re just cleaner syntax. \u0026ldquo;Arrow functions are just shorter syntax\u0026rdquo; They also don\u0026rsquo;t have their own this — important for OOP. Key Takeaways JS is single-threaded, interpreted, dynamically typed Async programming is the core superpower of JS — delegation + event loop Callbacks → Promises → Async/Await: each solves readability of the last map and filter are functional programming tools — prefer them over manual loops for transformations ","permalink":"/posts/01-js-foundation/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eBlog Summary:\u003c/strong\u003e The bedrock of the course. Covers why JS exists, how it runs, and the async programming model that makes JS unique and powerful for web development.\u003c/p\u003e\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-why-programming-languages\"\u003e1. Why Programming Languages?\u003c/h2\u003e\n\u003cp\u003e[SOURCE — COURSE MATERIAL]\u003c/p\u003e\n\u003cp\u003eComputers understand only binary (0s and 1s). Languages exist so humans can write readable instructions that compilers/interpreters then convert to binary.\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eHuman-written code → Compiler → 01010101 → CPU executes\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003cstrong\u003eThe flow:\u003c/strong\u003e\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eDeveloper writes high-level code (JS, Python, C++)\u003c/li\u003e\n\u003cli\u003eA compiler/runtime converts it to machine code\u003c/li\u003e\n\u003cli\u003eCPU executes machine code from RAM\u003c/li\u003e\n\u003cli\u003eSSD holds the program at rest; RAM holds it while running\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr\u003e\n\u003ch2 id=\"2-compiled-vs-interpreted-scripting-languages\"\u003e2. Compiled vs Interpreted (Scripting) Languages\u003c/h2\u003e\n\u003cp\u003e[SOURCE — COURSE MATERIAL]\u003c/p\u003e","title":"01 : Js Foundation"},{"content":" Last time we ended with a static Pac-Man frame. Looked fine. Did nothing. Every time you refreshed the page it was the same, frozen moment.\nThat\u0026rsquo;s not a game. That\u0026rsquo;s a painting.\nThe difference between a painting and a game is time. Things need to update. And for that, you need an animation loop.\nWhy not setInterval If you\u0026rsquo;ve done any JS, your first instinct for \u0026ldquo;run this repeatedly\u0026rdquo; is probably setInterval. It works, technically. But it has problems.\nsetInterval fires at a fixed time interval regardless of what the browser is doing. If the browser is busy, frames pile up. If the tab is hidden, it still runs, wasting CPU. And it\u0026rsquo;s not tied to the display\u0026rsquo;s refresh rate, so you get choppy animation even on fast machines.\nrequestAnimationFrame fixes all of this:\nfires just before the browser paints the next frame, so it\u0026rsquo;s in sync with the display automatically pauses when the tab is hidden passes a timestamp to your callback so you can measure elapsed time runs at 60fps on most screens (or whatever the screen\u0026rsquo;s native refresh is) The pattern is simple:\nfunction loop(timestamp) { // update your game state // draw everything requestAnimationFrame(loop); // schedule the next frame } requestAnimationFrame(loop); // kick it off Notice: requestAnimationFrame(loop) inside loop itself. Each frame schedules the next one. To stop the animation you just don\u0026rsquo;t call it again.\nThe clear, update, draw cycle Every frame follows the same three steps, always in this order:\n1. Clear \u0026ndash; erase everything from the previous frame. 2. Update \u0026ndash; move things, check collisions, advance game state. 3. Draw \u0026ndash; render everything in its new position.\nIf you skip the clear step, you get ghost trails \u0026ndash; the old positions stay visible behind the moving object. Sometimes that\u0026rsquo;s an effect you want, but usually you want a clean slate each frame.\nfunction loop(timestamp) { // 1. clear ctx.fillStyle = \u0026#34;#000\u0026#34;; ctx.fillRect(0, 0, W, H); // 2. update (we\u0026#39;ll add real state here soon) x += vx; y += vy; // 3. draw ctx.beginPath(); ctx.arc(x, y, 20, 0, Math.PI * 2); ctx.fillStyle = \u0026#34;#86efac\u0026#34;; ctx.fill(); requestAnimationFrame(loop); } Try that with a ball and give it some velocity (vx = 2, vy = 1.5). You\u0026rsquo;ll see it move. Add some bounce logic when it hits the walls. That\u0026rsquo;s literally the foundation of every canvas game.\nDelta time \u0026ndash; making speed frame-rate independent Here\u0026rsquo;s a problem you\u0026rsquo;ll hit eventually: if you always move things by a fixed amount each frame, the game runs faster on a 120fps monitor than a 60fps one.\nThe fix is delta time. Instead of \u0026ldquo;move 2 pixels per frame\u0026rdquo;, you say \u0026ldquo;move 200 pixels per second\u0026rdquo;. Then each frame you multiply by how long that frame actually took.\nlet lastTime = 0; function loop(timestamp) { const dt = (timestamp - lastTime) / 1000; // seconds since last frame lastTime = timestamp; ctx.fillStyle = \u0026#34;#000\u0026#34;; ctx.fillRect(0, 0, W, H); x += speed * dt; // speed is in pixels per second now y += speed * dt; // draw... requestAnimationFrame(loop); } dt at 60fps is roughly 0.016 (16ms in seconds). So speed * dt at 200px/s gives you about 3.3 pixels per frame. Same on 120fps, just at smaller steps. The object moves at the same real-world speed regardless.\nFor Snake we actually don\u0026rsquo;t want continuous movement \u0026ndash; we want discrete steps on a grid. So we\u0026rsquo;ll handle it differently. But for Flappy Bird in Part 3, delta time matters a lot.\n// heads up: on the very first frame, dt can be huge because lastTime starts at 0. Guard against it: const dt = Math.min((timestamp - lastTime) / 1000, 0.1). The Math.min caps it at 100ms so a single bad frame doesn't teleport your objects. State vs render \u0026ndash; why this matters This sounds like architecture talk but it\u0026rsquo;s actually practical.\nThe idea is: keep your game state (positions, scores, directions, lives) completely separate from your drawing code. Your update logic changes state. Your draw functions read state and paint it. They never mix.\nBad pattern:\n// mixing state mutation with drawing ctx.fillRect(player.x++, player.y, 20, 20); // don\u0026#39;t do this Good pattern:\n// update function update() { player.x += player.vx; } // draw function draw() { ctx.fillRect(player.x, player.y, 20, 20); } function loop() { ctx.fillRect(0, 0, W, H); update(); draw(); requestAnimationFrame(loop); } Why does it matter? Because later when you add game over screens, pause menus, reset logic \u0026ndash; you\u0026rsquo;ll want to be able to stop updating without stopping drawing (e.g. a frozen pause screen). If your state and rendering are tangled together, that becomes painful.\nKeyboard input Canvas doesn\u0026rsquo;t handle focus or input natively. You listen on window or document:\nconst keys = {}; window.addEventListener(\u0026#34;keydown\u0026#34;, (e) =\u0026gt; { keys[e.key] = true; e.preventDefault(); // stops arrow keys from scrolling the page }); window.addEventListener(\u0026#34;keyup\u0026#34;, (e) =\u0026gt; { keys[e.key] = false; }); Then in your update function:\nfunction update() { if (keys[\u0026#34;ArrowLeft\u0026#34;]) player.x -= 3; if (keys[\u0026#34;ArrowRight\u0026#34;]) player.x += 3; if (keys[\u0026#34;ArrowUp\u0026#34;]) player.y -= 3; if (keys[\u0026#34;ArrowDown\u0026#34;]) player.y += 3; } The keys object approach handles multiple keys pressed at the same time naturally, which keydown events alone can\u0026rsquo;t do cleanly.\n// puzzle 02 =\u003e before the snake build Make a ball that bounces around the canvas. Start it somewhere in the middle with a velocity of vx = 3, vy = 2. When it hits a wall, reverse the relevant velocity component. Use requestAnimationFrame.\nOnce you have that working, add this: when you press Space, the ball changes to a random color. Use the keys pattern above but for one-off key presses (hint: keydown event, not the keys object).\nGenuinely try this. It's not a filler exercise. The bounce math you work out here is the same logic that handles ball physics in Breakout, Pong, and a dozen other games.\nProject: Snake // project 02 of 05 — snake (complete) Snake is perfect for learning canvas because it forces you to think about grid-based game state, collision detection and a game loop -- all the fundamentals. And it's genuinely fun to play when you're done.\nWe\u0026rsquo;ll build it in stages. Don\u0026rsquo;t skip ahead.\nStep 1: Grid system and constants Snake lives on a grid. Set that up first:\nconst canvas = document.getElementById(\u0026#34;c\u0026#34;); const ctx = canvas.getContext(\u0026#34;2d\u0026#34;); const COLS = 20; const ROWS = 20; const CELL = 28; // pixels per cell canvas.width = COLS * CELL; canvas.height = ROWS * CELL + 40; // extra 40 for score bar const W = canvas.width; const H = canvas.height; Working with a grid means every position is a {x, y} in grid coordinates (not pixels). You convert to pixels when drawing: gridX * CELL.\nStep 2: Game state let snake = [ { x: 10, y: 10 }, // head { x: 9, y: 10 }, { x: 8, y: 10 }, ]; let direction = { x: 1, y: 0 }; // moving right let nextDir = { x: 1, y: 0 }; // buffered input let food = { x: 15, y: 10 }; let score = 0; let gameOver = false; let moveTimer = 0; const MOVE_INTERVAL = 120; // ms between snake moves Two directions: direction (current) and nextDir (buffered). The reason is that if the snake is moving right and you press down then right really fast, you don\u0026rsquo;t want the snake to briefly go left. Buffering the input and applying it only once per move step prevents this.\nStep 3: Input window.addEventListener(\u0026#34;keydown\u0026#34;, (e) =\u0026gt; { switch (e.key) { case \u0026#34;ArrowUp\u0026#34;: if (direction.y !== 1) nextDir = { x: 0, y: -1 }; break; case \u0026#34;ArrowDown\u0026#34;: if (direction.y !== -1) nextDir = { x: 0, y: 1 }; break; case \u0026#34;ArrowLeft\u0026#34;: if (direction.x !== 1) nextDir = { x: -1, y: 0 }; break; case \u0026#34;ArrowRight\u0026#34;: if (direction.x !== -1) nextDir = { x: 1, y: 0 }; break; } e.preventDefault(); }); The checks like if (direction.y !== 1) prevent reversing directly into yourself. If you\u0026rsquo;re going up (y: -1), you can\u0026rsquo;t go down (y: 1) \u0026ndash; that\u0026rsquo;s the condition it checks.\nStep 4: The move function function moveSnake() { direction = nextDir; const head = snake[0]; const newHead = { x: head.x + direction.x, y: head.y + direction.y, }; // wall collision if ( newHead.x \u0026lt; 0 || newHead.x \u0026gt;= COLS || newHead.y \u0026lt; 0 || newHead.y \u0026gt;= ROWS ) { gameOver = true; return; } // self collision if (snake.some((seg) =\u0026gt; seg.x === newHead.x \u0026amp;\u0026amp; seg.y === newHead.y)) { gameOver = true; return; } snake.unshift(newHead); // add new head // did we eat food? if (newHead.x === food.x \u0026amp;\u0026amp; newHead.y === food.y) { score++; spawnFood(); // don\u0026#39;t pop the tail -- snake gets longer } else { snake.pop(); // remove tail to keep length constant } } The grow mechanic is clever: when you eat food, you skip the pop(). The tail just stays where it was, making the snake one segment longer. Simple but elegant.\nStep 5: Food spawning function spawnFood() { let pos; do { pos = { x: Math.floor(Math.random() * COLS), y: Math.floor(Math.random() * ROWS), }; } while (snake.some((seg) =\u0026gt; seg.x === pos.x \u0026amp;\u0026amp; seg.y === pos.y)); food = pos; } The do...while loop keeps picking random positions until it finds one that isn\u0026rsquo;t occupied by the snake. For a short snake this is basically instant, for a very long one it could get slow \u0026ndash; but that\u0026rsquo;s a problem for another day.\nStep 6: Drawing function draw() { // background ctx.fillStyle = \u0026#34;#0d110c\u0026#34;; ctx.fillRect(0, 0, W, H); // grid lines (subtle) ctx.strokeStyle = \u0026#34;rgba(134, 239, 172, 0.04)\u0026#34;; ctx.lineWidth = 0.5; for (let x = 0; x \u0026lt;= COLS; x++) { ctx.beginPath(); ctx.moveTo(x * CELL, 0); ctx.lineTo(x * CELL, ROWS * CELL); ctx.stroke(); } for (let y = 0; y \u0026lt;= ROWS; y++) { ctx.beginPath(); ctx.moveTo(0, y * CELL); ctx.lineTo(COLS * CELL, y * CELL); ctx.stroke(); } // food ctx.fillStyle = \u0026#34;#fca5a5\u0026#34;; ctx.beginPath(); ctx.arc( food.x * CELL + CELL / 2, food.y * CELL + CELL / 2, CELL / 2 - 4, 0, Math.PI * 2, ); ctx.fill(); // snake body snake.forEach((seg, i) =\u0026gt; { const alpha = 1 - (i / snake.length) * 0.6; // fade toward tail ctx.fillStyle = i === 0 ? \u0026#34;#86efac\u0026#34; // head is bright : `rgba(134, 239, 172, ${alpha})`; // body fades const padding = i === 0 ? 2 : 3; ctx.fillRect( seg.x * CELL + padding, seg.y * CELL + padding, CELL - padding * 2, CELL - padding * 2, ); }); // score bar ctx.fillStyle = \u0026#34;rgba(0,0,0,0.5)\u0026#34;; ctx.fillRect(0, ROWS * CELL, W, 40); ctx.fillStyle = \u0026#34;#86efac\u0026#34;; ctx.font = \u0026#34;14px JetBrains Mono\u0026#34;; ctx.textAlign = \u0026#34;left\u0026#34;; ctx.textBaseline = \u0026#34;middle\u0026#34;; ctx.fillText(`SCORE ${score}`, 12, ROWS * CELL + 20); ctx.textAlign = \u0026#34;right\u0026#34;; ctx.fillText(`LENGTH ${snake.length}`, W - 12, ROWS * CELL + 20); // game over overlay if (gameOver) { ctx.fillStyle = \u0026#34;rgba(0, 0, 0, 0.7)\u0026#34;; ctx.fillRect(0, 0, W, H); ctx.fillStyle = \u0026#34;#fca5a5\u0026#34;; ctx.font = \u0026#34;bold 36px Space Grotesk\u0026#34;; ctx.textAlign = \u0026#34;center\u0026#34;; ctx.textBaseline = \u0026#34;middle\u0026#34;; ctx.fillText(\u0026#34;GAME OVER\u0026#34;, W / 2, H / 2 - 24); ctx.fillStyle = \u0026#34;#788571\u0026#34;; ctx.font = \u0026#34;14px JetBrains Mono\u0026#34;; ctx.fillText(`score: ${score}`, W / 2, H / 2 + 16); ctx.fillText(\u0026#34;press R to restart\u0026#34;, W / 2, H / 2 + 40); } } Step 7: The game loop let lastTime = 0; function loop(timestamp) { const dt = timestamp - lastTime; lastTime = timestamp; if (!gameOver) { moveTimer += dt; if (moveTimer \u0026gt;= MOVE_INTERVAL) { moveTimer = 0; moveSnake(); } } draw(); requestAnimationFrame(loop); } Notice the move timer pattern. We\u0026rsquo;re not moving the snake every frame \u0026ndash; that would be way too fast. Instead we accumulate time and only move when enough has passed. Change MOVE_INTERVAL to make the game faster or slower.\nStep 8: Restart function reset() { snake = [ { x: 10, y: 10 }, { x: 9, y: 10 }, { x: 8, y: 10 }, ]; direction = { x: 1, y: 0 }; nextDir = { x: 1, y: 0 }; score = 0; gameOver = false; moveTimer = 0; spawnFood(); } window.addEventListener(\u0026#34;keydown\u0026#34;, (e) =\u0026gt; { if (e.key === \u0026#34;r\u0026#34; || e.key === \u0026#34;R\u0026#34;) reset(); // ... rest of keydown handler }); // Start spawnFood(); requestAnimationFrame(loop); That\u0026rsquo;s the whole game. Not a library call in sight.\nIdeas to explore yourself The game works, but it\u0026rsquo;s pretty barebones. Some things to add on your own before Part 3:\nSpeed it up as the snake gets longer (decrease MOVE_INTERVAL with score) Add a special food that flashes and disappears after a few seconds, worth more points Wrap-around walls instead of death (snake comes out the other side) High score stored in localStorage A proper start screen before the game begins These aren\u0026rsquo;t hard, and figuring them out yourself is the whole point.\n// checkpoint -- part 02 I understand why requestAnimationFrame is better than setInterval I can implement the clear, update, draw cycle I understand delta time and why frame-rate independence matters I can handle keyboard input cleanly with the keys object pattern I understand the state vs render separation and why it helps I built a playable Snake game from scratch I tried at least one of the extension ideas on my own // up next — Part 03: Gravity, Input and the Feel of Things\nSnake was grid-based and discrete. In Part 3 we go continuous -- real velocity, real gravity, real physics feel. We'll build Flappy Bird, which means learning how to tune numbers until something feels right. It's a skill game developers actually spend a lot of time on.\n","permalink":"/canvas/canvas-02-animation-snake/","summary":"\u003c!--\n  NOTE FOR HUGO SETUP:\n  This post uses inline HTML. Make sure your hugo.yaml has:\n\n  markup:\n    goldmark:\n      renderer:\n        unsafe: true\n--\u003e\n\u003cstyle\u003e\n.cv-post {\n  --cv-green:   #86efac;\n  --cv-cyan:    #5eead4;\n  --cv-amber:   #fbbf24;\n  --cv-red:     #fca5a5;\n  --cv-border:  rgba(134, 239, 172, 0.15);\n}\n.cv-puzzle {\n  background: rgba(251, 191, 36, 0.04);\n  border: 1px solid rgba(251, 191, 36, 0.2);\n  border-left: 3px solid #fbbf24;\n  border-radius: 6px;\n  padding: 20px 24px;\n  margin: 32px 0;\n}\n.cv-puzzle-label {\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 0.68rem;\n  letter-spacing: 0.14em;\n  text-transform: uppercase;\n  color: #fbbf24;\n  margin-bottom: 12px;\n}\n.cv-puzzle p, .cv-puzzle li { color: #c4a855; font-size: 0.94rem; }\n.cv-puzzle strong { color: #fbbf24; }\n.cv-puzzle code {\n  background: rgba(251, 191, 36, 0.08);\n  border: 1px solid rgba(251, 191, 36, 0.2);\n  padding: 1px 6px; border-radius: 3px; font-size: 0.85em;\n}\n.cv-checkpoint {\n  background: rgba(134, 239, 172, 0.03);\n  border: 1px solid rgba(134, 239, 172, 0.18);\n  border-left: 3px solid #86efac;\n  border-radius: 6px;\n  padding: 20px 24px;\n  margin: 32px 0;\n}\n.cv-cp-label {\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 0.68rem;\n  letter-spacing: 0.14em;\n  text-transform: uppercase;\n  color: #86efac;\n  margin-bottom: 14px;\n}\n.cv-checkpoint ul { list-style: none; padding: 0; margin: 0; }\n.cv-checkpoint ul li {\n  display: flex; align-items: flex-start; gap: 10px;\n  font-size: 0.9rem; color: #8aad8e; margin-bottom: 8px; cursor: pointer;\n}\n.cv-cb {\n  width: 15px; height: 15px;\n  border: 1px solid #2a4a2e; border-radius: 2px; flex-shrink: 0;\n  margin-top: 2px; background: #0d110c;\n  display: flex; align-items: center; justify-content: center;\n  transition: all 0.15s; font-size: 9px; font-weight: bold; color: transparent;\n}\n.cv-cb.done { background: #86efac; border-color: #86efac; color: #0d110c; }\n.cv-note {\n  background: rgba(94, 234, 212, 0.04);\n  border: 1px solid rgba(94, 234, 212, 0.18);\n  border-left: 3px solid #5eead4;\n  border-radius: 6px;\n  padding: 16px 22px; margin: 24px 0;\n  font-size: 0.93rem; color: #78b8b0;\n}\n.cv-note strong { color: #5eead4; }\n.cv-note code {\n  background: rgba(94, 234, 212, 0.08);\n  padding: 1px 6px; border-radius: 3px; font-size: 0.85em;\n}\n.cv-project {\n  background: rgba(134, 239, 172, 0.02);\n  border: 1px solid rgba(134, 239, 172, 0.12);\n  border-radius: 8px; padding: 24px 28px; margin: 36px 0;\n}\n.cv-project-header {\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 0.7rem; letter-spacing: 0.16em; text-transform: uppercase;\n  color: #86efac; margin-bottom: 16px; padding-bottom: 12px;\n  border-bottom: 1px solid rgba(134, 239, 172, 0.1);\n}\n.cv-project p { color: #9ab89e; font-size: 0.94rem; }\n.cv-next {\n  background: rgba(18, 22, 16, 0.7);\n  border: 1px solid rgba(134, 239, 172, 0.12);\n  border-radius: 8px; padding: 22px 26px;\n  margin: 40px 0 0 0; text-align: center;\n}\n.cv-next p { color: #788571; font-size: 0.9rem; margin: 0; }\n.cv-next strong { color: #86efac; }\n\u003c/style\u003e\n\u003cdiv class=\"cv-post\"\u003e\n\u003cp\u003eLast time we ended with a static Pac-Man frame. Looked fine. Did nothing. Every time you refreshed the page it was the same, frozen moment.\u003c/p\u003e","title":"Canvas 02 : Making Things Move"},{"content":" Blog Summary: Moves from JS fundamentals to building real backend servers. Covers the Node.js runtime, the HTTP protocol, creating servers with Express, testing with Postman, and version control with Git.\n1. JavaScript Runtimes [SOURCE — COURSE MATERIAL]\nECMAScript — The Spec ECMAScript defines the core language: var, const, let, function, Date, setTimeout, etc. It\u0026rsquo;s the specification, not the implementation.\nBrowser JS = ECMAScript + Browser APIs Browser JS: ├── ECMAScript (core language) ├── setTimeout / setInterval ├── fetch (HTTP requests) ├── document (DOM access) └── localStorage Node.js = ECMAScript + Backend APIs Node.js: ├── ECMAScript (core language) ├── setTimeout ├── fs (file system) ├── http (create HTTP servers) └── path, crypto, os... How Node.js Was Born [ADDED — IMPORTANT BACKGROUND]\nChrome\u0026#39;s V8 engine (C++) → extracted → Node.js runtime added (compiles JS → machine code) (fs, http, etc.) Ryan Dahl took V8 and wrapped it with C++ to add OS-level capabilities. Node became the way to run JS on servers.\nBun — The Newer Alternative Written in Zig, significantly faster than Node. Drop-in replacement for most use cases. The course focuses on Node.js.\n2. What Can Node.js Do? [SOURCE — COURSE MATERIAL]\nCreate CLI tools Video players, games HTTP Servers ← our focus 3. HTTP Protocol [SOURCE — COURSE MATERIAL]\nWhat Is HTTP? HyperText Transfer Protocol — the standard way browsers (clients) talk to backends (servers).\nBrowser/Client Server (Node.js/Express) │ │ │ ─── HTTP Request ──────────────→ │ │ (method, URL, headers, body) │ │ │ │ ←── HTTP Response ────────────── │ │ (status code, headers, body) │ Think of HTTP as a Function Call Function Concept HTTP Equivalent Function name URL / Route Arguments Request body / query params / headers Function body Server-side logic Return value Response body What Happens When You Visit a URL Browser parses the URL DNS lookup — converts google.com → IP address (like contacts → phone number) TCP/TLS handshake (establish connection) HTTP request sent Server processes \u0026amp; responds Browser renders response 4. HTTP Methods [SOURCE — COURSE MATERIAL]\nGET — Retrieve data (read-only, no body) POST — Create new resource (has body) PUT — Replace a resource entirely DELETE — Remove a resource PATCH — Partial update (not covered but common) Medical analogy:\nGET → Doctor consultation (check up) POST → Insert a new kidney PUT → Replace an existing kidney DELETE → Remove a kidney 5. HTTP Status Codes [SOURCE — COURSE MATERIAL]\n200 — OK, success 201 — Created (good response to POST) 400 — Bad Request (client sent wrong data) 401 — Unauthorized (not logged in) 403 — Forbidden (logged in but no permission) 404 — Not Found (route doesn\u0026#39;t exist) 411 — Length Required (missing input) 500 — Internal Server Error (your backend crashed) 6. Creating an HTTP Server with Express [SOURCE — COURSE MATERIAL]\nnpm init -y npm install express // index.js — minimal Express server const express = require(\u0026#34;express\u0026#34;); const app = express(); // Middleware to parse JSON bodies app.use(express.json()); // GET route app.get(\u0026#34;/\u0026#34;, (req, res) =\u0026gt; { res.json({ message: \u0026#34;Hello World\u0026#34; }); }); // POST route with body app.post(\u0026#34;/user\u0026#34;, (req, res) =\u0026gt; { const { name, age } = req.body; res.json({ created: true, name, age }); }); // Query params: GET /sum?a=5\u0026amp;b=3 app.get(\u0026#34;/sum\u0026#34;, (req, res) =\u0026gt; { const a = parseInt(req.query.a); const b = parseInt(req.query.b); res.json({ result: a + b }); }); // Route params: GET /user/123 app.get(\u0026#34;/user/:id\u0026#34;, (req, res) =\u0026gt; { const id = req.params.id; res.json({ userId: id }); }); app.listen(3000, () =\u0026gt; { console.log(\u0026#34;Server running on port 3000\u0026#34;); }); 7. The Kidney Hospital — Building a Real CRUD API [SOURCE — COURSE MATERIAL]\nThis in-memory CRUD example is the capstone of HTTP basics:\nconst express = require(\u0026#34;express\u0026#34;); const app = express(); app.use(express.json()); // In-memory \u0026#34;database\u0026#34; const users = [ { name: \u0026#34;John\u0026#34;, kidneys: [{ healthy: false }, { healthy: true }], }, ]; // GET — how many kidneys, which are healthy app.get(\u0026#34;/kidney\u0026#34;, (req, res) =\u0026gt; { const user = users[0]; const total = user.kidneys.length; const healthy = user.kidneys.filter((k) =\u0026gt; k.healthy).length; res.json({ total, healthy, unhealthy: total - healthy }); }); // POST — add a new kidney app.post(\u0026#34;/kidney\u0026#34;, (req, res) =\u0026gt; { const { healthy } = req.body; users[0].kidneys.push({ healthy }); res.json({ message: \u0026#34;Kidney added\u0026#34; }); }); // PUT — make all unhealthy kidneys healthy app.put(\u0026#34;/kidney\u0026#34;, (req, res) =\u0026gt; { const user = users[0]; const hasUnhealthy = user.kidneys.some((k) =\u0026gt; !k.healthy); if (!hasUnhealthy) { return res.status(411).json({ message: \u0026#34;All kidneys already healthy\u0026#34; }); } user.kidneys = user.kidneys.map((k) =\u0026gt; ({ healthy: true })); res.json({ message: \u0026#34;All kidneys are now healthy\u0026#34; }); }); // DELETE — remove all unhealthy kidneys app.delete(\u0026#34;/kidney\u0026#34;, (req, res) =\u0026gt; { const user = users[0]; const hasUnhealthy = user.kidneys.some((k) =\u0026gt; !k.healthy); if (!hasUnhealthy) { return res.status(411).json({ message: \u0026#34;No unhealthy kidneys to remove\u0026#34; }); } user.kidneys = user.kidneys.filter((k) =\u0026gt; k.healthy); res.json({ message: \u0026#34;Unhealthy kidneys removed\u0026#34; }); }); app.listen(3000); 8. Testing with Postman [SOURCE — COURSE MATERIAL]\nPostman is a GUI tool to send HTTP requests without a browser.\nWhy needed: Browsers can only send GET requests via the address bar. To test POST, PUT, DELETE, use Postman (or curl).\n# curl equivalent curl -X POST http://localhost:3000/kidney \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{\u0026#34;healthy\u0026#34;: true}\u0026#39; 9. Git — Version Control [SOURCE — COURSE MATERIAL]\nWhat Is Git? A distributed version control system that tracks changes in files, allows collaboration, and enables reverting to previous states.\nGit vs GitHub Git GitHub Local tool, installed on your machine Cloud hosting for git repos Tracks changes Provides web interface + collaboration Free, open source Free tier + paid plans Works without GitHub Requires Git Core Concepts Working Directory → Staging Area → Local Repository → Remote (GitHub) (edit files) (git add) (git commit) (git push) Blob: Binary file content (content-addressed by SHA1 hash) Tree: Represents a directory (holds blobs and sub-trees) Commit: Snapshot of the repo. Has pointer to parent commit → forms a linked list Essential Git Commands # Setup git init # initialize new repo git clone \u0026lt;url\u0026gt; # copy remote repo locally # Daily workflow git status # see what\u0026#39;s changed git add . # stage all changes git add file.js # stage specific file git commit -m \u0026#34;feat: add login\u0026#34; # save snapshot git push origin main # upload to GitHub git pull origin main # download latest changes # Branching git branch feature/login # create branch git checkout feature/login # switch to branch git checkout -b feature/login # create + switch (shortcut) git merge feature/login # merge into current branch # History git log --oneline # compact commit history git diff # see unstaged changes Branching Model main: C1 ─── C2 ─── C3 ─── C4 ─── [Merge] \\ / feature: C1 ─── C2 ────────── Merge Conflicts Occur when two branches edit the same line differently.\n\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt; HEAD (your branch) const x = 5; ======= const x = 10; \u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; feature/login (incoming) Resolution: Manually pick one version (or combine), delete the markers, then git add + git commit.\ngit log --merge # see conflicting commits git status # see unmerged files Exercises Quick (10–15 min) Create an Express server with a GET /greet?name=Alice route that returns { message: \u0026quot;Hello, Alice!\u0026quot; }.\nHint 1: req.query.name\nHint 2: Use template literal `Hello, ${name}!`\nIntermediate (30–60 min) Build a simple todo list API in memory:\nGET /todos — list all todos POST /todos — add a new todo { title, completed: false } PUT /todos/:id — mark a todo as completed DELETE /todos/:id — delete a todo Expected behavior: All operations work correctly. Deleting a non-existent todo returns 404.\nChallenge (2–4 hours) Build a student grade tracker API:\nAdd students with name + grades array Get average grade for a student Get the top 3 students by average A route that returns students who are failing (average \u0026lt; 40) Common Confusions Confusion Reality \u0026ldquo;GET can\u0026rsquo;t have a body\u0026rdquo; Technically allowed but not conventional — use query params \u0026ldquo;git add . saves my work\u0026rdquo; No. git commit saves. add only stages. \u0026ldquo;git push is like saving\u0026rdquo; Push sends to remote. Always commit before push. \u0026ldquo;Status code doesn\u0026rsquo;t matter, just return data\u0026rdquo; Standards exist so clients can handle responses correctly Key Takeaways Node.js = V8 + backend APIs (fs, http) — not a language, a runtime HTTP is a request-response protocol: client sends request, server responds Express makes creating routes simple: app.get/post/put/delete(path, handler) Always validate inputs and return meaningful status codes Git tracks history; GitHub hosts it. Branch → develop → PR → merge ","permalink":"/posts/02-nodejs/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eBlog Summary:\u003c/strong\u003e Moves from JS fundamentals to building real backend servers. Covers the Node.js runtime, the HTTP protocol, creating servers with Express, testing with Postman, and version control with Git.\u003c/p\u003e\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-javascript-runtimes\"\u003e1. JavaScript Runtimes\u003c/h2\u003e\n\u003cp\u003e[SOURCE — COURSE MATERIAL]\u003c/p\u003e\n\u003ch3 id=\"ecmascript--the-spec\"\u003eECMAScript — The Spec\u003c/h3\u003e\n\u003cp\u003eECMAScript defines the core language: \u003ccode\u003evar\u003c/code\u003e, \u003ccode\u003econst\u003c/code\u003e, \u003ccode\u003elet\u003c/code\u003e, \u003ccode\u003efunction\u003c/code\u003e, \u003ccode\u003eDate\u003c/code\u003e, \u003ccode\u003esetTimeout\u003c/code\u003e, etc. It\u0026rsquo;s the specification, not the implementation.\u003c/p\u003e\n\u003ch3 id=\"browser-js--ecmascript--browser-apis\"\u003eBrowser JS = ECMAScript + Browser APIs\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eBrowser JS:\n  ├── ECMAScript (core language)\n  ├── setTimeout / setInterval\n  ├── fetch (HTTP requests)\n  ├── document (DOM access)\n  └── localStorage\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"nodejs--ecmascript--backend-apis\"\u003eNode.js = ECMAScript + Backend APIs\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eNode.js:\n  ├── ECMAScript (core language)\n  ├── setTimeout\n  ├── fs (file system)\n  ├── http (create HTTP servers)\n  └── path, crypto, os...\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"how-nodejs-was-born\"\u003eHow Node.js Was Born\u003c/h3\u003e\n\u003cp\u003e[ADDED — IMPORTANT BACKGROUND]\u003c/p\u003e","title":"02 : Node.js, HTTP Servers, Express \u0026 Git"},{"content":" Snake moved in discrete steps on a grid. Clean, predictable, easy to reason about. What we\u0026rsquo;re building now is different. Flappy Bird is continuous \u0026ndash; the bird is constantly falling, velocity accumulates, and the pipes come at you from the right with no fixed grid to think in.\nThis is where things start to feel like a real game.\nVelocity and acceleration In Part 2 we briefly mentioned that objects have a position and a velocity. Let\u0026rsquo;s be more precise about what that means in code.\nPosition is where the object is right now. Velocity is how fast position changes per frame. Acceleration is how fast velocity changes per frame.\n// the three variables that describe any moving object let y = 200; // current position let vy = 0; // velocity (pixels per second, or per frame) let ay = 0.5; // acceleration (gravity pulling down) // each frame: vy += ay; // gravity pulls velocity downward y += vy; // velocity moves position That\u0026rsquo;s it. That\u0026rsquo;s gravity in three lines. Every frame, vy gets a little more positive (more downward), so the bird falls faster and faster. When you press jump, you just set vy to a negative number \u0026ndash; upward velocity that gravity then decelerates back to zero.\nRun this in your head for a few frames:\nFrame vy before ay added vy after y change 1 0 +0.5 0.5 +0.5 2 0.5 +0.5 1.0 +1.0 3 1.0 +0.5 1.5 +1.5 jump 1.5 reset -8 -8 after -8 +0.5 -7.5 -7.5 That acceleration-to-zero-to-positive arc is what gives jump mechanics their feel. Flappy Bird\u0026rsquo;s jump is abrupt and short. Mario\u0026rsquo;s is floatier. The numbers are different but the math is the same.\nGame states Our Snake game had a simple gameOver boolean. For Flappy Bird we need something slightly more structured. The game has three distinct states:\nIDLE: the start screen, bird bobs gently, waiting for input PLAYING: the game is running, pipes come, physics active DEAD: the bird hit something, brief freeze, then show score and restart prompt Using a string or an enum-style object keeps things readable:\nconst STATE = { IDLE: \u0026#34;idle\u0026#34;, PLAYING: \u0026#34;playing\u0026#34;, DEAD: \u0026#34;dead\u0026#34;, }; let gameState = STATE.IDLE; Then in your loop:\nfunction update(dt) { if (gameState === STATE.PLAYING) { updateBird(dt); updatePipes(dt); checkCollisions(); } if (gameState === STATE.IDLE) { updateIdleAnimation(dt); } } This is much cleaner than a pile of booleans. And it scales \u0026ndash; if you add a pause screen or a countdown, it\u0026rsquo;s just another state.\nProcedural pipe generation Flappy Bird\u0026rsquo;s pipes are infinite and random. You don\u0026rsquo;t pre-place them \u0026ndash; you generate them as the game scrolls.\nThe approach: keep a list of pipes, add a new one every N pixels of scroll, and remove pipes that have gone off the left edge.\nconst pipes = []; const PIPE_GAP = 140; // vertical space between top and bottom pipe const PIPE_WIDTH = 52; const PIPE_SPEED = 180; // pixels per second const PIPE_SPAWN = 270; // horizontal distance between pipes let distanceSinceLastPipe = 0; Each pipe is an object with just x and gapY (the vertical center of the gap):\nfunction spawnPipe() { const gapY = 80 + Math.random() * (H - 200); // random gap center pipes.push({ x: W, gapY }); } function updatePipes(dt) { distanceSinceLastPipe += PIPE_SPEED * dt; if (distanceSinceLastPipe \u0026gt;= PIPE_SPAWN) { spawnPipe(); distanceSinceLastPipe = 0; } pipes.forEach((pipe) =\u0026gt; (pipe.x -= PIPE_SPEED * dt)); // remove pipes that went off screen while (pipes.length \u0026amp;\u0026amp; pipes[0].x \u0026lt; -PIPE_WIDTH) { pipes.shift(); score++; } } Removing from the front with shift() works because pipes always exit the screen in the order they were added.\n// puzzle 03 =\u003e before collision detection Pause here and think through this: how would you detect if the bird (a small rectangle) has hit a pipe?\nEach pipe has an x position and a gapY center. The gap has height PIPE_GAP. The top pipe goes from y=0 down to gapY - PIPE_GAP/2. The bottom pipe goes from gapY + PIPE_GAP/2 down to the bottom of the screen.\nThe bird has an x, y and a radius. Write the collision condition as a boolean expression before reading on.\nThis is called AABB collision (axis-aligned bounding box) and it's used everywhere. Worth figuring out yourself.\nCollision detection The basic idea for rectangle vs rectangle: two rectangles are overlapping if and only if they overlap on both the X axis and the Y axis simultaneously.\nFor the bird (circle approximated as a small box) vs a pipe:\nfunction checkCollisions() { // floor and ceiling if (bird.y - bird.r \u0026lt; 0 || bird.y + bird.r \u0026gt; floorY) { die(); return; } pipes.forEach((pipe) =\u0026gt; { const pipeLeft = pipe.x; const pipeRight = pipe.x + PIPE_WIDTH; const gapTop = pipe.gapY - PIPE_GAP / 2; const gapBottom = pipe.gapY + PIPE_GAP / 2; // is the bird horizontally overlapping with this pipe? const horizontalOverlap = bird.x + bird.r \u0026gt; pipeLeft \u0026amp;\u0026amp; bird.x - bird.r \u0026lt; pipeRight; if (!horizontalOverlap) return; // if horizontally overlapping, check if bird is outside the gap const hitTopPipe = bird.y - bird.r \u0026lt; gapTop; const hitBottomPipe = bird.y + bird.r \u0026gt; gapBottom; if (hitTopPipe || hitBottomPipe) { die(); } }); } function die() { gameState = STATE.DEAD; // add a little screen shake here later } Building Flappy Bird // project 03 of 05 — flappy bird (complete) Everything above snaps together now. Set up the constants, build the state machine, draw the pipes and bird, handle the one input. Let's go.\nFull setup const canvas = document.getElementById(\u0026#34;c\u0026#34;); const ctx = canvas.getContext(\u0026#34;2d\u0026#34;); const W = 360; const H = 640; canvas.width = W; canvas.height = H; const STATE = { IDLE: \u0026#34;idle\u0026#34;, PLAYING: \u0026#34;playing\u0026#34;, DEAD: \u0026#34;dead\u0026#34; }; let gameState = STATE.IDLE; const GRAVITY = 1400; // px/s^2 const JUMP_VEL = -420; // px/s (negative = up) const PIPE_SPEED = 180; const PIPE_GAP = 150; const PIPE_WIDTH = 54; const PIPE_SPAWN = 280; const FLOOR_Y = H - 80; const bird = { x: 80, y: H / 2, vy: 0, r: 14 }; const pipes = []; let score = 0; let bestScore = parseInt(localStorage.getItem(\u0026#34;flappy_best\u0026#34;) || \u0026#34;0\u0026#34;); let distanceSinceLastPipe = PIPE_SPAWN; let idleTime = 0; Using real units (px/s, px/s^2) with delta time gives consistent behavior across frame rates.\nDrawing functions function drawBackground() { // sky gradient const sky = ctx.createLinearGradient(0, 0, 0, FLOOR_Y); sky.addColorStop(0, \u0026#34;#0a1628\u0026#34;); sky.addColorStop(1, \u0026#34;#1a3a5c\u0026#34;); ctx.fillStyle = sky; ctx.fillRect(0, 0, W, FLOOR_Y); // ground ctx.fillStyle = \u0026#34;#2d5a1b\u0026#34;; ctx.fillRect(0, FLOOR_Y, W, H - FLOOR_Y); ctx.fillStyle = \u0026#34;#3d7a25\u0026#34;; ctx.fillRect(0, FLOOR_Y, W, 8); } function drawPipes() { pipes.forEach((pipe) =\u0026gt; { const gapTop = pipe.gapY - PIPE_GAP / 2; const gapBottom = pipe.gapY + PIPE_GAP / 2; // pipe color with a lighter edge ctx.fillStyle = \u0026#34;#4a9e2f\u0026#34;; // top pipe body ctx.fillRect(pipe.x, 0, PIPE_WIDTH, gapTop); // top pipe cap ctx.fillStyle = \u0026#34;#5cb83a\u0026#34;; ctx.fillRect(pipe.x - 4, gapTop - 20, PIPE_WIDTH + 8, 20); // bottom pipe ctx.fillStyle = \u0026#34;#4a9e2f\u0026#34;; ctx.fillRect(pipe.x, gapBottom, PIPE_WIDTH, FLOOR_Y - gapBottom); ctx.fillStyle = \u0026#34;#5cb83a\u0026#34;; ctx.fillRect(pipe.x - 4, gapBottom, PIPE_WIDTH + 8, 20); }); } function drawBird() { const rotation = Math.max(-0.5, Math.min(bird.vy / 600, 1.2)); ctx.save(); ctx.translate(bird.x, bird.y); ctx.rotate(rotation); // body ctx.beginPath(); ctx.arc(0, 0, bird.r, 0, Math.PI * 2); ctx.fillStyle = \u0026#34;#fbbf24\u0026#34;; ctx.fill(); ctx.strokeStyle = \u0026#34;#d97706\u0026#34;; ctx.lineWidth = 2; ctx.stroke(); // eye ctx.beginPath(); ctx.arc(6, -4, 4, 0, Math.PI * 2); ctx.fillStyle = \u0026#34;#fff\u0026#34;; ctx.fill(); ctx.beginPath(); ctx.arc(7, -4, 2, 0, Math.PI * 2); ctx.fillStyle = \u0026#34;#1a1a1a\u0026#34;; ctx.fill(); // beak ctx.beginPath(); ctx.moveTo(10, 1); ctx.lineTo(18, 0); ctx.lineTo(10, 5); ctx.closePath(); ctx.fillStyle = \u0026#34;#f97316\u0026#34;; ctx.fill(); ctx.restore(); } function drawHUD() { if (gameState === STATE.PLAYING || gameState === STATE.DEAD) { ctx.fillStyle = \u0026#34;#fff\u0026#34;; ctx.font = \u0026#34;bold 42px Space Grotesk\u0026#34;; ctx.textAlign = \u0026#34;center\u0026#34;; ctx.textBaseline = \u0026#34;top\u0026#34;; ctx.fillText(score, W / 2, 30); } if (gameState === STATE.IDLE) { ctx.fillStyle = \u0026#34;rgba(0,0,0,0.5)\u0026#34;; ctx.fillRect(0, 0, W, H); ctx.fillStyle = \u0026#34;#fbbf24\u0026#34;; ctx.font = \u0026#34;bold 36px Space Grotesk\u0026#34;; ctx.textAlign = \u0026#34;center\u0026#34;; ctx.textBaseline = \u0026#34;middle\u0026#34;; ctx.fillText(\u0026#34;FLAPPY BIRD\u0026#34;, W / 2, H / 2 - 50); ctx.fillStyle = \u0026#34;#c8d1c1\u0026#34;; ctx.font = \u0026#34;14px JetBrains Mono\u0026#34;; ctx.fillText(\u0026#34;tap or press space to start\u0026#34;, W / 2, H / 2); ctx.fillStyle = \u0026#34;#788571\u0026#34;; ctx.font = \u0026#34;12px JetBrains Mono\u0026#34;; ctx.fillText(`best: ${bestScore}`, W / 2, H / 2 + 30); } if (gameState === STATE.DEAD) { ctx.fillStyle = \u0026#34;rgba(0,0,0,0.6)\u0026#34;; ctx.fillRect(0, 0, W, H); ctx.fillStyle = \u0026#34;#fca5a5\u0026#34;; ctx.font = \u0026#34;bold 32px Space Grotesk\u0026#34;; ctx.textAlign = \u0026#34;center\u0026#34;; ctx.textBaseline = \u0026#34;middle\u0026#34;; ctx.fillText(\u0026#34;OUCH\u0026#34;, W / 2, H / 2 - 60); ctx.fillStyle = \u0026#34;#c8d1c1\u0026#34;; ctx.font = \u0026#34;16px JetBrains Mono\u0026#34;; ctx.fillText(`score: ${score}`, W / 2, H / 2 - 20); ctx.fillText(`best: ${bestScore}`, W / 2, H / 2 + 10); ctx.fillStyle = \u0026#34;#788571\u0026#34;; ctx.font = \u0026#34;13px JetBrains Mono\u0026#34;; ctx.fillText(\u0026#34;tap or space to restart\u0026#34;, W / 2, H / 2 + 50); } } The ctx.save() and ctx.restore() around the bird drawing is important \u0026ndash; ctx.rotate() affects everything drawn after it, on the whole canvas. save() snapshots the current transform state and restore() brings it back. Without them, every draw call after the bird would be rotated too.\nUpdate functions function updateBird(dt) { bird.vy += GRAVITY * dt; bird.y += bird.vy * dt; } function updatePipes(dt) { distanceSinceLastPipe += PIPE_SPEED * dt; if (distanceSinceLastPipe \u0026gt;= PIPE_SPAWN) { pipes.push({ x: W, gapY: 120 + Math.random() * (FLOOR_Y - 240) }); distanceSinceLastPipe = 0; } pipes.forEach((p) =\u0026gt; (p.x -= PIPE_SPEED * dt)); while (pipes.length \u0026amp;\u0026amp; pipes[0].x \u0026lt; -PIPE_WIDTH) { pipes.shift(); score++; if (score \u0026gt; bestScore) { bestScore = score; localStorage.setItem(\u0026#34;flappy_best\u0026#34;, bestScore); } } } function checkCollisions() { if (bird.y + bird.r \u0026gt;= FLOOR_Y || bird.y - bird.r \u0026lt;= 0) { die(); return; } pipes.forEach((p) =\u0026gt; { const hor = bird.x + bird.r \u0026gt; p.x \u0026amp;\u0026amp; bird.x - bird.r \u0026lt; p.x + PIPE_WIDTH; if (!hor) return; if ( bird.y - bird.r \u0026lt; p.gapY - PIPE_GAP / 2 || bird.y + bird.r \u0026gt; p.gapY + PIPE_GAP / 2 ) die(); }); } function die() { if (gameState !== STATE.PLAYING) return; gameState = STATE.DEAD; } function jump() { if (gameState === STATE.IDLE) { gameState = STATE.PLAYING; bird.vy = JUMP_VEL; return; } if (gameState === STATE.DEAD) { resetGame(); return; } bird.vy = JUMP_VEL; } function resetGame() { bird.y = H / 2; bird.vy = 0; pipes.length = 0; score = 0; distanceSinceLastPipe = PIPE_SPAWN; gameState = STATE.PLAYING; bird.vy = JUMP_VEL; } The loop and input let lastTime = 0; function loop(timestamp) { const dt = Math.min((timestamp - lastTime) / 1000, 0.05); lastTime = timestamp; if (gameState === STATE.PLAYING) { updateBird(dt); updatePipes(dt); checkCollisions(); } if (gameState === STATE.IDLE) { // gentle hovering idle animation idleTime += dt; bird.y = H / 2 + Math.sin(idleTime * 2.5) * 8; } drawBackground(); drawPipes(); drawBird(); drawHUD(); requestAnimationFrame(loop); } window.addEventListener(\u0026#34;keydown\u0026#34;, (e) =\u0026gt; { if (e.key === \u0026#34; \u0026#34; || e.key === \u0026#34;ArrowUp\u0026#34;) { e.preventDefault(); jump(); } }); canvas.addEventListener(\u0026#34;click\u0026#34;, jump); canvas.addEventListener( \u0026#34;touchstart\u0026#34;, (e) =\u0026gt; { e.preventDefault(); jump(); }, { passive: false }, ); requestAnimationFrame(loop); Tuning the feel Here\u0026rsquo;s the thing about Flappy Bird: it\u0026rsquo;s famously brutal. The original game has very specific numbers that make it feel that particular way. Your numbers will probably feel slightly different \u0026ndash; and that\u0026rsquo;s fine, they\u0026rsquo;re yours.\nSome things to mess with:\nGRAVITY lower (like 900) makes it floatier and forgiving JUMP_VEL more negative (like -500) makes jumps more powerful PIPE_GAP larger (like 200) makes it way easier PIPE_SPEED higher makes it more intense Spend 10 minutes changing just these numbers. You\u0026rsquo;ll get a feel for how each one changes the game. That\u0026rsquo;s basically what game feel tuning is.\n// on ctx.save / ctx.restore: these save and restore the entire canvas state -- transform, fillStyle, strokeStyle, lineWidth, everything. Use them whenever you need to apply a transform (translate, rotate, scale) for just one drawing operation. Always pair them -- an unmatched save() is a slow memory leak. // checkpoint -- part 03 I understand velocity and acceleration (position += velocity, velocity += acceleration) I can implement a game state machine with more than 2 states I can generate infinite procedural obstacles I can implement AABB collision detection I know how to use ctx.save() and ctx.restore() for isolated transforms I built a playable Flappy Bird and tuned the feel myself // up next — Part 04: Pixels Are Just Numbers\nWe've been drawing shapes and managing movement. In Part 4 we go deeper into what canvas actually stores -- raw pixel data. We'll cover drawImage, sprite sheets, and getImageData which lets you read and modify individual pixels. The project is the full Pac-Man game with actual ghost AI, a tile map and proper collision.\n","permalink":"/canvas/canvas-03-physics-flappybird/","summary":"\u003c!--\n  NOTE FOR HUGO SETUP:\n  This post uses inline HTML. Make sure unsafe rendering is enabled in hugo.yaml.\n--\u003e\n\u003cstyle\u003e\n.cv-post {\n  --cv-green: #86efac; --cv-cyan: #5eead4;\n  --cv-amber: #fbbf24; --cv-red:  #fca5a5;\n}\n.cv-puzzle {\n  background: rgba(251,191,36,.04); border: 1px solid rgba(251,191,36,.2);\n  border-left: 3px solid #fbbf24; border-radius: 6px; padding: 20px 24px; margin: 32px 0;\n}\n.cv-puzzle-label {\n  font-family: 'JetBrains Mono',monospace; font-size: .68rem;\n  letter-spacing: .14em; text-transform: uppercase; color: #fbbf24; margin-bottom: 12px;\n}\n.cv-puzzle p, .cv-puzzle li { color: #c4a855; font-size: .94rem; }\n.cv-puzzle strong { color: #fbbf24; }\n.cv-puzzle code { background: rgba(251,191,36,.08); border: 1px solid rgba(251,191,36,.2); padding: 1px 6px; border-radius: 3px; font-size: .85em; }\n.cv-checkpoint {\n  background: rgba(134,239,172,.03); border: 1px solid rgba(134,239,172,.18);\n  border-left: 3px solid #86efac; border-radius: 6px; padding: 20px 24px; margin: 32px 0;\n}\n.cv-cp-label { font-family: 'JetBrains Mono',monospace; font-size: .68rem; letter-spacing: .14em; text-transform: uppercase; color: #86efac; margin-bottom: 14px; }\n.cv-checkpoint ul { list-style: none; padding: 0; margin: 0; }\n.cv-checkpoint ul li { display: flex; align-items: flex-start; gap: 10px; font-size: .9rem; color: #8aad8e; margin-bottom: 8px; cursor: pointer; }\n.cv-cb { width: 15px; height: 15px; border: 1px solid #2a4a2e; border-radius: 2px; flex-shrink: 0; margin-top: 2px; background: #0d110c; display: flex; align-items: center; justify-content: center; transition: all .15s; font-size: 9px; font-weight: bold; color: transparent; }\n.cv-cb.done { background: #86efac; border-color: #86efac; color: #0d110c; }\n.cv-note { background: rgba(94,234,212,.04); border: 1px solid rgba(94,234,212,.18); border-left: 3px solid #5eead4; border-radius: 6px; padding: 16px 22px; margin: 24px 0; font-size: .93rem; color: #78b8b0; }\n.cv-note strong { color: #5eead4; }\n.cv-note code { background: rgba(94,234,212,.08); padding: 1px 6px; border-radius: 3px; font-size: .85em; }\n.cv-project { background: rgba(134,239,172,.02); border: 1px solid rgba(134,239,172,.12); border-radius: 8px; padding: 24px 28px; margin: 36px 0; }\n.cv-project-header { font-family: 'JetBrains Mono',monospace; font-size: .7rem; letter-spacing: .16em; text-transform: uppercase; color: #86efac; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid rgba(134,239,172,.1); }\n.cv-project p { color: #9ab89e; font-size: .94rem; }\n.cv-next { background: rgba(18,22,16,.7); border: 1px solid rgba(134,239,172,.12); border-radius: 8px; padding: 22px 26px; margin: 40px 0 0 0; text-align: center; }\n.cv-next p { color: #788571; font-size: .9rem; margin: 0; }\n.cv-next strong { color: #86efac; }\n\u003c/style\u003e\n\u003cdiv class=\"cv-post\"\u003e\n\u003cp\u003eSnake moved in discrete steps on a grid. Clean, predictable, easy to reason about. What we\u0026rsquo;re building now is different. Flappy Bird is continuous \u0026ndash; the bird is constantly falling, velocity accumulates, and the pipes come at you from the right with no fixed grid to think in.\u003c/p\u003e","title":"Canvas 03 : Gravity, Input and the Feel of Things"},{"content":" Blog Summary: Adds production patterns to the Express server: middleware chains, JWT-based auth, schema validation with Zod, and persistent storage with MongoDB via Mongoose.\n1. Middlewares [SOURCE — COURSE MATERIAL]\nThe Problem Every route needs auth checks, input validation, logging. Repeating these in every handler is messy.\n// BAD — repeated logic in every route app.get(\u0026#34;/kidney\u0026#34;, (req, res) =\u0026gt; { // auth check here // input validation here // actual logic here }); app.put(\u0026#34;/kidney\u0026#34;, (req, res) =\u0026gt; { // auth check here (copied!) // input validation here (copied!) // actual logic here }); What Is Middleware? A function that runs between the request and the route handler. Has access to req, res, and next.\nHospital analogy:\nPatient enters → Insurance check → Blood test → BP check → Doctor Request arrives → Auth check → Input validation → Logger → Route handler Middleware Syntax // Define middleware function authMiddleware(req, res, next) { const username = req.headers.username; const password = req.headers.password; if (username !== \u0026#34;admin\u0026#34; || password !== \u0026#34;secret\u0026#34;) { return res.status(403).json({ message: \u0026#34;Unauthorized\u0026#34; }); } next(); // pass control to next middleware/handler } function inputValidation(req, res, next) { const kidneyId = Number(req.query.kidneyId); if (isNaN(kidneyId) || kidneyId \u0026lt; 1 || kidneyId \u0026gt; 2) { return res .status(411) .json({ message: \u0026#34;Invalid kidney ID (must be 1 or 2)\u0026#34; }); } next(); } // Apply to specific route app.get(\u0026#34;/kidney\u0026#34;, authMiddleware, inputValidation, (req, res) =\u0026gt; { res.json({ message: \u0026#34;Kidney data\u0026#34; }); }); // Apply to all routes app.use(authMiddleware); Global Error Handler // Must have 4 params to be recognized as error handler app.use((err, req, res, next) =\u0026gt; { console.error(err.stack); res.status(500).json({ message: \u0026#34;Something went wrong\u0026#34; }); }); // In routes, trigger it with next(err) app.get(\u0026#34;/data\u0026#34;, (req, res, next) =\u0026gt; { try { // risky operation } catch (err) { next(err); // forwards to error handler } }); Global Request Counter (Assignment Example) let requestCount = 0; app.use((req, res, next) =\u0026gt; { requestCount++; console.log(`Request #${requestCount}: ${req.method} ${req.url}`); next(); }); 2. Input Validation with Zod [SOURCE — COURSE MATERIAL]\nThe Problem with Manual Validation // Manual — doesn\u0026#39;t scale if (!req.body.username || typeof req.body.username !== \u0026#34;string\u0026#34;) { return res.status(411).json({ error: \u0026#34;Invalid username\u0026#34; }); } if (!req.body.age || req.body.age \u0026lt; 1 || req.body.age \u0026gt; 120) { return res.status(411).json({ error: \u0026#34;Invalid age\u0026#34; }); } // What if there are 20 fields? Zod — Schema Validation npm install zod const { z } = require(\u0026#34;zod\u0026#34;); // Define schema const signupSchema = z.object({ username: z.string().min(3).max(20), email: z.string().email(), age: z.number().min(1).max(120), password: z.string().min(8), }); // Use in middleware or route app.post(\u0026#34;/signup\u0026#34;, (req, res) =\u0026gt; { const result = signupSchema.safeParse(req.body); if (!result.success) { return res.status(411).json({ errors: result.error.errors, }); } // result.data is now type-safe and validated const { username, email, age, password } = result.data; // ... proceed with signup }); Zod types cheat sheet:\nz.string() // string z.string().email() // valid email z.string().url() // valid URL z.number() // number z.number().min(0) // non-negative z.boolean() // boolean z.array(z.string()) // array of strings z.enum([\u0026#39;admin\u0026#39;,\u0026#39;user\u0026#39;]) // one of these values z.optional(z.string()) // optional field z.object({ ... }) // nested object 3. Authentication [SOURCE — COURSE MATERIAL]\nWhy Authentication? Anyone can hit your backend with Postman. Auth ensures only legitimate users access protected resources.\nKey Cryptography Concepts Hashing:\nOne-directional — cannot reverse Same input → same output always Tiny input change → completely different output Use for storing passwords const bcrypt = require(\u0026#34;bcrypt\u0026#34;); // Store password const hashed = await bcrypt.hash(\u0026#34;mypassword\u0026#34;, 10); // \u0026#34;$2b$10$...\u0026#34; — never store plaintext // Check password on login const matches = await bcrypt.compare(\u0026#34;mypassword\u0026#34;, hashed); // true or false Encryption:\nTwo-directional (encrypt + decrypt) Requires a key/password Used for data that needs to be retrieved JWT (JSON Web Tokens):\nNeither hashing nor encryption — it\u0026rsquo;s a digital signature Anyone can decode the payload (it\u0026rsquo;s base64) But only the server with the secret can verify it\u0026rsquo;s valid Structure: header.payload.signature eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiIxMjMifQ.ABC123... header (base64) payload (base64) signature Local Storage:\nBrowser storage (persists across page reloads) Commonly used to store JWT tokens [ADDED — EXPLANATION] For more security, httpOnly cookies are preferred over localStorage (not accessible via JS) Auth Flow Signup: 1. User sends {username, password} 2. Server hashes password, stores in DB 3. Server returns JWT token Login (Signin): 1. User sends {username, password} 2. Server finds user, compares hashed passwords 3. If match: server returns JWT token Protected Routes: 1. User sends request with JWT in Authorization header 2. Server verifies JWT signature 3. If valid: allow access, extract userId from payload Implementation with jsonwebtoken npm install jsonwebtoken const jwt = require(\u0026#34;jsonwebtoken\u0026#34;); const JWT_SECRET = \u0026#34;your-secret-key-use-env-variable-in-production\u0026#34;; // Sign a token (on login/signup) const token = jwt.sign( { userId: user._id, username: user.username }, // payload JWT_SECRET, { expiresIn: \u0026#34;7d\u0026#34; }, // token expires in 7 days ); // Verify token (middleware) function authMiddleware(req, res, next) { const authHeader = req.headers.authorization; // Expected format: \u0026#34;Bearer \u0026lt;token\u0026gt;\u0026#34; if (!authHeader || !authHeader.startsWith(\u0026#34;Bearer \u0026#34;)) { return res.status(403).json({ message: \u0026#34;No token provided\u0026#34; }); } const token = authHeader.split(\u0026#34; \u0026#34;)[1]; try { const decoded = jwt.verify(token, JWT_SECRET); req.userId = decoded.userId; // attach to request next(); } catch (err) { return res.status(403).json({ message: \u0026#34;Invalid token\u0026#34; }); } } Complete Signup/Signin Example const express = require(\u0026#34;express\u0026#34;); const jwt = require(\u0026#34;jsonwebtoken\u0026#34;); const app = express(); app.use(express.json()); const users = []; // in-memory for now const JWT_SECRET = \u0026#34;mysecret\u0026#34;; app.post(\u0026#34;/signup\u0026#34;, (req, res) =\u0026gt; { const { username, password } = req.body; users.push({ username, password }); // TODO: hash in production! res.json({ message: \u0026#34;User created\u0026#34; }); }); app.post(\u0026#34;/signin\u0026#34;, (req, res) =\u0026gt; { const { username, password } = req.body; const user = users.find( (u) =\u0026gt; u.username === username \u0026amp;\u0026amp; u.password === password, ); if (!user) { return res.status(403).json({ message: \u0026#34;Invalid credentials\u0026#34; }); } const token = jwt.sign({ username }, JWT_SECRET); res.json({ token }); }); app.get(\u0026#34;/users\u0026#34;, authMiddleware, (req, res) =\u0026gt; { const userList = users.map((u) =\u0026gt; ({ username: u.username })); res.json({ users: userList }); }); 4. Fetch API (Browser-Side HTTP) [SOURCE — COURSE MATERIAL]\nThree Ways to Send HTTP Requests Browser address bar — GET only Postman — all methods, manual testing Fetch API — programmatic, from your own code (frontend JS) // Browser / frontend JavaScript async function getUsers() { try { const response = await fetch(\u0026#34;https://api.example.com/users\u0026#34;); const data = await response.json(); console.log(data); } catch (error) { console.error(\u0026#34;Request failed:\u0026#34;, error); } } // POST with body and headers async function login(username, password) { const response = await fetch(\u0026#34;https://api.example.com/signin\u0026#34;, { method: \u0026#34;POST\u0026#34;, headers: { \u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;, Authorization: `Bearer ${localStorage.getItem(\u0026#34;token\u0026#34;)}`, }, body: JSON.stringify({ username, password }), }); const data = await response.json(); return data; } 5. MongoDB \u0026amp; Mongoose [SOURCE — COURSE MATERIAL]\nWhy Not In-Memory? Problem 1: Server restarts wipe all data Problem 2: Multiple server instances don\u0026#39;t share memory Solution: External database Architecture Browser → Express Server → MongoDB ↑ Auth, logic, business rules Why can\u0026rsquo;t users hit DB directly?\nBrowsers don\u0026rsquo;t speak MongoDB\u0026rsquo;s protocol DBs have no concept of per-user access control Exposing DB directly = security disaster MongoDB Concepts Cluster (group of servers) └── Database (e.g., \u0026#34;myapp\u0026#34;) ├── Collection/Table (e.g., \u0026#34;users\u0026#34;) │ ├── Document: { name: \u0026#34;Alice\u0026#34;, age: 25 } │ └── Document: { name: \u0026#34;Bob\u0026#34;, age: 30 } └── Collection (e.g., \u0026#34;courses\u0026#34;) Schemaless — no fixed structure enforced by DB itself Mongoose — adds schema on top for validation \u0026amp; autocomplete Mongoose CRUD npm install mongoose const mongoose = require(\u0026#34;mongoose\u0026#34;); // 1. Connect mongoose.connect(\u0026#34;mongodb+srv://user:pass@cluster.mongodb.net/myapp\u0026#34;); // 2. Define Schema const userSchema = new mongoose.Schema({ username: { type: String, required: true, unique: true }, email: { type: String, required: true }, password: { type: String, required: true }, createdAt: { type: Date, default: Date.now }, }); // 3. Create Model const User = mongoose.model(\u0026#34;User\u0026#34;, userSchema); // CREATE const newUser = new User({ username: \u0026#34;alice\u0026#34;, email: \u0026#34;alice@ex.com\u0026#34;, password: \u0026#34;hash\u0026#34;, }); await newUser.save(); // or: await User.create({ username: \u0026#34;bob\u0026#34;, email: \u0026#34;bob@ex.com\u0026#34;, password: \u0026#34;hash\u0026#34; }); // READ const allUsers = await User.find({}); const alice = await User.findOne({ username: \u0026#34;alice\u0026#34; }); const byId = await User.findById(\u0026#34;64abc123...\u0026#34;); // UPDATE await User.updateOne( { username: \u0026#34;alice\u0026#34; }, { $set: { email: \u0026#34;new@email.com\u0026#34; } }, ); await User.findByIdAndUpdate(id, { email: \u0026#34;new@email.com\u0026#34; }, { new: true }); // DELETE await User.deleteOne({ username: \u0026#34;alice\u0026#34; }); await User.findByIdAndDelete(id); Course App Data Model // Users const userSchema = new mongoose.Schema({ email: String, password: String, name: String, age: Number, }); // Admins const adminSchema = new mongoose.Schema({ email: String, password: String, name: String, }); // Courses const courseSchema = new mongoose.Schema({ title: String, description: String, price: Number, }); // Purchases (join table) const purchaseSchema = new mongoose.Schema({ userId: mongoose.Schema.Types.ObjectId, courseId: mongoose.Schema.Types.ObjectId, timestamp: { type: Date, default: Date.now }, }); Exercises Quick (15 min) Add a middleware to an Express app that logs METHOD /path — timestamp for every request.\nHint 1: app.use((req, res, next) =\u0026gt; { ... })\nHint 2: new Date().toISOString() for timestamp\nIntermediate (45 min) Build a full auth system:\nPOST /signup — create user with hashed password (use bcrypt) POST /signin — return JWT if credentials match GET /profile — protected route, return user info from JWT payload Hint 1: jwt.sign({ userId }, secret, { expiresIn: '1d' })\nHint 2: Extract token: req.headers.authorization.split(' ')[1]\nChallenge (2–3 hours) Build a course marketplace backend with MongoDB:\nAdmins can create courses Users can sign up, sign in, purchase courses Users can only see courses they\u0026rsquo;ve purchased Proper auth on all protected routes using Zod validation Common Confusions Confusion Reality \u0026ldquo;JWT is encrypted\u0026rdquo; No. Payload is base64-encoded (decodable). The signature verifies integrity. \u0026ldquo;Hashing and encryption are the same\u0026rdquo; Hashing is one-way. Encryption is reversible. \u0026ldquo;next() is optional\u0026rdquo; Without next(), request will hang. Always call it OR send a response. \u0026ldquo;MongoDB is truly schemaless\u0026rdquo; MongoDB is, but Mongoose adds schemas for safety. Use them. Key Takeaways Middleware = reusable request processing (auth, validation, logging) Use Zod for schema validation — scales far better than manual if/else JWT flow: sign on login → send in Authorization header → verify on protected routes MongoDB is schemaless but Mongoose adds structure — use schemas Architecture: Browser ↔ Express ↔ MongoDB (users never touch DB directly) ","permalink":"/posts/03-middlewares-auth/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eBlog Summary:\u003c/strong\u003e Adds production patterns to the Express server: middleware chains, JWT-based auth, schema validation with Zod, and persistent storage with MongoDB via Mongoose.\u003c/p\u003e\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-middlewares\"\u003e1. Middlewares\u003c/h2\u003e\n\u003cp\u003e[SOURCE — COURSE MATERIAL]\u003c/p\u003e\n\u003ch3 id=\"the-problem\"\u003eThe Problem\u003c/h3\u003e\n\u003cp\u003eEvery route needs auth checks, input validation, logging. Repeating these in every handler is messy.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-js\" data-lang=\"js\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// BAD — repeated logic in every route\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003eapp\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eget\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/kidney\u0026#34;\u003c/span\u003e, (\u003cspan style=\"color:#a6e22e\"\u003ereq\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eres\u003c/span\u003e) =\u0026gt; {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#75715e\"\u003e// auth check here\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e  \u003cspan style=\"color:#75715e\"\u003e// input validation here\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e  \u003cspan style=\"color:#75715e\"\u003e// actual logic here\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e});\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003eapp\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eput\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/kidney\u0026#34;\u003c/span\u003e, (\u003cspan style=\"color:#a6e22e\"\u003ereq\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eres\u003c/span\u003e) =\u0026gt; {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#75715e\"\u003e// auth check here (copied!)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e  \u003cspan style=\"color:#75715e\"\u003e// input validation here (copied!)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e  \u003cspan style=\"color:#75715e\"\u003e// actual logic here\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e});\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"what-is-middleware\"\u003eWhat Is Middleware?\u003c/h3\u003e\n\u003cp\u003eA function that runs \u003cstrong\u003ebetween\u003c/strong\u003e the request and the route handler. Has access to \u003ccode\u003ereq\u003c/code\u003e, \u003ccode\u003eres\u003c/code\u003e, and \u003ccode\u003enext\u003c/code\u003e.\u003c/p\u003e","title":"03 : Middlewares, Authentication, Zod \u0026 MongoDB"},{"content":" So far we\u0026rsquo;ve been drawing everything from scratch using shapes, paths and arcs. That works fine for simple games. But Pac-Man has a detailed maze, animated ghosts and sprite-based characters. Drawing all that by hand every frame is going to get tedious fast.\nThis part is about working with images \u0026ndash; how to load them, how to draw them, how to cut pieces out of a sprite sheet, and how to reach down into the actual pixel data when you need to.\ndrawImage \u0026ndash; three ways to use it The drawImage function is overloaded \u0026ndash; it accepts different argument combinations:\n// 1. just draw the image at position (x, y) ctx.drawImage(image, x, y); // 2. draw with explicit width and height (scales the image) ctx.drawImage(image, x, y, width, height); // 3. the full version -- crop a section from the image and draw it ctx.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh); The third version is the one that matters for sprite sheets. The s params are the source rectangle (which part of the image to take), and the d params are the destination rectangle (where and how big to draw it on the canvas).\nLoading an image before drawing it:\nconst img = new Image(); img.src = \u0026#34;spritesheet.png\u0026#34;; img.onload = () =\u0026gt; { // safe to draw now ctx.drawImage(img, 0, 0); }; Always draw inside onload. If you call drawImage before the image has loaded, nothing appears \u0026ndash; canvas doesn\u0026rsquo;t throw an error, it just silently does nothing. This is a frustrating bug when you first hit it.\nSprite sheets A sprite sheet is a single image file that contains multiple frames or characters laid out in a grid. Instead of loading 20 separate images, you load one and cut out the piece you need each time.\nSay you have a ghost sprite sheet with 4 animation frames, each frame being 32x32 pixels, laid out horizontally:\n[ frame0 ][ frame1 ][ frame2 ][ frame3 ] 0,0 32,0 64,0 96,0 To draw frame 2:\nconst FRAME_W = 32; const FRAME_H = 32; const frameIndex = 2; ctx.drawImage( spriteSheet, frameIndex * FRAME_W, 0, // source x, y FRAME_W, FRAME_H, // source width, height drawX, drawY, // destination x, y FRAME_W, FRAME_H, // destination width, height ); For animation, you cycle frameIndex over time:\nlet animTimer = 0; let frameIndex = 0; const FRAME_COUNT = 4; const FRAME_DURATION = 0.15; // seconds per frame function updateAnimation(dt) { animTimer += dt; if (animTimer \u0026gt;= FRAME_DURATION) { animTimer = 0; frameIndex = (frameIndex + 1) % FRAME_COUNT; } } Simple, and it works for any sprite animation.\ngetImageData \u0026ndash; reading raw pixels This is where things get genuinely interesting. Canvas gives you access to the raw pixel data of anything drawn on it:\nconst imageData = ctx.getImageData(x, y, width, height); const pixels = imageData.data; // Uint8ClampedArray pixels is a flat array where every 4 consecutive values represent one pixel: [R, G, B, A, R, G, B, A, ...]. To get the pixel at position (px, py):\nfunction getPixel(imageData, px, py) { const index = (py * imageData.width + px) * 4; return { r: imageData.data[index], g: imageData.data[index + 1], b: imageData.data[index + 2], a: imageData.data[index + 3], }; } And to set a pixel:\nfunction setPixel(imageData, px, py, r, g, b, a) { const index = (py * imageData.width + px) * 4; imageData.data[index] = r; imageData.data[index + 1] = g; imageData.data[index + 2] = b; imageData.data[index + 3] = a; } After modifying the data, you push it back to the canvas:\nctx.putImageData(imageData, x, y); This is how you do real image processing on canvas. Invert colors, convert to grayscale, apply blur, do color replacement \u0026ndash; all of it is just math on those four numbers per pixel.\n// puzzle 04 =\u003e pixel math Load any image onto the canvas with drawImage, then use getImageData to get the pixel data. Now write code to:\nInvert all colors: r = 255 - r, same for g and b Convert to grayscale: replace r, g, b all with 0.299*r + 0.587*g + 0.114*b (those weights match human visual perception) These are the two most common pixel operations and they teach you the pattern for everything else. Try both before reading the Pac-Man section.\nTile maps \u0026ndash; the foundation of 2D game worlds Pac-Man\u0026rsquo;s maze isn\u0026rsquo;t drawn as a bunch of rectangles. It\u0026rsquo;s defined as a 2D grid where each cell has a value: 0 for empty, 1 for wall, 2 for dot, 3 for power pellet, etc. That grid is called a tile map.\nconst TILE = { EMPTY: 0, WALL: 1, DOT: 2, PELLET: 3, GHOST_H: 4, // ghost house }; const map = [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 3, 1, 1, 2, 1, 1, 1, 2, 1, 2, 1, 1, 1, 2, 1, 1, 3, 1], // ... and so on ]; To draw the tile map:\nconst TILE_SIZE = 28; function drawMap() { map.forEach((row, r) =\u0026gt; { row.forEach((cell, c) =\u0026gt; { const x = c * TILE_SIZE; const y = r * TILE_SIZE; if (cell === TILE.WALL) { ctx.fillStyle = \u0026#34;#1a6fa8\u0026#34;; ctx.fillRect(x, y, TILE_SIZE, TILE_SIZE); } else if (cell === TILE.DOT) { ctx.beginPath(); ctx.arc(x + TILE_SIZE / 2, y + TILE_SIZE / 2, 3, 0, Math.PI * 2); ctx.fillStyle = \u0026#34;#e8d5a3\u0026#34;; ctx.fill(); } else if (cell === TILE.PELLET) { ctx.beginPath(); ctx.arc(x + TILE_SIZE / 2, y + TILE_SIZE / 2, 8, 0, Math.PI * 2); ctx.fillStyle = \u0026#34;#e8d5a3\u0026#34;; ctx.fill(); } }); }); } For collision, instead of checking against every wall rectangle, you just look up the tile at the position you\u0026rsquo;re moving to:\nfunction getTile(px, py) { const col = Math.floor(px / TILE_SIZE); const row = Math.floor(py / TILE_SIZE); if (row \u0026lt; 0 || row \u0026gt;= map.length || col \u0026lt; 0 || col \u0026gt;= map[0].length) return TILE.WALL; return map[row][col]; } function isWall(px, py) { return getTile(px, py) === TILE.WALL; } This is much faster than any distance calculation, and it\u0026rsquo;s how nearly every 2D game does it.\nBuilding full Pac-Man // project 04 of 05 — full pac-man This is the biggest project in the series. We're not cutting corners -- proper tile map, moving Pac-Man, four ghosts with different personalities, power pellet logic, lives, score and a proper game loop. It's a lot, but every concept comes from something we've already covered.\nThe complete tile map This is a 19x21 cell maze (simplified from the original but recognizable):\nconst MAP_COLS = 19; const MAP_ROWS = 21; const T = (TILE_SIZE = 28); const INITIAL_MAP = [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 3, 1, 1, 2, 1, 1, 1, 2, 1, 2, 1, 1, 1, 2, 1, 1, 3, 1], [1, 2, 1, 1, 2, 1, 1, 1, 2, 1, 2, 1, 1, 1, 2, 1, 1, 2, 1], [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 2, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 2, 1], [1, 2, 2, 2, 2, 1, 2, 2, 2, 1, 2, 2, 2, 1, 2, 2, 2, 2, 1], [1, 1, 1, 1, 2, 1, 1, 1, 0, 1, 0, 1, 1, 1, 2, 1, 1, 1, 1], [1, 1, 1, 1, 2, 1, 0, 0, 0, 0, 0, 0, 0, 1, 2, 1, 1, 1, 1], [1, 1, 1, 1, 2, 1, 0, 4, 4, 4, 4, 4, 0, 1, 2, 1, 1, 1, 1], [0, 0, 0, 0, 2, 0, 0, 4, 4, 4, 4, 4, 0, 0, 2, 0, 0, 0, 0], [1, 1, 1, 1, 2, 1, 0, 4, 4, 4, 4, 4, 0, 1, 2, 1, 1, 1, 1], [1, 1, 1, 1, 2, 1, 0, 0, 0, 0, 0, 0, 0, 1, 2, 1, 1, 1, 1], [1, 1, 1, 1, 2, 1, 0, 1, 1, 1, 1, 1, 0, 1, 2, 1, 1, 1, 1], [1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 2, 1, 1, 2, 1, 1, 1, 2, 1, 2, 1, 1, 1, 2, 1, 1, 2, 1], [1, 3, 2, 1, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 1, 2, 3, 1], [1, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 1], [1, 2, 2, 2, 2, 1, 2, 2, 2, 1, 2, 2, 2, 1, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ]; We clone this at the start so we can reset without the original data being mutated:\nlet map = INITIAL_MAP.map((row) =\u0026gt; [...row]); Pac-Man movement Pac-Man moves tile by tile but feels smooth because we animate between tiles:\nconst pacman = { // tile position tileX: 9, tileY: 15, // pixel position (interpolated) x: 9 * T + T / 2, y: 15 * T + T / 2, dir: { x: 0, y: 0 }, nextDir: { x: 0, y: 0 }, speed: 150, // px/s mouthAngle: 0, mouthOpen: true, }; function updatePacman(dt) { // animate mouth pacman.mouthAngle += dt * 5; const mouth = Math.abs(Math.sin(pacman.mouthAngle)) * 0.35; // try to turn in requested direction const nx = pacman.tileX + pacman.nextDir.x; const ny = pacman.tileY + pacman.nextDir.y; if (getTile(nx * T + T / 2, ny * T + T / 2) !== TILE.WALL) { pacman.dir = { ...pacman.nextDir }; } // move in current direction pacman.x += pacman.dir.x * pacman.speed * dt; pacman.y += pacman.dir.y * pacman.speed * dt; // snap to grid when crossing a tile center const targetX = Math.round(pacman.x / T) * T; const targetY = Math.round(pacman.y / T) * T; if (Math.abs(pacman.x - targetX) \u0026lt; 2 \u0026amp;\u0026amp; Math.abs(pacman.y - targetY) \u0026lt; 2) { pacman.tileX = Math.floor(pacman.x / T); pacman.tileY = Math.floor(pacman.y / T); // check for wall ahead const aheadX = (pacman.tileX + pacman.dir.x) * T + T / 2; const aheadY = (pacman.tileY + pacman.dir.y) * T + T / 2; if (getTile(aheadX, aheadY) === TILE.WALL) { pacman.dir = { x: 0, y: 0 }; } // eat dot const tile = map[pacman.tileY]?.[pacman.tileX]; if (tile === TILE.DOT) { map[pacman.tileY][pacman.tileX] = TILE.EMPTY; score += 10; } if (tile === TILE.PELLET) { map[pacman.tileY][pacman.tileX] = TILE.EMPTY; score += 50; frightenGhosts(); } } } Ghost AI The real Pac-Man ghosts have distinct personalities based on their scatter/chase targets. We\u0026rsquo;ll implement simplified but functional versions:\nBlinky (red): always chases Pac-Man directly Pinky (pink): targets 4 tiles ahead of Pac-Man Inky (cyan): semi-random (good for chaos) Clyde (orange): chases when far, scatters when close const ghosts = [ { name: \u0026#34;blinky\u0026#34;, tileX: 9, tileY: 9, x: 9 * T + T / 2, y: 9 * T + T / 2, color: \u0026#34;#fca5a5\u0026#34;, dir: { x: 1, y: 0 }, frightened: false, speed: 130, }, { name: \u0026#34;pinky\u0026#34;, tileX: 9, tileY: 10, x: 9 * T + T / 2, y: 10 * T + T / 2, color: \u0026#34;#f9a8d4\u0026#34;, dir: { x: 0, y: -1 }, frightened: false, speed: 130, }, { name: \u0026#34;inky\u0026#34;, tileX: 8, tileY: 10, x: 8 * T + T / 2, y: 10 * T + T / 2, color: \u0026#34;#5eead4\u0026#34;, dir: { x: 0, y: 1 }, frightened: false, speed: 130, }, { name: \u0026#34;clyde\u0026#34;, tileX: 10, tileY: 10, x: 10 * T + T / 2, y: 10 * T + T / 2, color: \u0026#34;#fdba74\u0026#34;, dir: { x: 0, y: -1 }, frightened: false, speed: 130, }, ]; function getGhostTarget(ghost) { if (ghost.frightened) { // move semi-randomly when frightened return { x: Math.floor(Math.random() * MAP_COLS), y: Math.floor(Math.random() * MAP_ROWS), }; } switch (ghost.name) { case \u0026#34;blinky\u0026#34;: return { x: pacman.tileX, y: pacman.tileY }; case \u0026#34;pinky\u0026#34;: return { x: pacman.tileX + pacman.dir.x * 4, y: pacman.tileY + pacman.dir.y * 4, }; case \u0026#34;inky\u0026#34;: return { x: pacman.tileX + (Math.random() \u0026gt; 0.5 ? 2 : -2), y: pacman.tileY + (Math.random() \u0026gt; 0.5 ? 2 : -2), }; case \u0026#34;clyde\u0026#34;: { const dx = ghost.tileX - pacman.tileX; const dy = ghost.tileY - pacman.tileY; const dist = Math.sqrt(dx * dx + dy * dy); return dist \u0026gt; 8 ? { x: pacman.tileX, y: pacman.tileY } : { x: 0, y: MAP_ROWS - 1 }; } } } function updateGhost(ghost, dt) { // simple direction choosing at intersections const atCenter = Math.abs(ghost.x - (ghost.tileX * T + T / 2)) \u0026lt; 2 \u0026amp;\u0026amp; Math.abs(ghost.y - (ghost.tileY * T + T / 2)) \u0026lt; 2; if (atCenter) { const target = getGhostTarget(ghost); const dirs = [ { x: 1, y: 0 }, { x: -1, y: 0 }, { x: 0, y: 1 }, { x: 0, y: -1 }, ]; const validDirs = dirs.filter((d) =\u0026gt; { // can\u0026#39;t reverse if (d.x === -ghost.dir.x \u0026amp;\u0026amp; d.y === -ghost.dir.y) return false; const nx = ghost.tileX + d.x; const ny = ghost.tileY + d.y; return getTile(nx * T + T / 2, ny * T + T / 2) !== TILE.WALL; }); if (validDirs.length \u0026gt; 0) { // pick direction closest to target ghost.dir = validDirs.reduce((best, d) =\u0026gt; { const nx = ghost.tileX + d.x; const ny = ghost.tileY + d.y; const dist = Math.hypot(nx - target.x, ny - target.y); const bestDist = Math.hypot( ghost.tileX + best.x - target.x, ghost.tileY + best.y - target.y, ); return dist \u0026lt; bestDist ? d : best; }); } ghost.tileX += ghost.dir.x; ghost.tileY += ghost.dir.y; } const speed = ghost.frightened ? ghost.speed * 0.5 : ghost.speed; ghost.x += ghost.dir.x * speed * dt; ghost.y += ghost.dir.y * speed * dt; } let frightenTimer = 0; function frightenGhosts() { ghosts.forEach((g) =\u0026gt; (g.frightened = true)); frightenTimer = 8; // 8 seconds of fright } function updateFrighten(dt) { if (frightenTimer \u0026gt; 0) { frightenTimer -= dt; if (frightenTimer \u0026lt;= 0) ghosts.forEach((g) =\u0026gt; (g.frightened = false)); } } Ghost-Pac-Man collision function checkGhostCollisions() { ghosts.forEach((ghost) =\u0026gt; { const dx = ghost.x - pacman.x; const dy = ghost.y - pacman.y; if (Math.hypot(dx, dy) \u0026lt; T * 0.8) { if (ghost.frightened) { // eat the ghost ghost.frightened = false; ghost.tileX = 9; ghost.tileY = 9; ghost.x = 9 * T + T / 2; ghost.y = 9 * T + T / 2; score += 200; } else { // Pac-Man dies lives--; if (lives \u0026lt;= 0) gameState = STATE.GAME_OVER; else resetPositions(); } } }); } Drawing everything function drawPacman() { const mouth = Math.abs(Math.sin(pacman.mouthAngle)) * 0.35; const facingAngle = Math.atan2(pacman.dir.y, pacman.dir.x); ctx.save(); ctx.translate(pacman.x, pacman.y); ctx.rotate(facingAngle); ctx.beginPath(); ctx.moveTo(0, 0); ctx.arc(0, 0, T / 2 - 3, mouth, Math.PI * 2 - mouth); ctx.closePath(); ctx.fillStyle = \u0026#34;#fbbf24\u0026#34;; ctx.fill(); ctx.restore(); } function drawGhost(ghost) { const x = ghost.x; const y = ghost.y; const r = T / 2 - 3; const color = ghost.frightened ? frightenTimer \u0026lt; 2 \u0026amp;\u0026amp; Math.floor(Date.now() / 300) % 2 ? \u0026#34;#fff\u0026#34; : \u0026#34;#1a3fa8\u0026#34; : ghost.color; ctx.beginPath(); ctx.arc(x, y - r * 0.2, r, Math.PI, 0); ctx.lineTo(x + r, y + r * 1.1); ctx.quadraticCurveTo(x + r * 0.6, y + r * 0.7, x + r * 0.3, y + r * 1.1); ctx.quadraticCurveTo(x, y + r * 0.7, x - r * 0.3, y + r * 1.1); ctx.quadraticCurveTo(x - r * 0.6, y + r * 0.7, x - r, y + r * 1.1); ctx.lineTo(x - r, y - r * 0.2); ctx.closePath(); ctx.fillStyle = color; ctx.fill(); if (!ghost.frightened) { // eyes [ [-r * 0.35, -r * 0.2], [r * 0.35, -r * 0.2], ].forEach(([ex, ey]) =\u0026gt; { ctx.beginPath(); ctx.arc(x + ex, y + ey, r * 0.28, 0, Math.PI * 2); ctx.fillStyle = \u0026#34;#fff\u0026#34;; ctx.fill(); ctx.beginPath(); ctx.arc( x + ex + ghost.dir.x * 2, y + ey + ghost.dir.y * 2, r * 0.14, 0, Math.PI * 2, ); ctx.fillStyle = \u0026#34;#1a3fa8\u0026#34;; ctx.fill(); }); } } The frightened ghost flashing when frightenTimer \u0026lt; 2 is a classic Pac-Man mechanic \u0026ndash; warning the player that the ghosts are about to return to normal.\nWin condition function checkWin() { const dotsLeft = map .flat() .filter((t) =\u0026gt; t === TILE.DOT || t === TILE.PELLET).length; if (dotsLeft === 0) { gameState = STATE.WIN; } } That\u0026rsquo;s the whole game. The full loop ties everything together:\nfunction loop(ts) { const dt = Math.min((ts - lastTime) / 1000, 0.05); lastTime = ts; if (gameState === STATE.PLAYING) { updatePacman(dt); ghosts.forEach((g) =\u0026gt; updateGhost(g, dt)); updateFrighten(dt); checkGhostCollisions(); checkWin(); } ctx.fillStyle = \u0026#34;#000\u0026#34;; ctx.fillRect(0, 0, W, H); drawMap(); drawPacman(); ghosts.forEach(drawGhost); drawHUD(); requestAnimationFrame(loop); } // on performance: drawing the tile map by iterating every cell every frame is fine for a 19x21 grid. For a 500x500 grid it would be slow. The optimization is an offscreen canvas -- draw the static tiles once to a hidden canvas, then drawImage that whole thing to the main canvas each frame. One draw call vs thousands. We'll touch on this more in Part 5. // checkpoint -- part 04 I can load and draw images with drawImage (all three argument forms) I understand sprite sheets and how to cut frames out of them I can read and modify pixel data with getImageData / putImageData I understand tile maps and how to use them for collision I built a full Pac-Man with ghost AI and win/lose logic // up next — Part 05: Fake Depth, Real Math\nThe final part is the most mathematically interesting. We leave game mechanics behind and get into transformations, matrix math and the trick that makes 3D rendering possible on a 2D canvas. Then we use pixel manipulation to build an image-to-ASCII converter and a live webcam ASCII renderer. No library, no WebGL.\n","permalink":"/canvas/canvas-04-images-pacman/","summary":"\u003c!--\n  NOTE FOR HUGO SETUP:\n  unsafe: true required in markup.goldmark.renderer\n--\u003e\n\u003cstyle\u003e\n.cv-post { --cv-green:#86efac;--cv-cyan:#5eead4;--cv-amber:#fbbf24;--cv-red:#fca5a5; }\n.cv-puzzle { background:rgba(251,191,36,.04);border:1px solid rgba(251,191,36,.2);border-left:3px solid #fbbf24;border-radius:6px;padding:20px 24px;margin:32px 0; }\n.cv-puzzle-label { font-family:'JetBrains Mono',monospace;font-size:.68rem;letter-spacing:.14em;text-transform:uppercase;color:#fbbf24;margin-bottom:12px; }\n.cv-puzzle p,.cv-puzzle li { color:#c4a855;font-size:.94rem; }\n.cv-puzzle strong { color:#fbbf24; }\n.cv-puzzle code { background:rgba(251,191,36,.08);border:1px solid rgba(251,191,36,.2);padding:1px 6px;border-radius:3px;font-size:.85em; }\n.cv-checkpoint { background:rgba(134,239,172,.03);border:1px solid rgba(134,239,172,.18);border-left:3px solid #86efac;border-radius:6px;padding:20px 24px;margin:32px 0; }\n.cv-cp-label { font-family:'JetBrains Mono',monospace;font-size:.68rem;letter-spacing:.14em;text-transform:uppercase;color:#86efac;margin-bottom:14px; }\n.cv-checkpoint ul { list-style:none;padding:0;margin:0; }\n.cv-checkpoint ul li { display:flex;align-items:flex-start;gap:10px;font-size:.9rem;color:#8aad8e;margin-bottom:8px;cursor:pointer; }\n.cv-cb { width:15px;height:15px;border:1px solid #2a4a2e;border-radius:2px;flex-shrink:0;margin-top:2px;background:#0d110c;display:flex;align-items:center;justify-content:center;transition:all .15s;font-size:9px;font-weight:bold;color:transparent; }\n.cv-cb.done { background:#86efac;border-color:#86efac;color:#0d110c; }\n.cv-note { background:rgba(94,234,212,.04);border:1px solid rgba(94,234,212,.18);border-left:3px solid #5eead4;border-radius:6px;padding:16px 22px;margin:24px 0;font-size:.93rem;color:#78b8b0; }\n.cv-note strong { color:#5eead4; }\n.cv-note code { background:rgba(94,234,212,.08);padding:1px 6px;border-radius:3px;font-size:.85em; }\n.cv-project { background:rgba(134,239,172,.02);border:1px solid rgba(134,239,172,.12);border-radius:8px;padding:24px 28px;margin:36px 0; }\n.cv-project-header { font-family:'JetBrains Mono',monospace;font-size:.7rem;letter-spacing:.16em;text-transform:uppercase;color:#86efac;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid rgba(134,239,172,.1); }\n.cv-project p { color:#9ab89e;font-size:.94rem; }\n.cv-next { background:rgba(18,22,16,.7);border:1px solid rgba(134,239,172,.12);border-radius:8px;padding:22px 26px;margin:40px 0 0 0;text-align:center; }\n.cv-next p { color:#788571;font-size:.9rem;margin:0; }\n.cv-next strong { color:#86efac; }\n\u003c/style\u003e\n\u003cdiv class=\"cv-post\"\u003e\n\u003cp\u003eSo far we\u0026rsquo;ve been drawing everything from scratch using shapes, paths and arcs. That works fine for simple games. But Pac-Man has a detailed maze, animated ghosts and sprite-based characters. Drawing all that by hand every frame is going to get tedious fast.\u003c/p\u003e","title":"Canvas 04 : Pixels Are Just Numbers"},{"content":" Blog Summary: Transitions from backend to frontend. Explores raw DOM manipulation, its limitations, and why React exists. Also covers MongoDB/Mongoose in depth with a complete full-stack data model.\n1. Browser JavaScript \u0026amp; the DOM [SOURCE — COURSE MATERIAL]\nWhat Is the DOM? Document Object Model — the browser\u0026rsquo;s in-memory representation of a web page as a tree of objects.\nHTML File: DOM Tree: \u0026lt;html\u0026gt; document \u0026lt;body\u0026gt; └── html \u0026lt;div id=\u0026#34;app\u0026#34;\u0026gt; └── body \u0026lt;h1\u0026gt;Hello\u0026lt;/h1\u0026gt; └── div#app \u0026lt;button\u0026gt;Click\u0026lt;/button\u0026gt; ├── h1 (\u0026#34;Hello\u0026#34;) \u0026lt;/div\u0026gt; └── button (\u0026#34;Click\u0026#34;) \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; JavaScript can read and modify this tree in real-time → dynamic web pages.\nJavaScript in the Browser vs Node.js Browser JS: Node.js: ├── ECMAScript (shared) ├── ECMAScript (shared) ├── document (DOM) ├── fs (file system) ├── window ├── http ├── fetch ├── path ├── localStorage └── process └── setTimeout/setInterval 2. Raw DOM Manipulation [SOURCE — COURSE MATERIAL]\nAccessing Elements \u0026lt;!-- HTML --\u0026gt; \u0026lt;input id=\u0026#34;num1\u0026#34; type=\u0026#34;number\u0026#34; /\u0026gt; \u0026lt;input id=\u0026#34;num2\u0026#34; type=\u0026#34;number\u0026#34; /\u0026gt; \u0026lt;button onclick=\u0026#34;calculateSum()\u0026#34;\u0026gt;Add\u0026lt;/button\u0026gt; \u0026lt;div id=\u0026#34;result\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; // JavaScript — accessing by ID function calculateSum() { const a = document.getElementById(\u0026#34;num1\u0026#34;).value; const b = document.getElementById(\u0026#34;num2\u0026#34;).value; const sum = parseInt(a) + parseInt(b); document.getElementById(\u0026#34;result\u0026#34;).textContent = `Sum: ${sum}`; } // Other selectors document.getElementsByClassName(\u0026#34;my-class\u0026#34;); // HTMLCollection document.querySelector(\u0026#34;#id\u0026#34;); // first match document.querySelectorAll(\u0026#34;.class\u0026#34;); // NodeList Classes vs IDs \u0026lt;!-- Classes: reusable styling --\u0026gt; \u0026lt;div class=\u0026#34;card\u0026#34;\u0026gt;Card 1\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;card\u0026#34;\u0026gt;Card 2\u0026lt;/div\u0026gt; \u0026lt;!-- IDs: unique identifier for JS access --\u0026gt; \u0026lt;div id=\u0026#34;main-header\u0026#34;\u0026gt;Header\u0026lt;/div\u0026gt; Rule: Use classes for CSS styling. Use IDs for JS targeting.\nCreating \u0026amp; Modifying Elements // Create const div = document.createElement(\u0026#34;div\u0026#34;); div.textContent = \u0026#34;New element\u0026#34;; div.className = \u0026#34;todo-item\u0026#34;; div.setAttribute(\u0026#34;data-id\u0026#34;, \u0026#34;123\u0026#34;); // Add to page document.getElementById(\u0026#34;container\u0026#34;).appendChild(div); // Remove div.parentNode.removeChild(div); // or div.remove(); // modern // Modify div.innerHTML = \u0026#34;\u0026lt;strong\u0026gt;Bold text\u0026lt;/strong\u0026gt;\u0026#34;; div.style.color = \u0026#34;red\u0026#34;; div.classList.add(\u0026#34;active\u0026#34;); div.classList.remove(\u0026#34;inactive\u0026#34;); 3. Why DOM Manipulation Is Hard to Scale [SOURCE — COURSE MATERIAL]\nThe TODO App Problem // BAD: imperative DOM manipulation for a TODO app let todos = []; function addTodo(text) { const li = document.createElement(\u0026#34;li\u0026#34;); li.textContent = text; li.setAttribute(\u0026#34;id\u0026#34;, `todo-${todos.length}`); document.getElementById(\u0026#34;list\u0026#34;).appendChild(li); todos.push({ text, completed: false }); } // Now what if you get updated todos from a server? // You only have addTodo — no updateTodo, removeTodo // The DOM and your data are out of sync! The Core Problem: No Central State Data (todos array) DOM elements │ │ ↓ ↓ [updated] [out of sync] When data changes, you have to manually figure out which DOM elements to add/remove/update. This doesn\u0026rsquo;t scale.\nThe Dumb Solution (Clear Everything) function renderTodos(todos) { const list = document.getElementById(\u0026#34;list\u0026#34;); list.innerHTML = \u0026#34;\u0026#34;; // clear all todos.forEach((todo) =\u0026gt; addTodo(todo)); // re-add all } Problem: Destroys and recreates all DOM nodes even if only one changed. Terrible for performance.\nThe Ideal Solution State (data) → \u0026#34;Diff\u0026#34; function → Minimal DOM updates Developer updates state → Framework figures out what changed → Updates only those DOM nodes This is exactly what React does.\n4. Why Frontend Frameworks Exist [SOURCE — COURSE MATERIAL]\nThe Evolution 1995-2000: Vanilla JS / Direct DOM manipulation ↓ (got painful, code became spaghetti) 2006-2010: jQuery (simplified DOM manipulation + cross-browser) ↓ (still messy for large apps, no state management) 2013+: Angular, React, Vue (declarative + state management) ↓ Today: React dominates, Vue/Svelte alternatives What React Solves Problem: Solution: ───────── ───────── No central state → useState hook (single source of truth) Manual DOM sync → React reconciler (Virtual DOM diffing) Code repetition → Components (reusable UI pieces) Hard to read → JSX (HTML-like syntax in JS) The Three Things You Need for a Dynamic UI Update a state variable (developer\u0026rsquo;s job) Diff old vs new state (React\u0026rsquo;s job — the reconciler) Apply minimal DOM changes (React\u0026rsquo;s job) 5. Introduction to React [SOURCE — COURSE MATERIAL]\nnpm create vite@latest my-app -- --template react cd my-app npm install npm run dev React Is Just JS + JSX // JSX — looks like HTML but is JavaScript // Gets compiled to: React.createElement(\u0026#39;div\u0026#39;, {className: \u0026#39;box\u0026#39;}, \u0026#39;Hello\u0026#39;) function MyComponent() { return ( \u0026lt;div className=\u0026#34;box\u0026#34;\u0026gt; {\u0026#34; \u0026#34;} {/* class → className in JSX */} \u0026lt;h1\u0026gt;Hello React\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;World\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; ); } Under the hood:\nYour JSX code → Babel/Vite compiler → React.createElement() calls → DOM updates 6. MongoDB Deep Dive — CRUD with Mongoose [SOURCE — COURSE MATERIAL]\n3 Database Jargons Cluster (deployed server group) └── Database (logical partition, e.g., \u0026#34;courseapp\u0026#34;) ├── Collection = Table (e.g., \u0026#34;users\u0026#34;) └── Collection (e.g., \u0026#34;courses\u0026#34;) Why HTTP Server Over Direct DB Access? Browser ──✗──→ MongoDB (won\u0026#39;t work) Browser ──→ Express ──→ MongoDB (correct) Reasons:\nBrowsers don\u0026rsquo;t speak MongoDB\u0026rsquo;s binary protocol MongoDB has no fine-grained user permission system HTTP server provides auth, rate limiting, business logic Complete Mongoose Schema Setup const mongoose = require(\u0026#34;mongoose\u0026#34;); mongoose.connect(process.env.MONGO_URI); // User schema const UserSchema = new mongoose.Schema({ username: { type: String, required: true, unique: true }, email: { type: String, required: true }, password: { type: String, required: true }, purchasedCourses: [{ type: mongoose.Schema.Types.ObjectId, ref: \u0026#34;Course\u0026#34; }], }); // Course schema const CourseSchema = new mongoose.Schema({ title: { type: String, required: true }, description: String, price: { type: Number, required: true }, imageUrl: String, creatorId: { type: mongoose.Schema.Types.ObjectId, ref: \u0026#34;Admin\u0026#34; }, }); // Purchase schema (join/relation) const PurchaseSchema = new mongoose.Schema({ userId: { type: mongoose.Schema.Types.ObjectId, ref: \u0026#34;User\u0026#34;, required: true }, courseId: { type: mongoose.Schema.Types.ObjectId, ref: \u0026#34;Course\u0026#34;, required: true, }, purchasedAt: { type: Date, default: Date.now }, }); const User = mongoose.model(\u0026#34;User\u0026#34;, UserSchema); const Course = mongoose.model(\u0026#34;Course\u0026#34;, CourseSchema); const Purchase = mongoose.model(\u0026#34;Purchase\u0026#34;, PurchaseSchema); CRUD Operations // CREATE const user = await User.create({ username: \u0026#34;alice\u0026#34;, email: \u0026#34;alice@test.com\u0026#34;, password: hashedPassword, }); // READ — various patterns const allUsers = await User.find({}); const oneUser = await User.findOne({ username: \u0026#34;alice\u0026#34; }); const byId = await User.findById(userId); // With filter const expCourses = await Course.find({ price: { $gt: 1000 } }); // Populate (join) const user = await User.findById(id).populate(\u0026#34;purchasedCourses\u0026#34;); // UPDATE await User.updateOne( { _id: userId }, // filter { $set: { email: \u0026#34;new@email.com\u0026#34; } }, // update ); const updated = await User.findByIdAndUpdate( id, { email: \u0026#34;new@email.com\u0026#34; }, { new: true }, // return updated doc ); // DELETE await User.deleteOne({ _id: userId }); await User.findByIdAndDelete(id); Custom Schema Methods [SOURCE — COURSE MATERIAL]\n// Attach methods to schema UserSchema.methods.isValidPassword = async function (password) { return bcrypt.compare(password, this.password); }; // Use on instance const user = await User.findOne({ email }); const valid = await user.isValidPassword(inputPassword); Full Auth + DB Example app.post(\u0026#34;/signup\u0026#34;, async (req, res) =\u0026gt; { const { username, password, email } = req.body; try { const hashed = await bcrypt.hash(password, 10); const user = await User.create({ username, email, password: hashed }); const token = jwt.sign({ userId: user._id }, JWT_SECRET); res.json({ token }); } catch (err) { if (err.code === 11000) { // duplicate key return res.status(409).json({ message: \u0026#34;Username taken\u0026#34; }); } res.status(500).json({ message: \u0026#34;Server error\u0026#34; }); } }); Exercises Quick (15 min) Create a static HTML page with two input boxes and a button. On button click, fetch the sum from https://sum-server.100xdevs.com/sum?a=X\u0026amp;b=Y and display it.\nHint 1: Use fetch with query params\nHint 2: document.getElementById('result').textContent = data.answer\nIntermediate (45 min) Build a course catalog API with MongoDB:\nPOST /course — create a course (admin only, JWT auth) GET /courses — list all courses (public) POST /purchase — purchase a course (user auth) GET /purchased — get user\u0026rsquo;s purchased courses Challenge (3 hours) Build a full-stack mini-project:\nFrontend HTML page that shows courses Backend Express API with MongoDB Users can sign up, sign in, purchase courses Protected routes via JWT No React yet — raw DOM manipulation only Common Confusions Confusion Reality \u0026ldquo;document exists in Node.js\u0026rdquo; No. document is browser-only. In Node use fs, not document. \u0026ldquo;MongoDB stores tables\u0026rdquo; It stores collections of documents (JSON-like). Tables are SQL terminology. \u0026ldquo;update replaces the document\u0026rdquo; Without $set, yes! Always use { $set: {...} } to update specific fields. \u0026ldquo;React replaces HTML\u0026rdquo; React compiles TO HTML. The browser still renders HTML in the end. Key Takeaways The DOM is the browser\u0026rsquo;s live tree of HTML elements — JS can read/modify it Raw DOM manipulation doesn\u0026rsquo;t scale → React abstracts it with state + reconciliation MongoDB stores JSON documents in collections; Mongoose adds schema validation CRUD = Create, Read, Update, Delete — the four database primitives Always use $set in MongoDB updates to avoid accidentally overwriting whole documents ","permalink":"/posts/04-dom-mongodb/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eBlog Summary:\u003c/strong\u003e Transitions from backend to frontend. Explores raw DOM manipulation, its limitations, and why React exists. Also covers MongoDB/Mongoose in depth with a complete full-stack data model.\u003c/p\u003e\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-browser-javascript--the-dom\"\u003e1. Browser JavaScript \u0026amp; the DOM\u003c/h2\u003e\n\u003cp\u003e[SOURCE — COURSE MATERIAL]\u003c/p\u003e\n\u003ch3 id=\"what-is-the-dom\"\u003eWhat Is the DOM?\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eDocument Object Model\u003c/strong\u003e — the browser\u0026rsquo;s in-memory representation of a web page as a tree of objects.\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eHTML File:                    DOM Tree:\n\u0026lt;html\u0026gt;                          document\n  \u0026lt;body\u0026gt;                         └── html\n    \u0026lt;div id=\u0026#34;app\u0026#34;\u0026gt;                    └── body\n      \u0026lt;h1\u0026gt;Hello\u0026lt;/h1\u0026gt;                       └── div#app\n      \u0026lt;button\u0026gt;Click\u0026lt;/button\u0026gt;                    ├── h1 (\u0026#34;Hello\u0026#34;)\n    \u0026lt;/div\u0026gt;                                      └── button (\u0026#34;Click\u0026#34;)\n  \u0026lt;/body\u0026gt;\n\u0026lt;/html\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eJavaScript can read and modify this tree in real-time → \u003cstrong\u003edynamic web pages\u003c/strong\u003e.\u003c/p\u003e","title":"04 : DOM, Why Frontend Frameworks \u0026 MongoDB Deep Dive"},{"content":" We\u0026rsquo;ve built four games. We know the loop, the physics, pixel data, tile maps. This final part doesn\u0026rsquo;t build a game \u0026ndash; it goes sideways into something more visually interesting. We\u0026rsquo;re going to fake 3D on a 2D canvas using just math, and then we\u0026rsquo;re going to turn live webcam video into ASCII art in real time.\nBoth of these will make people ask \u0026ldquo;wait, how did you do that?\u0026rdquo; when they see them. Good.\nTransforms \u0026ndash; translate, rotate, scale You\u0026rsquo;ve seen ctx.save() and ctx.restore() already. The transform functions they protect are:\nctx.translate(x, y); // move the origin point ctx.rotate(angle); // rotate around origin (radians) ctx.scale(sx, sy); // scale on x and y axes The key thing to understand is these transform the coordinate system, not the objects. When you call ctx.translate(100, 100), you\u0026rsquo;re saying \u0026ldquo;from now on, treat (100, 100) as if it were (0, 0)\u0026rdquo;. Then when you draw at (0, 0), it appears at (100, 100) on the actual canvas.\nThis is confusing at first but it makes certain things much cleaner. Drawing a rotated object around its own center:\nctx.save(); ctx.translate(objectX, objectY); // move origin to object center ctx.rotate(angle); // rotate around that center ctx.fillRect(-width / 2, -height / 2, width, height); // draw centered at origin ctx.restore(); Without translate you\u0026rsquo;d have to calculate the rotated corners manually every time. With translate you just rotate and draw at 0,0.\nThe transform stack save() pushes the current state onto a stack. restore() pops it. You can nest them:\nctx.save(); // A ctx.translate(100, 100); ctx.save(); // B ctx.rotate(0.5); ctx.fillRect(-20, -20, 40, 40); ctx.restore(); // back to A state (translation still active) ctx.fillRect(0, 0, 10, 10); // drawn at (100, 100) but not rotated ctx.restore(); // back to no transforms This is how you build hierarchical transforms \u0026ndash; like a robot arm where rotating the shoulder also rotates the forearm. Each segment saves, transforms relative to its parent, draws, restores.\nThe 3D trick \u0026ndash; projection Here\u0026rsquo;s the thing about 3D rendering: it\u0026rsquo;s not magic. It\u0026rsquo;s math on points.\nA 3D point has three coordinates (x, y, z). To draw it on a 2D screen you project it \u0026ndash; you calculate where it would appear if you were looking at it through a camera.\nThe simplest projection is perspective projection. Objects farther away appear smaller. The formula is:\nscreenX = x * focalLength / (z + focalLength) + centerX screenY = y * focalLength / (z + focalLength) + centerY focalLength controls how strong the perspective effect is. Higher = more telephoto (flatter). Lower = more fisheye (more distorted).\nIn code:\nfunction project(x, y, z, focalLength, cx, cy) { const scale = focalLength / (z + focalLength); return { x: x * scale + cx, y: y * scale + cy, scale, // useful for size scaling and depth sorting }; } That\u0026rsquo;s actually the core of 3D rendering. Everything else \u0026ndash; rotation matrices, lighting, textures \u0026ndash; is built on top of this basic projection.\nRotation matrices To rotate a 3D object you apply rotation matrices. For a cube spinning around the Y axis:\nfunction rotateY(x, y, z, angle) { const cos = Math.cos(angle); const sin = Math.sin(angle); return { x: x * cos - z * sin, y: y, z: x * sin + z * cos, }; } function rotateX(x, y, z, angle) { const cos = Math.cos(angle); const sin = Math.sin(angle); return { x: x, y: y * cos - z * sin, z: y * sin + z * cos, }; } You feed a 3D point in, you get a rotated 3D point back. Then you project it to 2D and draw it.\n// puzzle 05 =\u003e think before reading A cube has 8 corners. Write down their 3D coordinates if the cube is centered at the origin and has a side length of 2. (Hint: each coordinate is either -1 or +1.)\nThen think: to draw the edges of the cube, which pairs of corners need to be connected? A cube has 12 edges. How do you know which 8 corners are connected by those 12 edges?\nSpend a minute on this before reading. Drawing the wireframe cube is just drawing lines between the right pairs of projected points. If you have the corner list and the edge list, the code is five lines.\nBuilding the 3D rotating cube // project 05a — wireframe 3D cube (no library, no WebGL) const canvas = document.getElementById(\u0026#34;c\u0026#34;); const ctx = canvas.getContext(\u0026#34;2d\u0026#34;); const W = (canvas.width = 500); const H = (canvas.height = 500); // 8 corners of a unit cube centered at origin const vertices = [ [-1, -1, -1], // 0: left bottom back [1, -1, -1], // 1: right bottom back [1, 1, -1], // 2: right top back [-1, 1, -1], // 3: left top back [-1, -1, 1], // 4: left bottom front [1, -1, 1], // 5: right bottom front [1, 1, 1], // 6: right top front [-1, 1, 1], // 7: left top front ]; // pairs of vertex indices forming edges const edges = [ [0, 1], [1, 2], [2, 3], [3, 0], // back face [4, 5], [5, 6], [6, 7], [7, 4], // front face [0, 4], [1, 5], [2, 6], [3, 7], // connecting edges ]; const FOCAL = 400; const SCALE = 100; // cube size let angleX = 0.4; let angleY = 0; function project(x, y, z) { const scale = FOCAL / (z + FOCAL); return { x: x * scale + W / 2, y: y * scale + H / 2, z }; } function rotateX(v, a) { return [ v[0], v[1] * Math.cos(a) - v[2] * Math.sin(a), v[1] * Math.sin(a) + v[2] * Math.cos(a), ]; } function rotateY(v, a) { return [ v[0] * Math.cos(a) + v[2] * Math.sin(a), v[1], -v[0] * Math.sin(a) + v[2] * Math.cos(a), ]; } function loop(ts) { angleY += 0.01; ctx.fillStyle = \u0026#34;#0d110c\u0026#34;; ctx.fillRect(0, 0, W, H); // transform all vertices const projected = vertices.map((v) =\u0026gt; { let p = rotateX(v, angleX); p = rotateY(p, angleY); p = [p[0] * SCALE, p[1] * SCALE, p[2] * SCALE]; return project(p[0], p[1], p[2]); }); // draw edges ctx.strokeStyle = \u0026#34;#86efac\u0026#34;; ctx.lineWidth = 1.5; edges.forEach(([a, b]) =\u0026gt; { const pa = projected[a]; const pb = projected[b]; // fade edges based on depth (z) for a subtle depth cue const avgZ = (pa.z + pb.z) / 2; const alpha = 0.3 + (0.7 * (avgZ + SCALE)) / (SCALE * 2); ctx.strokeStyle = `rgba(134, 239, 172, ${alpha})`; ctx.beginPath(); ctx.moveTo(pa.x, pa.y); ctx.lineTo(pb.x, pb.y); ctx.stroke(); }); // draw vertices as dots projected.forEach((p) =\u0026gt; { ctx.beginPath(); ctx.arc(p.x, p.y, 3, 0, Math.PI * 2); ctx.fillStyle = \u0026#34;#5eead4\u0026#34;; ctx.fill(); }); requestAnimationFrame(loop); } requestAnimationFrame(loop); Run that. You\u0026rsquo;ll see a wireframe cube rotating in 3D. On a completely flat 2D canvas. No WebGL, no library. Just 8 points, a rotation matrix, and a projection formula.\nMaking it solid \u0026ndash; depth sorting The wireframe looks cool but to fill the faces you need to draw them back-to-front (painter\u0026rsquo;s algorithm \u0026ndash; same as the painter\u0026rsquo;s model we talked about in Part 1). Sort faces by their average Z depth and draw the farthest first:\nconst faces = [ [0, 1, 2, 3], // back [4, 5, 6, 7], // front [0, 3, 7, 4], // left [1, 2, 6, 5], // right [3, 2, 6, 7], // top [0, 1, 5, 4], // bottom ]; const faceColors = [ \u0026#34;#1a3a2a\u0026#34;, \u0026#34;#2a5a3a\u0026#34;, \u0026#34;#1a4a3a\u0026#34;, \u0026#34;#2a4a2a\u0026#34;, \u0026#34;#3a6a4a\u0026#34;, \u0026#34;#1a2a1a\u0026#34;, ]; function drawSolidCube(projected) { const sortedFaces = faces .map((f, i) =\u0026gt; ({ indices: f, color: faceColors[i], depth: f.reduce((sum, vi) =\u0026gt; sum + projected[vi].z, 0) / f.length, })) .sort((a, b) =\u0026gt; a.depth - b.depth); // back to front sortedFaces.forEach(({ indices, color }) =\u0026gt; { ctx.beginPath(); ctx.moveTo(projected[indices[0]].x, projected[indices[0]].y); for (let i = 1; i \u0026lt; indices.length; i++) { ctx.lineTo(projected[indices[i]].x, projected[indices[i]].y); } ctx.closePath(); ctx.fillStyle = color; ctx.fill(); ctx.strokeStyle = \u0026#34;#86efac\u0026#34;; ctx.lineWidth = 1; ctx.stroke(); }); } Add mouse drag to control the rotation and you have a proper interactive 3D object. This is the actual foundation of how software renderers work. Games like Quake were doing this math (much more of it) in real time on CPUs with no GPU help.\nImage to ASCII Now we use everything from Part 4 \u0026ndash; pixel data \u0026ndash; to do something creative.\nThe idea: load an image, sample its pixels, measure the brightness of each sample, then replace it with an ASCII character where dense characters (like @, #, M) represent dark areas and light characters (like ., ) represent bright areas.\nconst ASCII_CHARS = \u0026#34;@#S%?*+;:,. \u0026#34;; // dark to light const SAMPLE_SIZE = 8; // pixels per character function brightnessToChar(brightness) { const index = Math.floor((brightness / 255) * (ASCII_CHARS.length - 1)); return ASCII_CHARS[index]; } function imageToAscii(img) { // draw image to canvas const offscreen = document.createElement(\u0026#34;canvas\u0026#34;); const cols = Math.floor(img.width / SAMPLE_SIZE); const rows = Math.floor(img.height / SAMPLE_SIZE); offscreen.width = img.width; offscreen.height = img.height; const offCtx = offscreen.getContext(\u0026#34;2d\u0026#34;); offCtx.drawImage(img, 0, 0); const imageData = offCtx.getImageData(0, 0, img.width, img.height); const pixels = imageData.data; const result = []; for (let row = 0; row \u0026lt; rows; row++) { let line = \u0026#34;\u0026#34;; for (let col = 0; col \u0026lt; cols; col++) { const px = col * SAMPLE_SIZE; const py = row * SAMPLE_SIZE; const i = (py * img.width + px) * 4; const r = pixels[i]; const g = pixels[i + 1]; const b = pixels[i + 2]; // perceived brightness (same weights as grayscale conversion) const brightness = 0.299 * r + 0.587 * g + 0.114 * b; line += brightnessToChar(brightness); } result.push(line); } return result; } Now draw the ASCII art to canvas:\nfunction drawAscii(lines, canvas, ctx) { const charW = 6; const charH = 10; canvas.width = lines[0].length * charW; canvas.height = lines.length * charH; ctx.fillStyle = \u0026#34;#0d110c\u0026#34;; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.font = \u0026#34;9px JetBrains Mono\u0026#34;; ctx.fillStyle = \u0026#34;#86efac\u0026#34;; ctx.textBaseline = \u0026#34;top\u0026#34;; lines.forEach((line, row) =\u0026gt; { ctx.fillText(line, 0, row * charH); }); } Result: any image becomes a grid of characters that looks exactly like the original at a distance but is made entirely of text. This is the classic ASCII art effect.\nFor a colored version, instead of using one fillStyle, you sample the actual RGB and set the color per character:\n// inside the loop, after getting r, g, b: ctx.fillStyle = `rgb(${r},${g},${b})`; ctx.fillText(char, col * charW, row * charH); Colored ASCII art looks genuinely impressive.\nLive webcam to ASCII This is basically the same thing but with a \u0026lt;video\u0026gt; element as the source instead of an image. The browser can draw a video frame to canvas directly with drawImage.\n\u0026lt;video id=\u0026#34;webcam\u0026#34; autoplay playsinline style=\u0026#34;display:none\u0026#34;\u0026gt;\u0026lt;/video\u0026gt; \u0026lt;canvas id=\u0026#34;c\u0026#34;\u0026gt;\u0026lt;/canvas\u0026gt; const video = document.getElementById(\u0026#34;webcam\u0026#34;); const canvas = document.getElementById(\u0026#34;c\u0026#34;); const ctx = canvas.getContext(\u0026#34;2d\u0026#34;); async function startWebcam() { try { const stream = await navigator.mediaDevices.getUserMedia({ video: true }); video.srcObject = stream; await video.play(); requestAnimationFrame(renderLoop); } catch (err) { console.error(\u0026#34;no webcam access:\u0026#34;, err); } } // offscreen canvas for pixel sampling const sample = document.createElement(\u0026#34;canvas\u0026#34;); const sampleCtx = sample.getContext(\u0026#34;2d\u0026#34;); const COLS = 80; const ROWS = 45; function renderLoop() { if (video.readyState \u0026gt;= video.HAVE_ENOUGH_DATA) { // draw current frame to tiny offscreen canvas for sampling sample.width = COLS; sample.height = ROWS; sampleCtx.drawImage(video, 0, 0, COLS, ROWS); const data = sampleCtx.getImageData(0, 0, COLS, ROWS).data; // size the output canvas const charW = 8; const charH = 14; canvas.width = COLS * charW; canvas.height = ROWS * charH; ctx.fillStyle = \u0026#34;#000\u0026#34;; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.font = \u0026#34;12px JetBrains Mono\u0026#34;; ctx.textBaseline = \u0026#34;top\u0026#34;; for (let row = 0; row \u0026lt; ROWS; row++) { for (let col = 0; col \u0026lt; COLS; col++) { const i = (row * COLS + col) * 4; const r = data[i]; const g = data[i + 1]; const b = data[i + 2]; const brightness = 0.299 * r + 0.587 * g + 0.114 * b; const char = ASCII_CHARS[ Math.floor((brightness / 255) * (ASCII_CHARS.length - 1)) ]; ctx.fillStyle = `rgb(${r},${g},${b})`; ctx.fillText(char, col * charW, row * charH); } } } requestAnimationFrame(renderLoop); } startWebcam(); The sampling trick (draw to a 80x45 offscreen canvas first) means you only read 3600 pixels per frame instead of the full camera resolution which could be 1920x1080. Sampling at the target resolution rather than downsampling in JS makes it fast enough for real-time.\n// on CORS and images: if you try to call getImageData on a canvas that has a cross-origin image drawn on it, you'll get a security error (\"tainted canvas\"). To avoid this, either use images from your own domain, or set img.crossOrigin = 'anonymous' and serve the image with the right CORS headers. The webcam version doesn't have this issue because video input isn't a cross-origin resource. Where canvas ends and WebGL begins Honestly, canvas can do a lot more than most people think. But it does have limits worth knowing:\nCanvas is great for:\n2D games (everything we built) Data visualization Image processing Generative art Anything under ~100k draw calls per frame Canvas starts to struggle with:\nComplex 3D scenes (more faces, lighting, shadows \u0026ndash; the math gets expensive) Particle systems with millions of particles Heavy real-time image filters Anything that benefits from GPU parallelism That\u0026rsquo;s where WebGL comes in. WebGL runs code directly on the GPU, which is massively parallel. The 3D cube we built updates 8 points per frame on the CPU. A real 3D scene might have millions of polygons \u0026ndash; you need GPU for that.\nBut here\u0026rsquo;s what I want you to take away: WebGL\u0026rsquo;s fundamental ideas are exactly what we covered. Vertices, transformations, projection, the painter\u0026rsquo;s algorithm \u0026ndash; all the same. The difference is that WebGL\u0026rsquo;s API is lower-level and the code runs on the GPU instead of the CPU.\nIf you ever want to go down that path, everything in Part 5 is directly applicable.\n// checkpoint -- part 05 and the whole series I understand ctx.translate, rotate, scale and the transform stack I understand perspective projection (3D point to 2D screen) I can apply rotation matrices to 3D points I built a fake-3D rotating cube with depth sorting I understand pixel brightness sampling and ASCII mapping I built an image-to-ASCII converter I built the live webcam ASCII renderer I know where canvas ends and when to reach for WebGL What you\u0026rsquo;ve actually built Let\u0026rsquo;s be real about what happened here. You started with a blank rectangle. Five parts later you\u0026rsquo;ve built:\nA static game frame using raw shape drawing Snake \u0026ndash; grid game, full animation loop, input, collision Flappy Bird \u0026ndash; continuous physics, procedural generation, game feel tuning Pac-Man \u0026ndash; tile maps, sprite animation, ghost AI with personalities A 3D rotating cube using only math and a 2D API An ASCII art renderer that works on images and live video None of it needed a game engine. No Phaser, no Three.js, no p5.js. Just the canvas API and JavaScript.\nThat\u0026rsquo;s not to say libraries are bad \u0026ndash; they\u0026rsquo;re not. But now you understand what they\u0026rsquo;re abstracting. When you pick up Phaser for a bigger project, you\u0026rsquo;ll know what\u0026rsquo;s happening under the hood, and that makes a real difference when things break or when you want to do something the library doesn\u0026rsquo;t support.\n// series complete If you worked through all five parts properly -- actually coding, doing the puzzles, trying the extension ideas -- you now have a genuinely solid grip on the HTML Canvas API.\nSome good next directions: OffscreenCanvas + Web Workers for heavy rendering off the main thread. WebGL fundamentals (the site webglfundamentals.org is excellent). Generative art -- use everything you've learned but with no rules, just make things that look interesting. That last one is underrated.\nThere's no better way to solidify this than building something you actually want to make. Go do that.\n","permalink":"/canvas/canvas-05-3d-ascii/","summary":"\u003c!--\n  NOTE FOR HUGO SETUP:\n  unsafe: true required in markup.goldmark.renderer\n--\u003e\n\u003cstyle\u003e\n.cv-post { --cv-green:#86efac;--cv-cyan:#5eead4;--cv-amber:#fbbf24;--cv-red:#fca5a5; }\n.cv-puzzle { background:rgba(251,191,36,.04);border:1px solid rgba(251,191,36,.2);border-left:3px solid #fbbf24;border-radius:6px;padding:20px 24px;margin:32px 0; }\n.cv-puzzle-label { font-family:'JetBrains Mono',monospace;font-size:.68rem;letter-spacing:.14em;text-transform:uppercase;color:#fbbf24;margin-bottom:12px; }\n.cv-puzzle p,.cv-puzzle li { color:#c4a855;font-size:.94rem; }\n.cv-puzzle strong { color:#fbbf24; }\n.cv-puzzle code { background:rgba(251,191,36,.08);border:1px solid rgba(251,191,36,.2);padding:1px 6px;border-radius:3px;font-size:.85em; }\n.cv-checkpoint { background:rgba(134,239,172,.03);border:1px solid rgba(134,239,172,.18);border-left:3px solid #86efac;border-radius:6px;padding:20px 24px;margin:32px 0; }\n.cv-cp-label { font-family:'JetBrains Mono',monospace;font-size:.68rem;letter-spacing:.14em;text-transform:uppercase;color:#86efac;margin-bottom:14px; }\n.cv-checkpoint ul { list-style:none;padding:0;margin:0; }\n.cv-checkpoint ul li { display:flex;align-items:flex-start;gap:10px;font-size:.9rem;color:#8aad8e;margin-bottom:8px;cursor:pointer; }\n.cv-cb { width:15px;height:15px;border:1px solid #2a4a2e;border-radius:2px;flex-shrink:0;margin-top:2px;background:#0d110c;display:flex;align-items:center;justify-content:center;transition:all .15s;font-size:9px;font-weight:bold;color:transparent; }\n.cv-cb.done { background:#86efac;border-color:#86efac;color:#0d110c; }\n.cv-note { background:rgba(94,234,212,.04);border:1px solid rgba(94,234,212,.18);border-left:3px solid #5eead4;border-radius:6px;padding:16px 22px;margin:24px 0;font-size:.93rem;color:#78b8b0; }\n.cv-note strong { color:#5eead4; }\n.cv-note code { background:rgba(94,234,212,.08);padding:1px 6px;border-radius:3px;font-size:.85em; }\n.cv-project { background:rgba(134,239,172,.02);border:1px solid rgba(134,239,172,.12);border-radius:8px;padding:24px 28px;margin:36px 0; }\n.cv-project-header { font-family:'JetBrains Mono',monospace;font-size:.7rem;letter-spacing:.16em;text-transform:uppercase;color:#86efac;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid rgba(134,239,172,.1); }\n.cv-project p { color:#9ab89e;font-size:.94rem; }\n.cv-finish {\n  background: linear-gradient(135deg, rgba(134,239,172,0.05), rgba(94,234,212,0.05));\n  border: 1px solid rgba(134,239,172,0.25);\n  border-radius: 10px;\n  padding: 28px 32px;\n  margin: 48px 0 0 0;\n  text-align: center;\n}\n.cv-finish h3 { color: #86efac; font-family: 'Space Grotesk', sans-serif; margin: 0 0 12px 0; }\n.cv-finish p { color: #788571; font-size: 0.92rem; margin: 0 0 8px 0; }\n.cv-finish strong { color: #c8d1c1; }\n\u003c/style\u003e\n\u003cdiv class=\"cv-post\"\u003e\n\u003cp\u003eWe\u0026rsquo;ve built four games. We know the loop, the physics, pixel data, tile maps. This final part doesn\u0026rsquo;t build a game \u0026ndash; it goes sideways into something more visually interesting. We\u0026rsquo;re going to fake 3D on a 2D canvas using just math, and then we\u0026rsquo;re going to turn live webcam video into ASCII art in real time.\u003c/p\u003e","title":"Canvas 05 : Fake Depth, Real Math"},{"content":" Blog Summary: Mastering React\u0026rsquo;s core mental model — state, components, JSX, and re-rendering. Understanding how to structure apps and why React re-renders when and how it does.\n1. The React Mental Model [SOURCE — COURSE MATERIAL]\nEvery frontend app has two things:\nState Components ───── ────────── The data The view function (what changes) state → rendered HTML Key insight: You never manipulate the DOM directly. You update state. React figures out the DOM.\nDeveloper updates state → React reconciler calculates diff → React updates DOM Analogy: You are the CA\u0026rsquo;s client. You provide updated financial data (state). The CA (React) re-calculates your taxes (DOM) and files it for you.\n2. React vs Vanilla JS — Side by Side [SOURCE — COURSE MATERIAL]\nVanilla JS Counter (the hard way) let count = 0; function updateCounter() { document.getElementById(\u0026#34;counter\u0026#34;).textContent = count; } document.getElementById(\u0026#34;increment\u0026#34;).addEventListener(\u0026#34;click\u0026#34;, () =\u0026gt; { count++; updateCounter(); }); React Counter (the clean way) import { useState } from \u0026#34;react\u0026#34;; function Counter() { const [count, setCount] = useState(0); return ( \u0026lt;div\u0026gt; \u0026lt;p\u0026gt;{count}\u0026lt;/p\u0026gt; \u0026lt;button onClick={() =\u0026gt; setCount(count + 1)}\u0026gt;Increment\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; ); } Difference: In React, you just call setCount. React handles the DOM update.\n3. JSX — JavaScript + HTML Syntax [SOURCE — COURSE MATERIAL]\nJSX is syntactic sugar. It looks like HTML but is JavaScript.\n// JSX (what you write) const element = \u0026lt;h1 className=\u0026#34;title\u0026#34;\u0026gt;Hello\u0026lt;/h1\u0026gt;; // What Babel compiles it to: const element = React.createElement(\u0026#34;h1\u0026#34;, { className: \u0026#34;title\u0026#34; }, \u0026#34;Hello\u0026#34;); JSX Rules // 1. Must return ONE root element // BAD: return ( \u0026lt;h1\u0026gt;Title\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;Text\u0026lt;/p\u0026gt; ); // GOOD: wrap in a div or Fragment return ( \u0026lt;div\u0026gt; \u0026lt;h1\u0026gt;Title\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;Text\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; ); // Or use Fragment (no extra DOM node) return ( \u0026lt;\u0026gt; \u0026lt;h1\u0026gt;Title\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;Text\u0026lt;/p\u0026gt; \u0026lt;/\u0026gt; ); // 2. class → className \u0026lt;div className=\u0026#34;box\u0026#34;\u0026gt;...\u0026lt;/div\u0026gt; // 3. JavaScript expressions in curly braces const name = \u0026#34;Alice\u0026#34;; \u0026lt;h1\u0026gt;Hello, {name}!\u0026lt;/h1\u0026gt; // 4. Self-closing tags must close \u0026lt;input /\u0026gt; // not \u0026lt;input\u0026gt; \u0026lt;img src=\u0026#34;...\u0026#34; /\u0026gt; // 5. Event handlers use camelCase \u0026lt;button onClick={handleClick}\u0026gt;Click\u0026lt;/button\u0026gt; // not onclick \u0026lt;input onChange={handleChange} /\u0026gt; 4. Components [SOURCE — COURSE MATERIAL]\nA component is a reusable function that returns JSX.\n// Functional Component (modern standard) function Button({ label, onClick, color }) { return ( \u0026lt;button onClick={onClick} style={{ backgroundColor: color }}\u0026gt; {label} \u0026lt;/button\u0026gt; ); } // Props — data passed TO a component (read-only!) function App() { return ( \u0026lt;div\u0026gt; \u0026lt;Button label=\u0026#34;Save\u0026#34; onClick={() =\u0026gt; save()} color=\u0026#34;green\u0026#34; /\u0026gt; \u0026lt;Button label=\u0026#34;Delete\u0026#34; onClick={() =\u0026gt; remove()} color=\u0026#34;red\u0026#34; /\u0026gt; \u0026lt;/div\u0026gt; ); } Component Composition // Components can contain other components function Card({ children }) { return \u0026lt;div className=\u0026#34;card\u0026#34;\u0026gt;{children}\u0026lt;/div\u0026gt;; } function UserCard({ name, email }) { return ( \u0026lt;Card\u0026gt; \u0026lt;h2\u0026gt;{name}\u0026lt;/h2\u0026gt; \u0026lt;p\u0026gt;{email}\u0026lt;/p\u0026gt; \u0026lt;/Card\u0026gt; ); } function App() { return ( \u0026lt;div\u0026gt; \u0026lt;UserCard name=\u0026#34;Alice\u0026#34; email=\u0026#34;alice@test.com\u0026#34; /\u0026gt; \u0026lt;UserCard name=\u0026#34;Bob\u0026#34; email=\u0026#34;bob@test.com\u0026#34; /\u0026gt; \u0026lt;/div\u0026gt; ); } 5. useState — State Management [SOURCE — COURSE MATERIAL]\nimport { useState } from \u0026#34;react\u0026#34;; function TodoApp() { // [currentValue, setterFunction] = useState(initialValue) const [todos, setTodos] = useState([]); const [inputText, setInputText] = useState(\u0026#34;\u0026#34;); function addTodo() { if (!inputText.trim()) return; setTodos([...todos, { text: inputText, done: false }]); setInputText(\u0026#34;\u0026#34;); } function toggleTodo(index) { const newTodos = todos.map((todo, i) =\u0026gt; i === index ? { ...todo, done: !todo.done } : todo, ); setTodos(newTodos); } return ( \u0026lt;div\u0026gt; \u0026lt;input value={inputText} onChange={(e) =\u0026gt; setInputText(e.target.value)} placeholder=\u0026#34;Add a todo...\u0026#34; /\u0026gt; \u0026lt;button onClick={addTodo}\u0026gt;Add\u0026lt;/button\u0026gt; \u0026lt;ul\u0026gt; {todos.map((todo, index) =\u0026gt; ( \u0026lt;li key={index} onClick={() =\u0026gt; toggleTodo(index)} style={{ textDecoration: todo.done ? \u0026#34;line-through\u0026#34; : \u0026#34;none\u0026#34; }} \u0026gt; {todo.text} \u0026lt;/li\u0026gt; ))} \u0026lt;/ul\u0026gt; \u0026lt;/div\u0026gt; ); } Critical Rules for State // ❌ WRONG — mutating state directly (React won\u0026#39;t re-render) todos.push(newTodo); setTodos(todos); // same reference, no re-render // ✅ CORRECT — create a NEW array setTodos([...todos, newTodo]); // ❌ WRONG — mutating an object user.name = \u0026#34;Alice\u0026#34;; setUser(user); // same reference // ✅ CORRECT — spread operator setUser({ ...user, name: \u0026#34;Alice\u0026#34; }); 6. Re-Rendering — When and Why [SOURCE — COURSE MATERIAL]\nA component re-renders when:\nIts own state variable changes Its parent re-renders (even if props didn\u0026rsquo;t change) Its props change function Parent() { const [count, setCount] = useState(0); return ( \u0026lt;div\u0026gt; \u0026lt;button onClick={() =\u0026gt; setCount((c) =\u0026gt; c + 1)}\u0026gt;+\u0026lt;/button\u0026gt; \u0026lt;Child /\u0026gt; {/* This ALSO re-renders when Parent does! */} \u0026lt;/div\u0026gt; ); } function Child() { console.log(\u0026#34;Child re-rendered\u0026#34;); // runs every time Parent re-renders return \u0026lt;p\u0026gt;I am Child\u0026lt;/p\u0026gt;; } State Object for a Counter App // Example state shape const appState = { currentCount: 5, }; // LinkedIn topbar state const topbarState = { notificationCount: 7, jobsCount: 3, messagingCount: 2, networkCount: 12, }; 7. Connecting Frontend to Backend [SOURCE — COURSE MATERIAL]\nimport { useState, useEffect } from \u0026#34;react\u0026#34;; function UserList() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); useEffect(() =\u0026gt; { fetch(\u0026#34;https://api.example.com/users\u0026#34;, { headers: { Authorization: `Bearer ${localStorage.getItem(\u0026#34;token\u0026#34;)}`, }, }) .then((res) =\u0026gt; res.json()) .then((data) =\u0026gt; { setUsers(data.users); setLoading(false); }); }, []); // empty array = run once on mount if (loading) return \u0026lt;div\u0026gt;Loading...\u0026lt;/div\u0026gt;; return ( \u0026lt;ul\u0026gt; {users.map((user) =\u0026gt; ( \u0026lt;li key={user.id}\u0026gt;{user.name}\u0026lt;/li\u0026gt; ))} \u0026lt;/ul\u0026gt; ); } 8. Creating a React App [SOURCE — COURSE MATERIAL]\n# Vite (recommended — fast) npm create vite@latest my-app -- --template react cd my-app npm install npm run dev # development server npm run build # production build Project Structure my-app/ ├── src/ │ ├── App.jsx ← main component │ ├── main.jsx ← entry point (renders App) │ └── components/ ← your custom components ├── public/ │ └── index.html └── package.json Exercises Quick (15 min) Build a counter with:\nDisplay current count Increment button (+1) Decrement button (-1) Reset button (back to 0) Counter turns red if negative Hint 1: style={{ color: count \u0026lt; 0 ? 'red' : 'black' }}\nHint 2: Three separate onClick handlers\nIntermediate (45 min) Build a color picker:\nThree sliders for R, G, B (0–255) A box that updates its background color based on slider values Display the hex value below the box Hint 1: rgb(${r}, ${g}, ${b})\nHint 2: Convert to hex: r.toString(16).padStart(2, '0')\nChallenge (2–3 hours) Build a full CRUD todo app connected to your backend:\nShow todos fetched from API Add todo (POST to backend) Mark complete (PUT to backend) Delete todo (DELETE from backend) Loading state + error handling Common Confusions Confusion Reality \u0026ldquo;I can modify props inside a component\u0026rdquo; Props are read-only. Only parent can change them. \u0026ldquo;State changes are synchronous\u0026rdquo; No. setState is async. New value not available until next render. \u0026ldquo;Class and className are the same\u0026rdquo; class is reserved in JS. JSX uses className. \u0026ldquo;key can be array index\u0026rdquo; Avoid. Use unique IDs. Index causes bugs when list order changes. \u0026ldquo;A component re-renders only when its state changes\u0026rdquo; Also when parent re-renders. Key Takeaways React = State + Components. You manage state, React manages DOM. JSX compiles to React.createElement() — it\u0026rsquo;s just JavaScript State is immutable — always create new arrays/objects with spread operator Components are functions that return JSX and accept props Every state change triggers a re-render of that component and all its children useEffect is for side effects (API calls, timers) — covered deeply in Week 6 ","permalink":"/posts/05-react-deep-dive/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eBlog Summary:\u003c/strong\u003e Mastering React\u0026rsquo;s core mental model — state, components, JSX, and re-rendering. Understanding how to structure apps and why React re-renders when and how it does.\u003c/p\u003e\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-the-react-mental-model\"\u003e1. The React Mental Model\u003c/h2\u003e\n\u003cp\u003e[SOURCE — COURSE MATERIAL]\u003c/p\u003e\n\u003cp\u003eEvery frontend app has two things:\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eState              Components\n─────              ──────────\nThe data           The view function\n(what changes)     state → rendered HTML\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003cstrong\u003eKey insight:\u003c/strong\u003e You never manipulate the DOM directly. You update state. React figures out the DOM.\u003c/p\u003e","title":"05 : React Deep Dive"},{"content":" Blog Summary: Deep dive into React\u0026rsquo;s hook system. Covers when and why each hook exists, common pitfalls, reconciliation internals, and performance optimization patterns.\n1. Reconciliation — How React Updates the DOM [SOURCE — COURSE MATERIAL]\nThe CA Analogy You = developer (provide state/data) CA (Chartered Accountant) = React reconciler Bank statements = state Tax filing = DOM update React receives your updated state and calculates what changed in the DOM — it doesn\u0026rsquo;t re-create everything.\nState change → React compares old Virtual DOM vs new Virtual DOM → Finds minimal set of changes (diffing) → Applies only those changes to real DOM What Is a Re-render? The component function gets called again React computes new virtual DOM React diffs old vs new virtual DOM Updates only changed real DOM nodes [ADDED — EXPLANATION] You can verify a re-render by adding console.log('Rendered') inside a component. Every time it logs, the component re-rendered.\nHow to Minimize Re-renders Principle: Keep state as low in the tree as possible // BAD — App re-renders every time header changes function App() { const [headerTitle, setHeaderTitle] = useState(\u0026#34;Hello\u0026#34;); return ( \u0026lt;\u0026gt; \u0026lt;Header title={headerTitle} /\u0026gt; \u0026lt;ExpensiveComponent /\u0026gt; {/* re-renders unnecessarily */} \u0026lt;/\u0026gt; ); } // GOOD — Push state down to Header function Header() { const [title, setTitle] = useState(\u0026#34;Hello\u0026#34;); return \u0026lt;h1\u0026gt;{title}\u0026lt;/h1\u0026gt;; } function App() { return ( \u0026lt;\u0026gt; \u0026lt;Header /\u0026gt; \u0026lt;ExpensiveComponent /\u0026gt; {/* never re-renders */} \u0026lt;/\u0026gt; ); } React.memo — Prevent Unnecessary Child Re-renders import { memo } from \u0026#34;react\u0026#34;; // Without memo: re-renders whenever parent does // With memo: only re-renders if its props change const ExpensiveChild = memo(function ExpensiveChild({ data }) { console.log(\u0026#34;Child rendered\u0026#34;); return \u0026lt;div\u0026gt;{data}\u0026lt;/div\u0026gt;; }); 2. Component Return \u0026amp; Fragments [SOURCE — COURSE MATERIAL]\n// Must return single root element // Option 1: wrap in div (adds extra DOM node) return ( \u0026lt;div\u0026gt; \u0026lt;h1\u0026gt;Title\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;Content\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; ); // Option 2: Fragment (no extra DOM node — preferred) return ( \u0026lt;\u0026gt; \u0026lt;h1\u0026gt;Title\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;Content\u0026lt;/p\u0026gt; \u0026lt;/\u0026gt; ); // Option 3: explicit Fragment (when you need a key) return ( \u0026lt;React.Fragment key={id}\u0026gt; \u0026lt;h1\u0026gt;Title\u0026lt;/h1\u0026gt; \u0026lt;/React.Fragment\u0026gt; ); 3. Keys in Lists [SOURCE — COURSE MATERIAL]\nKeys help React identify which items changed, added, or removed.\n// ❌ BAD — using index as key { todos.map((todo, index) =\u0026gt; \u0026lt;TodoItem key={index} todo={todo} /\u0026gt;); } // Problem: if order changes, React thinks wrong items updated // ✅ GOOD — use stable unique ID { todos.map((todo) =\u0026gt; \u0026lt;TodoItem key={todo.id} todo={todo} /\u0026gt;); } Why keys matter:\nOld list: [A(id:1), B(id:2), C(id:3)] New list: [B(id:2), A(id:1), C(id:3)] ← reordered With IDs: React moves elements (efficient) Without IDs (index): React re-renders all (wasteful) 4. Wrapper / Children Components [SOURCE — COURSE MATERIAL]\n// Card wrapper component function Card({ children, title }) { return ( \u0026lt;div className=\u0026#34;card-wrapper\u0026#34;\u0026gt; \u0026lt;div className=\u0026#34;card-header\u0026#34;\u0026gt;{title}\u0026lt;/div\u0026gt; \u0026lt;div className=\u0026#34;card-body\u0026#34;\u0026gt; {children} {/* render whatever is passed inside */} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ); } // Usage — pass any JSX as children function App() { return ( \u0026lt;Card title=\u0026#34;User Info\u0026#34;\u0026gt; \u0026lt;p\u0026gt;Name: Alice\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;Email: alice@example.com\u0026lt;/p\u0026gt; \u0026lt;/Card\u0026gt; ); } 5. Hooks Overview [SOURCE — COURSE MATERIAL]\nHooks are functions starting with use that \u0026ldquo;hook into\u0026rdquo; React features from functional components.\nCommon hooks: ├── useState — manage component state ├── useEffect — run side effects ├── useMemo — memoize expensive computations ├── useCallback — memoize function references ├── useRef — mutable ref that doesn\u0026#39;t trigger re-renders └── useContext — consume context values (Week 7) Rules of Hooks:\nOnly call hooks at the top level (not inside loops/conditions) Only call hooks from React functions (not regular JS) 6. useEffect [SOURCE — COURSE MATERIAL]\nWhat Is a Side Effect? Anything that reaches outside the component:\nAPI calls (fetch data) Timers (setTimeout, setInterval) Manual DOM manipulation Event listener registration Car race analogy: You\u0026rsquo;re racing 100 laps. A pit stop is a side effect — you do it from time to time, not every lap.\nSyntax useEffect(() =\u0026gt; { // side effect code here return () =\u0026gt; { // cleanup (optional) — runs when component unmounts or deps change }; }, [dependency1, dependency2]); // dependency array Dependency Array Behaviors // 1. Empty array [] — runs ONCE on mount (component appears) useEffect(() =\u0026gt; { fetchData(); }, []); // 2. No array — runs on EVERY render (usually a bug) useEffect(() =\u0026gt; { console.log(\u0026#34;Rendered!\u0026#34;); }); // ← no array // 3. With dependencies — runs when listed values change useEffect(() =\u0026gt; { fetchUserData(userId); }, [userId]); // re-runs whenever userId changes Fetching Data Example function TodoList() { const [todos, setTodos] = useState([]); const [loading, setLoading] = useState(true); useEffect(() =\u0026gt; { async function fetchTodos() { const res = await fetch(\u0026#34;https://sum-server.100xdevs.com/todos\u0026#34;); const data = await res.json(); setTodos(data.todos); setLoading(false); } fetchTodos(); }, []); // once on mount if (loading) return \u0026lt;p\u0026gt;Loading...\u0026lt;/p\u0026gt;; return ( \u0026lt;ul\u0026gt; {todos.map((t) =\u0026gt; ( \u0026lt;li key={t.id}\u0026gt;{t.title}\u0026lt;/li\u0026gt; ))} \u0026lt;/ul\u0026gt; ); } Todo with Changing ID function TodoDetail({ todoId }) { const [todo, setTodo] = useState(null); useEffect(() =\u0026gt; { async function fetchTodo() { const res = await fetch( `https://sum-server.100xdevs.com/todo?id=${todoId}`, ); const data = await res.json(); setTodo(data.todo); } fetchTodo(); }, [todoId]); // re-fetch when todoId changes if (!todo) return \u0026lt;p\u0026gt;Loading...\u0026lt;/p\u0026gt;; return \u0026lt;div\u0026gt;{todo.title}\u0026lt;/div\u0026gt;; } Cleanup — Preventing Memory Leaks useEffect(() =\u0026gt; { const timer = setInterval(() =\u0026gt; { setCount((c) =\u0026gt; c + 1); }, 1000); return () =\u0026gt; clearInterval(timer); // cleanup when unmounting }, []); 7. useMemo — Memoize Expensive Computations [SOURCE — COURSE MATERIAL]\nProblem: Component re-renders trigger all calculations to re-run, even if their inputs didn\u0026rsquo;t change.\n// BAD — sum recalculates on every render function App() { const [count, setCount] = useState(0); const [inputN, setInputN] = useState(0); // This runs every time count changes — even though inputN didn\u0026#39;t! let sum = 0; for (let i = 1; i \u0026lt;= inputN; i++) { sum += i; } return ( \u0026lt;div\u0026gt; \u0026lt;button onClick={() =\u0026gt; setCount((c) =\u0026gt; c + 1)}\u0026gt;{count}\u0026lt;/button\u0026gt; \u0026lt;input value={inputN} onChange={(e) =\u0026gt; setInputN(Number(e.target.value))} /\u0026gt; \u0026lt;p\u0026gt; Sum 1 to {inputN}: {sum} \u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; ); } // GOOD — memoize sum; only recalculate when inputN changes import { useMemo } from \u0026#34;react\u0026#34;; function App() { const [count, setCount] = useState(0); const [inputN, setInputN] = useState(0); const sum = useMemo(() =\u0026gt; { let total = 0; for (let i = 1; i \u0026lt;= inputN; i++) total += i; return total; }, [inputN]); // only recalculate when inputN changes return ( \u0026lt;div\u0026gt; \u0026lt;button onClick={() =\u0026gt; setCount((c) =\u0026gt; c + 1)}\u0026gt;{count}\u0026lt;/button\u0026gt; \u0026lt;input value={inputN} onChange={(e) =\u0026gt; setInputN(Number(e.target.value))} /\u0026gt; \u0026lt;p\u0026gt; Sum 1 to {inputN}: {sum} \u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; ); } Crypto analogy: You have returns from 3 exchanges. You calculated the sum and gave it to your CA. Your income report arrived. Would you recalculate the crypto sum? No — it hasn\u0026rsquo;t changed.\n8. useCallback — Memoize Function References [SOURCE — COURSE MATERIAL]\nProblem: On every render, React recreates all functions inside the component. When these functions are passed as props to child components, the children re-render unnecessarily (even with React.memo).\n// BAD — sendRequest is recreated every render function Parent() { const [count, setCount] = useState(0); const sendRequest = function () { console.log(\u0026#34;Sending request...\u0026#34;); }; // Child sees a \u0026#34;new\u0026#34; sendRequest every render → re-renders even with memo return ( \u0026lt;div\u0026gt; \u0026lt;button onClick={() =\u0026gt; setCount((c) =\u0026gt; c + 1)}\u0026gt;Count: {count}\u0026lt;/button\u0026gt; \u0026lt;Child onRequest={sendRequest} /\u0026gt; \u0026lt;/div\u0026gt; ); } // GOOD — sendRequest is stable across renders import { useCallback } from \u0026#34;react\u0026#34;; function Parent() { const [count, setCount] = useState(0); const sendRequest = useCallback(function () { console.log(\u0026#34;Sending request...\u0026#34;); }, []); // empty deps = never recreate return ( \u0026lt;div\u0026gt; \u0026lt;button onClick={() =\u0026gt; setCount((c) =\u0026gt; c + 1)}\u0026gt;Count: {count}\u0026lt;/button\u0026gt; \u0026lt;Child onRequest={sendRequest} /\u0026gt; {/* stable reference */} \u0026lt;/div\u0026gt; ); } const Child = memo(function Child({ onRequest }) { console.log(\u0026#34;Child rendered\u0026#34;); return \u0026lt;button onClick={onRequest}\u0026gt;Send\u0026lt;/button\u0026gt;; }); 9. useRef — Mutable Values Without Re-renders [SOURCE — COURSE MATERIAL]\nuseRef gives you a box that persists across renders, but changing its .current does NOT trigger a re-render.\nimport { useRef } from \u0026#34;react\u0026#34;; // Common use case 1: Accessing DOM elements directly function TextInput() { const inputRef = useRef(null); function focusInput() { inputRef.current.focus(); // directly access DOM node } return ( \u0026lt;\u0026gt; \u0026lt;input ref={inputRef} type=\u0026#34;text\u0026#34; /\u0026gt; \u0026lt;button onClick={focusInput}\u0026gt;Focus Input\u0026lt;/button\u0026gt; \u0026lt;/\u0026gt; ); } // Common use case 2: Storing previous value function Counter() { const [count, setCount] = useState(0); const prevCount = useRef(0); useEffect(() =\u0026gt; { prevCount.current = count; }); return ( \u0026lt;p\u0026gt; Now: {count}, Before: {prevCount.current} \u0026lt;/p\u0026gt; ); } // Common use case 3: Storing mutable value (e.g., timer ID) function Timer() { const [seconds, setSeconds] = useState(0); const timerRef = useRef(null); function start() { timerRef.current = setInterval(() =\u0026gt; { setSeconds((s) =\u0026gt; s + 1); }, 1000); } function stop() { clearInterval(timerRef.current); } return ( \u0026lt;\u0026gt; \u0026lt;p\u0026gt;{seconds}s\u0026lt;/p\u0026gt; \u0026lt;button onClick={start}\u0026gt;Start\u0026lt;/button\u0026gt; \u0026lt;button onClick={stop}\u0026gt;Stop\u0026lt;/button\u0026gt; \u0026lt;/\u0026gt; ); } useRef vs useState:\nuseState useRef Triggers re-render Yes No Value persists Yes Yes Use for UI state DOM refs, timers, previous values 10. Custom Hooks [SOURCE — COURSE MATERIAL]\nExtract reusable logic into custom hooks. Must start with use.\n// Custom hook — useFetch function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() =\u0026gt; { setLoading(true); fetch(url) .then((res) =\u0026gt; res.json()) .then((data) =\u0026gt; { setData(data); setLoading(false); }) .catch((err) =\u0026gt; { setError(err); setLoading(false); }); }, [url]); return { data, loading, error }; } // Using it — much cleaner! function TodoList() { const { data, loading, error } = useFetch(\u0026#34;https://api.example.com/todos\u0026#34;); if (loading) return \u0026lt;p\u0026gt;Loading...\u0026lt;/p\u0026gt;; if (error) return \u0026lt;p\u0026gt;Error: {error.message}\u0026lt;/p\u0026gt;; return ( \u0026lt;ul\u0026gt; {data.todos.map((t) =\u0026gt; ( \u0026lt;li key={t.id}\u0026gt;{t.title}\u0026lt;/li\u0026gt; ))} \u0026lt;/ul\u0026gt; ); } Exercises Quick (15 min) Create a component that shows an auto-incrementing second counter using setInterval in useEffect. Make sure to cleanup the interval.\nHint 1: return () =\u0026gt; clearInterval(id) inside useEffect\nHint 2: Use functional update setCount(c =\u0026gt; c + 1) inside interval\nIntermediate (45 min) Build an app with:\nCounter (click to increment) Input for a number N Shows sum from 1 to N using useMemo A memoized child component (use React.memo + useCallback) that only re-renders when actually needed Challenge (2–3 hours) Build a custom useDebounce hook:\nTakes a value and a delay Returns the debounced value (only updates after delay ms of no changes) Use it in a search input that fires API requests with debouncing Common Confusions Confusion Reality \u0026ldquo;useEffect with [] runs every render\u0026rdquo; No. Empty [] = runs ONCE on mount. No array = every render. \u0026ldquo;useMemo is for caching API responses\u0026rdquo; No. Use for expensive synchronous calculations. Use useEffect for API calls. \u0026ldquo;useCallback makes functions run faster\u0026rdquo; No. It memoizes the function reference to prevent unnecessary child re-renders. \u0026ldquo;useRef causes re-renders\u0026rdquo; No. Changing ref.current does NOT trigger re-render. That\u0026rsquo;s the whole point. Key Takeaways useEffect runs after render; control when with dependency array useMemo caches computed values; recomputes only when deps change useCallback caches function references; prevents unnecessary child re-renders useRef = a box that persists between renders without causing re-renders Custom hooks = extract and reuse stateful logic across components Push state down the tree to minimize re-renders; use React.memo for expensive children ","permalink":"/posts/06-react-hooks/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eBlog Summary:\u003c/strong\u003e Deep dive into React\u0026rsquo;s hook system. Covers when and why each hook exists, common pitfalls, reconciliation internals, and performance optimization patterns.\u003c/p\u003e\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-reconciliation--how-react-updates-the-dom\"\u003e1. Reconciliation — How React Updates the DOM\u003c/h2\u003e\n\u003cp\u003e[SOURCE — COURSE MATERIAL]\u003c/p\u003e\n\u003ch3 id=\"the-ca-analogy\"\u003eThe CA Analogy\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eYou\u003c/strong\u003e = developer (provide state/data)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCA (Chartered Accountant)\u003c/strong\u003e = React reconciler\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eBank statements\u003c/strong\u003e = state\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTax filing\u003c/strong\u003e = DOM update\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eReact receives your updated state and calculates \u003cstrong\u003ewhat changed\u003c/strong\u003e in the DOM — it doesn\u0026rsquo;t re-create everything.\u003c/p\u003e","title":"06 : React Hooks: useEffect, useMemo, useCallback, useRef"},{"content":" Blog Summary: Covers client-side routing for SPAs, solves the prop drilling problem with Context API, and introduces Recoil as a production-grade state management solution.\n1. Routing in React [SOURCE — COURSE MATERIAL]\nJargon First Single Page Application (SPA):\nBrowser downloads ONE HTML file React controls what to show based on URL No full page reloads when navigating Client-Side Bundle:\nAll your React code compiled into JS files Browser downloads once, runs locally Client-Side Routing:\nURL changes don\u0026rsquo;t hit the server React intercepts and renders appropriate components Traditional (Multi-Page): /home → server returns home.html /about → server returns about.html (full reload) React SPA (Single-Page): /home → React shows \u0026lt;Home /\u0026gt; (no reload) /about → React shows \u0026lt;About /\u0026gt; (no reload, URL changes) React Router DOM npm install react-router-dom import { BrowserRouter, Routes, Route, Link, useNavigate, } from \u0026#34;react-router-dom\u0026#34;; // App.jsx — define routes function App() { return ( \u0026lt;BrowserRouter\u0026gt; \u0026lt;nav\u0026gt; \u0026lt;Link to=\u0026#34;/\u0026#34;\u0026gt;Home\u0026lt;/Link\u0026gt; \u0026lt;Link to=\u0026#34;/about\u0026#34;\u0026gt;About\u0026lt;/Link\u0026gt; \u0026lt;Link to=\u0026#34;/user/123\u0026#34;\u0026gt;User 123\u0026lt;/Link\u0026gt; \u0026lt;/nav\u0026gt; \u0026lt;Routes\u0026gt; \u0026lt;Route path=\u0026#34;/\u0026#34; element={\u0026lt;Home /\u0026gt;} /\u0026gt; \u0026lt;Route path=\u0026#34;/about\u0026#34; element={\u0026lt;About /\u0026gt;} /\u0026gt; \u0026lt;Route path=\u0026#34;/user/:id\u0026#34; element={\u0026lt;UserPage /\u0026gt;} /\u0026gt; \u0026lt;Route path=\u0026#34;*\u0026#34; element={\u0026lt;NotFound /\u0026gt;} /\u0026gt; {/* catch-all */} \u0026lt;/Routes\u0026gt; \u0026lt;/BrowserRouter\u0026gt; ); } // Accessing route params import { useParams } from \u0026#34;react-router-dom\u0026#34;; function UserPage() { const { id } = useParams(); return \u0026lt;h1\u0026gt;User ID: {id}\u0026lt;/h1\u0026gt;; } // Programmatic navigation function LoginForm() { const navigate = useNavigate(); async function handleLogin() { await login(); navigate(\u0026#34;/dashboard\u0026#34;); // redirect after login } } Lazy Loading Routes import { lazy, Suspense } from \u0026#34;react\u0026#34;; // Load component only when needed (code splitting) const Dashboard = lazy(() =\u0026gt; import(\u0026#34;./Dashboard\u0026#34;)); function App() { return ( \u0026lt;BrowserRouter\u0026gt; \u0026lt;Suspense fallback={\u0026lt;div\u0026gt;Loading...\u0026lt;/div\u0026gt;}\u0026gt; \u0026lt;Routes\u0026gt; \u0026lt;Route path=\u0026#34;/dashboard\u0026#34; element={\u0026lt;Dashboard /\u0026gt;} /\u0026gt; \u0026lt;/Routes\u0026gt; \u0026lt;/Suspense\u0026gt; \u0026lt;/BrowserRouter\u0026gt; ); } 2. Prop Drilling — The Problem [SOURCE — COURSE MATERIAL]\nProp drilling = passing props through multiple component layers just to reach a deeply nested component.\n// Problem: theme and user must pass through every layer function App() { const [user, setUser] = useState({ name: \u0026#34;Alice\u0026#34; }); return \u0026lt;Layout user={user} /\u0026gt;; } function Layout({ user }) { return \u0026lt;Sidebar user={user} /\u0026gt;; } function Sidebar({ user }) { return \u0026lt;UserProfile user={user} /\u0026gt;; } function UserProfile({ user }) { return \u0026lt;h2\u0026gt;{user.name}\u0026lt;/h2\u0026gt;; // finally uses it } Problems:\nIntermediate components receive props they don\u0026rsquo;t use Refactoring is painful — change in one place ripples everywhere Code becomes hard to read [SOURCE — COURSE MATERIAL]\n\u0026ldquo;Prop drilling doesn\u0026rsquo;t mean parent re-renders children. It\u0026rsquo;s the syntactic uneasiness when writing code.\u0026rdquo;\n3. Context API — Teleport State [SOURCE — COURSE MATERIAL]\nContext lets you share state without prop drilling — any component can access it directly.\nimport { createContext, useContext, useState } from \u0026#34;react\u0026#34;; // 1. Create context const UserContext = createContext(null); // 2. Provide it at the top level function App() { const [user, setUser] = useState({ name: \u0026#34;Alice\u0026#34; }); return ( \u0026lt;UserContext.Provider value={{ user, setUser }}\u0026gt; \u0026lt;Layout /\u0026gt; \u0026lt;/UserContext.Provider\u0026gt; ); } // 3. Consume anywhere in the tree — no prop drilling! function UserProfile() { const { user } = useContext(UserContext); return \u0026lt;h2\u0026gt;{user.name}\u0026lt;/h2\u0026gt;; } // Intermediate components need NO changes function Layout() { return \u0026lt;Sidebar /\u0026gt;; } function Sidebar() { return \u0026lt;UserProfile /\u0026gt;; } Theme Context Example // theme-context.js import { createContext, useContext, useState } from \u0026#34;react\u0026#34;; const ThemeContext = createContext(\u0026#34;light\u0026#34;); export function ThemeProvider({ children }) { const [theme, setTheme] = useState(\u0026#34;light\u0026#34;); return ( \u0026lt;ThemeContext.Provider value={{ theme, setTheme }}\u0026gt; {children} \u0026lt;/ThemeContext.Provider\u0026gt; ); } export function useTheme() { return useContext(ThemeContext); } // In any component: function Button() { const { theme, setTheme } = useTheme(); return ( \u0026lt;button className={theme} onClick={() =\u0026gt; setTheme((t) =\u0026gt; (t === \u0026#34;light\u0026#34; ? \u0026#34;dark\u0026#34; : \u0026#34;light\u0026#34;))} \u0026gt; Toggle Theme \u0026lt;/button\u0026gt; ); } Context Limitation [SOURCE — COURSE MATERIAL]\nContext solves prop drilling but does NOT fix unnecessary re-renders.\n// When Context value changes, ALL consumers re-render // even if the part they use didn\u0026#39;t change const ctx = useContext(AppContext); // If AppContext has { user, posts, theme } and only theme changed, // components using only user STILL re-render This is why Recoil (and Redux, Zustand) exist.\n4. State Management with Recoil [SOURCE — COURSE MATERIAL]\nRecoil is a state management library that solves Context\u0026rsquo;s re-render problem.\nnpm install recoil Core Concepts Atom = unit of state (like useState but global) Selector = derived state (computed from atoms) Atoms // atoms.js — define global state import { atom } from \u0026#34;recoil\u0026#34;; export const networkCountAtom = atom({ key: \u0026#34;networkCount\u0026#34;, // unique key default: 102, // initial value }); export const notificationCountAtom = atom({ key: \u0026#34;notificationCount\u0026#34;, default: 0, }); export const jobsCountAtom = atom({ key: \u0026#34;jobsCount\u0026#34;, default: 3, }); Selectors — Derived State // Total notification count (sum of atoms) import { selector } from \u0026#34;recoil\u0026#34;; export const totalNotificationsSelector = selector({ key: \u0026#34;totalNotifications\u0026#34;, get: ({ get }) =\u0026gt; { const network = get(networkCountAtom); const notifications = get(notificationCountAtom); const jobs = get(jobsCountAtom); return network + notifications + jobs; }, }); Recoil Hooks import { useRecoilState, // [value, setter] — like useState useRecoilValue, // just the value (read-only) useSetRecoilState, // just the setter (write-only) } from \u0026#34;recoil\u0026#34;; function NotificationBadge() { const total = useRecoilValue(totalNotificationsSelector); return \u0026lt;span\u0026gt;{total}\u0026lt;/span\u0026gt;; } function NotificationPanel() { const [count, setCount] = useRecoilState(notificationCountAtom); return ( \u0026lt;div\u0026gt; Notifications: {count} \u0026lt;button onClick={() =\u0026gt; setCount((c) =\u0026gt; c + 1)}\u0026gt;+\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; ); } // Write-only — useful in handlers that don\u0026#39;t need current value function MarkAllRead() { const setNotifications = useSetRecoilState(notificationCountAtom); return \u0026lt;button onClick={() =\u0026gt; setNotifications(0)}\u0026gt;Mark all read\u0026lt;/button\u0026gt;; } RecoilRoot — Wrap Your App import { RecoilRoot } from \u0026#34;recoil\u0026#34;; function App() { return ( \u0026lt;RecoilRoot\u0026gt; \u0026lt;AppBar /\u0026gt; \u0026lt;Main /\u0026gt; \u0026lt;/RecoilRoot\u0026gt; ); } 5. Recoil Advanced — atomFamily \u0026amp; selectorFamily [SOURCE — COURSE MATERIAL]\nThe Problem with Multiple Atoms TODO app: you need one atom per todo But you don\u0026#39;t know how many todos there are upfront Creating atom1, atom2, atom3... manually doesn\u0026#39;t work atomFamily — Dynamic Atoms import { atomFamily } from \u0026#34;recoil\u0026#34;; // Creates an atom factory — pass an ID, get an atom const todoAtomFamily = atomFamily({ key: \u0026#34;todo\u0026#34;, default: (id) =\u0026gt; ({ id, title: \u0026#34;\u0026#34;, completed: false, }), }); // In component function TodoItem({ id }) { const [todo, setTodo] = useRecoilState(todoAtomFamily(id)); return ( \u0026lt;div\u0026gt; \u0026lt;p\u0026gt;{todo.title}\u0026lt;/p\u0026gt; \u0026lt;button onClick={() =\u0026gt; setTodo({ ...todo, completed: true })}\u0026gt; Complete \u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; ); } selectorFamily — Dynamic Selectors import { selectorFamily } from \u0026#34;recoil\u0026#34;; // Fetch todo from server based on ID const todoSelectorFamily = selectorFamily({ key: \u0026#34;todoFromServer\u0026#34;, get: (id) =\u0026gt; async () =\u0026gt; { const res = await fetch(`https://sum-server.100xdevs.com/todo?id=${id}`); const data = await res.json(); return data.todo; }, }); // Usage in component function TodoDetail({ id }) { const todo = useRecoilValue(todoSelectorFamily(id)); return \u0026lt;div\u0026gt;{todo.title}\u0026lt;/div\u0026gt;; } 6. Recoil Loadable — Handling Async State [SOURCE — COURSE MATERIAL]\nWhen selectors fetch async data, components need to handle loading/error states.\nimport { useRecoilValueLoadable } from \u0026#34;recoil\u0026#34;; function TodoDetail({ id }) { const loadable = useRecoilValueLoadable(todoSelectorFamily(id)); if (loadable.state === \u0026#34;loading\u0026#34;) { return \u0026lt;p\u0026gt;Loading...\u0026lt;/p\u0026gt;; } if (loadable.state === \u0026#34;hasError\u0026#34;) { return \u0026lt;p\u0026gt;Error: {loadable.contents.message}\u0026lt;/p\u0026gt;; } // loadable.state === \u0026#39;hasValue\u0026#39; const todo = loadable.contents; return \u0026lt;div\u0026gt;{todo.title}\u0026lt;/div\u0026gt;; } Asynchronous Selector (Fetches from Backend) const notificationsSelector = selector({ key: \u0026#34;notifications\u0026#34;, get: async () =\u0026gt; { const res = await fetch(\u0026#34;https://sum-server.100xdevs.com/notifications\u0026#34;); const data = await res.json(); return data; }, }); 7. State Management: Decision Tree [ADDED — EXPLANATION]\nNeed to share state between components? │ ├── Close in the tree (parent-child or siblings)? │ └── Use useState + props │ ├── Deeply nested but simple? │ └── Use Context API │ (warning: causes all consumers to re-render) │ └── Large app with frequent state changes? └── Use Recoil / Zustand / Redux ├── Recoil — atomic, fine-grained re-renders, selector support ├── Zustand — simpler API, less boilerplate └── Redux — most powerful, highest boilerplate, best devtools Exercises Quick (15 min) Add React Router to a simple app with 3 pages: Home, About, Contact. Add a navigation bar.\nHint 1: Wrap with \u0026lt;BrowserRouter\u0026gt; and use \u0026lt;Link\u0026gt; (not \u0026lt;a\u0026gt;)\nHint 2: 404 page: \u0026lt;Route path=\u0026quot;*\u0026quot; element={\u0026lt;NotFound /\u0026gt;} /\u0026gt;\nIntermediate (45 min) Build a dark/light theme toggle using Context API:\nTheme context with light or dark value All components consume it to apply correct styling One button anywhere in the app toggles the theme Challenge (3–4 hours) Build a LinkedIn-style notification header using Recoil:\nAtoms for: network count, job alerts, messages, notifications Selector that sums all for the total badge count Each section has +/- buttons to change its own count Total badge updates automatically via selector Async selector fetches initial values from https://sum-server.100xdevs.com/notifications Common Confusions Confusion Reality \u0026ldquo;Context fixes re-rendering\u0026rdquo; Context fixes prop drilling. Re-rendering is still an issue. Use Recoil/Zustand for that. \u0026ldquo;useRecoilState is like useState for atoms\u0026rdquo; Exactly right — same API, but global scope. \u0026ldquo;Selectors are like useEffect\u0026rdquo; No. Selectors derive values; useEffect performs side effects. \u0026ldquo;\u0026lt;Link\u0026gt; and \u0026lt;a\u0026gt; do the same thing\u0026rdquo; \u0026lt;a\u0026gt; causes full page reload. \u0026lt;Link\u0026gt; does client-side navigation. Key Takeaways React Router: wrap app in \u0026lt;BrowserRouter\u0026gt;, define \u0026lt;Route\u0026gt;s, use \u0026lt;Link\u0026gt; for navigation Prop drilling = syntactic pain, not a performance issue Context API: creates a teleport for state — createContext → Provider → useContext Context limitation: all consumers re-render when value changes Recoil: atoms (global state) + selectors (derived state) with fine-grained re-renders Use atomFamily/selectorFamily for dynamic collections (like todos with IDs) useRecoilValueLoadable handles async selectors with loading/error states ","permalink":"/posts/07-react-extra/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eBlog Summary:\u003c/strong\u003e Covers client-side routing for SPAs, solves the prop drilling problem with Context API, and introduces Recoil as a production-grade state management solution.\u003c/p\u003e\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-routing-in-react\"\u003e1. Routing in React\u003c/h2\u003e\n\u003cp\u003e[SOURCE — COURSE MATERIAL]\u003c/p\u003e\n\u003ch3 id=\"jargon-first\"\u003eJargon First\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eSingle Page Application (SPA):\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eBrowser downloads ONE HTML file\u003c/li\u003e\n\u003cli\u003eReact controls what to show based on URL\u003c/li\u003e\n\u003cli\u003eNo full page reloads when navigating\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eClient-Side Bundle:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAll your React code compiled into JS files\u003c/li\u003e\n\u003cli\u003eBrowser downloads once, runs locally\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eClient-Side Routing:\u003c/strong\u003e\u003c/p\u003e","title":"07 : Routing, Prop Drilling, Context API \u0026 Recoil"},{"content":"Welcome to the big leagues. If you\u0026rsquo;ve been following along, we’ve solved the prop-drilling problem using the Context API, and we even looked at Recoil to prevent unnecessary re-renders.\nSo right now, a very valid question is probably popping up in your head: \u0026ldquo;If Recoil and Context API solve my problems, why on earth am I learning Redux?\u0026rdquo;\nLet\u0026rsquo;s clear that up before we write a single line of code.\n❓ Why Redux instead of Context or Recoil? Context API is great for passing data deeply, but it\u0026rsquo;s not a state management tool. Whenever a Context value changes, every component consuming that Context re-renders. That’s a performance nightmare for large apps. Recoil solves the re-render issue beautifully with \u0026ldquo;Atoms.\u0026rdquo; It\u0026rsquo;s lightweight and React-native. But here is the brutal truth: Redux is the undisputed industry standard. If you get a job as a React developer tomorrow, there is a 90% chance the codebase uses Redux. Redux enforces a strict, predictable Unidirectional Data Flow. In enterprise apps with hundreds of components, this strictness prevents chaotic bugs. Redux DevTools: The debugging experience in Redux is practically magic. You can literally \u0026ldquo;time-travel\u0026rdquo; through your app\u0026rsquo;s state changes. Recoil cannot do this at the same level. 1. The Story of Redux (And Why We Use \u0026ldquo;Toolkit\u0026rdquo;) To understand Redux, you need to know a tiny bit of history.\nThe Dark Ages: Flux Years ago, Facebook created an architecture called Flux to handle state. It introduced the idea of a one-way data flow, but it allowed multiple stores. It was messy and hard to maintain.\nThe Renaissance: Vanilla Redux (2015) Dan Abramov and Andrew Clark took the ideas of Flux and perfected them into Redux. They introduced the golden rule: The Single Source of Truth. Your entire application\u0026rsquo;s state lives in ONE massive JavaScript object.\nThe Problem: Vanilla Redux was notoriously difficult to set up. It required massive amounts of \u0026ldquo;boilerplate\u0026rdquo; code. You had to create action types, action creators, reducers, and manually install middleware like redux-thunk just to make an API call. If you forgot to copy an old state array before updating it, your app broke. The Modern Era: Redux Toolkit (RTK) To stop developers from pulling their hair out, the Redux team created Redux Toolkit (RTK). RTK is the official, opinionated, \u0026ldquo;batteries-included\u0026rdquo; way to write Redux.\nIt writes the boilerplate for you. It sets up the Redux DevTools automatically. The biggest superpower: It includes a library called Immer.js under the hood. In vanilla Redux, directly mutating state (state.push(newItem)) was a cardinal sin. In RTK, you can write mutating code, and Immer safely translates it into immutable updates behind the scenes! 2. The Core Mental Model Before we code, burn these three concepts into your brain:\nThe Store: The global database for your frontend. It holds everything. Reducers (Slices): The only functions allowed to change the Store. You don\u0026rsquo;t update the Store directly; you ask a Reducer to do it for you. Dispatch \u0026amp; Selectors: Dispatch (useDispatch): The delivery boy. When a user clicks \u0026ldquo;Add Todo\u0026rdquo;, you dispatch an action to the Reducer. Selector (useSelector): The spyglass. How a component \u0026ldquo;selects\u0026rdquo; or reads specific data from the Store. Wait, is Redux a React thing? No. Redux is an independent JavaScript library. You can use it with Vue, Angular, or vanilla JS. To make it work with React, we need a bridge library called react-redux.\n3. Let\u0026rsquo;s Build: A Redux Toolkit Todo App We are going to build a Todo app. Let\u0026rsquo;s install the two packages we need:\nnpm install @reduxjs/toolkit react-redux Step 1: Create the Store (src/app/store.js) Every Redux app starts with a store. This is the easiest part.\nimport { configureStore } from \u0026#39;@reduxjs/toolkit\u0026#39;; // We will import our reducers here later export const store = configureStore({ reducer: {} // The store needs to know about all the reducers we create }); configureStore: This RTK method does the heavy lifting. It creates the store and automatically wires up the Redux DevTools extension for you. Step 2: Create a \u0026ldquo;Slice\u0026rdquo; (src/features/todo/todoSlice.js) In RTK, we organize our state into \u0026ldquo;Slices\u0026rdquo; (e.g., Auth Slice, Product Slice, Todo Slice). A slice contains the initial state and the reducers for that specific feature.\nimport { createSlice, nanoid } from \u0026#39;@reduxjs/toolkit\u0026#39;; // 1. How does the state look when the app first loads? const initialState = { todos: [{ id: 1, text: \u0026#34;Learn Redux Toolkit\u0026#34; }] }; // 2. Create the slice export const todoSlice = createSlice({ name: \u0026#39;todo\u0026#39;, // This name shows up in the Redux DevTools initialState, // Attach the initial state reducers: { // Reducers take TWO arguments: (state, action) addTodo: (state, action) =\u0026gt; { const newTodo = { id: nanoid(), // RTK gives us nanoid to generate unique IDs instantly! text: action.payload }; // Wait, we are mutating state directly?! // YES! RTK uses Immer.js behind the scenes. This is perfectly safe here. state.todos.push(newTodo); }, removeTodo: (state, action) =\u0026gt; { // action.payload will contain the ID of the todo we want to remove state.todos = state.todos.filter((todo) =\u0026gt; todo.id !== action.payload); } } }); // 3. EXPORTING (Pay close attention, this is where beginners get stuck) // Export the individual functions so our components can use them export const { addTodo, removeTodo } = todoSlice.actions; // Export the MAIN reducer so the Store can register it export default todoSlice.reducer; 🧠 Developer Insight: state vs action.payload state: Gives you access to the current values in this slice. Want to know what todos currently exist? Look in state.todos. action: When a component calls addTodo(\u0026quot;Buy Milk\u0026quot;), that string \u0026ldquo;Buy Milk\u0026rdquo; gets attached to action.payload. The payload is the data you pass in. Step 3: Register the Slice in the Store (src/app/store.js) Let\u0026rsquo;s go back to our store and tell it about our new slice.\nimport { configureStore } from \u0026#39;@reduxjs/toolkit\u0026#39;; import todoReducer from \u0026#39;../features/todo/todoSlice\u0026#39;; // Import the default export export const store = configureStore({ reducer: { todos: todoReducer // Now the store is aware of our todo feature! } }); Step 4: Wrap the App (src/main.jsx) React doesn\u0026rsquo;t know about Redux yet. We have to wrap our app in a \u0026lt;Provider\u0026gt; from react-redux.\nimport React from \u0026#39;react\u0026#39; import ReactDOM from \u0026#39;react-dom/client\u0026#39; import App from \u0026#39;./App.jsx\u0026#39; import { Provider } from \u0026#39;react-redux\u0026#39; import { store } from \u0026#39;./app/store\u0026#39; ReactDOM.createRoot(document.getElementById(\u0026#39;root\u0026#39;)).render( \u0026lt;Provider store={store}\u0026gt; \u0026lt;App /\u0026gt; \u0026lt;/Provider\u0026gt;, ) 4. Connecting Components to Redux The backend of our frontend is done. Now, how do our React components actually talk to this store?\nWriting Data: useDispatch (AddTodo.jsx) To send data to the store, we need the useDispatch hook. You can\u0026rsquo;t just call addTodo() normally; you have to dispatch it.\nimport React, { useState } from \u0026#39;react\u0026#39; import { useDispatch } from \u0026#39;react-redux\u0026#39; import { addTodo } from \u0026#39;../features/todo/todoSlice\u0026#39; function AddTodo() { const [input, setInput] = useState(\u0026#39;\u0026#39;) const dispatch = useDispatch() const addTodoHandler = (e) =\u0026gt; { e.preventDefault() // We DISPATCH the action, and pass our input as the payload dispatch(addTodo(input)) setInput(\u0026#39;\u0026#39;) // Clean up the form } return ( \u0026lt;form onSubmit={addTodoHandler}\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; value={input} onChange={(e) =\u0026gt; setInput(e.target.value)} placeholder=\u0026#34;Enter a Todo...\u0026#34; /\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;Add Todo\u0026lt;/button\u0026gt; \u0026lt;/form\u0026gt; ) } export default AddTodo Reading Data: useSelector (Todos.jsx) To read data from the store, we use useSelector. It gives us access to the entire global state object.\nimport React from \u0026#39;react\u0026#39; import { useSelector, useDispatch } from \u0026#39;react-redux\u0026#39; import { removeTodo } from \u0026#39;../features/todo/todoSlice\u0026#39; function Todos() { // Select the \u0026#39;todos\u0026#39; array from the store // (Remember we named it \u0026#39;todos\u0026#39; inside configureStore\u0026#39;s reducer object) const todos = useSelector((state) =\u0026gt; state.todos.todos) const dispatch = useDispatch() return ( \u0026lt;\u0026gt; \u0026lt;h2\u0026gt;My Todos\u0026lt;/h2\u0026gt; \u0026lt;ul\u0026gt; {todos.map((todo) =\u0026gt; ( \u0026lt;li key={todo.id}\u0026gt; {todo.text} \u0026lt;button // Dispatch removeTodo and pass the ID as the payload onClick={() =\u0026gt; dispatch(removeTodo(todo.id))} \u0026gt; Delete \u0026lt;/button\u0026gt; \u0026lt;/li\u0026gt; ))} \u0026lt;/ul\u0026gt; \u0026lt;/\u0026gt; ) } export default Todos 5. The Magic of Redux DevTools If you followed along, your app is working. But you must install the Redux DevTools Extension in Chrome/Edge.\nRight-click your app, click Inspect, and go to the Redux tab. Add a Todo in your app. Look at the DevTools: You will see an action called todo/addTodo logged in the timeline. Click the Diff tab. It will highlight exactly what changed in your state (e.g., green text showing a new object was added to the array). Look at the Slider at the bottom. You can grab it and slide backward in time to watch your Todos disappear and reappear on the UI exactly as you added them. This traceability is exactly why enterprise companies love Redux. If a bug happens in a massive app, you can look at the Action Log and see exactly which payload corrupted the state.\n🏋️‍♂️ Exercises to Build Muscle Memory Quick Exercise (15 mins): Add a \u0026ldquo;Clear All\u0026rdquo; button.\nGo to todoSlice.js and add a clearTodos: (state) =\u0026gt; {} reducer. What should the logic be to empty the array? Export it from .actions. Create a button in the UI that dispatches it. Intermediate Exercise (45 mins): Update a Todo\nAdd an updateTodo reducer. Hint: The action.payload will need to be an object: { id: 1, newText: \u0026quot;Updated string\u0026quot; }. Inside the reducer, use state.todos.map() or state.todos.find() to locate the todo by ID and change its .text property. Challenge Exercise (2 Hours): Multi-Slice Architecture Create a brand new Vite project for a \u0026ldquo;Shopping Cart\u0026rdquo;.\nCreate two slices: productSlice (holds a hardcoded array of products) and cartSlice (holds items added to cart). Register BOTH in configureStore. Create a component that lists products, and a button that dispatches the product into the cartSlice. Use useSelector to display a cart counter in a Navbar component. 🚨 Common Confusions \u0026amp; Pitfalls What you might think The Reality \u0026ldquo;I need to use spread operators ...state to update arrays.\u0026rdquo; In Vanilla Redux, yes. In RTK, NO. Immer.js allows you to do state.todos.push(). It feels illegal, but it\u0026rsquo;s the intended RTK way. \u0026ldquo;I forgot to export the reducer.\u0026rdquo; This is the #1 beginner error. You must export the actions (destructured) AND export default mySlice.reducer. If you get a blank state error, check this first. \u0026ldquo;Can I put API calls inside a Reducer?\u0026rdquo; NO. Reducers must be \u0026ldquo;Pure Functions\u0026rdquo; (synchronous, no side effects). For async code, RTK uses something called createAsyncThunk (which we will cover in advanced Redux). \u0026quot;action.payload is undefined!\u0026quot; If you dispatch an action without passing an argument: dispatch(addTodo()), the payload is undefined. You must pass data: dispatch(addTodo(\u0026quot;Hello\u0026quot;)). 📝 The Redux Toolkit Cheat Sheet Keep this handy when you forget the flow:\nSetup: npm i @reduxjs/toolkit react-redux The Store: configureStore({ reducer: { myFeature: mySliceReducer } }) The Provider: \u0026lt;Provider store={store}\u0026gt;\u0026lt;App /\u0026gt;\u0026lt;/Provider\u0026gt; The Slice: createSlice({ name, initialState, reducers: { myFunc: (state, action) =\u0026gt; {} } }) Read State: const data = useSelector(state =\u0026gt; state.myFeature.data) Write State: const dispatch = useDispatch(); dispatch(myFunc(payload)) Once you wrap your head around the setup, Redux becomes a highly predictable, satisfying way to build massive applications. You provide the data, you dispatch the action, and Redux handles the rest perfectly every time.\n","permalink":"/posts/08-react-redux/","summary":"\u003cp\u003eWelcome to the big leagues. If you\u0026rsquo;ve been following along, we’ve solved the prop-drilling problem using the Context API, and we even looked at Recoil to prevent unnecessary re-renders.\u003c/p\u003e\n\u003cp\u003eSo right now, a very valid question is probably popping up in your head: \u003cstrong\u003e\u0026ldquo;If Recoil and Context API solve my problems, why on earth am I learning Redux?\u0026rdquo;\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eLet\u0026rsquo;s clear that up before we write a single line of code.\u003c/p\u003e","title":"08 : Mastering Redux Toolkit"},{"content":" Blog Summary: React\u0026rsquo;s built-in hooks are great, but the real superpower is building your own. This covers custom hooks from scratch - data fetching, browser APIs, performance patterns, and the SWR library. By the end you will have a solid collection of hooks you can drop into any project.\nSo we have been through useState, useEffect, useMemo, useCallback, useRef. Those are all hooks React ships with. And honestly, they cover a lot.\nBut here is the thing nobody told me clearly when I was learning this. The whole hook system is not just a collection of built-in utilities. It is a design pattern. React gives you the primitives, and then it is on you to compose them into logic units that your components can share. That is what custom hooks are.\nOnce this clicked for me it changed the way I look at component code. Whenever I see a component that has a bunch of useState and useEffect calls all crammed together handling some specific concern, my first thought now is, that probably should be a hook.\n1. What Even Is a Custom Hook? A custom hook is just a JavaScript function. Nothing magical about it. The only two rules it has to follow are:\nIts name must start with use It must call at least one other hook internally (built-in or another custom hook) That is it. That is the whole definition.\nThe naming rule is not just a convention for fun. The React linter (ESLint plugin for hooks) uses it to enforce the rules of hooks. If your function calls useState but is not named use___, React cannot guarantee you are calling it correctly, and you lose the linter warnings. So always start with use.\n// This is a valid custom hook function useWindowSize() { const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight, }); // ... return size; } // This is NOT a custom hook (doesn\u0026#39;t use use prefix, React won\u0026#39;t treat it as one) function getWindowSize() { const [size, setSize] = useState({}); // \u0026lt;- this will break the rules of hooks } The real value of a custom hook is code reuse across components. If two components need the same stateful logic, you extract it into a hook. Both components call the hook and get their own isolated copy of that state. No sharing, no interference.\n2. Data Fetching Hooks - Where You Will Use This Most This is the category you will reach for constantly in real projects. Almost every screen in a React app needs to fetch some data, show a loader while waiting, handle errors. Writing that useEffect + useState combo in every component gets old fast.\nThe Problem First Let\u0026rsquo;s say you have this:\nfunction App() { const [todos, setTodos] = useState([]); useEffect(() =\u0026gt; { fetch(\u0026#34;https://sum-server.100xdevs.com/todos\u0026#34;) .then((res) =\u0026gt; res.json()) .then((data) =\u0026gt; setTodos(data.todos)); }, []); return ( \u0026lt;\u0026gt; {todos.map((todo) =\u0026gt; ( \u0026lt;div key={todo.id}\u0026gt; \u0026lt;p\u0026gt;{todo.title}\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;{todo.description}\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; ))} \u0026lt;/\u0026gt; ); } This works. But now you have another component that also needs todos. And another page that needs user data. And a profile page that needs posts. You keep writing the same useEffect + useState pattern over and over.\nStep 1 - Extract the Fetching Logic Pull it into a hook:\nfunction useTodos() { const [todos, setTodos] = useState([]); useEffect(() =\u0026gt; { fetch(\u0026#34;https://sum-server.100xdevs.com/todos\u0026#34;) .then((res) =\u0026gt; res.json()) .then((data) =\u0026gt; setTodos(data.todos)); }, []); return todos; } function App() { const todos = useTodos(); // \u0026lt;- entire fetch logic gone from the component return ( \u0026lt;\u0026gt; {todos.map((todo) =\u0026gt; ( \u0026lt;div key={todo.id}\u0026gt; \u0026lt;p\u0026gt;{todo.title}\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;{todo.description}\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; ))} \u0026lt;/\u0026gt; ); } The component went from knowing about fetch to knowing nothing about fetch. It just calls useTodos() and gets back data. Cleaner, and now any other component can call the same hook.\nStep 2 - Add a Loading State What shows while the data is being fetched? Right now, nothing. You probably want a loader.\nfunction useTodos() { const [todos, setTodos] = useState([]); const [loading, setLoading] = useState(true); useEffect(() =\u0026gt; { fetch(\u0026#34;https://sum-server.100xdevs.com/todos\u0026#34;) .then((res) =\u0026gt; res.json()) .then((data) =\u0026gt; { setTodos(data.todos); setLoading(false); }); }, []); return { todos, loading }; } function App() { const { todos, loading } = useTodos(); if (loading) return \u0026lt;div\u0026gt;Loading...\u0026lt;/div\u0026gt;; return ( \u0026lt;\u0026gt; {todos.map((todo) =\u0026gt; ( \u0026lt;div key={todo.id}\u0026gt; \u0026lt;p\u0026gt;{todo.title}\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;{todo.description}\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; ))} \u0026lt;/\u0026gt; ); } Now the hook returns an object with both todos and loading. The component destructures what it needs.\nStep 3 - Auto Refresh / Polling Some data needs to stay fresh. Think of a live score board, a notification count, or a stock price. You want to re-fetch every N seconds.\nfunction useTodos(n) { const [todos, setTodos] = useState([]); const [loading, setLoading] = useState(true); function getData() { fetch(\u0026#34;https://sum-server.100xdevs.com/todos\u0026#34;) .then((res) =\u0026gt; res.json()) .then((data) =\u0026gt; { setTodos(data.todos); setLoading(false); }); } useEffect(() =\u0026gt; { getData(); // fetch immediately const intervalId = setInterval(() =\u0026gt; { getData(); // then fetch every n seconds }, n * 1000); return () =\u0026gt; clearInterval(intervalId); // cleanup on unmount }, [n]); return { todos, loading }; } // Usage - refetch every 5 seconds function App() { const { todos, loading } = useTodos(5); if (loading) return \u0026lt;div\u0026gt;Loading...\u0026lt;/div\u0026gt;; return ( \u0026lt;\u0026gt; {todos.map((todo) =\u0026gt; ( \u0026lt;div key={todo.id}\u0026gt; \u0026lt;p\u0026gt;{todo.title}\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; ))} \u0026lt;/\u0026gt; ); } Notice the cleanup. The return () =\u0026gt; clearInterval(intervalId) inside useEffect runs when the component unmounts or when n changes. Without this, you get a memory leak - the interval keeps firing even after the component is gone, trying to set state on something that no longer exists. React will even yell at you in the console about it.\nA More General useFetch Hook The useTodos hook is very specific. In real projects it makes more sense to have a generic hook that takes any URL:\nfunction useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() =\u0026gt; { setLoading(true); setError(null); fetch(url) .then((res) =\u0026gt; { if (!res.ok) throw new Error(`HTTP error: ${res.status}`); return res.json(); }) .then((json) =\u0026gt; { setData(json); setLoading(false); }) .catch((err) =\u0026gt; { setError(err.message); setLoading(false); }); }, [url]); return { data, loading, error }; } // Usage function TodoList() { const { data, loading, error } = useFetch( \u0026#34;https://sum-server.100xdevs.com/todos\u0026#34;, ); if (loading) return \u0026lt;p\u0026gt;Loading...\u0026lt;/p\u0026gt;; if (error) return \u0026lt;p\u0026gt;Error: {error}\u0026lt;/p\u0026gt;; return ( \u0026lt;ul\u0026gt; {data.todos.map((t) =\u0026gt; ( \u0026lt;li key={t.id}\u0026gt;{t.title}\u0026lt;/li\u0026gt; ))} \u0026lt;/ul\u0026gt; ); } This pattern of returning { data, loading, error } from a data hook is so common it is practically a convention. You will see it everywhere including in the SWR library which we will look at later.\n3. Browser Functionality Hooks This category is about wrapping browser APIs into hooks. The browser exposes a ton of useful things. Network status, mouse position, window size, scroll position, geolocation, clipboard access. Using these directly in components is messy. Hooks clean that up.\nuseIsOnline - Network Status Build a hook that tells you if the user is online or offline. The browser gives you window.navigator.onLine for the current state, and online/offline events when it changes.\nfunction useIsOnline() { const [isOnline, setIsOnline] = useState(window.navigator.onLine); useEffect(() =\u0026gt; { function handleOnline() { setIsOnline(true); } function handleOffline() { setIsOnline(false); } window.addEventListener(\u0026#34;online\u0026#34;, handleOnline); window.addEventListener(\u0026#34;offline\u0026#34;, handleOffline); return () =\u0026gt; { window.removeEventListener(\u0026#34;online\u0026#34;, handleOnline); window.removeEventListener(\u0026#34;offline\u0026#34;, handleOffline); }; }, []); return isOnline; } // Usage function App() { const isOnline = useIsOnline(); return ( \u0026lt;div\u0026gt; {isOnline ? \u0026#34;You are online\u0026#34; : \u0026#34;You are offline - check your connection\u0026#34;} \u0026lt;/div\u0026gt; ); } The event listener cleanup is important here too. window.addEventListener needs a corresponding removeEventListener with the exact same function reference. This is why we define handleOnline and handleOffline as named functions before registering them - so we can pass the same reference to removeEventListener.\nuseMousePointer - Track Cursor Position function useMousePointer() { const [position, setPosition] = useState({ x: 0, y: 0 }); useEffect(() =\u0026gt; { function handleMouseMove(e) { setPosition({ x: e.clientX, y: e.clientY }); } window.addEventListener(\u0026#34;mousemove\u0026#34;, handleMouseMove); return () =\u0026gt; { window.removeEventListener(\u0026#34;mousemove\u0026#34;, handleMouseMove); }; }, []); return position; } // Usage function App() { const { x, y } = useMousePointer(); return ( \u0026lt;div style={{ height: \u0026#34;100vh\u0026#34; }}\u0026gt; Mouse position is {x}, {y} \u0026lt;/div\u0026gt; ); } Notice how the component knows nothing about addEventListener. It just gets x and y. You could use this in a tooltip, a custom cursor, a parallax effect, or anything that needs cursor coordinates.\nuseWindowSize - Responsive Logic in JS Sometimes you need to know the window size in JS, not just CSS. Like when you want to render a completely different component, not just hide/show one.\nfunction useWindowSize() { const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight, }); useEffect(() =\u0026gt; { function handleResize() { setSize({ width: window.innerWidth, height: window.innerHeight, }); } window.addEventListener(\u0026#34;resize\u0026#34;, handleResize); return () =\u0026gt; window.removeEventListener(\u0026#34;resize\u0026#34;, handleResize); }, []); return size; } // Usage - render different layout based on screen size function App() { const { width } = useWindowSize(); return width \u0026lt; 768 ? \u0026lt;MobileLayout /\u0026gt; : \u0026lt;DesktopLayout /\u0026gt;; } 4. Performance and Timer Hooks useInterval - Run Code Every N Seconds This one sounds simple but it is surprisingly easy to get wrong. Naive implementations have a stale closure bug where the callback sees old state values.\nHere is the correct approach:\nfunction useInterval(callback, delay) { const savedCallback = useRef(callback); // Always keep the ref updated to the latest callback useEffect(() =\u0026gt; { savedCallback.current = callback; }, [callback]); useEffect(() =\u0026gt; { if (delay === null) return; // null means stop const id = setInterval(() =\u0026gt; { savedCallback.current(); // call via ref, always latest version }, delay); return () =\u0026gt; clearInterval(id); }, [delay]); } // Usage - counter that increments every second function Timer() { const [count, setCount] = useState(0); useInterval(() =\u0026gt; { setCount((c) =\u0026gt; c + 1); }, 1000); return \u0026lt;p\u0026gt;Timer: {count}\u0026lt;/p\u0026gt;; } The useRef trick here is important. If you pass callback as a dependency of the second useEffect, the interval gets cleared and restarted every time callback changes (which is every render, because functions are recreated on every render). Instead, you store the latest version of callback in a ref, and the interval always calls savedCallback.current. The interval never restarts, but it always has access to the latest callback.\nThis is actually a pattern Dan Abramov wrote about in his blog in 2019, and it is still the correct way to do it.\nuseDebounce - Delay Until Typing Stops Debouncing is one of those things you will need constantly. Search inputs, form validation, window resize handlers. The idea is: don\u0026rsquo;t fire immediately, wait until the user has stopped doing the thing for X milliseconds.\nfunction useDebounce(value, delay) { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() =\u0026gt; { const timer = setTimeout(() =\u0026gt; { setDebouncedValue(value); }, delay); return () =\u0026gt; clearTimeout(timer); // cancel if value changes before delay }, [value, delay]); return debouncedValue; } Here is how it works: every time value changes, a new timeout is set. But the cleanup function from the previous useEffect run cancels the old timeout first. So if the user types fast, the timeouts keep getting cancelled and reset. Only when the user stops typing for delay milliseconds does the timeout actually complete and debouncedValue updates.\n// Usage - search input that delays API calls const SearchBar = () =\u0026gt; { const [inputValue, setInputValue] = useState(\u0026#34;\u0026#34;); const debouncedValue = useDebounce(inputValue, 500); // This effect only fires when debouncedValue changes (500ms after user stops typing) useEffect(() =\u0026gt; { if (debouncedValue) { console.log(\u0026#34;Searching for:\u0026#34;, debouncedValue); // make your API call here } }, [debouncedValue]); return ( \u0026lt;input type=\u0026#34;text\u0026#34; value={inputValue} onChange={(e) =\u0026gt; setInputValue(e.target.value)} placeholder=\u0026#34;Search...\u0026#34; /\u0026gt; ); }; Without debouncing, a user typing \u0026ldquo;react hooks\u0026rdquo; would fire 10 API calls. With 500ms debounce, it fires 1 call after they stop typing. That is the kind of thing that makes the difference between a polished app and a slow one.\n5. SWR - When You Don\u0026rsquo;t Want to Build This Yourself We have been building data fetching hooks from scratch. That is a great exercise to understand how they work. In production though, there is a library called SWR that does all of this and much more.\nSWR stands for Stale-While-Revalidate, which is a cache strategy: show stale data immediately while fetching fresh data in the background.\nnpm install swr import useSWR from \u0026#34;swr\u0026#34;; const fetcher = async (url) =\u0026gt; { const res = await fetch(url); return res.json(); }; function Profile() { const { data, error, isLoading } = useSWR( \u0026#34;https://sum-server.100xdevs.com/todos\u0026#34;, fetcher, ); if (error) return \u0026lt;div\u0026gt;Failed to load\u0026lt;/div\u0026gt;; if (isLoading) return \u0026lt;div\u0026gt;Loading...\u0026lt;/div\u0026gt;; return \u0026lt;div\u0026gt;You have {data.todos.length} todos!\u0026lt;/div\u0026gt;; } Compare this with what we built earlier. SWR gives you the same { data, error, isLoading } pattern but under the hood it also handles:\nRequest deduplication - if 10 components call useSWR with the same URL at the same time, only 1 HTTP request happens Cache - the data is cached and reused across components and page navigations Auto revalidation - refetches when you switch tabs back to the app (focus revalidation) Polling - useSWR(url, fetcher, { refreshInterval: 5000 }) for auto refresh Optimistic updates - update UI before the server confirms // Auto refresh with SWR function LiveScore() { const { data } = useSWR(\u0026#34;/api/score\u0026#34;, fetcher, { refreshInterval: 3000, // refetch every 3 seconds }); return \u0026lt;div\u0026gt;Score: {data?.score}\u0026lt;/div\u0026gt;; } SWR is a great choice for most data fetching needs. If you find yourself building a complex custom hook for fetching, check if SWR covers it first. Another popular alternative is React Query (TanStack Query) which has a similar API but more features for mutations.\n6. A Few More Hooks Worth Knowing These come up in real projects often enough that it is worth seeing them at least once.\nuseLocalStorage - Persist State Across Refreshes useState is lost on page refresh. useLocalStorage keeps it synced with localStorage so it survives:\nfunction useLocalStorage(key, initialValue) { const [storedValue, setStoredValue] = useState(() =\u0026gt; { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch { return initialValue; } }); const setValue = (value) =\u0026gt; { try { setStoredValue(value); window.localStorage.setItem(key, JSON.stringify(value)); } catch (error) { console.error(error); } }; return [storedValue, setValue]; } // Usage - exactly like useState but persists function ThemeToggle() { const [theme, setTheme] = useLocalStorage(\u0026#34;theme\u0026#34;, \u0026#34;light\u0026#34;); return ( \u0026lt;button onClick={() =\u0026gt; setTheme(theme === \u0026#34;light\u0026#34; ? \u0026#34;dark\u0026#34; : \u0026#34;light\u0026#34;)}\u0026gt; Current theme: {theme} \u0026lt;/button\u0026gt; ); } usePrevious - Remember the Last Value Sometimes you want to compare current value against previous value. Useful for animations, transition logic, or analytics:\nfunction usePrevious(value) { const ref = useRef(undefined); useEffect(() =\u0026gt; { ref.current = value; }); return ref.current; // returns value from previous render } function Counter() { const [count, setCount] = useState(0); const prevCount = usePrevious(count); return ( \u0026lt;div\u0026gt; \u0026lt;p\u0026gt; Now: {count}, Before: {prevCount} \u0026lt;/p\u0026gt; \u0026lt;button onClick={() =\u0026gt; setCount((c) =\u0026gt; c + 1)}\u0026gt;Increment\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; ); } This works because useEffect runs after the render. So when the component renders with the new count, ref.current still holds the previous value. Then useEffect updates the ref. On the next render, that old value is returned as prevCount.\n7. Rules and Things to Keep in Mind A few rules that apply to custom hooks the same way they apply to built-in hooks:\nDo not call hooks conditionally. React depends on hooks being called in the same order every render. This breaks that:\n// BAD function useBadHook(shouldFetch) { if (shouldFetch) { const [data, setData] = useState(null); // conditional hook call - broken } } // GOOD - put the condition inside the hook, not around it function useConditionalFetch(url, enabled) { const [data, setData] = useState(null); useEffect(() =\u0026gt; { if (!enabled) return; // condition inside the effect fetch(url) .then((res) =\u0026gt; res.json()) .then(setData); }, [url, enabled]); return data; } Each component gets its own state. Custom hooks do not share state between components. If ComponentA and ComponentB both call useTodos(), they each get their own independent todos state. The hook logic is shared, not the state.\nNaming matters. Be descriptive. useUser is better than useData. useDebounce is better than useDelay. Someone reading your code should know what the hook does from the name alone.\nThe Mini Project - useGithubProfile Let\u0026rsquo;s put it all together with a small project. Build a hook that fetches a GitHub user\u0026rsquo;s profile, with loading, error handling, and a debounced search input so you are not hammering the GitHub API on every keystroke.\n// useGithubProfile.js import { useState, useEffect } from \u0026#34;react\u0026#34;; function useGithubProfile(username) { const [profile, setProfile] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() =\u0026gt; { if (!username) return; setLoading(true); setError(null); fetch(`https://api.github.com/users/${username}`) .then((res) =\u0026gt; { if (!res.ok) throw new Error(\u0026#34;User not found\u0026#34;); return res.json(); }) .then((data) =\u0026gt; { setProfile(data); setLoading(false); }) .catch((err) =\u0026gt; { setError(err.message); setLoading(false); }); }, [username]); return { profile, loading, error }; } // useDebounce.js (same as above) function useDebounce(value, delay) { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() =\u0026gt; { const timer = setTimeout(() =\u0026gt; setDebouncedValue(value), delay); return () =\u0026gt; clearTimeout(timer); }, [value, delay]); return debouncedValue; } // App.jsx function App() { const [input, setInput] = useState(\u0026#34;\u0026#34;); const debouncedUsername = useDebounce(input, 600); const { profile, loading, error } = useGithubProfile(debouncedUsername); return ( \u0026lt;div\u0026gt; \u0026lt;input value={input} onChange={(e) =\u0026gt; setInput(e.target.value)} placeholder=\u0026#34;Enter GitHub username\u0026#34; /\u0026gt; {loading \u0026amp;\u0026amp; \u0026lt;p\u0026gt;Searching...\u0026lt;/p\u0026gt;} {error \u0026amp;\u0026amp; \u0026lt;p style={{ color: \u0026#34;red\u0026#34; }}\u0026gt;{error}\u0026lt;/p\u0026gt;} {profile \u0026amp;\u0026amp; ( \u0026lt;div\u0026gt; \u0026lt;img src={profile.avatar_url} alt={profile.login} width={80} /\u0026gt; \u0026lt;h2\u0026gt;{profile.name}\u0026lt;/h2\u0026gt; \u0026lt;p\u0026gt;{profile.bio}\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt; Repos: {profile.public_repos} | Followers: {profile.followers} \u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; )} \u0026lt;/div\u0026gt; ); } This is a real project you can ship. Two custom hooks working together, debouncing real API calls, handling loading and error states properly. Extend it with useLocalStorage to save recent searches and you have a pretty complete thing.\nCommon Confusions Confusion Reality \u0026ldquo;Custom hooks share state between components\u0026rdquo; No. Each component calling the hook gets its own state. The logic is shared, the state is not. \u0026ldquo;My hook must return something\u0026rdquo; You can return nothing. A hook that only sets up event listeners or timers does not need to return anything. \u0026ldquo;I should put all logic in a single big hook\u0026rdquo; No. Split by concern. One hook per logical unit. useAuth, useTodos, useTheme - not one useEverything. \u0026ldquo;useInterval is the same as setInterval in useEffect\u0026rdquo; Naive useInterval has a stale closure bug. The ref-based pattern above is the correct way. \u0026ldquo;SWR is just fetch with loading state\u0026rdquo; SWR adds caching, deduplication, background revalidation, focus refetching. It is a full data layer. Key Takeaways A custom hook is any function starting with use that calls at least one other hook inside They do not share state - each component gets its own isolated copy of hook state Data fetching hooks evolve: start simple, add loading, add error handling, add polling as needed Browser API hooks (useIsOnline, useMousePointer, useWindowSize) wrap event listeners cleanly and always need cleanup useDebounce is a must-have for any input that triggers API calls useInterval needs the ref pattern to avoid stale closure issues SWR handles the entire data fetching concern in production - caching, deduplication, revalidation When you see repeated useState + useEffect patterns in multiple components, that is a sign a custom hook is waiting to be extracted ","permalink":"/posts/09-custom-hooks/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eBlog Summary:\u003c/strong\u003e React\u0026rsquo;s built-in hooks are great, but the real superpower is building your own. This covers custom hooks from scratch - data fetching, browser APIs, performance patterns, and the SWR library. By the end you will have a solid collection of hooks you can drop into any project.\u003c/p\u003e\u003c/blockquote\u003e\n\u003chr\u003e\n\u003cp\u003eSo we have been through \u003ccode\u003euseState\u003c/code\u003e, \u003ccode\u003euseEffect\u003c/code\u003e, \u003ccode\u003euseMemo\u003c/code\u003e, \u003ccode\u003euseCallback\u003c/code\u003e, \u003ccode\u003euseRef\u003c/code\u003e. Those are all hooks React ships with. And honestly, they cover a lot.\u003c/p\u003e","title":"09 : Custom Hooks - Write Once, Use Everywhere"},{"content":" Blog Summary: JavaScript is great until your app crashes at 2am because someone passed a string where a number was expected. TypeScript fixes that. This post covers TypeScript from the ground up - types, interfaces, generics, enums, tsconfig, and how to actually use all of this in real code. By the end you will build a fully typed REST API with Express.\nSo I have been writing JavaScript for a while now. And at some point the codebase grew big enough that I started getting these bugs that should never exist. Like calling .toUpperCase() on something that turned out to be undefined at runtime. Or a function expecting an object with a name field but someone passing in just a string. These are not logic bugs. They are type bugs. And TypeScript is specifically designed to catch them before your code even runs.\nThe way I understand TypeScript is: it is JavaScript, but the compiler reads your code before executing it and goes \u0026ldquo;hey this doesn\u0026rsquo;t make sense, fix it.\u0026rdquo; That\u0026rsquo;s it. That is the core value.\n1. Strongly Typed vs Loosely Typed - The Foundation Before we get into TypeScript itself, it helps to understand why it exists.\nProgramming languages have a concept of typing. Some languages are strongly typed - meaning once you say a variable is a number, it stays a number. You cannot just reassign it to a string. Examples are Java, C++, Go, Rust. If you try to do something that doesn\u0026rsquo;t match the type, the code literally won\u0026rsquo;t compile. The error happens before execution.\nOther languages are loosely typed - the type of a variable can change freely at runtime. JavaScript, Python, PHP, Perl are in this category. This is flexible and fast to write, but it means type-related bugs only surface when the code actually runs. Sometimes in production. Sometimes at 2am.\n// JavaScript - loosely typed, this works fine let number = 10; number = \u0026#34;text\u0026#34;; // now it\u0026#39;s a string - no error, no warning // C++ - strongly typed, this won\u0026#39;t even compile int number = 10; number = \u0026#34;text\u0026#34;; // COMPILE ERROR People realized JavaScript is a very powerful language but it lacks types. So TypeScript was created - it adds a type layer on top of JavaScript while keeping all the flexibility you\u0026rsquo;re used to. You still write familiar JS syntax, but now you can annotate types and the TypeScript compiler will catch mismatches before your code runs.\n2. What Is TypeScript Exactly TypeScript is a programming language made and maintained by Microsoft. At its core it is a superset of JavaScript - which means every valid JavaScript file is also a valid TypeScript file. TypeScript only adds things on top. It never removes anything from JS.\nThe critical thing to understand early: TypeScript never runs directly in your browser or Node.js. Browsers only understand JavaScript. TypeScript must first be compiled (or more precisely, transpiled) down to plain JavaScript. That compiled output is what actually runs.\nyour TypeScript file (.ts) | | tsc (TypeScript compiler) | JavaScript file (.js) | Browser / Node.js runs it During this compilation step, TypeScript performs type checking. If there is a type error anywhere in your code, the compilation fails and it tells you exactly what\u0026rsquo;s wrong. No JavaScript file gets produced. This is the whole point - catch errors at compile time, not at runtime.\nThere are actually several tools that can compile TypeScript to JavaScript:\ntsc - the official TypeScript compiler from Microsoft esbuild - extremely fast, written in Go, used by Vite swc - written in Rust, used by Next.js ts-node - lets you run TypeScript files directly in Node without a separate compile step (for development) For learning, tsc is the right starting point. For production projects you will often use esbuild or swc because they are much faster.\n3. Setting Up - Your First TypeScript Project Let\u0026rsquo;s get this running locally.\nStep 1: Install TypeScript globally\nnpm install -g typescript This gives you the tsc command everywhere on your system.\nStep 2: Create a new project\nmkdir ts-playground cd ts-playground npm init -y npx tsc --init tsc --init creates a tsconfig.json file - this is the configuration file for the TypeScript compiler. We will go through the important settings in a bit.\nStep 3: Write your first TypeScript file\nCreate a.ts:\nconst x: number = 1; console.log(x); The : number part is the type annotation. You are telling TypeScript \u0026ldquo;this variable will always hold a number.\u0026rdquo;\nStep 4: Compile it\ntsc -b This produces a.js next to your a.ts file. Open it and you will see:\n\u0026#34;use strict\u0026#34;; const x = 1; console.log(x); Notice how the type annotation (: number) completely disappears in the output. TypeScript only exists at development time. The JS file is clean, plain JavaScript with no TypeScript anywhere.\nStep 5: See TypeScript catch an error\nNow change a.ts:\nlet x: number = 1; x = \u0026#34;harkirat\u0026#34;; // try to assign a string to a number variable console.log(x); Run tsc -b again. You will see something like:\na.ts:2:5 - error TS2322: Type \u0026#39;string\u0026#39; is not assignable to type \u0026#39;number\u0026#39;. Found 1 error. And crucially - no a.js file is created. The compilation failed. That is exactly the behavior you want. The error is caught before the code runs anywhere.\nTry it yourself: Before reading further, change the type from number to string and try assigning a number to it. What happens? Then try any as the type and assign both - what happens now? (Hint: any is the escape hatch that basically turns off type checking for that variable.)\n4. The tsconfig.json - Telling the Compiler How to Behave When you run npx tsc --init, you get a tsconfig.json with a lot of commented-out options. These are all the settings that control how TypeScript compiles your code. Most of them you will never touch, but a handful are important.\ntarget This tells the compiler which version of JavaScript to output.\n{ \u0026#34;compilerOptions\u0026#34;: { \u0026#34;target\u0026#34;: \u0026#34;ES2020\u0026#34; } } If you write an arrow function in TypeScript and set target to ES5, the compiler will convert it to a regular function expression in the output because ES5 doesn\u0026rsquo;t support arrow functions. If you set target to ES2020 or newer, it keeps the arrow function as is.\nTry it: write const greet = (name: string) =\u0026gt; \\Hello, ${name}!`and compile withtarget: \u0026ldquo;ES5\u0026rdquo;vstarget: \u0026ldquo;ES2020\u0026rdquo;`. The output is different even though the TypeScript input is the same.\nrootDir Where the compiler looks for your .ts source files.\n{ \u0026#34;compilerOptions\u0026#34;: { \u0026#34;rootDir\u0026#34;: \u0026#34;./src\u0026#34; } } Good practice is to put all TypeScript source in a src/ folder and compile from there. Keeps things clean.\noutDir Where the compiled .js files go.\n{ \u0026#34;compilerOptions\u0026#34;: { \u0026#34;rootDir\u0026#34;: \u0026#34;./src\u0026#34;, \u0026#34;outDir\u0026#34;: \u0026#34;./dist\u0026#34; } } With this setup: src/index.ts compiles to dist/index.js. This is the standard layout for TypeScript projects. You commit src/, you deploy dist/.\nnoImplicitAny { \u0026#34;compilerOptions\u0026#34;: { \u0026#34;noImplicitAny\u0026#34;: true } } Without a type annotation, TypeScript tries to infer the type. If it cannot infer it, it falls back to any. With noImplicitAny: true, that fallback is not allowed - TypeScript will throw an error forcing you to explicitly annotate.\n// noImplicitAny: true - this will error const greet = (name) =\u0026gt; `Hello, ${name}!`; // Error: Parameter \u0026#39;name\u0026#39; implicitly has an \u0026#39;any\u0026#39; type. // Fix it const greet = (name: string) =\u0026gt; `Hello, ${name}!`; This option is what separates a \u0026ldquo;TypeScript project\u0026rdquo; from a \u0026ldquo;JavaScript project with TypeScript installed.\u0026rdquo; Turn it on.\nremoveComments { \u0026#34;compilerOptions\u0026#34;: { \u0026#34;removeComments\u0026#34;: true } } Strips all comments from the compiled JS output. Smaller files, nothing revealing in production.\nA minimal practical tsconfig Here is a solid starting config for a Node.js TypeScript project:\n{ \u0026#34;compilerOptions\u0026#34;: { \u0026#34;target\u0026#34;: \u0026#34;ES2020\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;commonjs\u0026#34;, \u0026#34;rootDir\u0026#34;: \u0026#34;./src\u0026#34;, \u0026#34;outDir\u0026#34;: \u0026#34;./dist\u0026#34;, \u0026#34;noImplicitAny\u0026#34;: true, \u0026#34;strict\u0026#34;: true, \u0026#34;removeComments\u0026#34;: true, \u0026#34;esModuleInterop\u0026#34;: true }, \u0026#34;include\u0026#34;: [\u0026#34;src/**/*\u0026#34;], \u0026#34;exclude\u0026#34;: [\u0026#34;node_modules\u0026#34;] } The \u0026quot;strict\u0026quot;: true option is a catch-all that enables a bunch of strict checks including noImplicitAny, strictNullChecks, and a few others. Just turn it on. You will thank yourself later.\n5. Basic Types - The Building Blocks TypeScript has several primitive types that map directly to JavaScript types:\n// number - integers and decimals both let age: number = 25; let price: number = 9.99; // string let username: string = \u0026#34;eshan\u0026#34;; let greeting: string = `Hello, ${username}!`; // boolean let isLoggedIn: boolean = true; let hasPaid: boolean = false; // null and undefined let nothing: null = null; let missing: undefined = undefined; Typing function parameters and return types This is where TypeScript becomes genuinely useful. Functions in JavaScript are the biggest source of type bugs because there\u0026rsquo;s no enforcement on what goes in or comes out.\n// annotate parameters function greet(firstName: string): void { console.log(`Hello, ${firstName}!`); } greet(\u0026#34;Eshan\u0026#34;); // works greet(123); // ERROR: Argument of type \u0026#39;number\u0026#39; is not assignable to parameter of type \u0026#39;string\u0026#39; The : void after the parentheses is the return type. void means the function returns nothing (or undefined). If you return a value from a void function, TypeScript will catch that too.\n// function that returns a number function sum(a: number, b: number): number { return a + b; } // TypeScript can also infer the return type - you don\u0026#39;t always have to write it // but being explicit is clearer for others reading your code function multiply(a: number, b: number) { return a * b; // TypeScript infers this returns number } Quick exercise: Write a function called isAdult that takes an age: number and returns a boolean. Return true if age is 18 or more. Annotate both the parameter type and the return type explicitly. Try calling it with a string - what does TypeScript say?\nSolution function isAdult(age: number): boolean { if (age \u0026gt;= 18) { return true; } return false; } // Or the same thing, shorter: function isAdult(age: number): boolean { return age \u0026gt;= 18; } isAdult(20); // true isAdult(\u0026#34;twenty\u0026#34;); // ERROR at compile time - caught before it runs TypeScript infers the return type as boolean even without the annotation because it sees that both branches return boolean literals. But writing it explicitly is still good practice - it documents intent and TypeScript will error if you accidentally return the wrong type.\nType inference - TypeScript is smart You don\u0026rsquo;t always have to write the type. TypeScript figures it out from context:\nlet count = 5; // TypeScript infers: count is number count = \u0026#34;five\u0026#34;; // ERROR - TypeScript already knows count should be number const name = \u0026#34;Eshan\u0026#34;; // TypeScript infers: name is string (and specifically type \u0026#34;Eshan\u0026#34; for const) This is called type inference and it\u0026rsquo;s one of the things that makes TypeScript comfortable to use. You get type safety without writing types everywhere.\nFunctions as arguments (callbacks) You can type functions that are passed as arguments:\n// The type of the fn parameter says: \u0026#34;a function that takes no args and returns void\u0026#34; function runAfterDelay(fn: () =\u0026gt; void): void { setTimeout(fn, 1000); } runAfterDelay(() =\u0026gt; console.log(\u0026#34;hello\u0026#34;)); // works runAfterDelay(\u0026#34;not a function\u0026#34;); // ERROR The syntax () =\u0026gt; void is a function type. (a: number, b: string) =\u0026gt; boolean would be a function that takes a number and a string and returns a boolean.\n6. Interfaces - Typing Objects Primitive types are for simple values. For objects, TypeScript uses interfaces.\nAn interface is a blueprint that describes the shape of an object - what fields it has and what types those fields are.\ninterface User { firstName: string; lastName: string; email: string; age: number; } Now you can use User as a type anywhere:\nfunction printUser(user: User): void { console.log(`${user.firstName} ${user.lastName} - ${user.email}`); } // This works - object matches the interface exactly printUser({ firstName: \u0026#34;Eshan\u0026#34;, lastName: \u0026#34;Studio\u0026#34;, email: \u0026#34;eshan@example.com\u0026#34;, age: 22, }); // This fails - missing the \u0026#39;age\u0026#39; field printUser({ firstName: \u0026#34;Eshan\u0026#34;, lastName: \u0026#34;Studio\u0026#34;, email: \u0026#34;eshan@example.com\u0026#34;, // age is missing - ERROR }); Optional fields Sometimes a field might not always be present. Use ? to mark it optional:\ninterface User { firstName: string; lastName: string; email: string; age: number; phoneNumber?: string; // optional - may or may not exist } // Both of these are valid now const user1: User = { firstName: \u0026#34;A\u0026#34;, lastName: \u0026#34;B\u0026#34;, email: \u0026#34;a@b.com\u0026#34;, age: 20, }; const user2: User = { firstName: \u0026#34;A\u0026#34;, lastName: \u0026#34;B\u0026#34;, email: \u0026#34;a@b.com\u0026#34;, age: 20, phoneNumber: \u0026#34;9999\u0026#34;, }; Interfaces with functions Interfaces can also describe methods:\ninterface Person { name: string; age: number; greet(phrase: string): void; } Implementing interfaces with classes This is where interfaces connect to OOP. A class can implement an interface, promising that it will have all the fields and methods the interface describes:\ninterface Person { name: string; age: number; greet(phrase: string): void; } class Employee implements Person { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } greet(phrase: string): void { console.log(`${phrase} ${this.name}`); } } class Manager implements Person { name: string; age: number; department: string; constructor(name: string, age: number, department: string) { this.name = name; this.age = age; this.department = department; } greet(phrase: string): void { console.log(`${phrase} ${this.name} from ${this.department}`); } } Both Employee and Manager implement Person. This means you can write functions that accept Person and they will work with both classes. This is the concept of polymorphism - one interface, multiple implementations.\nfunction introduceEveryone(people: Person[]): void { people.forEach((person) =\u0026gt; person.greet(\u0026#34;Hello, I am\u0026#34;)); } introduceEveryone([ new Employee(\u0026#34;Alice\u0026#34;, 28), new Manager(\u0026#34;Bob\u0026#34;, 35, \u0026#34;Engineering\u0026#34;), ]); Exercise: Write an interface called Shape with a name: string field and a calculateArea(): number method. Then write two classes: Rectangle (with width and height) and Circle (with radius). Both should implement Shape. Calculate area correctly for each. Write a function that takes an array of Shape and prints the name and area of each. Try it with an array containing both rectangles and circles.\nSolution interface Shape { name: string; calculateArea(): number; } class Rectangle implements Shape { name = \u0026#34;Rectangle\u0026#34;; constructor( public width: number, public height: number, ) {} calculateArea(): number { return this.width * this.height; } } class Circle implements Shape { name = \u0026#34;Circle\u0026#34;; constructor(public radius: number) {} calculateArea(): number { return Math.PI * this.radius * this.radius; } } function printShapeInfo(shapes: Shape[]): void { shapes.forEach((shape) =\u0026gt; { console.log(`${shape.name}: area = ${shape.calculateArea().toFixed(2)}`); }); } printShapeInfo([new Rectangle(4, 5), new Circle(3), new Rectangle(10, 2)]); Notice the public width: number shorthand in the constructor. TypeScript allows you to declare and assign class properties directly in the constructor parameters using public, private, or protected. This is a very common pattern - it removes the need to write this.width = width in the body.\nAlso note: you could write an abstract class instead of an interface here, and the behavior would be similar. The difference is that abstract classes can have method implementations while interfaces are purely a description. For pure shape contracts like this, an interface is the right choice.\n7. Types - Interfaces But More Flexible type is another way to define types in TypeScript. On the surface it looks similar to interface:\ntype User = { firstName: string; lastName: string; age: number; }; But type has two superpowers that interface doesn\u0026rsquo;t.\nUnions - a value can be one of several types type StringOrNumber = string | number; function printId(id: StringOrNumber): void { console.log(`ID: ${id}`); } printId(101); // works printId(\u0026#34;abc\u0026#34;); // also works printId(true); // ERROR - boolean is not string or number This is something you genuinely cannot do with interface. You cannot say \u0026ldquo;this interface is either shape A or shape B.\u0026rdquo; But with type and the | operator, you can.\nUnion types become really useful when handling API responses or function arguments that can legitimately be different types:\ntype ApiResponse = | { success: true; data: string[]; } | { success: false; error: string; }; function handleResponse(res: ApiResponse): void { if (res.success) { console.log(res.data); // TypeScript knows data exists here } else { console.log(res.error); // TypeScript knows error exists here } } This pattern is called a discriminated union and it\u0026rsquo;s one of the most useful things in TypeScript. TypeScript is smart enough to narrow the type based on the condition you check.\nIntersection - combine multiple types into one type Employee = { name: string; startDate: Date; }; type Manager = { name: string; department: string; }; type TeamLead = Employee \u0026amp; Manager; const lead: TeamLead = { name: \u0026#34;Eshan\u0026#34;, startDate: new Date(), department: \u0026#34;Engineering\u0026#34;, }; \u0026amp; combines both types. The result must satisfy all fields of both. Think of | as \u0026ldquo;or\u0026rdquo; and \u0026amp; as \u0026ldquo;and.\u0026rdquo;\nWhen to use interface vs type This comes up a lot. The honest answer is: for most things they\u0026rsquo;re interchangeable. But a general rule that works well:\nUse interface when describing the shape of objects and classes - especially when you might need to implement it with a class. Use type when you need unions, intersections, or are working with primitives. In real codebases you will see both. Don\u0026rsquo;t overthink this distinction early on.\nQuick exercise: Create a type called Result that can be either { ok: true, value: number } or { ok: false, error: string }. Write a function divide(a: number, b: number): Result that returns the division result if b is not zero, and an error result if it is. TypeScript should force you to handle both cases at the call site.\nSolution type Result = { ok: true; value: number } | { ok: false; error: string }; function divide(a: number, b: number): Result { if (b === 0) { return { ok: false, error: \u0026#34;Cannot divide by zero\u0026#34; }; } return { ok: true, value: a / b }; } const result = divide(10, 2); if (result.ok) { console.log(\u0026#34;Result:\u0026#34;, result.value); // TypeScript knows .value exists } else { console.log(\u0026#34;Error:\u0026#34;, result.error); // TypeScript knows .error exists } Try accessing result.value without the if (result.ok) check. TypeScript will error because value only exists on the success variant. This is the whole point - TypeScript forces you to handle both cases, which means your code is actually correct.\n8. Arrays in TypeScript Typing arrays is straightforward - just add [] after the element type:\nconst numbers: number[] = [1, 2, 3, 4, 5]; const names: string[] = [\u0026#34;Alice\u0026#34;, \u0026#34;Bob\u0026#34;]; const flags: boolean[] = [true, false, true]; Or use the generic syntax (both are identical):\nconst numbers: Array\u0026lt;number\u0026gt; = [1, 2, 3]; Arrays of objects interface User { firstName: string; lastName: string; age: number; } const users: User[] = [ { firstName: \u0026#34;Alice\u0026#34;, lastName: \u0026#34;Smith\u0026#34;, age: 25 }, { firstName: \u0026#34;Bob\u0026#34;, lastName: \u0026#34;Jones\u0026#34;, age: 30 }, ]; // Now TypeScript knows what each element looks like users[0].firstName; // TypeScript autocompletes this users[0].nonExistent; // ERROR - field doesn\u0026#39;t exist on User Exercise: Given this interface and array:\ninterface User { firstName: string; lastName: string; age: number; } const users: User[] = [ { firstName: \u0026#34;Alice\u0026#34;, lastName: \u0026#34;Smith\u0026#34;, age: 17 }, { firstName: \u0026#34;Bob\u0026#34;, lastName: \u0026#34;Jones\u0026#34;, age: 30 }, { firstName: \u0026#34;Carol\u0026#34;, lastName: \u0026#34;White\u0026#34;, age: 16 }, { firstName: \u0026#34;Dave\u0026#34;, lastName: \u0026#34;Brown\u0026#34;, age: 22 }, ]; Write a function filterAdults(users: User[]): User[] that returns only users who are 18 or older. Use .filter(). Then write a second function getFullNames(users: User[]): string[] that uses .map() to get full names in the format \u0026ldquo;FirstName LastName\u0026rdquo;.\nSolution function filterAdults(users: User[]): User[] { return users.filter((user) =\u0026gt; user.age \u0026gt;= 18); } function getFullNames(users: User[]): string[] { return users.map((user) =\u0026gt; `${user.firstName} ${user.lastName}`); } const adults = filterAdults(users); console.log(getFullNames(adults)); // [\u0026#34;Bob Jones\u0026#34;, \u0026#34;Dave Brown\u0026#34;] Notice that the return types are explicitly annotated. TypeScript would infer them correctly even without the annotation, but writing them explicitly makes the function signature a clear contract: \u0026ldquo;give me an array of User, I give back an array of User.\u0026rdquo;\n9. Enums - Named Constants Enums give human-readable names to sets of constant values. They\u0026rsquo;re useful when you have a fixed set of options that something can be.\nLet\u0026rsquo;s say you\u0026rsquo;re building a game and you have arrow key inputs:\n// Without enum - passing raw numbers, confusing function handleKey(key: number) { // what does 0, 1, 2, 3 mean again? } handleKey(0); // up? down? nobody knows without checking docs // With enum enum Direction { Up, Down, Left, Right, } function handleKey(key: Direction): void { // clear what each value means } handleKey(Direction.Up); // obvious handleKey(Direction.Down); // obvious By default, enum values are numbers starting from 0. Direction.Up is 0, Direction.Down is 1, and so on. You can verify this:\nconsole.log(Direction.Up); // 0 console.log(Direction.Down); // 1 The final runtime value is still a number (or string). The enum is just a compile-time alias. Once TypeScript compiles this, the output will have the numeric values.\nCustomizing enum values enum Direction { Up = 1, // starts from 1 Down, // becomes 2 automatically Left, // becomes 3 Right, // becomes 4 } // Or string enums enum Status { Active = \u0026#34;ACTIVE\u0026#34;, Inactive = \u0026#34;INACTIVE\u0026#34;, Banned = \u0026#34;BANNED\u0026#34;, } String enums are often better for readability in debugging and in API responses because \u0026quot;ACTIVE\u0026quot; is clearer than 2 when you see it in logs or a database.\nCommon real-world use: HTTP status codes enum ResponseStatus { Success = 200, Created = 201, BadRequest = 400, Unauthorized = 401, NotFound = 404, InternalError = 500, } // In an Express route app.get(\u0026#34;/user/:id\u0026#34;, (req, res) =\u0026gt; { if (!req.params.id) { return res.status(ResponseStatus.BadRequest).json({ error: \u0026#34;Missing ID\u0026#34; }); } const user = findUser(req.params.id); if (!user) { return res .status(ResponseStatus.NotFound) .json({ error: \u0026#34;User not found\u0026#34; }); } res.status(ResponseStatus.Success).json({ user }); }); This is much clearer than res.status(404) scattered everywhere. And if the status codes ever change, you change them in one place.\nExercise: Create an enum called UserRole with values Admin, Editor, and Viewer. Write a function canEdit(role: UserRole): boolean that returns true only for Admin and Editor. Write another canDelete(role: UserRole): boolean that returns true only for Admin. Test it with all three roles.\nSolution enum UserRole { Admin = \u0026#34;ADMIN\u0026#34;, Editor = \u0026#34;EDITOR\u0026#34;, Viewer = \u0026#34;VIEWER\u0026#34;, } function canEdit(role: UserRole): boolean { return role === UserRole.Admin || role === UserRole.Editor; } function canDelete(role: UserRole): boolean { return role === UserRole.Admin; } console.log(canEdit(UserRole.Admin)); // true console.log(canEdit(UserRole.Editor)); // true console.log(canEdit(UserRole.Viewer)); // false console.log(canDelete(UserRole.Editor)); // false A note on enums vs union types: for simple cases like this, many modern TypeScript codebases prefer union types over enums:\ntype UserRole = \u0026#34;ADMIN\u0026#34; | \u0026#34;EDITOR\u0026#34; | \u0026#34;VIEWER\u0026#34;; This is simpler, requires no compilation magic, and works identically at runtime. Enums generate actual JavaScript code; union types of string literals are compile-only and produce zero runtime overhead. Both are valid - know that both options exist.\n10. Generics - Write Once, Work for Any Type Generics are one of those things that seem confusing at first but once they click, you use them all the time.\nThe problem generics solve: you want to write a function that works with multiple types but still preserves type information.\nHere is the problem without generics:\n// Returns the first element of an array // If we type this as any, we lose all type info function getFirst(arr: any[]): any { return arr[0]; } const first = getFirst([\u0026#34;hello\u0026#34;, \u0026#34;world\u0026#34;]); first.toUpperCase(); // no TypeScript help here - first is \u0026#39;any\u0026#39; first.someRandomMethod(); // TypeScript won\u0026#39;t catch this bug You could write separate functions for each type:\nfunction getFirstString(arr: string[]): string { return arr[0]; } function getFirstNumber(arr: number[]): number { return arr[0]; } // ... and so on for every type - this doesn\u0026#39;t scale Generics solve this elegantly:\nfunction getFirst\u0026lt;T\u0026gt;(arr: T[]): T { return arr[0]; } The \u0026lt;T\u0026gt; is a type parameter - a placeholder for whatever type you actually use. When you call the function:\nconst first = getFirst([\u0026#34;hello\u0026#34;, \u0026#34;world\u0026#34;]); // TypeScript infers T = string // first is now typed as string, not any first.toUpperCase(); // TypeScript knows this is safe - first is string first.someRandomMethod(); // ERROR - this method doesn\u0026#39;t exist on string const num = getFirst([1, 2, 3]); // TypeScript infers T = number // num is typed as number num.toFixed(2); // works - it\u0026#39;s a number You can also explicitly specify the type parameter when calling:\ngetFirst\u0026lt;string\u0026gt;([\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;]); getFirst\u0026lt;number\u0026gt;([1, 2, 3]); A generic API response wrapper This comes up constantly in real projects. Backend APIs typically return a response with some wrapper structure:\ninterface ApiResponse\u0026lt;T\u0026gt; { data: T; status: number; message: string; } // Now you can use it with any data type type UserResponse = ApiResponse\u0026lt;User\u0026gt;; // { data: User; status: number; message: string } type TodoListResponse = ApiResponse\u0026lt;Todo[]\u0026gt;; // { data: Todo[]; status: number; message: string } A generic pair function createPair\u0026lt;T, U\u0026gt;(first: T, second: U): [T, U] { return [first, second]; } const pair = createPair(\u0026#34;hello\u0026#34;, 42); // pair is [string, number] You can have multiple type parameters. By convention they\u0026rsquo;re named T, U, V, but you can use more descriptive names when it helps: TData, TError, etc.\nExercise: Write a generic function wrap\u0026lt;T\u0026gt;(value: T): { value: T; timestamp: number } that wraps any value in an object with the value and the current timestamp (use Date.now()). Call it with a string, a number, and a User object. Verify that TypeScript correctly infers the type of result.value in each case.\nSolution function wrap\u0026lt;T\u0026gt;(value: T): { value: T; timestamp: number } { return { value, timestamp: Date.now(), }; } const wrappedString = wrap(\u0026#34;hello\u0026#34;); wrappedString.value.toUpperCase(); // works - TypeScript knows value is string const wrappedNumber = wrap(42); wrappedNumber.value.toFixed(2); // works - TypeScript knows value is number interface User { name: string; age: number; } const wrappedUser = wrap\u0026lt;User\u0026gt;({ name: \u0026#34;Eshan\u0026#34;, age: 22 }); wrappedUser.value.name; // works - TypeScript knows value is User This pattern of wrapping data with metadata is extremely common. Think timestamps, pagination info, request IDs. Generics let you write the wrapper once and use it with any data type.\n11. Exporting and Importing - Splitting Code Across Files TypeScript follows the ES module system. Same import / export syntax you know from JavaScript, just with types involved.\nNamed exports // math.ts export function add(x: number, y: number): number { return x + y; } export function subtract(x: number, y: number): number { return x - y; } export interface MathResult { result: number; operation: string; } // main.ts import { add, subtract, MathResult } from \u0026#34;./math\u0026#34;; const result: MathResult = { result: add(5, 3), operation: \u0026#34;addition\u0026#34;, }; Default exports // Calculator.ts export default class Calculator { add(x: number, y: number): number { return x + y; } multiply(x: number, y: number): number { return x * y; } } // main.ts import Calculator from \u0026#34;./Calculator\u0026#34;; // no curly braces for default const calc = new Calculator(); console.log(calc.add(10, 5)); Exporting types You can export interfaces and types just like functions:\n// types.ts export interface User { id: string; name: string; email: string; } export type Status = \u0026#34;active\u0026#34; | \u0026#34;inactive\u0026#34; | \u0026#34;banned\u0026#34;; // user-service.ts import { User, Status } from \u0026#34;./types\u0026#34;; function updateUserStatus(user: User, status: Status): User { return { ...user }; } In larger projects you typically have a types.ts or types/index.ts file where all your shared interfaces and types live. This is a very clean pattern - one source of truth for all your data structures.\n12. A Few More Things You Will Encounter These topics come up regularly in real TypeScript codebases. They are not beginner things but you will hit them soon enough that it is worth at least seeing them.\nReadonly and Partial utility types TypeScript has built-in utility types that transform existing types:\ninterface User { id: string; name: string; email: string; } // Readonly - all fields become read-only, cannot be modified after creation const user: Readonly\u0026lt;User\u0026gt; = { id: \u0026#34;1\u0026#34;, name: \u0026#34;Eshan\u0026#34;, email: \u0026#34;e@e.com\u0026#34;, }; user.name = \u0026#34;test\u0026#34;; // ERROR - cannot assign to read-only property // Partial - all fields become optional // Useful for update operations where you only want to change some fields function updateUser(userId: string, updates: Partial\u0026lt;User\u0026gt;): void { // updates might have only \u0026#39;name\u0026#39;, or only \u0026#39;email\u0026#39;, or both - all fine } updateUser(\u0026#34;1\u0026#34;, { name: \u0026#34;New Name\u0026#34; }); // no need to pass all fields Pick and Omit interface User { id: string; name: string; email: string; password: string; createdAt: Date; } // Pick - take only specific fields type PublicUser = Pick\u0026lt;User, \u0026#34;id\u0026#34; | \u0026#34;name\u0026#34; | \u0026#34;email\u0026#34;\u0026gt;; // { id: string; name: string; email: string } // Omit - take all fields EXCEPT specific ones type SafeUser = Omit\u0026lt;User, \u0026#34;password\u0026#34;\u0026gt;; // { id: string; name: string; email: string; createdAt: Date } Pick and Omit are extremely useful when you don\u0026rsquo;t want to expose certain fields. For example, you never want to send password back to the client. Omit\u0026lt;User, \u0026quot;password\u0026quot;\u0026gt; ensures the return type never includes it.\nThe as keyword (type assertion) Sometimes TypeScript cannot figure out the type but you know better. as lets you assert a type:\nconst input = document.getElementById(\u0026#34;username\u0026#34;) as HTMLInputElement; input.value; // now TypeScript knows this is an input element and has .value Be careful with as. It bypasses type checking. If you are wrong about the type, you will get a runtime error. Use it when you have external data (DOM, JSON from APIs) where TypeScript genuinely cannot infer the type. Don\u0026rsquo;t use it to silence TypeScript errors that are actually warnings about your code.\nPatterns I Noticed After Using TypeScript for a While These are things nobody explains upfront but they save a lot of time.\nTypeScript does not fix bad code, it documents it. The types you write are a form of documentation. If your function takes a 200-field mega-object when it only needs two fields, TypeScript won\u0026rsquo;t complain - but it is still bad design. Use specific types for function parameters. A function that needs a user\u0026rsquo;s email and age should take { email: string; age: number } not the entire User object.\nStart with strict mode from day one. Turning on \u0026quot;strict\u0026quot;: true in your tsconfig when you already have a large codebase is painful. Do it from the start. Every project. Always.\nAvoid any like it has a communicable disease. Every time you write any, you are opting out of TypeScript. Your code compiles, TypeScript smiles, bugs happen at runtime. If you genuinely don\u0026rsquo;t know the type yet, use unknown instead - it forces you to narrow the type before doing anything with the value.\n// any - typescript just trusts you, can cause runtime errors function process(data: any) { data.someMethod(); // no error, might crash at runtime } // unknown - typescript forces you to check before using function process(data: unknown) { if (typeof data === \u0026#34;string\u0026#34;) { data.toUpperCase(); // safe, TypeScript knows it\u0026#39;s string here } } Types are erased at runtime. TypeScript types exist only in your source code. The compiled JavaScript has zero type information. This means you cannot do if (variable instanceof MyInterface) - interfaces don\u0026rsquo;t exist at runtime. For runtime type checking you still need regular JS techniques like typeof, instanceof, or a validation library.\nThe TypeScript error messages look scary but become readable. At first, TypeScript errors are walls of text. After a few weeks you learn to read them from bottom to top - the bottom line is usually the actual problem, the top part is context about where it happened.\nZod for runtime validation. TypeScript validates at compile time. But when data comes in from outside your code (HTTP request body, JSON file, user input), TypeScript has no idea if it actually matches your types. Zod is a library that lets you define schemas and validate data at runtime:\nimport { z } from \u0026#34;zod\u0026#34;; const UserSchema = z.object({ email: z.string().email(), age: z.number().min(0).max(150), }); // This validates the actual data at runtime, not just at compile time const parsed = UserSchema.parse(req.body); // parsed is now safely typed as { email: string; age: number } This combination - TypeScript for compile-time safety, Zod for runtime validation - is the standard pattern in production TypeScript backends.\nMini Projects - Build Something Real These projects touch every concept from this post. Start with the first one and work upward.\nProject 1: Todo CLI with Types (Beginner) Build a command-line todo manager using TypeScript. It should run with ts-node directly.\nWhat it should do:\nAdd a todo with a title and an optional priority (use an enum: Low, Medium, High) List all todos Mark a todo as done (by its ID) Filter todos by priority Types you will need: Todo interface with id, title, priority, done. Priority enum. Functions for each operation with proper type annotations.\nSolution with explanation // todo.ts import * as readline from \u0026#34;readline\u0026#34;; enum Priority { Low = \u0026#34;LOW\u0026#34;, Medium = \u0026#34;MEDIUM\u0026#34;, High = \u0026#34;HIGH\u0026#34;, } interface Todo { id: number; title: string; priority: Priority; done: boolean; createdAt: Date; } let todos: Todo[] = []; let nextId = 1; function addTodo(title: string, priority: Priority = Priority.Medium): Todo { const todo: Todo = { id: nextId++, title, priority, done: false, createdAt: new Date(), }; todos.push(todo); return todo; } function markDone(id: number): boolean { const todo = todos.find((t) =\u0026gt; t.id === id); if (!todo) return false; todo.done = true; return true; } function filterByPriority(priority: Priority): Todo[] { return todos.filter((t) =\u0026gt; t.priority === priority); } function listTodos(): void { if (todos.length === 0) { console.log(\u0026#34;No todos yet.\u0026#34;); return; } todos.forEach((todo) =\u0026gt; { const status = todo.done ? \u0026#34;[x]\u0026#34; : \u0026#34;[ ]\u0026#34;; console.log(`${status} #${todo.id} [${todo.priority}] ${todo.title}`); }); } // Quick test addTodo(\u0026#34;Learn TypeScript basics\u0026#34;, Priority.High); addTodo(\u0026#34;Build a project\u0026#34;, Priority.Medium); addTodo(\u0026#34;Read docs\u0026#34;, Priority.Low); addTodo(\u0026#34;Write tests\u0026#34;, Priority.High); markDone(1); console.log(\u0026#34;All todos:\u0026#34;); listTodos(); console.log(\u0026#34;\\nHigh priority only:\u0026#34;); filterByPriority(Priority.High).forEach((t) =\u0026gt; { console.log(` ${t.done ? \u0026#34;[x]\u0026#34; : \u0026#34;[ ]\u0026#34;} ${t.title}`); }); Run it with: npx ts-node todo.ts\nNotice how the type system made the code almost write itself. When you wrote todo.id++ you got an error because id should not be modified. When you tried to access a field that doesn\u0026rsquo;t exist on Todo, TypeScript caught it. This is the everyday value.\nProject 2: Typed Express API (Intermediate) Build a REST API for a simple user management system. This is the real thing - TypeScript + Express together, which is how almost every Node.js production backend is written.\nSetup:\nmkdir typed-api \u0026amp;\u0026amp; cd typed-api npm init -y npm install express npm install -D typescript ts-node @types/node @types/express nodemon npx tsc --init Update tsconfig.json:\n{ \u0026#34;compilerOptions\u0026#34;: { \u0026#34;target\u0026#34;: \u0026#34;ES2020\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;commonjs\u0026#34;, \u0026#34;rootDir\u0026#34;: \u0026#34;./src\u0026#34;, \u0026#34;outDir\u0026#34;: \u0026#34;./dist\u0026#34;, \u0026#34;strict\u0026#34;: true, \u0026#34;esModuleInterop\u0026#34;: true } } Add to package.json scripts:\n\u0026#34;scripts\u0026#34;: { \u0026#34;dev\u0026#34;: \u0026#34;nodemon --exec ts-node src/index.ts\u0026#34;, \u0026#34;build\u0026#34;: \u0026#34;tsc\u0026#34;, \u0026#34;start\u0026#34;: \u0026#34;node dist/index.js\u0026#34; } What to build:\n// src/types.ts export interface User { id: string; name: string; email: string; role: \u0026#34;admin\u0026#34; | \u0026#34;user\u0026#34;; createdAt: Date; } export type CreateUserBody = Omit\u0026lt;User, \u0026#34;id\u0026#34; | \u0026#34;createdAt\u0026#34;\u0026gt;; export type UpdateUserBody = Partial\u0026lt;Omit\u0026lt;User, \u0026#34;id\u0026#34; | \u0026#34;createdAt\u0026#34;\u0026gt;\u0026gt;; export interface ApiResponse\u0026lt;T\u0026gt; { data: T | null; error: string | null; status: number; } // src/index.ts import express, { Request, Response } from \u0026#34;express\u0026#34;; import { v4 as uuidv4 } from \u0026#34;uuid\u0026#34;; // npm install uuid \u0026amp;\u0026amp; npm install -D @types/uuid import { User, CreateUserBody, UpdateUserBody, ApiResponse } from \u0026#34;./types\u0026#34;; const app = express(); app.use(express.json()); let users: User[] = []; // GET all users app.get(\u0026#34;/users\u0026#34;, (req: Request, res: Response) =\u0026gt; { const response: ApiResponse\u0026lt;User[]\u0026gt; = { data: users, error: null, status: 200, }; res.json(response); }); // POST create user app.post(\u0026#34;/users\u0026#34;, (req: Request\u0026lt;{}, {}, CreateUserBody\u0026gt;, res: Response) =\u0026gt; { const { name, email, role } = req.body; if (!name || !email || !role) { const response: ApiResponse\u0026lt;null\u0026gt; = { data: null, error: \u0026#34;Missing required fields: name, email, role\u0026#34;, status: 400, }; return res.status(400).json(response); } const newUser: User = { id: uuidv4(), name, email, role, createdAt: new Date(), }; users.push(newUser); const response: ApiResponse\u0026lt;User\u0026gt; = { data: newUser, error: null, status: 201, }; res.status(201).json(response); }); // PATCH update user app.patch( \u0026#34;/users/:id\u0026#34;, (req: Request\u0026lt;{ id: string }, {}, UpdateUserBody\u0026gt;, res: Response) =\u0026gt; { const user = users.find((u) =\u0026gt; u.id === req.params.id); if (!user) { const response: ApiResponse\u0026lt;null\u0026gt; = { data: null, error: \u0026#34;User not found\u0026#34;, status: 404, }; return res.status(404).json(response); } // Partial update - only update fields that were sent Object.assign(user, req.body); const response: ApiResponse\u0026lt;User\u0026gt; = { data: user, error: null, status: 200, }; res.json(response); }, ); // DELETE user app.delete(\u0026#34;/users/:id\u0026#34;, (req: Request\u0026lt;{ id: string }\u0026gt;, res: Response) =\u0026gt; { const index = users.findIndex((u) =\u0026gt; u.id === req.params.id); if (index === -1) { return res .status(404) .json({ data: null, error: \u0026#34;User not found\u0026#34;, status: 404 }); } users.splice(index, 1); res.json({ data: null, error: null, status: 200 }); }); app.listen(3000, () =\u0026gt; { console.log(\u0026#34;Server running on port 3000\u0026#34;); }); Run with npm run dev. Test with curl or Postman:\n# Create a user curl -X POST http://localhost:3000/users \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{\u0026#34;name\u0026#34;: \u0026#34;Eshan\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;e@e.com\u0026#34;, \u0026#34;role\u0026#34;: \u0026#34;admin\u0026#34;}\u0026#39; # List all users curl http://localhost:3000/users # Update - only sending name, email stays the same curl -X PATCH http://localhost:3000/users/{ID_FROM_ABOVE} \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{\u0026#34;name\u0026#34;: \u0026#34;Eshan Updated\u0026#34;}\u0026#39; The Partial\u0026lt;\u0026gt; and Omit\u0026lt;\u0026gt; utility types are doing real work here. CreateUserBody ensures you cannot accidentally include an id in a creation request. UpdateUserBody makes all fields optional so you can PATCH with just the fields you want to change. This is the actual pattern used in production APIs.\nExtension ideas (try these yourself):\nAdd input validation using Zod (validate the request body shape at runtime) Add a GET /users/:id route Add filtering by role: GET /users?role=admin Move the users data to a simple JSON file for persistence between restarts Project 3: GitHub Stats Dashboard (Intermediate, No Backend Needed) Build a typed React app that shows GitHub user statistics. This uses TypeScript + React + fetch, everything typed end to end.\nSetup:\nnpm create vite@latest github-stats -- --template react-ts cd github-stats npm install npm run dev // src/types.ts export interface GithubUser { login: string; name: string | null; avatar_url: string; public_repos: number; followers: number; following: number; bio: string | null; location: string | null; created_at: string; } export interface GithubRepo { id: number; name: string; description: string | null; stargazers_count: number; forks_count: number; language: string | null; html_url: string; } export type FetchState\u0026lt;T\u0026gt; = | { status: \u0026#34;idle\u0026#34; } | { status: \u0026#34;loading\u0026#34; } | { status: \u0026#34;success\u0026#34;; data: T } | { status: \u0026#34;error\u0026#34;; error: string }; // src/hooks/useGithub.ts import { useState } from \u0026#34;react\u0026#34;; import { GithubUser, GithubRepo, FetchState } from \u0026#34;../types\u0026#34;; export function useGithubUser() { const [state, setState] = useState\u0026lt;FetchState\u0026lt;GithubUser\u0026gt;\u0026gt;({ status: \u0026#34;idle\u0026#34;, }); const [repos, setRepos] = useState\u0026lt;FetchState\u0026lt;GithubRepo[]\u0026gt;\u0026gt;({ status: \u0026#34;idle\u0026#34;, }); async function fetchUser(username: string): Promise\u0026lt;void\u0026gt; { setState({ status: \u0026#34;loading\u0026#34; }); setRepos({ status: \u0026#34;loading\u0026#34; }); try { const [userRes, reposRes] = await Promise.all([ fetch(`https://api.github.com/users/${username}`), fetch( `https://api.github.com/users/${username}/repos?sort=stars\u0026amp;per_page=5`, ), ]); if (!userRes.ok) { setState({ status: \u0026#34;error\u0026#34;, error: \u0026#34;User not found\u0026#34; }); setRepos({ status: \u0026#34;idle\u0026#34; }); return; } const userData: GithubUser = await userRes.json(); const reposData: GithubRepo[] = await reposRes.json(); setState({ status: \u0026#34;success\u0026#34;, data: userData }); setRepos({ status: \u0026#34;success\u0026#34;, data: reposData }); } catch { setState({ status: \u0026#34;error\u0026#34;, error: \u0026#34;Something went wrong\u0026#34; }); setRepos({ status: \u0026#34;idle\u0026#34; }); } } return { userState: state, reposState: repos, fetchUser }; } // src/App.tsx import { useState } from \u0026#34;react\u0026#34;; import { useGithubUser } from \u0026#34;./hooks/useGithub\u0026#34;; export default function App() { const [input, setInput] = useState(\u0026#34;\u0026#34;); const { userState, reposState, fetchUser } = useGithubUser(); function handleSearch() { if (input.trim()) { fetchUser(input.trim()); } } return ( \u0026lt;div style={{ maxWidth: 600, margin: \u0026#34;0 auto\u0026#34;, padding: \u0026#34;2rem\u0026#34; }}\u0026gt; \u0026lt;h1\u0026gt;GitHub Stats\u0026lt;/h1\u0026gt; \u0026lt;div style={{ display: \u0026#34;flex\u0026#34;, gap: \u0026#34;0.5rem\u0026#34;, marginBottom: \u0026#34;2rem\u0026#34; }}\u0026gt; \u0026lt;input value={input} onChange={(e) =\u0026gt; setInput(e.target.value)} onKeyDown={(e) =\u0026gt; e.key === \u0026#34;Enter\u0026#34; \u0026amp;\u0026amp; handleSearch()} placeholder=\u0026#34;GitHub username\u0026#34; style={{ flex: 1, padding: \u0026#34;0.5rem\u0026#34; }} /\u0026gt; \u0026lt;button onClick={handleSearch}\u0026gt;Search\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; {userState.status === \u0026#34;loading\u0026#34; \u0026amp;\u0026amp; \u0026lt;p\u0026gt;Loading...\u0026lt;/p\u0026gt;} {userState.status === \u0026#34;error\u0026#34; \u0026amp;\u0026amp; ( \u0026lt;p style={{ color: \u0026#34;red\u0026#34; }}\u0026gt;{userState.error}\u0026lt;/p\u0026gt; )} {userState.status === \u0026#34;success\u0026#34; \u0026amp;\u0026amp; ( \u0026lt;div\u0026gt; \u0026lt;img src={userState.data.avatar_url} alt={userState.data.login} style={{ width: 80, borderRadius: \u0026#34;50%\u0026#34; }} /\u0026gt; \u0026lt;h2\u0026gt;{userState.data.name ?? userState.data.login}\u0026lt;/h2\u0026gt; {userState.data.bio \u0026amp;\u0026amp; \u0026lt;p\u0026gt;{userState.data.bio}\u0026lt;/p\u0026gt;} \u0026lt;p\u0026gt; Repos: {userState.data.public_repos} | Followers:{\u0026#34; \u0026#34;} {userState.data.followers} \u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; )} {reposState.status === \u0026#34;success\u0026#34; \u0026amp;\u0026amp; ( \u0026lt;div style={{ marginTop: \u0026#34;1rem\u0026#34; }}\u0026gt; \u0026lt;h3\u0026gt;Top Repos\u0026lt;/h3\u0026gt; {reposState.data.map((repo) =\u0026gt; ( \u0026lt;div key={repo.id} style={{ borderBottom: \u0026#34;1px solid #eee\u0026#34;, padding: \u0026#34;0.5rem 0\u0026#34; }} \u0026gt; \u0026lt;a href={repo.html_url} target=\u0026#34;_blank\u0026#34; rel=\u0026#34;noreferrer\u0026#34;\u0026gt; {repo.name} \u0026lt;/a\u0026gt; \u0026lt;span\u0026gt; ⭐ {repo.stargazers_count}\u0026lt;/span\u0026gt; {repo.language \u0026amp;\u0026amp; \u0026lt;span\u0026gt; | {repo.language}\u0026lt;/span\u0026gt;} {repo.description \u0026amp;\u0026amp; ( \u0026lt;p style={{ margin: \u0026#34;0.2rem 0\u0026#34;, color: \u0026#34;#666\u0026#34; }}\u0026gt; {repo.description} \u0026lt;/p\u0026gt; )} \u0026lt;/div\u0026gt; ))} \u0026lt;/div\u0026gt; )} \u0026lt;/div\u0026gt; ); } The FetchState\u0026lt;T\u0026gt; discriminated union type is a genuinely useful pattern. Instead of having three separate booleans (isLoading, isError, isSuccess) that can all be true simultaneously in a buggy state, you have one state that can only ever be one thing at a time. TypeScript will narrow it correctly inside each if block. This is a pattern worth taking with you into every project.\nCommon Confusions Confusion Reality \u0026ldquo;TypeScript runs in the browser\u0026rdquo; No. TypeScript compiles to JavaScript first. The browser runs the compiled JS. \u0026ldquo;TypeScript and JavaScript types are the same thing\u0026rdquo; TS types only exist at compile time. At runtime it is all plain JavaScript - no types anywhere. \u0026ldquo;any is fine for now, I\u0026rsquo;ll fix it later\u0026rdquo; You won\u0026rsquo;t. Use unknown instead if you genuinely don\u0026rsquo;t know. \u0026ldquo;interface and type are completely different\u0026rdquo; For object shapes they\u0026rsquo;re nearly identical. Use interface for classes and extendable shapes, type for unions and intersections. \u0026ldquo;TypeScript makes my code safer at runtime\u0026rdquo; It doesn\u0026rsquo;t. External data (API responses, user input) is untyped at runtime. You need Zod or similar for that. \u0026ldquo;Generics are only for library authors\u0026rdquo; You\u0026rsquo;ll use generics constantly for API response wrappers, utility functions, and custom hooks. Key Takeaways TypeScript is JavaScript with types. It compiles to plain JS. The browser never sees TypeScript. The biggest value is catching errors at compile time, not at runtime in production. interface for object shapes and class contracts. type for unions and intersections. Generics let you write functions and types that work with any type while preserving type information. Enums give named constants - useful for finite sets of values like roles, statuses, directions. strict: true in tsconfig from day one. Always. Avoid any. Use unknown when you don\u0026rsquo;t know the type yet. Utility types like Partial, Pick, Omit, Readonly save a lot of boilerplate. TypeScript cannot validate runtime data. Use Zod for that. The pattern type FetchState\u0026lt;T\u0026gt; = { status: \u0026quot;idle\u0026quot; } | { status: \u0026quot;loading\u0026quot; } | { status: \u0026quot;success\u0026quot;; data: T } | { status: \u0026quot;error\u0026quot;; error: string } is something you will use in almost every async-heavy project. ","permalink":"/posts/10-typescripts/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eBlog Summary:\u003c/strong\u003e JavaScript is great until your app crashes at 2am because someone passed a string where a number was expected. TypeScript fixes that. This post covers TypeScript from the ground up - types, interfaces, generics, enums, tsconfig, and how to actually use all of this in real code. By the end you will build a fully typed REST API with Express.\u003c/p\u003e\u003c/blockquote\u003e\n\u003chr\u003e\n\u003cp\u003eSo I have been writing JavaScript for a while now. And at some point the codebase grew big enough that I started getting these bugs that should never exist. Like calling \u003ccode\u003e.toUpperCase()\u003c/code\u003e on something that turned out to be \u003ccode\u003eundefined\u003c/code\u003e at runtime. Or a function expecting an object with a \u003ccode\u003ename\u003c/code\u003e field but someone passing in just a string. These are not logic bugs. They are type bugs. And TypeScript is specifically designed to catch them before your code even runs.\u003c/p\u003e","title":"10 : TypeScript - JavaScript But It Yells at You First"},{"content":" There is a very specific moment where git stops making sense. You know add, commit, push. Then someone says \u0026ldquo;just fork the repo, set upstream, rebase your branch onto main and open a PR.\u0026rdquo; You nod. You have no idea what any of that means.\nThat is what this covers. Not a full git textbook, not a list of every flag. Just the things you will actually hit in real work, explained in a way that makes them click. If you are reading this with zero experience, that is fine. Start from the top and go section by section. Everything builds on what came before.\nthe mental model (actually important, takes 2 minutes) Before running a single command, this is the one concept worth getting right.\nGit does not track changes the way most people imagine. It does not store a list of edits like \u0026ldquo;you added line 5 and deleted line 12.\u0026rdquo; Instead, it stores snapshots. Every commit is a complete picture of your entire project at that exact moment. This is why switching between commits is instant, why branching costs almost nothing, and why certain undo operations behave the way they do.\nThree places your code can live at any point in time:\nEvery single git command either moves data between these three areas or reads from them. Once you internalize this, the output of git status becomes completely readable without guessing.\nsetup (do this once before anything else) Before you can use git, it needs to know who you are. Every commit you make will be stamped with this name and email, so use something real.\n$ git config --global user.name \"Your Name\" $ git config --global user.email \"you@example.com\" $ git config --global core.editor nano # nano is safest for beginners # other options: nvim, \"code --wait\" $ git config --global init.defaultBranch main $ git config --global alias.lg \"log --oneline --graph --all --decorate\" # check everything that got set $ git config --global --list That alias.lg line is worth its weight. Instead of a wall of text when you run git log, running git lg gives you a visual tree of your branches and commits. You will use it constantly once you have it.\nA quick note on editors: if you are just starting out, set it to nano. If you accidentally end up in vim without setting this, type :q! and press enter to escape.\npractice: verify your setup Run each config command above in your terminal, replacing the name and email with your own. Run git config --global --list and confirm your name, email, and editor appear in the output. Create a test folder anywhere: mkdir git-practice \u0026\u0026 cd git-practice Run git init and then git lg -- you will see an empty result, which is fine. The alias is working. starting a repo Two situations: starting fresh, or working with an existing project.\n# --- starting a brand new project --- $ mkdir my-project \u0026\u0026 cd my-project $ git init # this creates a hidden .git/ folder inside your project # that folder is git. it contains your entire history. # delete .git/ and all git tracking vanishes. your files stay. # --- getting someone else's project --- $ git clone https://github.com/username/repo.git $ git clone https://github.com/username/repo.git my-folder-name # custom folder name When you clone, git automatically saves the source URL under the nickname origin. That is all a \u0026ldquo;remote\u0026rdquo; is: a saved URL with a name you can refer to later. We will come back to remotes in detail.\npractice: init your first repo Create a new folder called hello-git and navigate into it. Run git init. Run ls -la (on Mac/Linux) or dir /a (on Windows) and confirm you see a .git folder. Create a file: echo \"hello world\" \u003e readme.txt Run git status and read the output carefully. You should see readme.txt listed as an untracked file. the daily cycle This is what you will do every single time you work on a project. Get comfortable with this loop before moving on.\n$ git status # always start here. always. $ git add index.html # stage one specific file $ git add src/ # stage everything in a folder $ git add . # stage every changed file from here down $ git add -p # stage piece by piece, hunk by hunk (powerful) $ git commit -m \"add login page\" $ git push git add -p is worth learning even as a beginner, even though most tutorials skip it. It walks you through each changed section of each file and asks what to do with it. The options are:\ny to stage this chunk n to skip it s to split it into smaller pieces q to quit This matters when you have changed two unrelated things in the same file and want to put them in separate commits. Commits should be focused and logical, not just \u0026ldquo;everything I changed today.\u0026rdquo;\nWriting good commit messages. A helpful rule: your commit message should complete this sentence: \u0026ldquo;If applied, this commit will ___\u0026rdquo;. Keep the first line under 72 characters. Many open source projects use a prefix format like feat:, fix:, docs:, refactor:, chore:. It is not required everywhere but it is a good habit that makes history readable.\nGood: fix: prevent crash when user email is empty Bad: stuff or fix or asdfgh\nreading git status output $ git status Changes to be committed: # staged. will go into your next commit. new file: login.html Changes not staged for commit: # modified but not yet staged. modified: index.html Untracked files: # git sees these files but is not tracking them. notes.txt One thing that trips people up: the same file can appear in both \u0026ldquo;to be committed\u0026rdquo; and \u0026ldquo;not staged\u0026rdquo; at once. This happens when you stage a file, then edit it again. Staging takes a snapshot of the file at that exact moment. The newer edit is sitting in your working directory as a separate version that has not been staged yet. Run git add on it again to capture the latest version.\nlooking at history and differences $ git log --oneline # compact commit list $ git lg # your alias: visual branch tree (use this) $ git diff # what changed but is NOT staged yet $ git diff --staged # what IS staged (what will go into the next commit) $ git diff HEAD~1 HEAD # compare the last two commits $ git show HEAD # full diff of the latest commit $ git show abc1234:style.css # see a file exactly as it was at any commit $ git log -S \"functionName\" # find when a specific string appeared or disappeared HEAD just means \u0026ldquo;wherever you are right now.\u0026rdquo; Think of it as a bookmark. HEAD~1 is one commit behind your current position, HEAD~3 is three back. The tilde means \u0026ldquo;go back this many commits.\u0026rdquo;\ngit log -S is called the pickaxe search and most people never discover it. It searches your entire project history for commits that added or removed a specific string. When something breaks and you have no idea which commit caused it, this is often the fastest way to find out.\npractice: the daily cycle Inside your hello-git folder, create two files: index.html and style.css. Run git status and confirm both show as untracked. Stage only index.html with git add index.html. Run git status again. Notice that index.html is staged and style.css is still untracked. Two different states in the same status output. Commit with a good message: git commit -m \"feat: add initial html structure\" Now stage and commit style.css separately. Run git lg and see your two commits in the tree. .gitignore Some files should never go into your git history. Dependencies (like node_modules/) are huge and can be reinstalled. Build output can be regenerated. And API keys should never, ever be committed.\nThe .gitignore file tells git which files and folders to completely ignore.\n.gitignore # dependencies (these get installed, not committed) node_modules/ .venv/ __pycache__/ # build output (generated automatically) dist/ build/ *.pyc # secrets. NEVER commit these. .env .env.local .env.*.local # OS and editor clutter .DS_Store *.swp *.log about .env files: bots scan GitHub constantly looking for API keys and database passwords. if you commit a .env file with real credentials to a public repo, rotate those keys immediately. even if you delete the file one minute later, the keys are already in your history and already being scraped. the right approach is to commit a .env.example file with fake placeholder values so teammates know what variables the project needs, but never the real values. There is one important rule about .gitignore: it only works on files that are not yet tracked by git. If a file was already committed once, adding it to .gitignore does nothing. You need to explicitly tell git to stop tracking it:\n$ git rm --cached .env # stop tracking this file, but keep it on disk $ git rm -r --cached node_modules/ # same thing for a whole directory # after running these, add the file to .gitignore and then commit The --cached flag is the key part. Without it, git rm deletes the file from your disk too. With it, git removes the file from tracking but leaves your actual file alone.\n// quick tip: GitHub maintains a large collection of ready-made .gitignore templates for every language and framework at github.com/github/gitignore. When starting a new project, grab the right template from there instead of writing one from scratch. branches A branch is just a pointer to a commit. Nothing more. When you create a branch, git creates a new pointer. When you commit on that branch, the pointer moves forward to your new commit. No files are duplicated. No folders are copied. Branching is fast because it is literally just creating a small file that contains a commit hash.\nwhat branches look like in git's history HEAD is a pointer to whatever branch you are currently on. When you make a commit, your current branch moves forward. When you switch branches, HEAD just points somewhere else.\n$ git branch # list all local branches $ git branch -a # list local AND remote branches $ git switch -c feature/login # create a new branch and switch to it $ git switch main # switch to an existing branch $ git branch -d feature/login # delete a branch (only after merging) $ git branch -D feature/login # force delete even if not merged $ git branch -vv # see branches with tracking info and ahead/behind status git switch is the modern command for changing branches. Older tutorials use git checkout for this and it works fine, but checkout does way too many different things depending on what arguments you pass it. switch is clearer and was introduced specifically to replace that part of checkout.\nWhen to branch: always. For every feature, every bug fix, every experiment. Main should only ever hold working, stable code. All actual development happens on branches. This is not just team etiquette. It protects your own work from yourself. You can experiment freely on a branch, and if it goes wrong you just delete it.\nA good naming convention for branches:\nfeature/user-authentication fix/crash-on-empty-form docs/update-readme chore/upgrade-dependencies practice: creating and switching branches Inside your practice repo, create a new branch: git switch -c feature/about-page Run git branch and confirm you are now on the new branch (it will have a * next to it). Create a file called about.html and add some text to it. Stage and commit it: git add about.html \u0026\u0026 git commit -m \"feat: add about page\" Switch back to main: git switch main Run ls (or dir on Windows). Notice that about.html is gone from your folder. It exists on the feature branch, not on main. This is how branches work. Run git lg and see the branch structure visually. merging and merge conflicts When your feature branch is ready, you bring it back into main:\n# first, switch to the branch you want to merge INTO $ git switch main $ git merge feature/login There are two possible outcomes when merging:\nFast-forward merge: If main has not had any new commits since you branched off it, git simply slides the main pointer forward to your branch\u0026rsquo;s tip. Clean, simple, no extra commit created.\nMerge commit: If main has new commits that your branch does not have, git creates a new \u0026ldquo;merge commit\u0026rdquo; that has two parents, one from each branch. This commit records where the two lines of work came back together. It looks slightly messier in history but it is totally normal and fine.\nwhen conflicts happen A conflict happens when both branches changed the same lines of the same file in different ways. Git cannot decide which version to keep, so it stops and asks you to decide.\n$ git merge feature/login CONFLICT (content): Merge conflict in index.html Automatic merge failed; fix conflicts and then commit the result. Open the file in your editor and you will see markers that git inserted:\nindex.html with conflict markers \u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt; HEAD \u0026lt;title\u0026gt;Portfolio\u0026lt;/title\u0026gt; ======= \u0026lt;title\u0026gt;My Portfolio by Eshan\u0026lt;/title\u0026gt; \u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; feature/login Reading this:\nEverything between \u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt; HEAD and ======= is your current branch\u0026rsquo;s version Everything between ======= and \u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; is what is coming in from the branch you are merging You decide what the final result should look like. Maybe you want one version, maybe the other, maybe a combination. Edit the file to exactly what you want, then delete all the conflict markers (the \u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;, =======, and \u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; lines). Save the file, then:\n$ git add index.html # tell git the conflict in this file is resolved $ git commit # git will pre-fill a merge commit message for you # changed your mind and want to abandon the whole merge: $ git merge --abort # resets everything back to before you ran merge // conflict tools: VS Code highlights conflict markers and shows clickable buttons labeled \"Accept Current Change\", \"Accept Incoming Change\", and \"Accept Both Changes\". For complex conflicts with many files, this is much easier than editing raw markers by hand. Most editors have similar features built in or available as extensions. practice: creating and resolving a conflict On main, edit readme.txt to say \"version from main\" and commit it. Create a new branch: git switch -c conflict-test Edit the same readme.txt to say \"version from branch\" and commit it. Switch back to main: git switch main Run git merge conflict-test. You will get a conflict. Open the file, read the markers, decide what to keep, remove all markers. Stage the file and commit. Conflict resolved. remotes: origin, upstream, and what they actually are This section trips people up more than almost anything else in git. Read it carefully.\nA remote is nothing more than a saved URL with a nickname. That is the entire concept. When you clone a repo, git saves the source URL under the name origin automatically. You can add more remotes, rename them, or remove them whenever you want. There is nothing magical about the names \u0026ldquo;origin\u0026rdquo; or \u0026ldquo;upstream,\u0026rdquo; they are just the conventions people follow.\n# see your current remotes $ git remote -v origin git@github.com:you/repo.git (fetch) origin git@github.com:you/repo.git (push) # add a new remote with a name $ git remote add upstream git@github.com:original/repo.git # remove a remote $ git remote remove upstream # change the URL of an existing remote $ git remote set-url origin git@github.com:you/new-repo.git Now the four commands that move code between local and remote:\ngit push sends your local commits up to the remote. Nothing on the remote changes until you push.\ngit fetch downloads new commits from the remote but does NOT touch any of your local branches. You are just downloading information. Safe to run anytime.\ngit pull is fetch plus merge in one step. Downloads new commits and immediately merges them into your current branch.\ngit pull --rebase is fetch plus rebase. Downloads new commits and replays your local commits on top of them. Usually produces cleaner history than a regular pull.\n# pushing $ git push origin main $ git push origin feature/login $ git push -u origin feature/login # -u sets up tracking between local and remote branch # after this, plain \"git push\" works without arguments $ git push --delete origin old-branch # delete a remote branch # fetching and pulling $ git fetch origin # download from origin, touch nothing local $ git fetch --all # download from every remote you have $ git pull # fetch + merge $ git pull --rebase # fetch + rebase (cleaner, prefer this) The -u flag on git push sets up tracking. It links your local branch to the corresponding remote branch so git knows the relationship. You only need to do this once per branch. After that, git push and git pull with no arguments will know where to go.\nSSH setup (stop typing passwords forever) HTTPS authentication works, but every push requires typing your username and password or a personal access token. SSH keys fix this permanently. You generate a key pair, give GitHub your public key, and everything authenticates silently from then on.\n# step 1: generate your key pair (ed25519 is the current recommended type) $ ssh-keygen -t ed25519 -C \"you@example.com\" # press enter to accept the default save location (~/.ssh/id_ed25519) # optionally add a passphrase for extra security, or just press enter for none # step 2: print your PUBLIC key and copy the entire output $ cat ~/.ssh/id_ed25519.pub # step 3: go to GitHub in your browser # Settings → SSH and GPG Keys → New SSH Key → paste → Save # step 4: test that it works $ ssh -T git@github.com Hi username! You've successfully authenticated... # step 5: if an existing repo uses HTTPS, switch it to SSH $ git remote set-url origin git@github.com:username/repo.git The key pair works like a lock and key. Your private key (id_ed25519, no .pub) stays on your machine and you never share it with anyone. The public key (id_ed25519.pub) is what you paste into GitHub. When you connect, GitHub uses the public key to verify that you have the matching private key, without you ever sending the private key over the network.\nAfter this is set up, always clone using SSH URLs (git@github.com:user/repo.git) instead of HTTPS URLs.\nforking and contributing to open source This is the workflow that most beginners want to learn but find confusing. It will make complete sense by the end of this section.\nWhen you want to contribute to a project you do not own, you cannot push directly to it. You need to fork it first. Forking creates a full copy of the repo under your own GitHub account. You have complete push access to your fork. You make your changes there, then open a pull request asking the original project to pull your changes in.\nBy convention:\nThe original repo that you do not own is called upstream Your fork on GitHub is called origin the complete contribution flow, step by step Go to the repo on GitHub. Click Fork in the top right. GitHub creates your-username/repo under your account. Clone your fork to your machine: git clone git@github.com:your-username/repo.git \u0026\u0026 cd repo Add the original repo as a second remote named upstream: git remote add upstream git@github.com:original-owner/repo.git Verify both remotes exist: git remote -v should show both origin and upstream entries. Create a branch for your specific change: git switch -c fix/typo-in-readme Make your changes, stage them, commit with a clear message. Push your branch to your fork: git push -u origin fix/typo-in-readme Go to GitHub. You will see a yellow banner asking if you want to open a pull request from your recently pushed branch. Click it. Write a description explaining what you changed and why, then submit the PR. keeping your fork up to date After you fork a project, the original keeps getting new commits from other contributors. Before starting any new work, sync your local main with upstream first so you are not working from outdated code:\n$ git switch main $ git fetch upstream $ git merge upstream/main # bring upstream changes into your local main $ git push origin main # update your fork on GitHub too Then branch off that updated main for your new work. Make syncing with upstream a habit every time you sit down to work on an open source project.\npull requests in more detail A pull request is a proposal to merge your branch into someone else\u0026rsquo;s branch. When you open one on GitHub, the PR page shows all your commits, the full diff of every file you changed, and a discussion thread.\nReviewers can leave comments on specific lines of code. You push new commits to the same branch and they appear in the PR automatically. No need to close and reopen anything. When a reviewer approves, someone with merge access clicks the merge button.\n// PR tips: keep them focused. one PR should do one thing. a PR that adds a feature, refactors three files, fixes an unrelated bug, and updates the README will sit in review for a long time because it is hard to review. smaller, focused PRs get merged faster. also, always check if the project has a CONTRIBUTING.md file before writing a single line of code. it tells you exactly how they want contributions structured, what tests to run, and what to put in your PR description. practice: the fork workflow Go to github.com/firstcontributions/first-contributions. This repo exists specifically for practicing the fork workflow with no risk. Fork it to your account. Clone your fork to your machine. Add the original as upstream. Create a branch, add your name to the contributors list as the README instructs. Push and open a pull request. Real maintainers will merge it. rebase Rebase is the concept that confuses the most people, but it is actually straightforward once you see what it is doing.\nRebase takes your commits and replays them one by one on top of a different starting point. The most common use case is updating a feature branch that has fallen behind main.\nE\u0026rsquo; and F\u0026rsquo; are brand new commit objects with the same changes as E and F, but now applied on top of D instead of B. The history looks as if you had started your branch from the latest main all along. Linear and clean.\n$ git switch feature/my-thing $ git fetch upstream $ git rebase upstream/main # if a conflict shows up during rebase: # 1. open the file, fix the conflict, delete the markers $ git add . $ git rebase --continue # move on to replaying the next commit # if you want to completely cancel and go back to before: $ git rebase --abort # puts everything back exactly as it was # after a successful rebase, the branch history was rewritten # so you need to force push (safely) $ git push --force-with-lease origin feature/my-thing about force pushing: always use --force-with-lease instead of -f or --force. The difference is important. -f overwrites the remote branch blindly no matter what. --force-with-lease checks first whether someone else pushed to the branch since your last fetch, and refuses to proceed if they did. It protects you from overwriting other people's work. Never force push to main or any branch that multiple people are actively working on. Rebase vs merge, when to use which: Use rebase to update your own feature branches and keep history linear. Use merge when combining shared branches where multiple people have committed, because rewriting shared history breaks everyone else\u0026rsquo;s local references.\ninteractive rebase: cleaning up messy commits You have been working on something for a few days and have seven commits with messages like \u0026ldquo;wip\u0026rdquo;, \u0026ldquo;fix\u0026rdquo;, \u0026ldquo;fix again\u0026rdquo;, \u0026ldquo;ok this time\u0026rdquo;. Before opening a pull request, clean them up:\n$ git rebase -i HEAD~4 # open an editor to edit the last 4 commits Your editor opens with the commits listed oldest first:\ninteractive rebase editor pick 3f7a2c1 add login form pick 9a1b3e2 fix typo pick c4d5e6f add validation pick 7f8a9b0 wip # change the word \"pick\" to one of these actions: # s or squash -- combine into the previous commit, merge both messages # f or fixup -- combine into the previous commit, discard this message # r or reword -- keep this commit but edit its message # d or drop -- delete this commit entirely Change the last three pick words to f (fixup), save and close the editor. Four commits become one clean commit with the first message. Then push with --force-with-lease.\npractice: interactive rebase Create a branch and make four small commits with bad messages like \"wip\", \"test\", \"asdf\", \"ok\". Run git lg to see the four commits. Run git rebase -i HEAD~4. Change the second, third, and fourth entries from pick to f. Save and close. Run git lg again. Four commits are now one. stash You are in the middle of building something when you need to switch to a different branch to fix a bug. Your current work is not ready to commit. Stash saves your in-progress changes temporarily so you can switch without losing anything.\n$ git stash push -m \"half-done login form\" # save with a descriptive name $ git stash # save with no name (harder to remember) $ git stash -u # also stash untracked files $ git stash list # see everything currently stashed $ git stash pop # apply the most recent stash and delete it $ git stash apply stash@{2} # apply a specific stash but keep it in the list $ git stash drop stash@{0} # delete a specific stash $ git stash clear # delete every stash Stash works like a stack. stash@{0} is always the most recently stashed item. stash@{1} is the one before that. Always use -m to give your stash a name if you plan to have more than one. An unnamed list of five stashes becomes impossible to navigate quickly.\npractice: using stash Edit a file in your practice repo without committing. Run git stash push -m \"work in progress\". Run git status. Your working directory is now clean. Switch to another branch, do something, switch back. Run git stash pop. Your changes are back. undoing things This is where a lot of people get anxious because mistakes feel permanent. They mostly are not. Here is the full map of undo operations:\nSituationCommandSafe? unstage a file, keep changes on diskgit restore --staged \u0026lt;file\u0026gt;yes discard all working directory changes to a filegit restore \u0026lt;file\u0026gt;destructive, no undo fix the last commit messagegit commit --amend -m \"new message\"local only add a forgotten file to the last commitgit add file \u0026\u0026 git commit --amend --no-editlocal only undo last commit, keep changes stagedgit reset --soft HEAD~1local only undo last commit, keep changes unstagedgit reset HEAD~1local only undo last commit, throw away all changesgit reset --hard HEAD~1destructive undo a commit already pushed to a shared branchgit revert abc1234yes, always safe The key distinction to understand:\ngit reset moves the branch pointer backwards, erasing commits from history. Safe only on local commits you have not pushed anywhere. If you reset past a commit that already exists on a shared remote, you will have a very bad time the next time you try to push.\ngit revert creates a brand new commit that is the exact inverse of the target commit. History stays intact. It is the always-safe option for undoing anything that has already been pushed to a shared branch.\n--amend replaces the last commit with a new commit object. This changes the commit hash. Do not amend commits that are already on a shared branch, for the same reason as force pushing.\nreflog: the real safety net Here is something most people do not know: git almost never actually deletes anything. Even after git reset --hard, your work is still sitting in git\u0026rsquo;s internal object store for about 30 days before garbage collection runs.\nEvery time HEAD moves (commit, checkout, merge, rebase, reset, anything) git logs it in the reflog. You can always look back and find what you had.\n$ git reflog abc1234 HEAD@{0}: reset: moving to HEAD~2 9f3a1ec HEAD@{1}: commit: add login validation 3b7d2f1 HEAD@{2}: commit: add login form # scenario: you ran \"git reset --hard HEAD~2\" by accident # your commits are still visible in the reflog at HEAD@{1} and HEAD@{2} # just reset forward to where you were $ git reset --hard 9f3a1ec # scenario: you deleted a branch and want it back $ git reflog | grep feature/deleted-branch $ git branch feature/deleted-branch 9f3a1ec # recreate it at that commit When something goes wrong, git reflog is the first thing to run before doing anything else. The hash you need is almost always in there. This single command has saved countless hours of work for developers who thought they had destroyed everything.\ncommon situations you will actually hit you committed to the wrong branch # you committed to main when you meant to commit to a feature branch # undo the commit on main, keep the changes $ git reset HEAD~1 # now create the right branch and commit there $ git switch -c feature/thing $ git add . \u0026\u0026 git commit -m \"your message\" # if you already pushed to main and need to undo that push too: $ git push origin main --force-with-lease push rejected because the remote has commits you do not have error: Updates were rejected because the remote contains work that you do not have locally. Integrate the remote changes before pushing again. # this means someone else (or you from another machine) pushed something # you need to pull their changes first, then push yours $ git pull --rebase origin main $ git push origin main detached HEAD state This sounds alarming but it is not a disaster. You land in detached HEAD state when you check out a specific commit hash directly instead of a branch name. HEAD is pointing at a commit instead of at a branch.\n$ git checkout abc1234 You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by switching back to a branch. # if you just wanted to look at an old commit and do not plan to make changes: $ git switch main # go back, nothing was lost or changed # if you made commits here that you want to keep: $ git switch -c my-new-branch # create a branch at your current position first # now those commits are attached to something permanent Commits you make in detached HEAD state are not deleted when you switch away. They just become unreachable by any branch. Creating a branch immediately saves them. If you switch away without creating a branch, they will eventually be cleaned up by garbage collection, but you can still recover them with git reflog for a while.\nyour branch is so far behind main that rebasing creates a nightmare of conflicts # first, see which commits are only on your feature branch $ git log --oneline main..feature/my-thing # strategy: start fresh from updated main, then cherry-pick only your commits $ git switch main \u0026\u0026 git pull $ git switch -c feature/my-thing-v2 $ git cherry-pick abc1234 def5678 # your commit hashes from the log above finding when a bug was introduced $ git show abc1234:src/app.js # see a file exactly as it was at a specific commit $ git log -S \"someFunction\" # find when that function appeared or disappeared $ git blame src/app.js # see who last changed every single line of a file cherry-pick Cherry-pick takes one specific commit from anywhere in your history and applies it to your current branch. The most common scenario: a bug fix was committed to a feature branch but you need it on main right now without merging the whole feature.\n$ git cherry-pick abc1234 # apply one commit to current branch $ git cherry-pick abc1234 def5678 # apply multiple commits in order $ git cherry-pick abc1234 --no-commit # apply the changes but do not auto-commit # lets you review or modify before committing Cherry-pick creates a new commit with the same changes but a different hash. The original commit stays exactly where it was. You are copying the changes, not moving the commit.\ngit bisect You know the code worked at some point last month, and it is broken now. There are 50 commits between then and now. You could check each one manually, or you could let git do a binary search.\nBinary search works like this: start at the middle. If the bug is there, the culprit is in the first half. If it is not there, the culprit is in the second half. Repeat with the relevant half. Each step eliminates half the remaining options.\n$ git bisect start $ git bisect bad # tell git the current commit is broken $ git bisect good v1.0.0 # tell git this earlier commit was working # git now checks out the commit halfway between those two points # you test your code manually $ git bisect good # if it works at this midpoint $ git bisect bad # if it is still broken # repeat this test-and-tell cycle # git narrows down until it names the exact commit that introduced the bug $ git bisect reset # return to your original branch when done 50 commits takes about 6 steps to narrow down. 100 commits takes 7 steps. This is one of those features that feels like magic the first time you use it.\ntags Tags are named pointers to specific commits. Unlike branches, they do not move when you add new commits. They are used to mark release versions.\n$ git tag -a v1.0.0 -m \"first stable release\" # annotated tag with a message $ git tag # list all tags $ git push origin v1.0.0 # push one specific tag $ git push origin --tags # push all tags at once $ git tag -d v1.0.0 # delete a tag locally $ git push origin --delete v1.0.0 # delete a tag from remote Use annotated tags (with -a) for releases rather than lightweight tags. Annotated tags store the tagger\u0026rsquo;s name, the date, and the message. GitHub automatically generates a Releases section on your repo page from annotated tags.\ngithub: the interface side issues Issues are GitHub\u0026rsquo;s built-in task and bug tracker. Before writing any code to contribute to an open source project, search the issues first to see if someone is already working on it or if the maintainers have already decided they do not want it. Opening an issue before writing code and waiting for a response is considered good practice. A lot of first-time contributors spend hours on a PR that gets immediately closed because the maintainers specifically do not want that feature.\ncode review On the Files Changed tab of any pull request, click any line number to leave an inline comment on that specific line. When you finish reviewing, you pick one of three options:\nComment: general feedback, no approval or block Approve: this is ready to merge Request Changes: something needs to be fixed before this should merge (blocks the PR until the author updates it and you re-review) branch protection rules Under Settings \u0026gt; Branches on GitHub, repo admins can configure rules for protected branches. Common settings include:\nRequire at least one approval before merging Require all CI checks to pass Block force pushes to main Require branches to be up to date before merging Any serious project has these enabled. This means nobody, not even the owner, can accidentally push broken code directly to main.\ngithub actions (CI/CD) GitHub Actions lets you run automated tasks whenever certain things happen in your repo, like a push or a new pull request. You write the configuration in a YAML file inside .github/workflows/:\n.github/workflows/test.yml name: Run Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - run: npm install - run: npm test With this file in place, every push and every pull request will automatically run your tests. The PR page shows a green checkmark or red X. Set branch protection to require this check and broken code cannot be merged, no matter who submits it.\nthe team workflow (how real projects operate) Most teams, from two people to hundreds, follow a workflow called GitHub Flow:\nmain is always deployable. Every commit on main works. Nobody pushes directly to it. All work happens on feature branches with descriptive names. When the work is ready, open a pull request. One or more teammates review it, leave comments, and approve or request changes. Once approved and all checks pass, it gets merged into main. The feature branch gets deleted. Deployment happens automatically from main via GitHub Actions. This is not complicated, but it is the thing that makes teams actually function without stepping on each other constantly.\nThere is a more elaborate model called Gitflow that adds dedicated develop, release, and hotfix branches. It exists for products that ship fixed versioned releases on a schedule, like mobile apps with an App Store review process. For web apps and most modern projects, GitHub Flow is simpler and sufficient.\ndaily work git statusalways first git add -pstage by hunk git commit -msave snapshot git push -u origin branchpush + track git pull --rebasesync cleanly branches git switch -c namecreate and switch git merge branchmerge into current git merge --abortcancel a merge git branch -d namedelete branch git branch -vvtracking info remotes + forks git remote -vsee all remotes git remote add upstream urladd original git fetch upstreamdownload only git rebase upstream/mainupdate branch git push --force-with-leasesafe force undo and recover git restore --stagedunstage git commit --amendfix last commit git reset --soft HEAD~1uncommit git revert abc1234safe undo pushed git reflogfind anything lost investigate git log -S \"string\"when did this appear git show hash:filefile at any commit git blame filewho changed what line git bisectbinary search a bug git diff main featurecompare branches advanced git stash push -msave temp work git rebase -i HEAD~nsquash commits git cherry-pick hashgrab one commit git tag -a v1.0.0mark a release git rm --cached filestop tracking // are you actually good now? I understand the three areas (working directory, staging, repository) and why a file can appear in two at once I can create branches, switch between them, merge them, and delete them confidently I can read conflict markers, resolve merge conflicts, and know how to abort if needed I know what origin and upstream mean and can set them up from scratch I can fork a repo, add upstream, sync it, create a branch, and open a pull request I know the difference between fetch, pull, and pull --rebase and when to use each I can rebase a feature branch onto main and clean up commits with interactive rebase I know when to use reset vs revert and understand why the difference matters for shared branches I know how to use reflog to recover things that looked permanently deleted Detached HEAD state does not scare me anymore and I know exactly how to handle it ","permalink":"/explore/git-and-github/","summary":"\u003cstyle\u003e\n.gt{--o:#f4845f;--b:#79c0ff;--g:#56d364;--r:#ff7b72;--y:#e3b341;--p:#d2a8ff;--bg:#0d1117;--bg2:#161b22;--bd:#30363d;--tx:#c9d1d9;--mu:#6e7681}\n.gt .term{background:#010409;border:1px solid #21262d;border-radius:8px;overflow:hidden;margin:18px 0;font-family:'JetBrains Mono',monospace}\n.gt .term .bar{background:#161b22;padding:8px 14px;display:flex;align-items:center;gap:7px;border-bottom:1px solid #21262d}\n.gt .term .lbl{font-size:11px;color:#6e7681;margin-left:auto}\n.gt .d1{width:11px;height:11px;border-radius:50%;background:#ff5f57}\n.gt .d2{width:11px;height:11px;border-radius:50%;background:#febc2e}\n.gt .d3{width:11px;height:11px;border-radius:50%;background:#28c840}\n.gt .term .body{padding:13px 18px;font-size:13px;line-height:1.9;color:#c9d1d9;overflow-x:auto}\n.gt .p{color:#56d364}.gt .cm{color:#3d444d}.gt .b{color:#79c0ff}.gt .y{color:#e3b341}.gt .r{color:#ff7b72}.gt .o{color:#f4845f}.gt .pu{color:#d2a8ff}\n.gt .warn{background:#1a0e0a;border:1px solid #3a1a10;border-left:3px solid #f4845f;border-radius:4px;padding:11px 16px;margin:16px 0;font-size:.91rem;color:#b87060}\n.gt .warn strong{color:#f4845f}\n.gt .tip{background:#0d1320;border:1px solid #1a2a3a;border-left:3px solid #79c0ff;border-radius:4px;padding:11px 16px;margin:16px 0;font-size:.91rem;color:#6a9ab8}\n.gt .tip strong{color:#79c0ff}\n.gt table{width:100%;border-collapse:collapse;margin:16px 0;font-size:.87rem}\n.gt th{font-family:'JetBrains Mono',monospace;font-size:.67rem;letter-spacing:.08em;text-transform:uppercase;color:#6e7681;text-align:left;padding:7px 12px;border-bottom:1px solid #21262d}\n.gt td{padding:8px 12px;border-bottom:1px solid #161b22;vertical-align:top;color:#8b949e}\n.gt tr:hover td{background:#161b22}\n.gt td:first-child{font-family:'JetBrains Mono',monospace;font-size:.82rem;color:#f4845f;white-space:nowrap}\n.gt .dia{border:1px solid #21262d;border-radius:8px;overflow:hidden;margin:20px 0;background:#0d1117}\n.gt .dia .dh{background:#161b22;padding:8px 14px;font-family:'JetBrains Mono',monospace;font-size:11px;color:#6e7681;border-bottom:1px solid #21262d}\n.gt .dia .dh::before{content:'◈  ';color:#f4845f}\n.gt .dia .db{padding:20px;overflow-x:auto}\n.gt .flow{display:flex;align-items:center;gap:0;flex-wrap:wrap;font-family:'JetBrains Mono',monospace;font-size:12px}\n.gt .fbox{border:1px solid #30363d;border-radius:5px;padding:10px 8px;background:#161b22;line-height:1.6;min-width:110px}\n.gt .farr{padding:0 10px;color:#444c56;display:flex;flex-direction:column;align-items:center}\n.gt .farr .ar{font-size:16px;line-height:1}\n.gt .farr .cmd{font-size:9px;color:#6e7681;font-family:'JetBrains Mono',monospace}\n.gt .diff{background:#0d1117;border:1px solid #21262d;border-radius:6px;overflow:hidden;margin:16px 0;font-family:'JetBrains Mono',monospace;font-size:12.5px}\n.gt .diff .dbar{background:#161b22;padding:6px 14px;font-size:11px;color:#6e7681;border-bottom:1px solid #21262d}\n.gt .dl{padding:1px 14px;line-height:1.85;white-space:pre}\n.gt .da{background:#0d2118;color:#56d364}.gt .dd{background:#1a0a0a;color:#ff7b72}.gt .dm{color:#6e7681}.gt .dc{color:#8b949e}\n.gt .ex{background:#0d1a14;border:1px solid #1a3a26;border-left:3px solid #56d364;border-radius:6px;padding:16px 20px;margin:22px 0}\n.gt .ex .xl{font-family:'JetBrains Mono',monospace;font-size:.67rem;letter-spacing:.13em;text-transform:uppercase;color:#56d364;margin-bottom:10px}\n.gt .ex p,.gt .ex li{color:#7dcca0;font-size:.92rem}\n.gt .ex strong{color:#56d364}\n.gt .ex ol,.gt .ex ul{padding-left:16px;margin:8px 0}.gt .ex li{margin-bottom:4px}\n.gt .ex code{background:#0d2218;color:#56d364;border:1px solid #1a3a26;padding:1px 6px;border-radius:3px;font-size:.83em}\n.gt .ck{background:#111118;border:1px solid #22223a;border-left:3px solid #d2a8ff;border-radius:6px;padding:14px 18px;margin:22px 0}\n.gt .ck .cl{font-family:'JetBrains Mono',monospace;font-size:.67rem;letter-spacing:.13em;text-transform:uppercase;color:#d2a8ff;margin-bottom:10px}\n.gt .ck ul{list-style:none;padding:0;margin:0}\n.gt .ck li{display:flex;align-items:flex-start;gap:9px;font-size:.89rem;color:#8b7ec8;margin-bottom:5px;cursor:pointer}\n.gt .cb{width:14px;height:14px;border:1px solid #444;border-radius:2px;flex-shrink:0;margin-top:3px;background:#1a1a2a;display:flex;align-items:center;justify-content:center;font-size:8px;font-weight:700;color:transparent;transition:all .15s}\n.gt .cb.on{background:#d2a8ff;border-color:#d2a8ff;color:#0d1117}\n.gt .sc{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:18px 0}\n@media(max-width:580px){.gt .sc{grid-template-columns:1fr}}\n.gt .sc .card{background:#0d1117;border:1px solid #21262d;border-radius:6px;padding:13px}\n.gt .sc .card h4{font-family:'JetBrains Mono',monospace;font-size:.66rem;letter-spacing:.09em;text-transform:uppercase;color:#f4845f;margin:0 0 9px}\n.gt .si{display:flex;justify-content:space-between;align-items:center;padding:3px 0;border-bottom:1px solid #161b22;font-family:'JetBrains Mono',monospace;font-size:11px}\n.gt .si:last-child{border:none}\n.gt .si .k{color:#f4845f}.gt .si .v{color:#6e7681;font-size:10.5px;text-align:right;max-width:58%}\n.gt .bvis{font-family:'JetBrains Mono',monospace;font-size:12.5px;line-height:2.1;color:#c9d1d9;padding:4px 0}\n.gt .bvis .gn{color:#56d364}.gt .bvis .bn{color:#79c0ff}.gt .bvis .sh{color:#e3b341}.gt .bvis .pt{color:#f4845f}.gt .bvis .mu{color:#6e7681}\n.gt .prac{background:#0f0f1a;border:1px solid #2a2a4a;border-left:3px solid #e3b341;border-radius:6px;padding:16px 20px;margin:22px 0}\n.gt .prac .pl{font-family:'JetBrains Mono',monospace;font-size:.67rem;letter-spacing:.13em;text-transform:uppercase;color:#e3b341;margin-bottom:10px}\n.gt .prac p,.gt .prac li{color:#a89060;font-size:.92rem}\n.gt .prac strong{color:#e3b341}\n.gt .prac ol,.gt .prac ul{padding-left:16px;margin:8px 0}.gt .prac li{margin-bottom:6px}\n.gt .prac code{background:#1a1500;color:#e3b341;border:1px solid #3a2a00;padding:1px 6px;border-radius:3px;font-size:.83em}\n\u003c/style\u003e\n\u003cdiv class=\"gt\"\u003e\n\u003cp\u003eThere is a very specific moment where git stops making sense. You know \u003ccode\u003eadd\u003c/code\u003e, \u003ccode\u003ecommit\u003c/code\u003e, \u003ccode\u003epush\u003c/code\u003e. Then someone says \u0026ldquo;just fork the repo, set upstream, rebase your branch onto main and open a PR.\u0026rdquo; You nod. You have no idea what any of that means.\u003c/p\u003e","title":"Git and GitHub: The Guide That Actually Makes You Comfortable"},{"content":" how to read this: this is dense and it's meant to be. don't rush it. open a terminal next to this tab while reading, and whenever I describe something, just try it. the goal isn't to finish reading, it's to understand. a section per day, practiced for real, is worth more than reading the whole thing once and forgetting it. the keyboard that broke my workflow I have a thing for mechanical keyboards. Not the expensive ones, I\u0026rsquo;m a broke CS student, but I was casually browsing Amazon looking for something cheap with a satisfying click. Found this Amazon Basics mechanical keyboard for like 900 rupees. Ordered it immediately.\nIt arrived, felt great. Good switches, decent build. But there was one problem I hadn\u0026rsquo;t checked for: the arrow keys were not distinct. They shared keys with other characters, and specifically \u0026ndash; the right arrow key was on the same key as forward slash /.\nNow if you\u0026rsquo;re a developer, you use / literally dozens of times every single line. Path separators, regex, URL strings, comments, division operators. And I also needed arrow keys for navigation. There was no good way to use both comfortably.\nI had two options: return it or adapt. Returning it felt like giving up. Adapting meant learning Neovim, something I had been putting off for months because it looked intimidating. The keyboard basically forced my hand.\nI started with the goal of just being functional. Survive in the editor, navigate files, edit code. But here\u0026rsquo;s the thing about me \u0026ndash; I cannot do anything halfway. I started watching YouTube videos about Neovim configs. Then I found GitHub repos with insane setups. Then I started reading plugin documentation. Then I got obsessed with making keymaps feel natural. Then I spent a weekend going through Treesitter\u0026rsquo;s textobjects plugin docs.\nThree months later I had built something I\u0026rsquo;m genuinely proud of. A config that\u0026rsquo;s not copied from one source but assembled from maybe fifteen different places, tons of trial and error, some AI help for the Lua parts I didn\u0026rsquo;t understand, pattern recognition from other people\u0026rsquo;s setups, and actual use. Every plugin in there has a reason. Every keymap was thought about.\nThis blog is that config, explained. And everything I learned using it over three months \u0026ndash; from the absolute basics of modes and motions to LSP, Telescope, git integration, custom snippets. You\u0026rsquo;re getting the full picture, the way I wish someone had written it for me when I started.\nsetting up Neovim Before anything else you need Neovim itself. The important thing here: the config uses features from Neovim 0.11+. Older versions won\u0026rsquo;t work correctly. Check what you have first:\n$ nvim --version If the output shows NVIM v0.11.0 or higher you\u0026rsquo;re fine. If it shows something older, or if the command doesn\u0026rsquo;t exist, install it now.\ninstall Neovim 0.11+ Arch / Manjaro Ubuntu / Debian Fedora macOS # Arch repos always have current versions, this just works $ sudo pacman -S neovim # verify $ nvim \u0026ndash;version\n# Ubuntu's apt repos often have outdated Neovim versions # Use the AppImage from GitHub for the latest stable release $ curl -LO https://github.com/neovim/neovim/releases/latest/download/nvim-linux-x86_64.appimage $ chmod u+x nvim-linux-x86_64.appimage $ sudo mv nvim-linux-x86_64.appimage /usr/local/bin/nvim # verify $ nvim \u0026ndash;version\n// note: on some minimal Ubuntu installs you may need sudo apt install fuse libfuse2 first for the AppImage to run. if you get a \"fuse: device not found\" error, that's why. # Fedora's repos are reasonably current $ sudo dnf install neovim # if the version is older than 0.11, use the AppImage method: $ curl -LO https://github.com/neovim/neovim/releases/latest/download/nvim-linux-x86_64.appimage $ chmod u+x nvim-linux-x86_64.appimage $ sudo mv nvim-linux-x86_64.appimage /usr/local/bin/nvim\n# Homebrew handles this cleanly $ brew install neovim # verify $ nvim \u0026ndash;version\nYou also need git (almost certainly already installed), node (for some LSP servers and live-server), and a Nerd Font set as your terminal font so the icons render correctly. If you see small squares where there should be icons, that\u0026rsquo;s the font. Grab one from nerdfonts.com \u0026ndash; I use JetBrainsMono Nerd Font.\ninstalling the config After three months of tweaking, this is the config I\u0026rsquo;m sharing. It has a good LSP setup for Python, JavaScript, TypeScript, C/C++, Lua, Rust, and more. Telescope for fuzzy finding, Neotree for file explorer, gitsigns for inline git diffs, Harpoon for quick file switching, nvim-surround, autoformatting with prettier/stylua/ruff, treesitter-based highlighting and text objects, a working snippet engine. It\u0026rsquo;s a solid base that won\u0026rsquo;t embarrass you.\n↓ \u0026nbsp; download nvim config (nvim.zip) Once you have it:\n# if you already have a neovim config, back it up first $ mv ~/.config/nvim ~/.config/nvim.bak # extract the zip and move it into place $ unzip nvim.zip $ mv nvim_new ~/.config/nvim\n# open neovim \u0026ndash; lazy.nvim (the plugin manager) will bootstrap itself # and start downloading all plugins automatically $ nvim\nThe first time you open Neovim with this config it will take a minute or two. You\u0026rsquo;ll see lazy.nvim installing everything. Let it finish. If any errors appear, press q to dismiss and then quit with :q and reopen. Mason (the LSP package manager) also runs in the background installing language servers \u0026ndash; watch the fidget spinner in the top-right corner to know when things are loading.\nAfter the initial setup, opening Neovim will be instant.\nverify the install # run this from inside neovim (press : then type this) :checkhealth # see all installed plugins and their status :Lazy\n# see all installed LSP servers and tools :Mason\nIf :checkhealth shows mostly green with a few yellow warnings, you\u0026rsquo;re fine. Red errors for things like node or python3 just mean those runtimes aren\u0026rsquo;t on your system yet and the relevant LSP servers won\u0026rsquo;t work until you install them. Everything else should be fine.\nwhat makes Neovim different: modes You\u0026rsquo;ve got Neovim installed. Now the actual learning starts, and the very first concept is the one that trips everyone up.\nEvery editor you\u0026rsquo;ve used before works the same way: you open a file, your cursor is somewhere, and whatever you type inserts text at that position. The keyboard always types text. That\u0026rsquo;s the only mode.\nNeovim has modes. The keyboard does completely different things depending on which mode you\u0026rsquo;re in.\nNORMAL INSERT VISUAL COMMAND Normal mode is the default and it\u0026rsquo;s where you should be most of the time. In normal mode, the keyboard is entirely for commands \u0026ndash; j moves down, d deletes, w jumps forward a word, gg goes to the top of the file. Nothing you type inserts text into the file.\nInsert mode is what you\u0026rsquo;re used to. Keys type text. You enter it from normal mode and leave it back to normal mode when you\u0026rsquo;re done typing.\nVisual mode is for selections. v selects character by character, V selects whole lines, Ctrl+v does block/column selection.\nCommand mode is for running ex commands. You enter it with :. This is where :w (save), :q (quit), :s/old/new/g (find and replace) live.\nThe current mode is always shown in the lualine statusbar at the bottom of the screen. You always know where you are.\nWhy does this design exist? Think about what you actually do when you\u0026rsquo;re coding. You write new text for maybe 30% of the time. The other 70% you\u0026rsquo;re navigating, selecting things, deleting, rearranging, searching. A regular editor gives you the mouse and some Ctrl+key combinations for all of that. Vim gives the entire keyboard, all the home row keys, no modifier needed, just for those operations. Once that keyboard real estate is yours, you use it constantly and efficiently.\ngetting out of insert mode In this config, jk and kj are both mapped to Escape in insert mode. Use this instead of reaching for the physical Escape key, which is too far up on the keyboard. jk is a quick two-finger roll on the home row and after a week it becomes completely unconscious. You\u0026rsquo;ll accidentally do it in browser text boxes.\nThe single most important habit to build first: stop living in insert mode. In VSCode the mental model is \u0026ldquo;cursor is always ready to type text.\u0026rdquo; In Neovim the mental model is: enter insert mode, type a sentence or a block of code, exit with jk, navigate, enter insert mode again, type, exit. Normal mode is home. Insert mode is a visit. The rhythm is burst-of-typing, exit, move, burst-of-typing, exit.\n// the mistake almost everyone makes: staying in insert mode and using arrow keys to navigate to a different position. if you're in insert mode and want to move somewhere, press jk first, navigate in normal mode, then enter insert mode again at the right position. this feels unnatural for a while, but it builds the right muscle memory. moving around without arrow keys The core navigation keys in normal mode are h, j, k, l for left, down, up, right. They\u0026rsquo;re on the home row of your right hand. Your hand doesn\u0026rsquo;t need to move. This sounds minor but over a full day of coding it genuinely adds up.\nThat said, h/j/k/l are only for small adjustments. Moving through a file one line at a time would be slow. These are the motions that actually cover distance:\nKeyWhat it does wJump forward to the start of the next word WSame, but treats anything-not-whitespace as one word (skips punctuation boundaries) bJump backward to the start of the current or previous word BBackward WORD version eForward to the end of the current or next word EEnd of WORD 0Start of line (column 0, absolute) ^First non-blank character of the line $End of line ggFirst line of the file GLast line of the file 50G or :50Jump to line 50 { }Jump up or down to the next empty line -- paragraph boundary H M LMove cursor to top, middle, or bottom of the visible screen Ctrl+dScroll down half page, cursor re-centers on screen Ctrl+uScroll up half page, cursor re-centers The small/WORD distinction matters in practice. In foo.bar, w stops at the dot. W skips the whole thing as one unit. In a URL like https://example.com/path, w stops at every slash and colon. W skips the whole URL. Use W, B, E when you want to jump at the level of tokens rather than individual word-characters.\n{ and } are underused by beginners. In real code, blank lines separate functions, classes, logical blocks. You can navigate through an entire file visiting every function boundary with { and }, no searching needed. I use these more than Ctrl+d/u for most files.\nThis config has relativenumber = true set, which shows the distance to every line from your cursor position rather than the absolute line number. So instead of counting in your head, you look at the number next to the line you want: it says 7, you press 7j to jump there, 7k to come back. Count-prefixed jumps become very natural once relative numbers are on.\nfinding characters on the current line KeyWhat it does f{char}Find next occurrence of char on this line, land on it F{char}Find previous occurrence, land on it t{char}Jump to just before the next char (think: \"until\") T{char}Jump to just after the previous char ;Repeat last f/F/t/T in the same direction ,Repeat in the opposite direction These become very useful combined with operators. f( on a function call jumps straight to the opening paren. t, in an argument list lands just before a comma. dt, means delete from cursor up to but not including the comma. cf( means change from cursor to the opening paren. Once f and t are reflex, editing individual lines gets noticeably faster.\nentering insert mode in the right position There are six ways to enter insert mode and each puts the cursor in a different place. Using only i and then navigating with arrow keys is the slowest approach.\nKeyWhere it puts you iInsert before cursor aAppend after cursor IInsert at first non-blank character of the line AAppend at end of line oOpen new line below, enter insert mode there OOpen new line above, enter insert mode there sDelete character under cursor, enter insert mode S or ccDelete entire line content, enter insert mode at start Adding something at the end of a line? A, not $ then a. New line below and start typing? o, not j then O. Clear the whole line and retype it? S, not 0d$i. Each one saves a few keystrokes and they happen constantly. After a week these become instinctive.\nthe jump list, your undo for navigation Every time you make a big jump \u0026ndash; G, /search, gd to go to a definition, * on a word \u0026ndash; Vim records your position. Ctrl+o walks backward through that list. Ctrl+i goes forward.\nThis becomes extremely useful once you\u0026rsquo;re using LSP. You press gd to jump to a function definition somewhere else in the file (or another file entirely), read it, then Ctrl+o and you\u0026rsquo;re right back where you were. I use this pair probably fifty times a day. It\u0026rsquo;s essentially a browser back/forward button for code navigation.\nthe grammar: operators, motions, text objects This is the section where Neovim stops feeling like a weird editor and starts making sense as a system. I remember the exact moment it clicked for me \u0026ndash; I was trying to delete the contents of a string literal and I thought \u0026ldquo;delete, inside, quotes\u0026rdquo; and typed di\u0026quot; and it just worked. That\u0026rsquo;s the grammar.\noptional[count] + operatorverb + motion or text objectnoun = exampled3w = delete 3 words You know d (delete), 3 (three times), w (word). You didn\u0026rsquo;t memorize d3w as a shortcut. You constructed it from vocabulary you already have. This is the fundamental difference between Vim and every other editor\u0026rsquo;s keyboard shortcuts. VSCode: memorize isolated facts. Vim: learn a grammar, generate thousands of combinations from a small set of primitives.\noperators, the verbs OperatorAction dDelete (cuts to register, can be pasted) cChange -- delete then immediately enter insert mode yYank (copy to register) \u003eIndent right \u003cIndent left =Auto-indent gcComment or uncomment (native Neovim 0.10+, no plugin) gUMake uppercase guMake lowercase Doubling any operator applies it to the whole line: dd deletes the line, yy yanks it, cc clears it and enters insert mode, \u0026gt;\u0026gt; indents it, gcc comments it.\ntext objects, the nouns that actually matter A motion describes a direction \u0026ndash; \u0026ldquo;three words forward.\u0026rdquo; A text object describes a shape \u0026ndash; \u0026ldquo;the thing inside these quotes,\u0026rdquo; \u0026ldquo;the whole function body,\u0026rdquo; \u0026ldquo;this paragraph.\u0026rdquo; They always need a prefix:\ni means inner \u0026ndash; the contents, without the surrounding delimiters a means around \u0026ndash; the contents plus the delimiters themselves Text ObjectWhat it selects iw / awinner word / a word including trailing whitespace iW / aWinner WORD / a WORD (whitespace-bounded) i\" / a\"inside double quotes / including the quote characters i' / a'inside single quotes / including them i` / a`inside backticks / including them (great for template literals) i( / a( or ib / abinside parentheses / including the parens i{ / a{ or iB / aBinside curly braces / including them i[ / a[inside square brackets / including them it / atinside HTML/XML tag / including the tags is / asinner sentence / around sentence ip / apinner paragraph / around paragraph if / afinner function body / around function (treesitter-aware) ia / aainner argument / around argument (treesitter-aware) ic / acinner class body / around class (treesitter-aware) The last three \u0026ndash; if, ia, ic \u0026ndash; come from the nvim-treesitter-textobjects plugin included in this config. They\u0026rsquo;re AST-aware, meaning they understand actual code structure rather than just matching brackets. dif on a Python function deletes the body correctly regardless of indentation complexity. dia on an argument in a function call removes exactly that argument and adjusts the commas. These sound like minor things but they\u0026rsquo;re genuinely impressive when you first feel them work.\nthe combos \u0026ndash; read them as sentences I learned these by saying the sentence in my head while typing the keys. It sounds silly but it works.\nKeysRead as / what happens ciw\"change inner word\" -- deletes word under cursor, drops into insert mode ready to type replacement ci\"\"change inside quotes\" -- clears string contents, cursor is inside empty quotes in insert mode ca(\"change around parens\" -- removes everything including the parens, insert mode di{\"delete inside braces\" -- clears a block body, useful for emptying a function yi(\"yank inside parens\" -- copies the arguments of a function call yif\"yank inner function\" -- copies the entire function body dif\"delete inner function\" -- removes the function body, keeps the signature cit\"change inside tag\" -- clears HTML tag content, enter insert mode inside vip\"visual inner paragraph\" -- visually selects the current code block gcip\"comment inner paragraph\" -- comments the whole current block gUiw\"uppercase inner word\" -- makes word under cursor ALL CAPS =ip\"auto-indent inner paragraph\" -- re-indents the current block dia\"delete inner argument\" -- removes a function argument cleanly 3dddelete three lines d$delete from cursor to end of line (same as D) \u003eibindent everything inside the current parens Once you have ten or fifteen of these internalized, you stop needing to look things up. You think \u0026ldquo;I want to change what\u0026rsquo;s inside these brackets\u0026rdquo; and your hands just type ci[. You didn\u0026rsquo;t memorize ci[ specifically \u0026ndash; you constructed it.\nthe dot key, repeat anything . repeats your last change. All of it \u0026ndash; the operator, the text object, and whatever you typed in insert mode. If you did ciw and typed newName, pressing . on another word deletes it and types newName. If you pressed A;jk to add a semicolon at the end of a line, j. does the same on the next line.\nThe practical principle: design edits to be repeatable. Instead of selecting twenty lines and changing everything at once, make the change on one, move to the next with n or j, press .. Instead of manually finding every instance of something, use the search-and-dot pattern described in the config keymaps section. The dot key turns any edit into a batch operation.\ncount prefixes Any operator or motion can be preceded by a number. 3w jumps three words forward. d3w deletes three words. 5j moves five lines down. 3dd deletes three lines. 2yy yanks two lines. With relative numbers on (which this config sets), you see the distance to every line on screen. You see 7 next to the function you want, you type 7j and you\u0026rsquo;re there.\nvisual mode v enters character visual mode, V selects whole lines, Ctrl+v enters block/column visual mode. You expand the selection with any motion, then apply an operator. viw selects inner word. vi{ selects inside braces. vip selects the paragraph.\nIn this config, \u0026lt; and \u0026gt; (indent/dedent) stay in visual mode after applying, so you can keep pressing \u0026gt; to keep indenting without re-selecting. And p in visual mode pastes without overwriting your yank register \u0026ndash; normally pasting over a selection kills what you had copied, this config routes the deleted selection to a blackhole so your clipboard stays intact.\nBlock visual (Ctrl+v) does something other editors can\u0026rsquo;t do natively. Select a column of text across multiple lines, press I, type something, press jk, and that text gets prepended to every selected line simultaneously. Select a column and d to delete that column across every line. This is the \u0026ldquo;multiple cursors\u0026rdquo; operation without needing a plugin.\nregisters, multiple clipboards When you yy or dd, where does it go? Into the default unnamed register \u0026quot;. When you p, it pastes from there. But Vim has multiple registers:\nRegisterWhat it holds \"Default -- last delete or yank (whichever was more recent) 0Yank-only register -- only yanks, never deletes. Always reliable. _Blackhole -- things sent here disappear, don't overwrite anything +System clipboard -- your OS copy/paste a through zNamed registers you control manually /Last search pattern The most useful thing to know: \u0026quot;0p pastes from the yank register specifically. This matters because dd (delete) overwrites the default register \u0026quot;. Say you yy something you want, then dd a line you don\u0026rsquo;t need \u0026ndash; now p gives you the deleted line, not what you yanked. \u0026quot;0p always gives you what you last yanked, no matter how many deletes happened since. This saves real frustration.\nIn this config, x is mapped to \u0026quot;_x \u0026ndash; deletes the character to the blackhole register. So deleting single characters never pollutes your clipboard. Small thing, genuinely nice quality of life. Also, Space+y and Space+Y explicitly yank to the system clipboard (\u0026quot;+y). Use these when you need to paste something outside of Neovim.\nTo use named registers: \u0026quot;ayiw yanks the current word into register a. \u0026quot;ap pastes it later. You can hold multiple completely independent things in memory this way \u0026ndash; very useful for complex refactors.\nsearching KeyWhat it does /patternSearch forward for pattern, Enter to confirm ?patternSearch backward for pattern nJump to next match NJump to previous match *Search for exact word under cursor (forward) #Search for exact word under cursor (backward) EscClear search highlights (mapped to :noh in this config) In this config, n and N are both mapped to auto-center the screen after jumping \u0026ndash; nzzzv and Nzzzv. This means every match you jump to appears in the middle of the screen. You never lose context cycling through results.\nThe config has ignorecase = true and smartcase = true together. This combination means: lowercase searches are case-insensitive (so /foo matches Foo, FOO, foo). But if you include any uppercase character in the search, it becomes case-sensitive (so /Foo only matches Foo). This is almost always the right behavior and you stop thinking about it quickly.\nThe * key deserves emphasis. Put the cursor on any identifier, press *, and every occurrence of that exact word in the file gets highlighted. Then n/N to cycle through them. This is your quick-find for the symbol under cursor, and it works instantly without typing a search pattern.\nmarks, navigation bookmarks Marks save your position so you can come back to it.\nKeyWhat it does maSet mark 'a' at current position (line + column) `aJump to exact position of mark 'a' 'aJump to line of mark 'a' (first non-blank char) ``Jump back to position before last big jump '0Jump to where you were when you last exited Neovim Lowercase marks a-z are local to the file. Uppercase A-Z are global and persist across files \u0026ndash; set mark M in one file, open another, 'M to go back to the exact position in the first file.\nThe pattern I use most: I\u0026rsquo;m deep in some implementation and need to check something in another part of the file. ma where I am, jump there with gg/pattern or gd, read what I need, 'a to come back instantly. No scrolling, no searching, just back to exactly where I was.\nmacros, automating repetitive edits Macros record any sequence of normal mode actions and replay them. They\u0026rsquo;re stored in registers, same as text.\nqa starts recording into register a. Do your operation. q stops. @a replays. @@ replays the last macro again. 50@a replays it fifty times.\nThe discipline that makes macros actually reliable: use text objects and motions, not character counts. If your macro does 3l (move three characters right), it will break on lines with slightly different structure. If it does f( (jump to next open paren), it works everywhere there\u0026rsquo;s a paren. Macros should describe structure, not raw physical keystroke positions.\nGood workflow: position at the consistent starting point of the first item, record, end at the consistent starting point for the next item (usually j to next line or } to next block), test with @a on one more item, then 98@@ for the rest.\nMacros live in registers, so \u0026quot;ap in insert mode literally pastes the macro as text. This means you can edit a macro after recording: paste it, fix the mistake, yank it back into register a with \u0026quot;ayy. Much cleaner than re-recording from scratch when you made one small error halfway through.\nthe config keymaps, the complete reference Your leader key is Space. Everything Space+... below means: press Space, then the rest. The config sets timeoutlen = 300, meaning you have 300ms between keys in a sequence. It feels fast but comfortable.\nbasics KeyWhat it does Ctrl+sSave file Ctrl+qQuit Space+snSave without triggering autoformat (when prettier is mangling something specific) EscClear search highlights Space+lwToggle line wrap Space+ssSave session to .session.vim in current directory Space+slLoad session from .session.vim editing helpers KeyWhat it does Alt+j / Alt+kMove current line down / up (works in normal and visual) Alt+dDuplicate current line below Space+jInteractive word replace -- type new name, then . to replace each next occurrence, n to skip Space+y / Space+YYank selection or line to system clipboard Space++ / Space+-Increment / decrement number under cursor xDelete character to blackhole (won't kill your yank register) Space+j maps to *``cgn. Here is exactly what that does: * searches for the word under cursor. `` (two backticks) jumps back to where you were before the * moved you. cgn changes the next search match. You\u0026rsquo;re now in insert mode \u0026ndash; type the replacement, press jk. From here, pressing . replaces the next occurrence. n skips one. This is surgical find-and-replace where you control every individual instance.\nbuffers and windows KeyWhat it does Tab / Shift+TabNext / previous buffer Space+xClose current buffer without closing the window split Space+bNew empty buffer Space+vSplit window vertically (new pane to the right) Space+hsSplit window horizontally (new pane below) Space+seMake all splits equal size Space+xsClose current split Ctrl+h/j/k/lMove focus between splits (also crosses tmux pane boundaries) Arrow keysResize the current split A buffer is a file loaded in memory. A window is a viewport (split) that shows a buffer. A tab is a whole layout of windows. Most of the time you\u0026rsquo;ll use buffers (Tab/Shift+Tab to cycle). Splits are for keeping a reference visible while you edit in another. Tabs are rare in practice.\nThe vim-tmux-navigator plugin in this config means Ctrl+h/j/k/l works seamlessly across both Neovim splits and tmux panes with the same keys. If you use tmux (my other blog covers this), you\u0026rsquo;ll find the navigation becomes completely unified.\ntabs KeyWhat it does Space+toOpen new tab Space+txClose current tab Space+tn / Space+tpNext / previous tab telescope, the fuzzy finder for everything Telescope is probably what you\u0026rsquo;ll use more than any other plugin. It\u0026rsquo;s a fuzzy finder covering files, text search across projects, buffers, git history, LSP symbols, diagnostics, help tags \u0026ndash; everything in one. Think VSCode\u0026rsquo;s command palette but significantly more capable.\nKeyWhat it does Space+sfFind files by name in the project Space+sgLive grep -- search file contents across the whole project Space+swSearch the word currently under cursor across entire project Space+sb or Space+SpaceSearch open buffers Space+smSearch marks Space+s. or Space+?Recently opened files Space+shSearch Neovim help tags -- this is very useful once you know it exists Space+sdSearch all current diagnostics (errors and warnings) Space+srResume -- reopen whatever Telescope was last showing Space+/Fuzzy search inside the current buffer only Space+s/Live grep across only your currently open files Space+sdsDocument symbols -- searchable list of all functions, classes, methods in current file Inside any Telescope picker, Ctrl+j/k navigate the list, Ctrl+l or Enter opens the selection, Esc or q (in normal mode) closes it.\nThe three I use constantly: Space+sf when I know the filename, Space+sg when I know some text inside the file, Space+sw on a symbol to see every place it\u0026rsquo;s used across the project. The last one replaces most of what I used \u0026ldquo;Find All References\u0026rdquo; for, before LSP\u0026rsquo;s gr handles it even better.\nSpace+sds deserves a mention too. In any large file, opening it and typing a function name lets you jump directly to any function or class in the file. Way faster than scrolling.\nThe git pickers: Space+gs opens a diff view of all changed files. Space+gc lets you browse commit history and jump into any commit. Space+gb for branches.\nneotree, the file explorer KeyWhat it does Space+eToggle sidebar file explorer on the left Space+wToggle floating file explorer \\Reveal current file in the tree (opens neotree focused on the current file) Space+ngsOpen git status in a floating neotree window Inside neotree, the standard file operations:\nKeyWhat it does aAdd file (supports bash brace expansion: src/{a,b,c}.js creates three files) AAdd directory dDelete rRename y / x / pCopy / cut / paste Enter or lOpen file sOpen in vertical split SOpen in horizontal split tOpen in new tab HToggle hidden files (dotfiles etc.) /Fuzzy find within the tree zClose all expanded nodes RRefresh the tree iShow file details -- size, modified date [g / ]gJump to previous / next git-modified file in the tree qClose neotree LSP, the intelligence layer LSP is Language Server Protocol. The idea: instead of every editor reimplementing autocomplete, go-to-definition, rename, etc. for every language separately, the editor and the language tool talk over a standardized protocol. The editor handles the UI. The language tool handles the understanding. Neovim is the editor. Language servers (separate programs that run in the background) are the tools. There are language servers for essentially every language.\nWhen you open a Python or JavaScript file, a language server starts in the background and connects to Neovim. You\u0026rsquo;ll see this in the fidget.nvim spinner in the top right corner of the screen. Once connected, Neovim gets real code intelligence for that file.\nmason, the package manager for language servers Before Mason, you\u0026rsquo;d install each language server yourself \u0026ndash; npm packages, pip packages, random binaries \u0026ndash; and then manually configure each one. Very painful.\nMason lives inside Neovim and manages all of this. It downloads language servers, formatters, and linters to a single place (~/.local/share/nvim/mason/). You can see its UI with :Mason to browse and install tools. In this config, ensure_installed = vim.tbl_keys(servers) tells mason to auto-install every server defined in the config the first time Neovim opens. You never need to touch :Mason manually unless you want to add something new.\nThe full stack:\n📦 Mason → downloads and installs the actual binary programs 🔗 mason-lspconfig → bridges installed servers to Neovim's native LSP system ⚙ nvim-lspconfig → provides default metadata (filetypes, binary paths) per server 🎭 none-ls → wraps standalone tools like prettier and stylua as fake LSP sources 📦 mason-null-ls → auto-installs those standalone tools via Mason Formatters like prettier and stylua are not LSP servers \u0026ndash; they\u0026rsquo;re standalone CLI tools that don\u0026rsquo;t speak the LSP protocol at all. none-ls wraps them and presents them to Neovim as if they were an LSP. This is how Ctrl+s saves and auto-formats \u0026ndash; a BufWritePre autocmd calls the formatter before every write.\nLSP keymaps These only work inside a file where a language server is active. They\u0026rsquo;re registered on LspAttach \u0026ndash; an event that fires when a server connects. If gd does nothing in a file, run :LspInfo to see what\u0026rsquo;s active.\nKeyWhat it does gdGo to definition -- jump to where this symbol is defined grGo to references -- list every place this symbol is used (opens in Telescope) gIGo to implementation gDGo to declaration KHover docs -- shows type, signature, docstring in a popup Space+DType definition Space+rnRename symbol across entire project -- every file, every reference, instantly Space+caCode action -- import suggestions, fix options, extract variable, refactor choices Space+dsDocument symbols -- searchable list of all functions and classes in this file Space+wsWorkspace symbols -- search symbols across the whole project [d / ]dJump to previous / next diagnostic (error or warning) Space+dOpen floating window showing the full diagnostic message Space+qSend all diagnostics to the quickfix list Space+doToggle diagnostics on/off for current buffer The workflow that replaced most of my VSCode usage: gd to jump to a definition, read it, Ctrl+o to come back. K on any symbol to see its type without leaving the file. ]d to cycle through errors instead of clicking red squiggles. Space+ca on an underlined error to get fix suggestions from the server. Space+rn to rename a variable \u0026ndash; it finds every reference in every file and renames them all simultaneously.\nWhen gr opens in Telescope you get a searchable list of every usage across the project. You can jump to any one, see the context, come back. Space+ds in a large file gives you a searchable function/class index so you can jump to any one directly.\nnvim-cmp, the completion popup In insert mode, a completion popup appears with suggestions from LSP, snippets, buffer words, and file paths. Navigation in the popup:\nKeyWhat it does Ctrl+j / Ctrl+kNavigate down / up in suggestions Tab / Shift+TabSame navigation, or jump between snippet placeholders EnterConfirm and insert the selected suggestion Ctrl+l / Ctrl+hJump forward / backward through snippet placeholders Ctrl+cManually trigger completion if popup closed LuaSnip, the snippet engine LuaSnip handles code snippets \u0026ndash; short trigger words that expand into templates with cursor stops you jump between. It\u0026rsquo;s separate from LSP autocomplete. LSP suggests real symbols from your actual codebase. Snippets expand predefined templates you trigger intentionally.\nThis config loads friendly-snippets, a massive pre-written collection for every major language (React hooks, Python class structures, JS imports, and hundreds more). Plus these custom ones defined specifically in this config:\nC++ \u0026ndash; type cppm and Tab in a .cpp file. Expands to a full competitive programming template with #include \u0026lt;bits/stdc++.h\u0026gt;, using namespace std;, and a main() with return 0;. Cursor lands inside main ready to type.\nLua \u0026ndash; type func and Tab. Expands to function name() with cursor on the name placeholder.\nHTML \u0026ndash; type ! and Tab. Expands to a complete HTML5 boilerplate. Cursor lands on the title field first, Tab again jumps into the body.\nSnippets have insert nodes \u0026ndash; named cursor stops where you fill in the variable parts. After expanding, each Tab press jumps to the next placeholder. Ctrl+l also jumps forward, Ctrl+h backward. When you\u0026rsquo;ve filled in the last placeholder, you\u0026rsquo;re done and the snippet is complete.\nTo add your own: open lua/plugins/autocompletion.lua, find the \u0026ldquo;Custom Snippets\u0026rdquo; comment, and add:\nluasnip.add_snippets(\u0026#34;javascript\u0026#34;, { s(\u0026#34;cl\u0026#34;, { -- trigger: \u0026#34;cl\u0026#34; t(\u0026#34;console.log(\u0026#34;), i(1, \u0026#34;value\u0026#34;), -- cursor stop 1, default text \u0026#34;value\u0026#34; t(\u0026#34;);\u0026#34;), }), }) s creates the snippet, t is static text, i is a cursor stop with optional default. A table inside t() is multi-line: t({\u0026quot;line one\u0026quot;, \u0026quot;line two\u0026quot;}). i(0) is always the final cursor position.\ngitsigns, inline git diffs Before this plugin I was running git diff constantly in the terminal to check what I changed. Now it\u0026rsquo;s all inline. Gitsigns adds colored bars in the sign column (the thin strip left of line numbers):\ngreen bar \u0026ndash; lines added since last commit yellow bar \u0026ndash; lines modified red symbol \u0026ndash; lines deleted KeyWhat it does ]h / [hJump to next / previous changed hunk Space+hpPreview the diff of the hunk under cursor in a popup Space+hsStage the hunk under cursor Space+hrReset the hunk under cursor back to HEAD Space+hS / Space+hRStage or reset the entire buffer Space+huUndo the last stage operation Space+hbShow full git blame for current line in a popup Space+tbToggle inline blame annotation on every line Space+hdDiff current file against HEAD Space+tdToggle showing deleted lines preview inline ]h / [h for jumping between hunks is particularly useful \u0026ndash; you can cycle through every change you made in a file without scrolling. For actual commits and push, Space+lg opens lazygit in a floating terminal. The full git TUI is easier for writing commit messages and resolving merge conflicts.\nharpoon, instant file switching Harpoon solves a specific problem: you\u0026rsquo;re always working with 3 or 4 files at any given moment in a task. Telescope is great for finding files cold. But once you know which files you need, opening Telescope every time adds friction. Harpoon lets you pin those files and jump to them with one keystroke.\nKeyWhat it does Space+HaAdd current file to the harpoon list Space+HhOpen harpoon menu -- navigate with j/k, Enter to jump, reorder files Space+H1 through Space+H4Jump directly to harpooned file 1, 2, 3, or 4 Space+Hn / Space+HpCycle to next / previous harpooned file Typical workflow: start a task, open your main files, Space+Ha on each one. Main implementation on slot 1, test file on slot 2, related module on slot 3. Jumping between them is now a single chord. This is one of those plugins where after a week you can\u0026rsquo;t imagine not having it.\nnvim-surround, wrapping pairs This handles surrounding pairs \u0026ndash; quotes, brackets, parens, HTML tags \u0026ndash; without manually navigating to both ends of a selection.\nKeyWhat it does ysiw\"Wrap word under cursor in double quotes ysiw(Wrap word in parentheses (with spaces: ( word )) ysiw)Wrap word in parentheses (tight: (word)) ysip\u0026lt;div\u0026gt;Wrap current paragraph in div tags yss\"Wrap entire line in quotes cs\"'Change surrounding double quotes to single quotes cs'`Change single quotes to backticks cst\u0026lt;p\u0026gt;Change surrounding HTML tag to p tag ds\"Delete surrounding double quotes ds(Delete surrounding parentheses dstDelete surrounding HTML tag S\" (visual mode)Surround the visual selection in double quotes ys means \u0026ldquo;you surround\u0026rdquo; \u0026ndash; add surroundings. cs means \u0026ldquo;change surround.\u0026rdquo; ds means \u0026ldquo;delete surround.\u0026rdquo; Once those three prefixes are in muscle memory you stop thinking about individual commands and just describe what you want. \u0026ldquo;Change the surrounding tag to a div\u0026rdquo; is cst\u0026lt;div\u0026gt;. This comes up constantly in HTML and JSX work.\ntreesitter navigation Beyond syntax highlighting, the treesitter setup in this config adds navigation motions that understand code structure:\nKeyWhat it does ]m / [mJump to start of next / previous function ]M / [MJump to end of next / previous function ]] / [[Jump to start of next / previous class ][ / []Jump to end of next / previous class Space+aSwap current parameter with the next one Space+ASwap current parameter with the previous one These use the actual syntax tree, not pattern matching. ]m finds the next real function declaration in whatever language you\u0026rsquo;re in, not just a line that looks like one. Combined with {/} for block-level navigation and f/t for line-level, you have navigation at every useful granularity.\nsearch and replace, bulk editing The substitute command replaces text with a pattern. The full form: :%s/pattern/replacement/flags.\nCommandWhat it does :s/old/new/Replace first match on current line :s/old/new/gReplace all matches on current line :%s/old/new/gReplace all matches in entire file :%s/old/new/gcReplace all, confirm each one :%s/old/new/giReplace all, case insensitive :'\u003c,'\u003es/old/new/gReplace in visual selection (auto-fills when you type : from visual) :5,20s/old/new/gReplace between lines 5 and 20 :%s/\\\u0026lt;word\\\u0026gt;/new/gReplace whole word only (won't touch \"wordpart\") This config has inccommand = \u0026quot;split\u0026quot; set. As you type a :s command, a preview split appears at the bottom of the screen showing exactly what will change before you confirm. You see the old and new text highlighted in real-time. Once you use this, going back to blind substitution feels wrong.\nthe global command :g/pattern/command runs any normal mode command on every line matching a pattern. It\u0026rsquo;s a force multiplier.\n:g/console.log/d \u0026#34; delete every line containing console.log :g/^$/d \u0026#34; delete all blank lines in the file :g/TODO/normal! \u0026gt;\u0026gt; \u0026#34; indent every TODO line by one level :g/import/y A \u0026#34; append every import line to register A You use it maybe once a week but when you need it nothing else comes close.\nthe quickfix list and project-wide replace When Telescope\u0026rsquo;s live grep (Space+sg) is open and you press Ctrl+q inside it, all the current matches get sent to the quickfix list \u0026ndash; a persistent list of file locations. :copen to see it, :cclose to close it, :cnext/:cprev to navigate.\nThe power move: Space+sg to search something across the project, Ctrl+q to send all matches to quickfix, then :cdo s/old/new/g to run the substitution on every matched file. Project-wide refactor in four keystrokes. This replaced my usage of VSCode\u0026rsquo;s \u0026ldquo;Replace in Files\u0026rdquo; entirely.\nthe options this config sets Reading through the config is how you build intuition for what\u0026rsquo;s possible. Here\u0026rsquo;s what every setting in core/options.lua actually does and why it\u0026rsquo;s there:\nrelativenumber = true \u0026ndash; line distances instead of absolute numbers. Makes count-prefixed jumps natural. Without this, 5j means counting in your head. With it, you just look.\nscrolloff = 8 \u0026ndash; always keep 8 lines visible above and below the cursor. The cursor never gets stranded at the very edge of the screen where you lose context.\ninccommand = \u0026quot;split\u0026quot; \u0026ndash; live preview for substitute commands. See changes before confirming. One of the best options in Neovim and off by default.\nundofile = true \u0026ndash; undo history is written to disk and persists across sessions. You can close a file, come back tomorrow, and still undo yesterday\u0026rsquo;s changes. Just works, no action needed.\nsmartcase with ignorecase together \u0026ndash; searches are case-insensitive until you type a capital letter, then case-sensitive. Almost always the right behavior.\nswapfile = false \u0026ndash; no .swapfile created. You have persistent undo and git. Swap files are unnecessary and create the \u0026ldquo;already open, recover?\u0026rdquo; popup annoyance.\nfoldmethod = \u0026quot;expr\u0026quot; with foldexpr = \u0026quot;nvim_treesitter#foldexpr()\u0026quot; \u0026ndash; treesitter-based code folding. za toggles a fold, zM closes all, zR opens all. The config sets foldenable = false so files open unfolded, but you can fold manually any time.\nlist = true with listchars = { trail = \u0026quot;·\u0026quot;, tab = \u0026quot;» \u0026quot; } \u0026ndash; shows trailing spaces as visible dots. You\u0026rsquo;ll notice them and clean them up. Stops accidentally committing whitespace issues.\nupdatetime = 250 \u0026ndash; how long Neovim waits idle before triggering CursorHold events. Lower value means diagnostics and hover docs respond faster. Default is 4000ms which feels slow.\ntimeoutlen = 300 \u0026ndash; window for completing a multi-key leader sequence. If you accidentally trigger sequences too easily, bump this to 400.\nexpandtab = true with tabstop = 4 and shiftwidth = 4 \u0026ndash; Tab key inserts 4 spaces. Consistent indentation. vim-sleuth (also in this config) overrides this per-file based on what the file already uses, so you always match existing code style automatically.\nthings I wish I had known earlier :help is actually good. Use Space+sh to open Telescope\u0026rsquo;s help search and type anything. The built-in documentation covers every option, every key, every function. When something doesn\u0026rsquo;t work as expected, :help is usually faster than googling.\nCtrl+o and Ctrl+i are your back/forward buttons. Navigate code like web pages. gd to jump somewhere, Ctrl+o to come back, Ctrl+i to go forward. I use this all day. The jump list is automatic \u0026ndash; you don\u0026rsquo;t manage it, just use it.\n:LspInfo is for debugging LSP issues. If gd or K don\u0026rsquo;t work in a file, run :LspInfo. It shows which servers are attached, whether they started correctly, what root directory they resolved. Most LSP problems are either the server not being installed or the wrong root directory, and :LspInfo tells you which.\n:checkhealth is the system diagnostic. Run it and get a full health report of every component. If something feels broken, this is the first command.\n:messages recovers error output. When an error flashes at the bottom and disappears before you read it, :messages has the full text.\nThe first week is genuinely slower. Things that took one second in VSCode take five seconds in Neovim while you\u0026rsquo;re still thinking about which key to press. This is completely normal. The learning curve is real. The payoff comes in week two when muscle memory starts forming and you stop consciously thinking about keys. By week three, going back to VSCode for anything feels like regression. Commit to two actual weeks of daily use before making any judgments.\nquick reference modes i a o O I Aenter insert jkexit insert v V Ctrl+vvisual modes :command mode navigation w b e W B Eword motion { }paragraph jump gg G 0 ^ $extremes f{c} t{c} ; ,find on line Ctrl+d / Ctrl+uscroll half page Ctrl+o / Ctrl+ijump list back / fwd operators + objects d c y \u003e \u003c gc gU guoperators iw i\" i( i{ if iainner objects aw a\" a( a{around objects .repeat last change u / Ctrl+rundo / redo \"0ppaste from yank register search /pat ?patsearch fwd / bwd n Nnext / prev match * #search word under cursor Space+j then .interactive replace :%s/a/b/gcsubstitute with preview telescope Space+sffind file Space+sggrep file contents Space+swsearch word under cursor Space+sdsearch diagnostics Space+sdsdocument symbols Space+/search current buffer lsp gdgo to definition grgo to references Khover docs Space+rnrename symbol Space+cacode action ]d / [dnext / prev error git + harpoon ]h / [hnext / prev hunk Space+hp / hs / hrpreview / stage / reset Space+tbtoggle line blame Space+lgopen lazygit Space+Haharpoon add file Space+H1..4jump to pinned file surround + misc ysiw\"surround word in quotes cs\"'change surrounding ds( / dstdelete surrounding ]m / [mnext / prev function Space+a / Space+Aswap parameters Alt+j / Alt+kmove line up / down ","permalink":"/explore/learning-neovim-from-scratch/","summary":"\u003c!--\n  NOTE FOR HUGO SETUP:\n  This post uses inline HTML. Add this to your hugo.toml:\n\n  [markup.goldmark.renderer]\n    unsafe = true\n\n  Also place the nvim config zip at:\n  static/downloads/nvim.zip\n  It will then be downloadable at /downloads/nvim.zip\n--\u003e\n\u003cstyle\u003e\n.nv-post {\n  --nv-green:  #a7c080;\n  --nv-teal:   #83c092;\n  --nv-yellow: #dbbc7f;\n  --nv-orange: #e69875;\n  --nv-red:    #e67e80;\n  --nv-blue:   #7fbbb3;\n  --nv-panel:  #1e2528;\n  --nv-border: #2d353b;\n  --nv-muted:  #475258;\n  --nv-text:   #d3c6aa;\n}\n.nv-key {\n  display: inline-block;\n  font-family: 'JetBrains Mono', 'Fira Code', monospace;\n  font-size: 0.78em;\n  background: #1e2326;\n  color: #d3c6aa;\n  border: 1px solid #3d484d;\n  border-bottom: 2px solid #4a5860;\n  padding: 1px 8px;\n  border-radius: 3px;\n  white-space: nowrap;\n}\n.nv-term {\n  background: #161c1e;\n  border: 1px solid #252d30;\n  border-radius: 8px;\n  overflow: hidden;\n  margin: 24px 0;\n  font-family: 'JetBrains Mono', 'Fira Code', monospace;\n}\n.nv-term-bar {\n  background: #1e2528;\n  padding: 9px 14px;\n  display: flex;\n  align-items: center;\n  gap: 7px;\n  border-bottom: 1px solid #252d30;\n}\n.nv-dot { width: 11px; height: 11px; border-radius: 50%; }\n.nv-dot-r { background: #ff5f57; }\n.nv-dot-y { background: #febc2e; }\n.nv-dot-g { background: #28c840; }\n.nv-term-body {\n  padding: 16px 20px;\n  font-size: 13px;\n  line-height: 1.85;\n  color: #a7c080;\n}\n.nv-term-body .p  { color: #83c092; }\n.nv-term-body .cm { color: #3a5040; }\n.nv-term-body .hi { color: #dbbc7f; }\n.nv-term-body .er { color: #e67e80; }\n.nv-warn {\n  background: #221510;\n  border: 1px solid #3d2510;\n  border-left: 3px solid #e69875;\n  border-radius: 4px;\n  padding: 14px 18px;\n  margin: 20px 0;\n  font-size: 0.92rem;\n  color: #b8886a;\n}\n.nv-warn strong { color: #e69875; }\n.nv-tip {\n  background: #131e22;\n  border: 1px solid #1e3038;\n  border-left: 3px solid #7fbbb3;\n  border-radius: 4px;\n  padding: 14px 18px;\n  margin: 20px 0;\n  font-size: 0.92rem;\n  color: #7aaa9a;\n}\n.nv-tip strong { color: #7fbbb3; }\n.nv-notice {\n  background: #1a2218;\n  border: 1px solid #a7c08033;\n  border-left: 3px solid #a7c080;\n  border-radius: 4px;\n  padding: 15px 20px;\n  margin: 24px 0;\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 0.82rem;\n  color: #7a9a7a;\n  line-height: 1.7;\n}\n.nv-notice strong { color: #a7c080; }\n.nv-keytable {\n  width: 100%;\n  border-collapse: collapse;\n  margin: 20px 0;\n  font-size: 0.88rem;\n}\n.nv-keytable th {\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 0.68rem;\n  letter-spacing: 0.09em;\n  text-transform: uppercase;\n  color: #475258;\n  text-align: left;\n  padding: 8px 12px;\n  border-bottom: 1px solid #2d353b;\n}\n.nv-keytable td {\n  padding: 10px 12px;\n  border-bottom: 1px solid #242c2f;\n  vertical-align: top;\n}\n.nv-keytable tr:hover td { background: #1e2528; }\n.nv-keytable td:first-child {\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 0.82rem;\n  color: #a7c080;\n  white-space: nowrap;\n  min-width: 160px;\n}\n.nv-keytable td:last-child { color: #6a8070; }\n.nv-mode-demo {\n  display: flex;\n  gap: 10px;\n  flex-wrap: wrap;\n  margin: 20px 0;\n}\n.nv-mode {\n  padding: 6px 16px;\n  border-radius: 4px;\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 0.75rem;\n  font-weight: 700;\n  letter-spacing: 0.08em;\n  border: 1px solid;\n}\n.nv-mode-normal { background: #2a3a2e; color: #a7c080; border-color: #a7c080; }\n.nv-mode-insert { background: #1f2e38; color: #7fbbb3; border-color: #7fbbb3; }\n.nv-mode-visual { background: #2e2838; color: #d699b6; border-color: #d699b6; }\n.nv-mode-cmd    { background: #332d20; color: #dbbc7f; border-color: #dbbc7f; }\n.nv-grammar {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  margin: 24px 0;\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 0.82rem;\n  flex-wrap: wrap;\n}\n.nv-gram-box {\n  padding: 10px 18px;\n  border-radius: 4px;\n  text-align: center;\n  line-height: 1.4;\n}\n.nv-gram-box .label { font-size: 0.6rem; letter-spacing: 0.1em; opacity: 0.6; display: block; margin-bottom: 4px; text-transform: uppercase; }\n.nv-gram-box .val   { font-size: 1rem; font-weight: 700; }\n.nv-gram-op  { background: #2a2a3e; border: 1px solid #404060; color: #d699b6; }\n.nv-gram-obj { background: #1e2d22; border: 1px solid #2a3d2e; color: #a7c080; }\n.nv-gram-cnt { background: #2a2218; border: 1px solid #3d3020; color: #dbbc7f; }\n.nv-gram-sep { color: #3d484d; font-size: 1.2rem; align-self: center; }\n.nv-gram-ex  { background: #131e22; border: 1px solid #1e3038; color: #7fbbb3; }\n.nv-hierarchy {\n  border-radius: 6px;\n  overflow: hidden;\n  border: 1px solid #2d353b;\n  margin: 24px 0;\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 13px;\n}\n.nv-h-row {\n  display: flex;\n  align-items: center;\n  padding: 13px 16px;\n  background: #1e2528;\n  border-bottom: 1px solid #252d30;\n  transition: background 0.1s;\n}\n.nv-h-row:last-child { border: none; }\n.nv-h-row:hover { background: #222c30; }\n.nv-h-icon  { margin-right: 12px; font-size: 15px; }\n.nv-h-name  { color: #d3c6aa; font-weight: 500; }\n.nv-h-arrow { color: #a7c080; margin: 0 10px; font-size: 11px; }\n.nv-h-desc  { color: #3d484d; font-size: 11px; margin-left: auto; text-align: right; }\n.nv-os-tabs {\n  display: flex;\n  gap: 0;\n  margin: 24px 0 0;\n  border-bottom: 1px solid #2d353b;\n}\n.nv-os-tab {\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 0.72rem;\n  letter-spacing: 0.06em;\n  padding: 8px 16px;\n  color: #475258;\n  cursor: pointer;\n  border-bottom: 2px solid transparent;\n  transition: all 0.2s;\n  user-select: none;\n}\n.nv-os-tab.active { color: #a7c080; border-bottom-color: #a7c080; }\n.nv-os-content { display: none; }\n.nv-os-content.active { display: block; }\n.nv-dl-btn {\n  display: inline-flex;\n  align-items: center;\n  gap: 10px;\n  background: #1e2d22;\n  border: 1px solid #a7c080;\n  color: #a7c080;\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 0.82rem;\n  padding: 12px 22px;\n  border-radius: 6px;\n  text-decoration: none;\n  margin: 16px 0;\n  transition: all 0.2s;\n}\n.nv-dl-btn:hover {\n  background: #253522;\n  box-shadow: 0 0 16px rgba(167,192,128,0.2);\n  text-decoration: none;\n  color: #a7c080;\n}\n.nv-cs-grid {\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  gap: 14px;\n  margin: 24px 0;\n}\n@media(max-width:600px){ .nv-cs-grid { grid-template-columns: 1fr; } }\n.nv-cs-card {\n  background: #1e2528;\n  border: 1px solid #2d353b;\n  border-radius: 6px;\n  padding: 16px;\n}\n.nv-cs-card h4 {\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 0.68rem;\n  letter-spacing: 0.1em;\n  text-transform: uppercase;\n  color: #a7c080;\n  margin: 0 0 12px;\n}\n.nv-cs-item {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: 5px 0;\n  border-bottom: 1px solid #252d30;\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 11.5px;\n}\n.nv-cs-item:last-child { border: none; }\n.nv-cs-item .k { color: #a7c080; }\n.nv-cs-item .d { color: #475258; font-size: 11px; text-align: right; max-width: 55%; }\n\u003c/style\u003e\n\u003cscript\u003e\nfunction switchTab(group, os) {\n  document.querySelectorAll('[data-group=\"'+group+'\"]').forEach(function(el) {\n    el.classList.remove('active');\n  });\n  document.querySelectorAll('[data-content=\"'+group+'\"]').forEach(function(el) {\n    el.classList.remove('active');\n  });\n  document.querySelector('[data-group=\"'+group+'\"][data-os=\"'+os+'\"]').classList.add('active');\n  document.querySelector('[data-content=\"'+group+'\"][data-os=\"'+os+'\"]').classList.add('active');\n}\n\u003c/script\u003e\n\u003cdiv class=\"nv-post\"\u003e\n\u003cdiv class=\"nv-notice\"\u003e\n\u003cstrong\u003ehow to read this:\u003c/strong\u003e this is dense and it's meant to be. don't rush it. open a terminal next to this tab while reading, and whenever I describe something, just try it. the goal isn't to finish reading, it's to understand. a section per day, practiced for real, is worth more than reading the whole thing once and forgetting it.\n\u003c/div\u003e\n\u003chr\u003e\n\u003ch2 id=\"the-keyboard-that-broke-my-workflow\"\u003ethe keyboard that broke my workflow\u003c/h2\u003e\n\u003cp\u003eI have a thing for mechanical keyboards. Not the expensive ones, I\u0026rsquo;m a broke CS student, but I was casually browsing Amazon looking for something cheap with a satisfying click. Found this Amazon Basics mechanical keyboard for like 900 rupees. Ordered it immediately.\u003c/p\u003e","title":"Learning Neovim From Scratch"},{"content":" ⚠ before you start reading: this is not a blog you just read and close. open a terminal right now and keep it next to this tab. every section has a small exercise, do it immediately, don't save it for \"later.\" if you just passively read this, you will forget everything in about 3 days. i promise. What even is tmux Ok so honestly, before I learned tmux, I was just opening like 4 separate terminal windows and alt-tabbing between them like a maniac. Running a server in one, editing code in another, watching logs in a third. It was genuinely messy.\nTmux solves this. It is a terminal multiplexer, that scary word just means it lets you run multiple terminals inside one single terminal window. And more importantly, it lets you detach from your work and come back to it later, exactly how you left it.\nImagine this: you start a long running process, a model training, a big build, whatever. You close your laptop and go eat. When you open it again, everything is still there, still running. That\u0026rsquo;s tmux. That\u0026rsquo;s the whole magic of it.\nThe real reason to learn tmux is not the split-screen thing. It\u0026rsquo;s the \u0026ldquo;my work persists even when I\u0026rsquo;m not looking at it\u0026rdquo; thing.\nAlso once you get comfortable with it, it genuinely starts to feel like a superpower. Your terminal becomes this organized, structured workspace and you stop losing context constantly. I\u0026rsquo;m not exaggerating.\nInstalling it Before anything else, check if it\u0026rsquo;s already on your machine:\n$ tmux -V If it prints something like tmux 3.3a you\u0026rsquo;re good. If not:\n# Ubuntu / Debian $ sudo apt install tmux # Arch / Manjaro $ sudo pacman -S tmux\n# Fedora $ sudo dnf install tmux\nVery lightweight, installs in seconds.\nExercise 01 =\u003e launch tmux for the first time Type this in your terminal and press Enter:\ntmux You should see your terminal looks slightly different, there's a bar at the bottom. That's tmux's status bar. It shows your session name, open windows, and some info.\nTo get out for now, type exit or press Ctrl + d. We'll explore properly in the next section.\nThe three-layer mental model This is the most important thing to understand before touching any shortcuts. Tmux has three layers and everything builds on top of this.\n🗂 Session → the \"project\" persists even when you're not looking 🪟 Window → like a browser tab, fills the whole screen ▪ Pane → a split inside a window, multiple terminals at once Think of it like this: a session is your whole project (say, \u0026ldquo;work\u0026rdquo; or \u0026ldquo;side-project\u0026rdquo;). Inside that session you have multiple windows (like tabs, one for your editor, one for running the server, one for git). And inside each window, you can split it into panes if you want two terminals side by side.\nYou don\u0026rsquo;t have to use all three. Most of the time you\u0026rsquo;ll use sessions + panes. But knowing this model makes every shortcut make sense.\nThe Prefix Key, the gatekeeper Almost every tmux shortcut starts with something called the Prefix key. It\u0026rsquo;s tmux\u0026rsquo;s way of knowing \u0026ldquo;ok, the next key you press is a command, not text.\u0026rdquo;\nThe default prefix is Ctrl + b. But in my config (which I\u0026rsquo;ll share later), I changed it to Ctrl + a because it\u0026rsquo;s way more comfortable to press.\nIn this blog I\u0026rsquo;ll write Prefix to mean Ctrl + a. If you\u0026rsquo;re on a fresh tmux without my config yet, use Ctrl + b , everything else is the same.\n// tip: the prefix key does nothing visible when you press it. press it, release, then quickly press the next key. it times out after about a second so don't be slow about it. Sessions, the superpower Sessions are genuinely the reason to use tmux. The concept is simple: a session keeps running even when you close your terminal or disconnect. You \u0026ldquo;detach\u0026rdquo; from it, go do something else, \u0026ldquo;attach\u0026rdquo; back later and everything is exactly how you left it.\nStarting a session # start tmux (creates a session with a random number name) $ tmux # start with a specific name, much better habit $ tmux new -s work $ tmux new -s side-project $ tmux new -s learning\nAlways name your sessions. \u0026ldquo;work\u0026rdquo;, \u0026ldquo;personal\u0026rdquo;, \u0026ldquo;project-name\u0026rdquo;, anything. It makes attaching back way easier.\nThe core session actions Key / CommandWhat it does Prefix + dDetach, leave the session running, go back to normal terminal tmux lsList all running sessions (run this outside tmux) tmux aAttach back to the last session tmux a -t workAttach to a specific session by name Prefix + sList and switch between sessions (inside tmux) Prefix + $Rename the current session tmux kill-session -t workKill a specific session tmux kill-serverNuclear option, kills absolutely everything Exercise 02 =\u003e the detach / attach cycle This is the core skill. Do this now, it takes 2 minutes:\nOutside tmux, run: tmux new -s myfirst You're now inside a session called \"myfirst\" Run something in it: echo \"hello from tmux\" Now detach: press Prefix + d, you'll drop back to your normal terminal Check the session is still alive: tmux ls you should see \"myfirst\" listed Attach back: tmux a -t myfirst See? Your terminal is exactly where you left it. // checkpoint -- sessions I can create a named session I can detach from a session with Prefix + d I can list sessions with tmux ls I can attach back with tmux a -t \u0026lt;name\u0026gt; Windows, your tabs Inside a session, you can have multiple windows. Think browser tabs, each one takes up the whole screen and you switch between them. The status bar shows all your open windows.\nI use windows when I want to completely separate concerns. Window 1 for editor, window 2 for git, window 3 for running tests. When I\u0026rsquo;m doing simple stuff, I just stick to panes.\nKeyWhat it does Prefix + cCreate a new window Prefix + ,Rename the current window Prefix + nGo to next window Prefix + pGo to previous window Prefix + 1..9Jump to window by number, super fast Prefix + wTree view, see all sessions and windows at once Prefix + \u0026Kill current window (asks you to confirm) Exercise 03 =\u003e working with windows Inside tmux, press Prefix + c, a new window opens, you'll see \"2\" in the status bar Press Prefix + , and rename it \"server\", type the name, press Enter Press Prefix + c again, rename it \"editor\" Now press Prefix + 1 to jump to window 1, then Prefix + 2 for window 2, Prefix + 3 for window 3 Press Prefix + w to see a tree view of everything, use arrow keys to navigate, press Enter to jump to one // gotcha: by default tmux starts windows at index 0. in my config I changed it to start at 1 (set -g base-index 1). that way Prefix+1 jumps to your first window, which is more intuitive. when you set up my config later, this is handled automatically. // checkpoint, windows I can create windows with Prefix + c I can rename a window with Prefix + , I can jump between windows by number I know what the tree view looks like Panes, splitting the screen Ok this is the visually impressive part. This is what people show off in their dev setup videos. But it\u0026rsquo;s also genuinely useful, seeing your code and your running server at the same time without switching is really nice once you get used to it.\nSplitting By default, the shortcuts are Prefix + % for vertical split and Prefix + \u0026quot; for horizontal. These are terrible to remember honestly. In my config I changed them to Prefix + v for vertical and Prefix + s for horizontal. v for vertical, s for stacked. Much more intuitive.\nFor now use the defaults. Once you set up my config it\u0026rsquo;ll be nicer.\nKeyWhat it does Prefix + %Split vertically (side by side), default Prefix + \"Split horizontally (top/bottom), default Prefix + vSplit vertically, my config Prefix + sSplit horizontally, my config Prefix + arrowsMove between panes Prefix + qShow pane numbers, tap a number to jump to that pane Prefix + zZoom, maximize current pane. Press again to undo. Prefix + xKill current pane Resizing Hold Ctrl after the prefix and press arrow keys for fine control. Alt for bigger jumps. Honestly I mostly just use zoom (Prefix + z) when I need to focus, rather than resizing.\nExercise 04 =\u003e build your first real three-pane setup This simulates an actual workflow:\nCreate a fresh session: tmux new -s myproject Split vertically: Prefix + % two panes side by side In the left pane run: echo \"this is my editor\" Move to right pane: Prefix + right arrow Split it horizontally: Prefix + \" now you have 3 panes total In this bottom-right pane run: echo \"this is my logs\" Try navigating between all 3 panes with arrow keys Press Prefix + q and see the numbers appear, immediately tap one to jump there Press Prefix + z on any pane to zoom in, then again to zoom out // the trick I use most: Prefix + q then immediately press the pane number. way faster than pressing the arrow key multiple times. once this is in muscle memory, moving around feels instant. // checkpoint -- panes I can split a window vertically and horizontally I can navigate between panes with arrow keys I know the Prefix + q trick to jump to panes by number I can zoom in/out with Prefix + z Copy Mode, keyboard warrior stuff One thing that\u0026rsquo;s slightly annoying when you first use tmux is that you can\u0026rsquo;t scroll up with your mouse wheel to see older output. Tmux captures the terminal. Copy mode is the solution, and once you get used to it, it\u0026rsquo;s actually better than using the mouse.\nHow it works:\nPress Prefix + [ to enter copy mode. You\u0026rsquo;ll see [0/0] appear in the top right corner. Now scroll with arrow keys, or Page Up / Page Down. If you set setw -g mode-keys vi in your config (which I do), you can use j k to scroll line by line and Ctrl+u / Ctrl+d for half-page jumps. To select text: press Space to start selection, move to end, press Enter to copy. To paste: Prefix + ] To exit without copying: press q or Escape // quick note: I also have set -g mouse on in my config, which means you can use your mouse to scroll in most cases anyway. copy mode becomes more useful for selecting and copying text precisely. both work together fine. Exercise 05 =\u003e scrolling through history Create a long output in a pane:\nfor i in $(seq 1 50); do echo \"line $i\"; done Press Prefix + [ to enter copy mode Use arrow keys to scroll up through the output Press q to exit copy mode The Config File By default tmux is not that comfortable. The prefix key is awkward, you can\u0026rsquo;t scroll with the mouse, no nice colors. The config file is where you fix all of this.\nWhere it lives ~/.config/tmux/tmux.conf Older setups use ~/.tmux.conf. Both work. I use the .config path because it\u0026rsquo;s cleaner (XDG standard).\nCreate it $ mkdir -p ~/.config/tmux $ touch ~/.config/tmux/tmux.conf $ nvim ~/.config/tmux/tmux.conf # or nano, vim, whatever Reloading without restarting After editing, tell tmux to read the new config:\ntmux source-file ~/.config/tmux/tmux.conf Or add this shortcut so you can press Prefix + r to reload:\nbind r source-file ~/.config/tmux/tmux.conf \\; display \u0026#34;Config reloaded!\u0026#34; Add that line, save, manually source once. After that Prefix + r always reloads for you.\nMy exact setup, the hacker dashboard Ok so this is my actual config. I\u0026rsquo;ll go through each section so you understand what each thing does before copy-pasting. Don\u0026rsquo;t blindly copy config files you don\u0026rsquo;t understand, you\u0026rsquo;ll run into problems and have no idea why.\nThis config uses TPM (Tmux Plugin Manager) for some plugins. I\u0026rsquo;ll explain how to install it too.\nStep 1 -\u0026gt; Install TPM $ git clone https://github.com/tmux-plugins/tpm ~/.config/tmux/plugins/tpm That\u0026rsquo;s it. TPM is just a folder, no system install needed.\nStep 2 -\u0026gt; The full config, explained ~/.config/tmux/tmux.conf # ── MODULE 1: THE CORE ────────────────────────────────────────── # Change prefix from Ctrl+b (default) to Ctrl+a # Ctrl+a is much more comfortable, left pinky on Ctrl, ring finger on \u0026lsquo;a\u0026rsquo; set -g prefix C-a unbind C-b bind C-a send-prefix unbind o\nset -g mouse on # scroll with mouse, click to focus panes set -g base-index 1 # windows start at 1, not 0 set -g renumber-windows on # close window 2 → window 3 becomes window 2 set -g detach-on-destroy off # closing a session drops you to another, not to shell set -g set-clipboard on # copies go to system clipboard\n# ── MODULE 2: NAVIGATION ──────────────────────────────────────── # vim-tmux-navigator: move between panes with Ctrl+hjkl, no prefix needed # works seamlessly across tmux panes AND neovim splits set -g @plugin \u0026lsquo;christoomey/vim-tmux-navigator\u0026rsquo;\n# v for vertical (side by side), s for stacked (top/bottom) # -c \u0026ldquo;#{pane_current_path}\u0026rdquo; means new pane opens in same directory bind v split-window -h -c \u0026ldquo;#{pane_current_path}\u0026quot; bind s split-window -v -c \u0026ldquo;#{pane_current_path}\u0026quot;\n# ── MODULE 3: THE DESIGN (Hacker Dashboard) ───────────────────── set -g status-position top # status bar at the top (more modern feel) set -g status-interval 3 # refresh the bar every 3 seconds set -g status-justify left\n# Dark base for the whole status bar set -g status-style \u0026lsquo;bg=#111111\u0026rsquo;\n# Left side: session name with a small icon set -g status-left \u0026rdquo;#[fg=#00ffcc,bg=#1e1e2e,bold] 󰚀 #S #[fg=#1e1e2e,bg=default]\u0026quot; set -g status-left-length 30\n# Right side: date and time in green/cyan set -g status-right \u0026rdquo;#[fg=#333333]#[fg=#00ff00,bg=#333333] 󰃭 %Y-%m-%d #[fg=#00ffcc]󱑒 %H:%M \u0026ldquo;\n# Active window tab: bold yellow highlight set -g window-status-current-format \u0026rdquo;#[fg=#111111,bg=#ffff00,bold] #I:#W \u0026ldquo; # Inactive window tabs: dimmed grey set -g window-status-format \u0026rdquo;#[fg=#666666,bg=default] #I:#W \u0026ldquo;\n# ── MODULE 4: PERSISTENCE (The Time Machine) ──────────────────── # tmux-resurrect: manually save/restore sessions # Prefix + Ctrl+s to save, Prefix + Ctrl+r to restore set -g @plugin \u0026lsquo;tmux-plugins/tmux-resurrect\u0026rsquo; # tmux-continuum: auto-saves every 15 min + auto-restores on tmux start set -g @plugin \u0026lsquo;tmux-plugins/tmux-continuum\u0026rsquo; set -g @continuum-restore \u0026lsquo;on\u0026rsquo; # If you use neovim: also restore your nvim sessions set -g @resurrect-strategy-nvim \u0026lsquo;session\u0026rsquo;\n# ── MODULE 5: SLEEK SESSION MANAGER ───────────────────────────── # tmux-sessionx: floating fuzzy session picker # Press Prefix + o to open it set -g @plugin \u0026lsquo;omerxx/tmux-sessionx\u0026rsquo; set -g @sessionx-bind \u0026lsquo;o\u0026rsquo; set -g @sessionx-window-height \u0026lsquo;75%\u0026rsquo; set -g @sessionx-window-width \u0026lsquo;75%\u0026rsquo;\n# ── INITIALIZE TPM (always keep this at the very bottom) ──────── set -g @plugin \u0026lsquo;tmux-plugins/tpm\u0026rsquo; run \u0026rsquo;~/.config/tmux/plugins/tpm/tpm\u0026rsquo;\nStep 3 -\u0026gt; Install the plugins After saving the config, open tmux (or reload it), then press:\nPrefix + I (capital I, like \u0026#34;Install\u0026#34;) TPM will pull all plugins from GitHub. You\u0026rsquo;ll see it downloading. When done, press Enter.\n// note about the icons: the status bar uses Nerd Font icons (those little symbols like 󰚀). if they show as weird boxes, you need to install a Nerd Font and set it as your terminal font. grab one from nerdfonts.com, I use JetBrainsMono Nerd Font. if you don't want to deal with fonts right now, just delete those icon characters from the status-left and status-right lines and it'll work fine with plain text. What each plugin actually does vim-tmux-navigator \u0026ndash; lets you press Ctrl + h/j/k/l to move between panes without using the prefix at all. And if you also use Neovim, the same keys cross the vim/tmux boundary seamlessly. You just navigate everywhere with Ctrl+hjkl and never think about it.\ntmux-resurrect \u0026ndash; saves your entire tmux state (sessions, windows, panes, running commands) to a file. Prefix + Ctrl+s to save, Prefix + Ctrl+r to restore. Super useful after a reboot.\ntmux-continuum \u0026ndash; same thing but automatic. Saves every 15 minutes, restores on startup. You don\u0026rsquo;t have to think about it at all.\ntmux-sessionx \u0026ndash; a beautiful floating fuzzy-finder for sessions. Press Prefix + o and get a popup where you can search and switch sessions, create new ones, even preview them. A nice upgrade from the basic list.\nExercise 06 =\u003e full setup from scratch Clone TPM: git clone https://github.com/tmux-plugins/tpm ~/.config/tmux/plugins/tpm Copy the config above into ~/.config/tmux/tmux.conf Start a fresh tmux: tmux Source the config: tmux source-file ~/.config/tmux/tmux.conf Install plugins: Prefix + I (capital I) Wait for it to finish, press Enter Notice the status bar moved to the top and looks different Try the new split keys: Prefix + v and Prefix + s Try navigating with Ctrl + h/l, no prefix needed Building muscle memory Reading about shortcuts and actually having them in your fingers are two completely different things. The brain learns by doing, not by reading. So here\u0026rsquo;s my honest recommendation for the first two weeks.\nWeek 1 =\u0026gt; force yourself to use just these four things Don\u0026rsquo;t try to learn everything at once. Seriously. Just these four:\nPrefix + d -\u0026gt; detach. Use this instead of closing your terminal. tmux a -\u0026gt; attach back. Build this rhythm: detach → do something → attach. Prefix + v -\u0026gt; split panes. Every time you want a second terminal, use this instead of opening a new window. Prefix + q, then number -\u0026gt; jump to a pane. Drill this until it\u0026rsquo;s automatic. That\u0026rsquo;s week 1. If you use just these 4 things consistently, tmux starts feeling normal within a few days.\nWeek 2 =\u0026gt; add these Prefix + c, Prefix + , -\u0026gt; create and name windows. Start organizing your work this way. Prefix + number -\u0026gt; jump to windows by number. Muscle memory for this is fast. Prefix + z -\u0026gt; zoom. Use when you need focus on one pane. Prefix + [ -\u0026gt; copy mode scrolling. Replace your mouse scrolling habit with this. The daily workflow drill Daily ritual, do this every single day for two weeks tmux new -s \u0026lt;project-name\u0026gt; or attach if it already exists Rename your first window with Prefix + , Split it once with Prefix + v, editor on left, terminal on right When you're done: Prefix + d to detach, don't close tmux Next day: tmux a and you're back exactly where you were Do this exact workflow every day for two weeks. After that, it will feel genuinely weird to not use tmux.\nThe three commands you should never forget If you forget everything else, remember these. I call them the survival kit:\n# list all running sessions $ tmux ls # attach to the last one $ tmux a\n# when everything is a mess and you want to start fresh $ tmux kill-server\nAnd inside tmux, if you forget any shortcut:\nPrefix + ? → shows every active shortcut (press q to close) This is the built-in manual. Any time you forget something, this is faster than googling.\nQuick reference cheatsheet Sessions tmux new -s \u0026lt;name\u0026gt;new session tmux lslist sessions tmux a -t \u0026lt;name\u0026gt;attach Prefix + ddetach Prefix + $rename session Prefix + osessionx picker Windows Prefix + cnew window Prefix + ,rename window Prefix + n / pnext / prev Prefix + 1-9jump by number Prefix + wtree view Prefix + \u0026kill window Panes Prefix + vsplit vertical Prefix + ssplit horizontal Ctrl + h/j/k/lnavigate (plugin) Prefix + q, Njump to pane N Prefix + zzoom / unzoom Prefix + xkill pane Copy / Misc Prefix + [enter copy mode q / Escexit copy mode Space → Enterselect + copy Prefix + ]paste Prefix + ?show all shortcuts Prefix + rreload config A few months ago I was alt-tabbing between 4 terminal windows like an idiot. Now I have one tmux session per project, everything is organized, and I can pick up exactly where I left off after a reboot because of the continuum plugin. The learning curve is real, first few days it\u0026rsquo;ll feel annoying and slow. Push through that. It clicks, and then it feels obvious that this is just how terminals should work.\nGood luck. And remember, the point is not to memorize shortcuts, it\u0026rsquo;s to build habits. Habits come from doing things repeatedly, not from reading about them once.\n","permalink":"/explore/learning-tmux/","summary":"\u003c!--\n  NOTE FOR HUGO SETUP:\n  This post uses inline HTML. To allow this, add the following to your hugo.toml / config.yaml:\n\n  [markup.goldmark.renderer]\n    unsafe = true\n\n  Without this, Hugo will strip the HTML blocks and the styling won't work.\n--\u003e\n\u003cstyle\u003e\n/* ── POST-SCOPED VARIABLES ──────────────────────────── */\n.tmux-post {\n  --tm-green:  #00ffcc;\n  --tm-yellow: #ffff00;\n  --tm-orange: #ff9500;\n  --tm-bg:     #111111;\n  --tm-bg2:    #0d0d0d;\n  --tm-border: #222222;\n  --tm-muted:  #555555;\n  --tm-text:   #c8c8c8;\n  --tm-white:  #eeeeee;\n}\n\n/* ── KEY BADGE ──────────────────────────────────────── */\n.tm-key {\n  display: inline-block;\n  font-family: 'JetBrains Mono', 'Fira Code', monospace;\n  font-size: 0.78em;\n  background: #1e1e1e;\n  color: #ddd;\n  border: 1px solid #3a3a3a;\n  border-bottom: 2px solid #4a4a4a;\n  padding: 1px 8px;\n  border-radius: 3px;\n  white-space: nowrap;\n}\n\n/* ── TERMINAL BLOCK ─────────────────────────────────── */\n.tm-term {\n  background: #080808;\n  border: 1px solid #1e1e1e;\n  border-radius: 8px;\n  overflow: hidden;\n  margin: 24px 0;\n  font-family: 'JetBrains Mono', 'Fira Code', monospace;\n}\n.tm-term-bar {\n  background: #161616;\n  padding: 9px 14px;\n  display: flex;\n  align-items: center;\n  gap: 7px;\n  border-bottom: 1px solid #1e1e1e;\n}\n.tm-dot {\n  width: 11px; height: 11px;\n  border-radius: 50%;\n}\n.tm-dot-r { background: #ff5f57; }\n.tm-dot-y { background: #febc2e; }\n.tm-dot-g { background: #28c840; }\n.tm-term-body {\n  padding: 16px 20px;\n  font-size: 13px;\n  line-height: 1.85;\n  color: #b8f0dc;\n}\n.tm-term-body .p  { color: #00ffcc; }\n.tm-term-body .cm { color: #3a5a4a; }\n\n/* ── EXERCISE BLOCK ─────────────────────────────────── */\n.tm-exercise {\n  background: #0a1a12;\n  border: 1px solid #1a3a24;\n  border-left: 3px solid #00ffcc;\n  border-radius: 6px;\n  padding: 20px 24px;\n  margin: 28px 0;\n}\n.tm-ex-label {\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 0.68rem;\n  letter-spacing: 0.14em;\n  text-transform: uppercase;\n  color: #00ffcc;\n  margin-bottom: 14px;\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n.tm-ex-label::before { content: '▶'; font-size: 0.55rem; }\n.tm-exercise p,\n.tm-exercise li { color: #9ecfba; font-size: 0.94rem; }\n.tm-exercise strong { color: #00ffcc; }\n.tm-exercise ol { padding-left: 18px; }\n.tm-exercise ol li { margin-bottom: 6px; }\n.tm-exercise code {\n  background: #0f2a1e;\n  color: #00ffcc;\n  border: 1px solid #1a3a24;\n  padding: 1px 6px;\n  border-radius: 3px;\n  font-size: 0.85em;\n}\n\n/* ── CHECKPOINT ─────────────────────────────────────── */\n.tm-checkpoint {\n  background: #111118;\n  border: 1px solid #22223a;\n  border-left: 3px solid #ffff00;\n  border-radius: 6px;\n  padding: 18px 22px;\n  margin: 28px 0;\n}\n.tm-cp-label {\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 0.68rem;\n  letter-spacing: 0.14em;\n  text-transform: uppercase;\n  color: #ffff00;\n  margin-bottom: 14px;\n}\n.tm-checkpoint ul {\n  list-style: none;\n  padding: 0;\n  margin: 0;\n}\n.tm-checkpoint ul li {\n  display: flex;\n  align-items: flex-start;\n  gap: 10px;\n  font-size: 0.9rem;\n  color: #9a9a66;\n  margin-bottom: 7px;\n  cursor: pointer;\n}\n.tm-cb {\n  width: 15px; height: 15px;\n  border: 1px solid #444;\n  border-radius: 2px;\n  flex-shrink: 0;\n  margin-top: 3px;\n  background: #1a1a1a;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  transition: all 0.15s;\n  font-size: 9px;\n  font-weight: bold;\n  color: transparent;\n}\n.tm-cb.done {\n  background: #ffff00;\n  border-color: #ffff00;\n  color: #000;\n}\n\n/* ── WARN / TIP ─────────────────────────────────────── */\n.tm-warn {\n  background: #1a0f08;\n  border: 1px solid #3a2000;\n  border-left: 3px solid #ff9500;\n  border-radius: 4px;\n  padding: 14px 18px;\n  margin: 20px 0;\n  font-size: 0.92rem;\n  color: #cc9966;\n}\n.tm-warn strong { color: #ff9500; }\n\n.tm-tip {\n  background: #0c0c1a;\n  border: 1px solid #20203a;\n  border-left: 3px solid #8888ff;\n  border-radius: 4px;\n  padding: 14px 18px;\n  margin: 20px 0;\n  font-size: 0.92rem;\n  color: #9999bb;\n}\n.tm-tip strong { color: #aaaaff; }\n\n/* ── NOTICE ─────────────────────────────────────────── */\n.tm-notice {\n  background: #0a1810;\n  border: 1px solid #00ffcc44;\n  border-left: 3px solid #00ffcc;\n  border-radius: 4px;\n  padding: 15px 20px;\n  margin: 24px 0;\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 0.82rem;\n  color: #88ccbb;\n  line-height: 1.7;\n}\n.tm-notice strong { color: #00ffcc; }\n\n/* ── KEYTABLE ────────────────────────────────────────── */\n.tm-keytable {\n  width: 100%;\n  border-collapse: collapse;\n  margin: 20px 0;\n  font-size: 0.88rem;\n}\n.tm-keytable th {\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 0.68rem;\n  letter-spacing: 0.09em;\n  text-transform: uppercase;\n  color: #555;\n  text-align: left;\n  padding: 8px 12px;\n  border-bottom: 1px solid #1e1e1e;\n}\n.tm-keytable td {\n  padding: 10px 12px;\n  border-bottom: 1px solid #141414;\n  vertical-align: top;\n}\n.tm-keytable tr:hover td { background: #111; }\n.tm-keytable td:first-child {\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 0.82rem;\n  color: #ffff00;\n  white-space: nowrap;\n}\n.tm-keytable td:last-child { color: #888; }\n\n/* ── HIERARCHY ───────────────────────────────────────── */\n.tm-hierarchy {\n  border-radius: 6px;\n  overflow: hidden;\n  border: 1px solid #1e1e1e;\n  margin: 24px 0;\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 13px;\n}\n.tm-h-row {\n  display: flex;\n  align-items: center;\n  padding: 13px 16px;\n  background: #0d0d0d;\n  border-bottom: 1px solid #161616;\n  transition: background 0.1s;\n}\n.tm-h-row:last-child { border: none; }\n.tm-h-row:hover { background: #141414; }\n.tm-h-icon { margin-right: 12px; font-size: 15px; }\n.tm-h-name { color: #eee; font-weight: 500; }\n.tm-h-arrow { color: #00ffcc; margin: 0 10px; font-size: 11px; }\n.tm-h-desc { color: #444; font-size: 11px; margin-left: auto; text-align: right; }\n\n/* ── CONFIG BLOCK ────────────────────────────────────── */\n.tm-config {\n  background: #070707;\n  border: 1px solid #1a1a1a;\n  border-radius: 8px;\n  overflow: hidden;\n  margin: 28px 0;\n}\n.tm-config-bar {\n  background: #101010;\n  padding: 10px 16px;\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 11px;\n  color: #444;\n  border-bottom: 1px solid #1a1a1a;\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n.tm-config-bar::before { content: '●'; color: #00ffcc; font-size: 8px; }\n.tm-config-body {\n  padding: 20px 24px;\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 12.5px;\n  line-height: 1.9;\n  overflow-x: auto;\n  white-space: pre;\n}\n.cc  { color: #2e4a3a; }   /* comment */\n.ck  { color: #ff9966; }   /* key     */\n.cv  { color: #66ccff; }   /* value   */\n.cs  { color: #88dd88; }   /* string  */\n.csec{ color: #00ffcc; font-weight: bold; display: block; margin-top: 6px; }\n\n/* ── CHEATSHEET GRID ─────────────────────────────────── */\n.tm-cs-grid {\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  gap: 14px;\n  margin: 24px 0;\n}\n@media(max-width:600px){ .tm-cs-grid { grid-template-columns: 1fr; } }\n.tm-cs-card {\n  background: #0d0d0d;\n  border: 1px solid #1a1a1a;\n  border-radius: 6px;\n  padding: 16px;\n}\n.tm-cs-card h4 {\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 0.68rem;\n  letter-spacing: 0.1em;\n  text-transform: uppercase;\n  color: #ffff00;\n  margin: 0 0 12px;\n}\n.tm-cs-item {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: 4px 0;\n  border-bottom: 1px solid #141414;\n  font-family: 'JetBrains Mono', monospace;\n  font-size: 11.5px;\n}\n.tm-cs-item:last-child { border: none; }\n.tm-cs-item .k { color: #00ffcc; }\n.tm-cs-item .d { color: #444; font-size: 11px; }\n\u003c/style\u003e\n\u003cdiv class=\"tmux-post\"\u003e\n\u003cdiv class=\"tm-notice\"\u003e\n\u003cstrong\u003e⚠ before you start reading:\u003c/strong\u003e this is not a blog you just read and close. open a terminal right now and keep it next to this tab. every section has a small exercise, do it immediately, don't save it for \"later.\" if you just passively read this, you will forget everything in about 3 days. i promise.\n\u003c/div\u003e\n\u003chr\u003e\n\u003ch2 id=\"what-even-is-tmux\"\u003eWhat even is tmux\u003c/h2\u003e\n\u003cp\u003eOk so honestly, before I learned tmux, I was just opening like 4 separate terminal windows and alt-tabbing between them like a maniac. Running a server in one, editing code in another, watching logs in a third. It was genuinely messy.\u003c/p\u003e","title":"Learning Tmux From Scratch"},{"content":"A Comprehensive, Self-Learning Docker Resource\n📑 Table of Contents PART 1: FOUNDATIONS Introduction to Docker Core Concepts Installation \u0026amp; Setup Your First Container PART 2: WORKING WITH CONTAINERS Container Lifecycle Container Management Port Mapping \u0026amp; Networking Container Logs \u0026amp; Debugging PART 3: CREATING IMAGES Understanding Dockerfiles Building Custom Images Dockerfile Best Practices Multi-Stage Builds PART 4: DATA PERSISTENCE Understanding Container Data Volumes Bind Mounts Volume Management PART 5: DOCKER COMPOSE Introduction to Docker Compose Docker Compose Syntax Multi-Container Applications Environment Variables \u0026amp; Secrets PART 6: NETWORKING Docker Networks Deep Dive Network Types Container Communication Custom Networks PART 7: ADVANCED TOPICS Resource Management Health Checks Security Best Practices Docker Registry \u0026amp; Hub Optimization Techniques PART 8: REAL-WORLD PROJECTS Project 1: Simple Web Application Project 2: Full-Stack MERN App Project 3: Microservices Architecture Project 4: Development Environment APPENDICES Complete Command Reference Troubleshooting Guide Common Patterns \u0026amp; Solutions Glossary PART 1: FOUNDATIONS 1. Introduction to Docker 1.1 What is Docker? Simple Definition: Docker is a platform that packages applications and their dependencies into containers - portable, isolated environments that run consistently across any computer.\nThe Problem Docker Solves:\nImagine you build an application on your laptop:\nUses Python 3.9 Needs PostgreSQL 13 Requires specific libraries Works perfectly on your machine ✅ You send it to a teammate:\nThey have Python 3.7 (different version!) PostgreSQL 14 installed Different operating system Your code crashes ❌ This is the infamous \u0026ldquo;It works on my machine\u0026rdquo; problem.\n1.2 How Docker Solves This Docker packages your application with EVERYTHING it needs:\nSpecific Python version Database Libraries Configuration files Operating system dependencies All bundled into a container that runs the same way everywhere.\n1.3 Real-World Analogy Without Docker: Your code is like furniture. Everyone has different homes (computers) with different layouts. Your furniture might not fit in someone else\u0026rsquo;s home.\nWith Docker: Docker creates a portable, pre-built room that contains your furniture AND the exact environment it needs. You can place this room in ANY building (computer), and it works exactly the same.\n1.4 Docker vs Virtual Machines Virtual Machine (VM):\nYour Computer └── Hypervisor └── Guest OS (entire operating system - 2GB+) └── Application Startup: Minutes Size: Gigabytes Resources: Heavy Docker Container:\nYour Computer └── Docker Engine └── Container (app + dependencies only - MBs) Startup: Seconds Size: Megabytes Resources: Lightweight Key Difference:\nVMs include entire operating system (slow, heavy) Containers share the host OS kernel (fast, lightweight) 1.5 When to Use Docker ✅ Perfect For:\nDevelopment environments (everyone has same setup) Testing different software versions Microservices architecture CI/CD pipelines Running databases locally without installation Deploying applications Isolating applications from each other ❌ Not Ideal For:\nDesktop GUI applications (though possible) Applications requiring direct hardware access Simple static websites (overkill) When you\u0026rsquo;re just running a single script once 1.6 Key Benefits Consistency: Works the same on dev, test, and production Isolation: Applications don\u0026rsquo;t interfere with each other Portability: Run anywhere Docker is installed Speed: Start/stop in seconds Efficiency: Multiple containers on one machine Clean System: Delete container = everything gone Version Control: Different versions side-by-side 2. Core Concepts 2.1 The Three Main Components Image (Blueprint/Recipe) What it is:\nA read-only template with instructions Contains application code, runtime, libraries, dependencies Can be shared and reused Stored in registries (like Docker Hub) Analogy: A recipe for chocolate cake\nExample: nginx:latest, python:3.11, mongo:6.0\nContainer (Running Instance) What it is:\nA runnable instance of an image Isolated environment where your app runs Can be started, stopped, deleted Changes are lost when deleted (unless using volumes) Analogy: The actual cake baked from the recipe\nImportant: You can create multiple containers from one image!\nRegistry (Recipe Library) What it is:\nStorage and distribution system for images Docker Hub is the default public registry Can be private or public Analogy: A cookbook library\nExample: hub.docker.com\n2.2 The Relationship Registry (Docker Hub) ↓ [Image] ← You download this (docker pull) ↓ ↓ (docker run) ↓ [Container 1] [Container 2] [Container 3] Running Running Stopped Real Example:\nDocker Hub ↓ nginx:latest (image) ↓ ↓ docker run ↓ [Website A] [Website B] [Website C] All separate containers from the same image! 2.3 Image Layers Images are built in layers (like a cake with multiple layers):\nLayer 4: Your application code ← Your files Layer 3: Application dependencies ← npm install, pip install Layer 2: Runtime environment ← Node.js, Python Layer 1: Base operating system ← Ubuntu, Alpine Why layers matter:\nEfficient storage (shared layers) Faster builds (cached layers) Smaller downloads (only new layers) Example: If you build two images both using Ubuntu:\nUbuntu is downloaded once Both images share that layer Only differences are stored separately 2.4 Container Isolation Each container has its own:\nFile system (can\u0026rsquo;t see other containers\u0026rsquo; files) Network interface (own IP address) Process tree (own running processes) Resources (allocated CPU/memory) Think of it like: Separate apartments in a building. Each is independent, but they share the building\u0026rsquo;s infrastructure.\n2.5 Mental Model Summary Remember this:\nDockerfile (Instructions) ↓ docker build ↓ Image (Template) ↓ docker run ↓ Container (Running app) Or with cooking analogy:\nRecipe Card ↓ Following the recipe ↓ Cake Mix (prepared) ↓ Baking ↓ Actual Cake (ready to eat) 3. Installation \u0026amp; Setup 3.1 Installing Docker Linux (Ubuntu/Debian) # Update package index sudo apt update # Install dependencies sudo apt install -y \\ ca-certificates \\ curl \\ gnupg \\ lsb-release # Add Docker\u0026#39;s official GPG key sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # Set up repository echo \\ \u0026#34;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \\ $(lsb_release -cs) stable\u0026#34; | sudo tee /etc/apt/sources.list.d/docker.list \u0026gt; /dev/null # Install Docker Engine sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin # Verify installation sudo docker run hello-world Post-Installation (Linux) Run Docker without sudo:\n# Create docker group sudo groupadd docker # Add your user to docker group sudo usermod -aG docker $USER # Log out and back in, or run: newgrp docker # Test without sudo docker run hello-world macOS Download Docker Desktop from docker.com Install the .dmg file Start Docker Desktop Verify: docker --version Windows Enable WSL 2 (Windows Subsystem for Linux) Download Docker Desktop from docker.com Install and restart Verify in PowerShell: docker --version 3.2 Verify Installation # Check Docker version docker --version # Should show: Docker version 24.x.x # Check Docker is running docker info # Run test container docker run hello-world # Should download and run successfully 3.3 Understanding Docker Architecture When you run Docker commands, here\u0026rsquo;s what happens:\nYour Terminal ↓ Docker CLI (docker command) ↓ Docker Daemon (dockerd - background service) ↓ Container Runtime (containerd) ↓ Containers Docker Daemon:\nBackground service that manages images and containers Must be running for Docker to work Check status: sudo systemctl status docker (Linux) 3.4 Configuration Files Linux:\nDocker daemon config: /etc/docker/daemon.json User config: ~/.docker/config.json macOS/Windows:\nSettings accessible through Docker Desktop GUI 3.5 Initial Setup Best Practices 1. Configure logging:\nCreate/edit /etc/docker/daemon.json:\n{ \u0026#34;log-driver\u0026#34;: \u0026#34;json-file\u0026#34;, \u0026#34;log-opts\u0026#34;: { \u0026#34;max-size\u0026#34;: \u0026#34;10m\u0026#34;, \u0026#34;max-file\u0026#34;: \u0026#34;3\u0026#34; } } Restart Docker: sudo systemctl restart docker\n2. Set data directory (if needed):\n{ \u0026#34;data-root\u0026#34;: \u0026#34;/path/to/your/docker/data\u0026#34; } 4. Your First Container 4.1 Hello World Run your first container:\ndocker run hello-world What happens step by step:\nDocker checks locally: \u0026ldquo;Do I have \u0026lsquo;hello-world\u0026rsquo; image?\u0026rdquo; Not found: \u0026ldquo;Let me download from Docker Hub\u0026rdquo; Downloads image: Pulls from registry Creates container: From the image Runs container: Executes its code Prints message: Shows output Exits: Container stops (job done) Read the output carefully! It explains what just happened.\n4.2 Running a Web Server Let\u0026rsquo;s run something more useful - nginx web server:\ndocker run -d -p 8080:80 nginx Breaking down the command:\ndocker run - Create and start a container -d - Detached mode (run in background) -p 8080:80 - Port mapping (your port 8080 → container port 80) nginx - Image name Test it: Open browser: http://localhost:8080\nYou should see \u0026ldquo;Welcome to nginx!\u0026rdquo;\nWhat just happened:\nDownloaded nginx image Started nginx web server in a container Made it accessible on your computer\u0026rsquo;s port 8080 4.3 Viewing Running Containers docker ps Output explanation:\nCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a1b2c3d4e5f6 nginx ... 1 min Up 0.0.0.0:8080-\u0026gt;80/tcp confident_shaw Columns:\nCONTAINER ID: Unique identifier (use first 3-4 chars in commands) IMAGE: Which image this container uses COMMAND: Command running inside container CREATED: When container was created STATUS: Current state (Up = running) PORTS: Port mappings NAMES: Auto-generated name (or custom if you set one) 4.4 Stopping a Container # Stop by ID (use first few characters) docker stop a1b2 # Or stop by name docker stop confident_shaw # Verify it stopped docker ps # Won\u0026#39;t show (it\u0026#39;s stopped) docker ps -a # Shows all containers including stopped 4.5 Starting a Stopped Container docker start a1b2 # Verify it\u0026#39;s running docker ps Browser should work again!\n4.6 Removing a Container # Must be stopped first docker stop a1b2 # Remove it docker rm a1b2 # Or force remove (even if running) docker rm -f a1b2 # Verify it\u0026#39;s gone docker ps -a 4.7 Auto-Remove Containers Use --rm flag to auto-delete when stopped:\ndocker run --rm -d -p 8080:80 nginx # When you stop it, it\u0026#39;s automatically removed docker stop \u0026lt;container_id\u0026gt; docker ps -a # Won\u0026#39;t show - it\u0026#39;s gone! Useful for: Temporary containers, testing\n4.8 Naming Containers Give containers meaningful names:\ndocker run -d --name my_webserver -p 8080:80 nginx Now you can use the name:\ndocker stop my_webserver docker start my_webserver docker logs my_webserver docker rm my_webserver Much easier than remembering IDs!\n4.9 Common First Container Mistakes ❌ Mistake 1: Port already in use\ndocker run -d -p 8080:80 nginx docker run -d -p 8080:80 nginx # ERROR! Solution: Use different host port: -p 8081:80\n❌ Mistake 2: Trying to remove running container\ndocker rm \u0026lt;container_id\u0026gt; # ERROR if running Solution: Stop first, or use -f: docker rm -f \u0026lt;container_id\u0026gt;\n❌ Mistake 3: Forgetting port mapping\ndocker run -d nginx # No -p flag Problem: Can\u0026rsquo;t access from browser (no port exposed) Solution: Always use -p for web services\n4.10 Quick Practice Exercise Task: Run three different nginx containers on different ports\n# Container 1 on port 8081 docker run -d --name web1 -p 8081:80 nginx # Container 2 on port 8082 docker run -d --name web2 -p 8082:80 nginx # Container 3 on port 8083 docker run -d --name web3 -p 8083:80 nginx # Verify all running docker ps # Test in browser: # http://localhost:8081 # http://localhost:8082 # http://localhost:8083 # Clean up docker stop web1 web2 web3 docker rm web1 web2 web3 4.11 Cheat Sheet: First Commands # Run container docker run \u0026lt;image\u0026gt; # Basic run docker run -d \u0026lt;image\u0026gt; # Background mode docker run -p 8080:80 \u0026lt;image\u0026gt; # With port mapping docker run --name myapp \u0026lt;image\u0026gt; # With custom name docker run --rm \u0026lt;image\u0026gt; # Auto-remove when stopped # List containers docker ps # Running containers docker ps -a # All containers docker ps -q # Only IDs # Control containers docker stop \u0026lt;id/name\u0026gt; # Stop container docker start \u0026lt;id/name\u0026gt; # Start stopped container docker restart \u0026lt;id/name\u0026gt; # Restart container docker rm \u0026lt;id/name\u0026gt; # Remove stopped container docker rm -f \u0026lt;id/name\u0026gt; # Force remove (even if running) # Get information docker logs \u0026lt;id/name\u0026gt; # View container logs docker inspect \u0026lt;id/name\u0026gt; # Detailed info PART 2: WORKING WITH CONTAINERS 5. Container Lifecycle 5.1 Understanding Container States A container can be in one of these states:\nCreated → Running → Paused → Stopped → Deleted ↑ ↓ ↑ └─────────┴───────────────────┘ (can be restarted) Visual representation:\ndocker run → Created + Running docker pause → Paused docker unpause → Running again docker stop → Stopped (gracefully) docker kill → Stopped (forcefully) docker start → Running again docker restart → Stopped + Running docker rm → Deleted (gone forever) 5.2 Creating vs Running Important distinction:\ndocker create - Creates container but doesn\u0026rsquo;t start it\ndocker create --name myapp nginx # Container exists but not running docker run - Creates AND starts container\ndocker run --name myapp nginx # Equivalent to: docker create + docker start When to use create: Rarely. Usually just use run.\n5.3 Stop vs Kill docker stop (Graceful shutdown):\ndocker stop myapp Sends SIGTERM signal (polite request to stop) Waits 10 seconds for cleanup If still running, sends SIGKILL (force stop) Use this normally docker kill (Force stop):\ndocker kill myapp Immediately sends SIGKILL (no cleanup time) Use only when stop doesn\u0026rsquo;t work Example scenario:\n# Web server handling requests docker stop webserver # Server finishes current requests, then stops ✅ docker kill webserver # Server stops immediately, requests may fail ❌ 5.4 Pause vs Stop docker pause - Freezes container (keeps in memory)\ndocker pause myapp # All processes frozen, but container still \u0026#34;running\u0026#34; # Uses RAM but no CPU docker unpause - Resumes frozen container\ndocker unpause myapp # Continues exactly where it left off When to use: Very specific scenarios (debugging, system snapshots)\ndocker stop - Actually stops the container\ndocker stop myapp # Container stopped, can be started later # State saved, but processes terminated 5.5 Restart Policies Tell Docker what to do when container stops:\n# Never restart (default) docker run -d --restart no nginx # Always restart (even after reboot) docker run -d --restart always nginx # Restart unless manually stopped docker run -d --restart unless-stopped nginx # Restart only on failure (with max attempts) docker run -d --restart on-failure:5 nginx Real-world usage:\n# Production database - always keep running docker run -d --restart always --name db postgres # Development server - don\u0026#39;t restart if I stopped it docker run -d --restart unless-stopped --name dev_server nginx Check restart policy:\ndocker inspect --format=\u0026#39;{{.HostConfig.RestartPolicy.Name}}\u0026#39; myapp 5.6 Exit Codes When container stops, it has an exit code:\n0 - Success (clean exit) 1 - Application error 137 - Killed by SIGKILL (docker kill) 139 - Segmentation fault 143 - Terminated by SIGTERM (docker stop) Check exit code:\ndocker ps -a # Look at STATUS column: # \u0026#34;Exited (0)\u0026#34; = success # \u0026#34;Exited (1)\u0026#34; = error Why this matters:\n# This container will restart only if it crashes (exit code != 0) docker run -d --restart on-failure my-app # Exit 0 = won\u0026#39;t restart (intentional stop) # Exit 1 = will restart (error, try again) 5.7 Container Lifecycle Example Practical scenario:\n# Day 1: Start a database docker run -d --name mydb --restart unless-stopped -p 5432:5432 postgres # Check it\u0026#39;s running docker ps # STATUS: Up X minutes # Day 2: Need to restart server (maintenance) sudo reboot # After reboot, container auto-starts (--restart unless-stopped) docker ps # STATUS: Up X seconds # Day 5: Need to upgrade database docker stop mydb # Gracefully stops, waits for queries to complete # Upgrade to new version docker rm mydb docker run -d --name mydb --restart unless-stopped -p 5432:5432 postgres:15 # Container won\u0026#39;t auto-restart on its own because we stopped it manually # But after reboot, it will start (unless-stopped policy) 5.8 Viewing Container History See changes made to a container:\ndocker diff \u0026lt;container_id\u0026gt; Output shows:\nA = Added file C = Changed file D = Deleted file Example:\ndocker run -d --name test nginx docker exec test touch /tmp/newfile.txt docker diff test # Shows: A /tmp/newfile.txt 5.9 Container Events Monitor Docker events in real-time:\ndocker events # In another terminal, start/stop containers # You\u0026#39;ll see events like: # container create # container start # container stop # container destroy Filter events:\n# Only container events docker events --filter type=container # Events from specific container docker events --filter container=myapp 5.10 Lifecycle Cheat Sheet # Create and start docker run -d --name app nginx # Create + start docker create --name app nginx # Create only docker start app # Start created/stopped container # Control running containers docker stop app # Graceful stop (10s timeout) docker stop -t 30 app # Stop with 30s timeout docker kill app # Force stop immediately docker restart app # Stop + start docker pause app # Freeze (keep in memory) docker unpause app # Resume frozen container # Restart policies --restart no # Never restart --restart always # Always restart --restart unless-stopped # Restart unless manually stopped --restart on-failure:5 # Restart max 5 times on error # Cleanup docker rm app # Remove stopped container docker rm -f app # Force remove (even running) docker rm $(docker ps -a -q) # Remove all stopped containers docker container prune # Remove all stopped containers # Information docker ps -a # All containers with status docker inspect app # Detailed info docker diff app # File changes docker events # Real-time events 6. Container Management 6.1 Inspecting Containers Get complete container information:\ndocker inspect \u0026lt;container_id\u0026gt; Returns JSON with everything:\nConfiguration Network settings Mounts State Resource limits Extract specific info:\n# Get IP address docker inspect -f \u0026#39;{{.NetworkSettings.IPAddress}}\u0026#39; myapp # Get current status docker inspect -f \u0026#39;{{.State.Status}}\u0026#39; myapp # Get port mappings docker inspect -f \u0026#39;{{.NetworkSettings.Ports}}\u0026#39; myapp # Get environment variables docker inspect -f \u0026#39;{{.Config.Env}}\u0026#39; myapp 6.2 Executing Commands in Running Containers docker exec - Run command in existing container:\n# Run bash shell (interactive) docker exec -it myapp bash # Run single command docker exec myapp ls /app # Run as specific user docker exec -u root myapp whoami Common use cases:\n# Debug inside container docker exec -it myapp bash ls cat /app/config.txt exit # Check logs inside container docker exec myapp cat /var/log/app.log # Database operations docker exec -it postgres_db psql -U postgres # Install debugging tools docker exec -it myapp apt update docker exec -it myapp apt install -y curl Important: exec runs in EXISTING container. Container must be running!\n6.3 Copying Files Copy files between host and container:\nFrom host to container:\ndocker cp /path/on/host/file.txt myapp:/path/in/container/ From container to host:\ndocker cp myapp:/path/in/container/file.txt /path/on/host/ Examples:\n# Copy config file into container docker cp config.json web_server:/app/config.json # Extract logs from container docker cp web_server:/var/log/app.log ./logs/ # Copy entire directory docker cp ./myapp/ container_name:/app/ # Copy from stopped container (works!) docker cp stopped_container:/data/backup.sql ./ 6.4 Viewing Logs docker logs - See container output:\n# View all logs docker logs myapp # Follow logs (live, like tail -f) docker logs -f myapp # Last 100 lines docker logs --tail 100 myapp # Logs since specific time docker logs --since 30m myapp # Last 30 minutes docker logs --since 2024-01-01 myapp # Since date # With timestamps docker logs -t myapp Practical examples:\n# Debug why container keeps crashing docker logs --tail 50 crashed_app # Monitor web server traffic docker logs -f --tail 20 nginx_server # Find errors docker logs myapp | grep ERROR # Save logs to file docker logs myapp \u0026gt; app_logs.txt 6.5 Resource Usage docker stats - Monitor resource usage:\n# All running containers docker stats # Specific containers docker stats container1 container2 # One-time snapshot (not continuous) docker stats --no-stream Output shows:\nCPU % - Percentage of CPU used MEM USAGE / LIMIT - Memory used vs limit MEM % - Percentage of memory used NET I/O - Network in/out BLOCK I/O - Disk read/write PIDS - Number of processes Example:\nCONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % a1b2c3d4e5f6 web 0.50% 50MiB / 2GiB 2.44% 6.6 Attaching to Containers Attach to running container\u0026rsquo;s output:\ndocker attach myapp What this does:\nShows container\u0026rsquo;s stdout/stderr Allows you to send input to container Ctrl+C stops the container! ⚠️ Detach without stopping: Press Ctrl+P then Ctrl+Q\nWhen to use:\nDebugging interactive applications Seeing output of foreground containers Usually better: Use docker logs -f instead (safer)\n6.7 Committing Containers Save container state as new image:\ndocker commit \u0026lt;container_id\u0026gt; my-new-image:v1 Example scenario:\n# Start Ubuntu container docker run -it --name customized ubuntu bash # Inside container, install software apt update apt install -y curl vim git exit # Save this container as new image docker commit customized ubuntu-with-tools:v1 # Now you can use this custom image docker run -it ubuntu-with-tools:v1 bash # curl, vim, git are already installed! Important:\nNot recommended for production (use Dockerfile instead) Good for quick experiments Creates large images 6.8 Renaming Containers docker rename old_name new_name Example:\ndocker run -d --name webserver nginx docker rename webserver production_web docker ps # Shows \u0026#34;production_web\u0026#34; 6.9 Updating Container Configuration Some settings can be updated without recreating:\n# Update restart policy docker update --restart always myapp # Update resource limits docker update --memory 512m myapp docker update --cpus 2 myapp # Update multiple containers docker update --restart always $(docker ps -q) 6.10 Export and Import Export container filesystem:\ndocker export myapp \u0026gt; myapp.tar Import as image:\ndocker import myapp.tar myapp:backup Use case: Backup, migration to system without registry access\n6.11 Container Management Best Practices 1. Always name your containers:\n# ❌ Bad docker run -d nginx # ✅ Good docker run -d --name production_web nginx 2. Use restart policies:\n# Production services docker run -d --restart unless-stopped --name db postgres 3. Clean up regularly:\n# Remove stopped containers docker container prune # Remove all stopped containers + unused resources docker system prune 4. Check logs before removing:\ndocker logs myapp \u0026gt; logs_backup.txt docker rm myapp 5. Use meaningful tags:\n# ❌ Bad docker commit myapp myapp # ✅ Good docker commit myapp myapp:v1.2-production 6.12 Container Management Cheat Sheet # Inspection docker inspect \u0026lt;container\u0026gt; # Full details (JSON) docker inspect -f \u0026#39;{{.State.Status}}\u0026#39; \u0026lt;c\u0026gt; # Extract specific field docker ps -a # List all containers docker stats # Resource usage docker top \u0026lt;container\u0026gt; # Running processes # Execution docker exec -it \u0026lt;container\u0026gt; bash # Interactive shell docker exec \u0026lt;container\u0026gt; \u0026lt;command\u0026gt; # Run command docker exec -u root \u0026lt;container\u0026gt; \u0026lt;cmd\u0026gt; # Run as specific user # Logs docker logs \u0026lt;container\u0026gt; # All logs docker logs -f \u0026lt;container\u0026gt; # Follow logs docker logs --tail 100 \u0026lt;container\u0026gt; # Last N lines docker logs --since 30m \u0026lt;container\u0026gt; # Time filter docker logs -t \u0026lt;container\u0026gt; # With timestamps # Files docker cp host_file \u0026lt;container\u0026gt;:/path # Copy to container docker cp \u0026lt;container\u0026gt;:/path host_file # Copy from container # Control docker attach \u0026lt;container\u0026gt; # Attach to container docker rename old new # Rename container docker update --restart always \u0026lt;container\u0026gt; # Update settings docker commit \u0026lt;container\u0026gt; \u0026lt;image\u0026gt; # Save as image docker export \u0026lt;container\u0026gt; \u0026gt; file.tar # Export filesystem # Cleanup docker rm \u0026lt;container\u0026gt; # Remove stopped docker rm -f \u0026lt;container\u0026gt; # Force remove docker container prune # Remove all stopped docker system prune # Remove unused resources 7. Port Mapping \u0026amp; Networking 7.1 Understanding Ports What is a port?\nThink of your computer as an apartment building:\nBuilding address = Your computer\u0026rsquo;s IP (localhost) Apartment number = Port (1 to 65535) Different services = Different apartments Common ports:\nPort 80 → HTTP (web servers) Port 443 → HTTPS (secure web) Port 22 → SSH Port 3000 → Node.js apps Port 3306 → MySQL Port 5432 → PostgreSQL Port 27017 → MongoDB Port 6379 → Redis Port 8080 → Alternative HTTP 7.2 The Port Mapping Problem Without Docker:\nYour Computer Port 80 → Web Server Simple!\nWith Docker:\nYour Computer Container (Isolated!) Port 80 ? Port 80 (web server) Problem: Container port 80 is INSIDE the container. Your computer can\u0026rsquo;t access it directly!\nSolution: Port mapping creates a tunnel:\nYour Computer Container Port 8080 ←──────→ Port 80 (mapped) (internal) 7.3 Port Mapping Syntax Basic format:\ndocker run -p HOST_PORT:CONTAINER_PORT image ↑ Your computer ↑ Inside container Examples:\n# Map port 8080 on host to port 80 in container docker run -d -p 8080:80 nginx # Access: http://localhost:8080 # Map port 3000 to port 3000 docker run -d -p 3000:3000 my-node-app # Access: http://localhost:3000 # Map port 5433 to port 5432 (PostgreSQL) docker run -d -p 5433:5432 postgres # Connect: localhost:5433 7.4 Why Different Ports? Scenario: Run 3 web servers simultaneously\n# ❌ This fails: docker run -d -p 80:80 --name web1 nginx docker run -d -p 80:80 --name web2 nginx # ERROR: Port 80 already in use! # ✅ This works: docker run -d -p 8081:80 --name web1 nginx docker run -d -p 8082:80 --name web2 nginx docker run -d -p 8083:80 --name web3 nginx Access:\nweb1: http://localhost:8081 web2: http://localhost:8082 web3: http://localhost:8083 All use port 80 INSIDE their containers, but different ports on YOUR computer!\n7.5 Multiple Port Mappings Map multiple ports for one container:\ndocker run -d \\ -p 3000:3000 \\ -p 8080:8080 \\ -p 9229:9229 \\ my-app Use case: App on 3000, admin panel on 8080, debugger on 9229\n7.6 Dynamic Port Mapping Let Docker choose random port:\ndocker run -d -P nginx # -P (capital P) = publish all exposed ports to random high ports Find assigned port:\ndocker ps # PORTS column shows: 0.0.0.0:32768-\u0026gt;80/tcp # Access: http://localhost:32768 Or programmatically:\ndocker port \u0026lt;container_id\u0026gt; 80 # Shows: 0.0.0.0:32768 7.7 Binding to Specific Interface Bind to localhost only (more secure):\ndocker run -d -p 127.0.0.1:8080:80 nginx # Only accessible from localhost, NOT from network Bind to specific IP:\ndocker run -d -p 192.168.1.100:8080:80 nginx # Only accessible from that specific IP Bind to all interfaces (default):\ndocker run -d -p 8080:80 nginx # Same as: -p 0.0.0.0:8080:80 # Accessible from anywhere 7.8 UDP Ports Default is TCP, but you can specify UDP:\n# UDP port docker run -d -p 53:53/udp dns-server # Both TCP and UDP docker run -d \\ -p 53:53/tcp \\ -p 53:53/udp \\ dns-server 7.9 Port Mapping vs EXPOSE EXPOSE in Dockerfile (documentation only):\nEXPOSE 80 Documents which port app uses Does NOT actually publish the port Like a note saying \u0026ldquo;this app listens on port 80\u0026rdquo; -p flag (actually publishes):\ndocker run -p 8080:80 nginx Actually makes port accessible Creates the tunnel Think of it:\nEXPOSE = Sign on door: \u0026ldquo;Office hours 9-5\u0026rdquo; -p = Actually opening the door 7.10 Checking Port Mappings View ports for running container:\n# Using docker ps docker ps # Look at PORTS column # Using docker port command docker port \u0026lt;container_name\u0026gt; # Check specific port docker port \u0026lt;container_name\u0026gt; 80 7.11 Common Port Mapping Patterns Pattern 1: Development (same port both sides):\ndocker run -d -p 3000:3000 my-dev-app # Easy to remember, matches your app\u0026#39;s config Pattern 2: Production (different ports):\ndocker run -d -p 80:3000 my-prod-app # Public sees port 80 (standard HTTP) # Container uses port 3000 internally Pattern 3: Multiple instances:\ndocker run -d -p 8081:80 --name instance1 nginx docker run -d -p 8082:80 --name instance2 nginx docker run -d -p 8083:80 --name instance3 nginx # Load balancing, testing, staging environments Pattern 4: Database access:\ndocker run -d -p 27017:27017 --name mongo mongo # Use default port for compatibility with tools 7.12 Troubleshooting Port Issues Problem: \u0026ldquo;Port already in use\u0026rdquo;\n# Find what\u0026#39;s using the port (Linux/Mac) sudo lsof -i :8080 # Kill the process kill -9 \u0026lt;PID\u0026gt; # Or use different port in Docker docker run -d -p 8081:80 nginx Problem: \u0026ldquo;Cannot access container from browser\u0026rdquo;\nCheck:\nContainer is running: docker ps Port mapping is correct: docker port \u0026lt;container\u0026gt; Firewall allows the port App inside container is listening on 0.0.0.0 (not 127.0.0.1) Problem: \u0026ldquo;Connection refused\u0026rdquo;\nPossible causes:\nApp inside container not running App listening on wrong port App listening on 127.0.0.1 instead of 0.0.0.0 Debug:\n# Check if app is running inside container docker exec \u0026lt;container\u0026gt; ps aux # Check what ports app is listening on docker exec \u0026lt;container\u0026gt; netstat -tlnp 7.13 Port Mapping Cheat Sheet # Basic port mapping -p 8080:80 # Host 8080 → Container 80 -p 3000:3000 # Same port both sides -p 127.0.0.1:8080:80 # Bind to localhost only -p 192.168.1.10:8080:80 # Bind to specific IP # Multiple ports -p 3000:3000 -p 8080:8080 # Map multiple ports # UDP ports -p 53:53/udp # UDP instead of TCP -p 53:53/tcp -p 53:53/udp # Both TCP and UDP # Dynamic ports -P # Publish all exposed ports to random ports # Check mappings docker ps # See PORTS column docker port \u0026lt;container\u0026gt; # Show all port mappings docker port \u0026lt;container\u0026gt; 80 # Show mapping for port 80 # Common patterns -p 80:3000 # Production (standard HTTP → app port) -p 8081:80 # Multiple instances -p 27017:27017 # Database (default port) 7.14 Real-World Example Running complete development stack:\n# Database docker run -d \\ --name dev_db \\ -p 5432:5432 \\ -e POSTGRES_PASSWORD=secret \\ postgres # Backend API docker run -d \\ --name dev_api \\ -p 3000:3000 \\ my-api-app # Frontend docker run -d \\ --name dev_frontend \\ -p 8080:80 \\ my-frontend-app # Redis cache docker run -d \\ --name dev_redis \\ -p 6379:6379 \\ redis Access:\nDatabase: localhost:5432 API: http://localhost:3000 Frontend: http://localhost:8080 Redis: localhost:6379 All running simultaneously, isolated, no conflicts!\n8. Container Logs \u0026amp; Debugging 8.1 Understanding Container Logs What are container logs?\nEverything written to:\nstdout (standard output) - Normal output stderr (standard error) - Error messages Example in code:\n// Node.js console.log(\u0026#34;Server started\u0026#34;); // → stdout → docker logs console.error(\u0026#34;ERROR!\u0026#34;); // → stderr → docker logs # Python print(\u0026#34;Hello\u0026#34;) # → stdout → docker logs import sys sys.stderr.write(\u0026#34;ERROR\\n\u0026#34;) # → stderr → docker logs 8.2 Viewing Logs Basic log viewing:\n# View all logs docker logs \u0026lt;container\u0026gt; # Real-time logs (follow) docker logs -f \u0026lt;container\u0026gt; # Last N lines docker logs --tail 50 \u0026lt;container\u0026gt; # Since timestamp docker logs --since 2024-01-01T00:00:00 \u0026lt;container\u0026gt; # Since relative time docker logs --since 30m \u0026lt;container\u0026gt; # Last 30 minutes docker logs --since 2h \u0026lt;container\u0026gt; # Last 2 hours # Until timestamp docker logs --until 2024-01-01T12:00:00 \u0026lt;container\u0026gt; # With timestamps docker logs -t \u0026lt;container\u0026gt; # Combine options docker logs -f --tail 100 --since 30m \u0026lt;container\u0026gt; 8.3 Log Drivers Docker supports different log drivers:\nDefault: json-file\nLogs stored as JSON Location: /var/lib/docker/containers/\u0026lt;container-id\u0026gt;/\u0026lt;container-id\u0026gt;-json.log Other drivers:\nnone - No logs syslog - System log journald - systemd journal gelf - Graylog fluentd - Fluentd awslogs - AWS CloudWatch Configure log driver:\n# For single container docker run -d \\ --log-driver json-file \\ --log-opt max-size=10m \\ --log-opt max-file=3 \\ nginx # Set globally in /etc/docker/daemon.json: { \u0026#34;log-driver\u0026#34;: \u0026#34;json-file\u0026#34;, \u0026#34;log-opts\u0026#34;: { \u0026#34;max-size\u0026#34;: \u0026#34;10m\u0026#34;, \u0026#34;max-file\u0026#34;: \u0026#34;3\u0026#34; } } Why log rotation matters: Without limits, logs can fill your disk!\n8.4 Debugging Running Containers Technique 1: Execute shell inside container\n# Get bash shell docker exec -it \u0026lt;container\u0026gt; bash # Or sh if bash not available docker exec -it \u0026lt;container\u0026gt; sh # Inside container, investigate: ls ps aux cat /var/log/app.log env Technique 2: Check running processes\ndocker top \u0026lt;container\u0026gt; Technique 3: View resource usage\ndocker stats \u0026lt;container\u0026gt; Technique 4: Inspect configuration\ndocker inspect \u0026lt;container\u0026gt; 8.5 Common Debugging Scenarios Scenario 1: Container keeps restarting\n# Check logs for errors docker logs --tail 100 \u0026lt;container\u0026gt; # Check exit code docker ps -a # Look at STATUS: \u0026#34;Exited (1)\u0026#34; means error # See why it crashed docker inspect --format=\u0026#39;{{.State.ExitCode}}\u0026#39; \u0026lt;container\u0026gt; docker inspect --format=\u0026#39;{{.State.Error}}\u0026#39; \u0026lt;container\u0026gt; Scenario 2: Container won\u0026rsquo;t start\n# Remove -d to see output docker run -it \u0026lt;image\u0026gt; # You\u0026#39;ll see errors immediately # Check image is valid docker images docker inspect \u0026lt;image\u0026gt; Scenario 3: Can\u0026rsquo;t connect to containerized app\n# Check container is running docker ps # Check port mapping docker port \u0026lt;container\u0026gt; # Check app is listening inside container docker exec \u0026lt;container\u0026gt; netstat -tlnp # Check app logs docker logs \u0026lt;container\u0026gt; # Test from inside container docker exec \u0026lt;container\u0026gt; curl http://localhost:80 Scenario 4: Out of memory\n# Check resource usage docker stats \u0026lt;container\u0026gt; # Check OOM (Out of Memory) kills docker inspect --format=\u0026#39;{{.State.OOMKilled}}\u0026#39; \u0026lt;container\u0026gt; # Set memory limit docker run -d --memory 512m \u0026lt;image\u0026gt; 8.6 Debugging Stopped Containers Container stopped but you need to investigate:\n# View logs from stopped container docker logs \u0026lt;stopped_container\u0026gt; # Check exit code docker inspect --format=\u0026#39;{{.State.ExitCode}}\u0026#39; \u0026lt;stopped_container\u0026gt; # Start it with different command to debug docker run -it --entrypoint bash \u0026lt;image\u0026gt; # Now you can poke around 8.7 Health Checks Add health check to monitor container health:\ndocker run -d \\ --health-cmd=\u0026#34;curl -f http://localhost/ || exit 1\u0026#34; \\ --health-interval=30s \\ --health-timeout=3s \\ --health-retries=3 \\ nginx Check health status:\ndocker ps # STATUS shows: \u0026#34;healthy\u0026#34; or \u0026#34;unhealthy\u0026#34; docker inspect --format=\u0026#39;{{.State.Health.Status}}\u0026#39; \u0026lt;container\u0026gt; 8.8 Installing Debug Tools Container might not have debugging tools:\n# Example: minimal Alpine image docker exec -it \u0026lt;container\u0026gt; sh # No curl, no vim, no nothing! # Install them: apk update apk add curl vim tcpdump # Now you can debug curl http://localhost Common packages:\n# Debian/Ubuntu apt update apt install -y curl wget vim netcat-openbsd net-tools # Alpine apk update apk add curl wget vim netcat-openbsd bind-tools # Red Hat/CentOS yum install -y curl wget vim nc net-tools 8.9 Capturing and Analyzing Logs Save logs to file:\ndocker logs \u0026lt;container\u0026gt; \u0026gt; app.log 2\u0026gt;\u0026amp;1 Search logs:\n# Find errors docker logs \u0026lt;container\u0026gt; | grep ERROR # Find specific string docker logs \u0026lt;container\u0026gt; | grep \u0026#34;user login\u0026#34; # Count occurrences docker logs \u0026lt;container\u0026gt; | grep ERROR | wc -l # Last 1000 lines with errors docker logs --tail 1000 \u0026lt;container\u0026gt; | grep ERROR Filter by time:\n# Errors in last hour docker logs --since 1h \u0026lt;container\u0026gt; | grep ERROR # Activity today docker logs --since $(date +%Y-%m-%d) \u0026lt;container\u0026gt; 8.10 Debugging Network Issues Check container\u0026rsquo;s network settings:\n# Get IP address docker inspect --format=\u0026#39;{{.NetworkSettings.IPAddress}}\u0026#39; \u0026lt;container\u0026gt; # Get all network info docker inspect --format=\u0026#39;{{json .NetworkSettings}}\u0026#39; \u0026lt;container\u0026gt; | jq # Ping from one container to another docker exec container1 ping container2 Test connectivity:\n# From inside container docker exec \u0026lt;container\u0026gt; curl http://other-service:3000 # From host to container curl http://localhost:8080 # DNS resolution docker exec \u0026lt;container\u0026gt; nslookup google.com 8.11 Debugging File Issues Check if file exists:\ndocker exec \u0026lt;container\u0026gt; ls -la /app/config.json View file contents:\ndocker exec \u0026lt;container\u0026gt; cat /app/config.json Copy file out for inspection:\ndocker cp \u0026lt;container\u0026gt;:/app/config.json ./config.json cat config.json Check file permissions:\ndocker exec \u0026lt;container\u0026gt; ls -l /app/ 8.12 Debugging Environment Variables Check environment variables:\n# All environment variables docker exec \u0026lt;container\u0026gt; env # Specific variable docker exec \u0026lt;container\u0026gt; echo $DATABASE_URL # From docker inspect docker inspect --format=\u0026#39;{{.Config.Env}}\u0026#39; \u0026lt;container\u0026gt; 8.13 Debugging Cheat Sheet # Logs docker logs \u0026lt;container\u0026gt; # All logs docker logs -f \u0026lt;container\u0026gt; # Follow (real-time) docker logs --tail 100 \u0026lt;container\u0026gt; # Last 100 lines docker logs --since 30m \u0026lt;container\u0026gt; # Last 30 minutes docker logs -t \u0026lt;container\u0026gt; # With timestamps docker logs \u0026lt;container\u0026gt; | grep ERROR # Find errors # Interactive debugging docker exec -it \u0026lt;container\u0026gt; bash # Get shell docker exec -it \u0026lt;container\u0026gt; sh # Get sh (if no bash) docker exec \u0026lt;container\u0026gt; ps aux # See processes docker exec \u0026lt;container\u0026gt; env # See environment vars # Resource issues docker stats \u0026lt;container\u0026gt; # Resource usage docker top \u0026lt;container\u0026gt; # Running processes docker inspect \u0026lt;container\u0026gt; # Full details # Network debugging docker inspect --format=\u0026#39;{{.NetworkSettings.IPAddress}}\u0026#39; \u0026lt;c\u0026gt; # Get IP docker exec \u0026lt;container\u0026gt; ping other-container # Test connectivity docker exec \u0026lt;container\u0026gt; curl localhost:3000 # Test service docker port \u0026lt;container\u0026gt; # Check port mappings # File debugging docker exec \u0026lt;container\u0026gt; ls -la /app # List files docker exec \u0026lt;container\u0026gt; cat /app/file.txt # View file docker cp \u0026lt;container\u0026gt;:/app/file.txt ./ # Copy file out # Container state docker ps -a # All containers + status docker inspect --format=\u0026#39;{{.State.ExitCode}}\u0026#39; \u0026lt;c\u0026gt; # Exit code docker inspect --format=\u0026#39;{{.State.Error}}\u0026#39; \u0026lt;c\u0026gt; # Error message docker inspect --format=\u0026#39;{{.State.Health}}\u0026#39; \u0026lt;c\u0026gt; # Health status # Install debugging tools (inside container) # Debian/Ubuntu apt update \u0026amp;\u0026amp; apt install -y curl vim netcat # Alpine apk update \u0026amp;\u0026amp; apk add curl vim netcat-openbsd 8.14 Common Error Messages \u0026ldquo;docker: Error response from daemon: Conflict\u0026rdquo;\nContainer name already exists Solution: Remove old container or use different name \u0026ldquo;docker: Error response from daemon: driver failed programming external connectivity on endpoint\u0026rdquo;\nPort already in use Solution: Use different port or stop process using it \u0026ldquo;docker: Error response from daemon: No such container\u0026rdquo;\nContainer doesn\u0026rsquo;t exist or wrong name/ID Solution: Check with docker ps -a \u0026ldquo;exec format error\u0026rdquo;\nWrong architecture (ARM vs x86) Solution: Use correct image for your CPU \u0026ldquo;OCI runtime exec failed\u0026rdquo;\nCommand not found in container Solution: Use correct command or install it first PART 3: CREATING IMAGES 9. Understanding Dockerfiles 9.1 What is a Dockerfile? Definition: A text file containing instructions to build a Docker image. It\u0026rsquo;s like a recipe that tells Docker how to create your image step by step.\nAnalogy:\nDockerfile = Recipe card with instructions Building = Following the recipe Image = The prepared cake mix Container = Baking and serving the cake 9.2 Dockerfile Structure Basic structure:\n# Comment INSTRUCTION arguments # Example: FROM ubuntu:20.04 RUN apt update CMD echo \u0026#34;Hello\u0026#34; Rules:\nInstructions are case-insensitive (but UPPERCASE is convention) Each instruction creates a layer Lines starting with # are comments Must start with FROM (except ARG before FROM) 9.3 Basic Dockerfile Example Create file named Dockerfile (no extension):\n# Use Ubuntu as base FROM ubuntu:20.04 # Install curl RUN apt update \u0026amp;\u0026amp; apt install -y curl # Print message when container starts CMD echo \u0026#34;Container is running!\u0026#34; Build it:\ndocker build -t myimage . Run it:\ndocker run myimage # Output: Container is running! 9.4 FROM Instruction Syntax:\nFROM \u0026lt;image\u0026gt;:\u0026lt;tag\u0026gt; FROM \u0026lt;image\u0026gt;@\u0026lt;digest\u0026gt; Purpose: Specify base image to build upon\nExamples:\n# Use Ubuntu 20.04 FROM ubuntu:20.04 # Use Python 3.11 FROM python:3.11 # Use Node.js 18 Alpine (smaller) FROM node:18-alpine # Use specific digest (immutable) FROM nginx@sha256:abc123... # Scratch (empty image, for minimal builds) FROM scratch Choosing base images:\n# Full OS (larger, more features) FROM ubuntu:20.04 # ~77MB # Language runtimes FROM python:3.11 # ~900MB FROM python:3.11-slim # ~150MB FROM python:3.11-alpine # ~50MB FROM node:18 # ~900MB FROM node:18-slim # ~200MB FROM node:18-alpine # ~170MB # Minimal FROM alpine:3.17 # ~7MB FROM scratch # 0MB (empty!) Best practice: Use specific tags, not latest\n# ❌ Bad (version can change) FROM python:latest # ✅ Good (predictable) FROM python:3.11-slim 9.5 RUN Instruction Syntax:\nRUN \u0026lt;command\u0026gt; RUN [\u0026#34;executable\u0026#34;, \u0026#34;param1\u0026#34;, \u0026#34;param2\u0026#34;] Purpose: Execute commands during image build\nExamples:\n# Install packages RUN apt update \u0026amp;\u0026amp; apt install -y curl vim # Create directory RUN mkdir -p /app/data # Download file RUN curl -O https://example.com/file.tar.gz # Python packages RUN pip install flask requests # Node packages RUN npm install express # Multiple commands RUN apt update \u0026amp;\u0026amp; \\ apt install -y curl wget \u0026amp;\u0026amp; \\ rm -rf /var/lib/apt/lists/* Shell form vs Exec form:\n# Shell form (runs in shell: /bin/sh -c) RUN apt update # Exec form (no shell) RUN [\u0026#34;apt\u0026#34;, \u0026#34;update\u0026#34;] Best practices:\n# ❌ Bad - Creates multiple layers RUN apt update RUN apt install -y curl RUN apt install -y vim # ✅ Good - Single layer, cleanup RUN apt update \u0026amp;\u0026amp; \\ apt install -y curl vim \u0026amp;\u0026amp; \\ rm -rf /var/lib/apt/lists/* 9.6 COPY Instruction Syntax:\nCOPY \u0026lt;src\u0026gt; \u0026lt;dest\u0026gt; COPY [\u0026#34;\u0026lt;src\u0026gt;\u0026#34;, \u0026#34;\u0026lt;dest\u0026gt;\u0026#34;] Purpose: Copy files from host to image\nExamples:\n# Copy single file COPY app.py /app/ # Copy directory COPY ./myapp /app/ # Copy multiple files COPY file1.txt file2.txt /app/ # Copy and rename COPY config.json /app/production-config.json # Copy with specific ownership COPY --chown=user:group app.py /app/ COPY vs ADD:\n# COPY - Simple file copying (preferred) COPY app.py /app/ # ADD - Can extract tarballs, download URLs (avoid unless needed) ADD archive.tar.gz /app/ # Auto-extracts ADD http://example.com/file /app/ # Downloads (unreliable) Best practice: Use COPY unless you specifically need ADD\u0026rsquo;s features\n9.7 WORKDIR Instruction Syntax:\nWORKDIR /path/to/directory Purpose: Set working directory for subsequent instructions\nExamples:\n# Set working directory WORKDIR /app # Now all commands run in /app COPY . . # Copies to /app RUN npm install # Runs in /app CMD [\u0026#34;node\u0026#34;, \u0026#34;server.js\u0026#34;] # Runs from /app # Can be used multiple times WORKDIR /app WORKDIR data # Now in /app/data WORKDIR logs # Now in /app/data/logs Why use WORKDIR:\n# ❌ Without WORKDIR (messy) RUN mkdir /app COPY app.py /app/ RUN cd /app \u0026amp;\u0026amp; python app.py # ✅ With WORKDIR (clean) WORKDIR /app COPY app.py . RUN python app.py 9.8 CMD Instruction Syntax:\nCMD [\u0026#34;executable\u0026#34;, \u0026#34;param1\u0026#34;, \u0026#34;param2\u0026#34;] # Exec form (preferred) CMD command param1 param2 # Shell form Purpose: Default command when container starts\nExamples:\n# Run Python app CMD [\u0026#34;python\u0026#34;, \u0026#34;app.py\u0026#34;] # Run Node.js server CMD [\u0026#34;node\u0026#34;, \u0026#34;server.js\u0026#34;] # Run bash CMD [\u0026#34;/bin/bash\u0026#34;] # Shell form CMD python app.py Important: Only ONE CMD in Dockerfile (last one wins)\nCMD echo \u0026#34;First\u0026#34; CMD echo \u0026#34;Second\u0026#34; # This one is used CMD echo \u0026#34;Third\u0026#34; # This one is used (others ignored) Can be overridden:\n# In Dockerfile CMD [\u0026#34;echo\u0026#34;, \u0026#34;default\u0026#34;] # When running docker run myimage # Uses CMD: \u0026#34;default\u0026#34; docker run myimage echo \u0026#34;new\u0026#34; # Overrides CMD: \u0026#34;new\u0026#34; 9.9 ENTRYPOINT Instruction Syntax:\nENTRYPOINT [\u0026#34;executable\u0026#34;, \u0026#34;param1\u0026#34;] ENTRYPOINT command param1 Purpose: Configure container as executable\nENTRYPOINT vs CMD:\nCMD - Can be overridden:\nCMD [\u0026#34;echo\u0026#34;, \u0026#34;hello\u0026#34;] docker run myimage # Runs: echo hello docker run myimage echo bye # Runs: echo bye (overridden) ENTRYPOINT - Fixed command:\nENTRYPOINT [\u0026#34;echo\u0026#34;] docker run myimage # Runs: echo docker run myimage hello # Runs: echo hello (adds to entrypoint) Combined ENTRYPOINT + CMD:\nENTRYPOINT [\u0026#34;echo\u0026#34;] CMD [\u0026#34;default message\u0026#34;] docker run myimage # Runs: echo \u0026#34;default message\u0026#34; docker run myimage \u0026#34;custom\u0026#34; # Runs: echo \u0026#34;custom\u0026#34; Practical example:\n# Database container ENTRYPOINT [\u0026#34;docker-entrypoint.sh\u0026#34;] CMD [\u0026#34;postgres\u0026#34;] # Running: docker run postgres # Runs: docker-entrypoint.sh postgres docker run postgres psql # Runs: docker-entrypoint.sh psql 9.10 ENV Instruction Syntax:\nENV \u0026lt;key\u0026gt;=\u0026lt;value\u0026gt; ENV \u0026lt;key\u0026gt; \u0026lt;value\u0026gt; Purpose: Set environment variables\nExamples:\n# Single variable ENV NODE_ENV=production # Multiple variables ENV PORT=3000 \\ DB_HOST=localhost \\ DB_PORT=5432 # Used in subsequent instructions ENV APP_HOME=/app WORKDIR $APP_HOME COPY . $APP_HOME Accessed at runtime:\nENV DATABASE_URL=postgres://localhost/mydb CMD echo \u0026#34;Connecting to $DATABASE_URL\u0026#34; Override at runtime:\ndocker run -e DATABASE_URL=postgres://prod.db/mydb myimage 9.11 EXPOSE Instruction Syntax:\nEXPOSE \u0026lt;port\u0026gt; EXPOSE \u0026lt;port\u0026gt;/\u0026lt;protocol\u0026gt; Purpose: Document which ports the app listens on\nExamples:\n# Expose HTTP port EXPOSE 80 # Expose custom port EXPOSE 3000 # Multiple ports EXPOSE 80 443 # UDP port EXPOSE 53/udp # Both TCP and UDP EXPOSE 53/tcp 53/udp Important: EXPOSE is documentation only! It does NOT publish the port.\n# In Dockerfile EXPOSE 80 # Still need -p when running docker run -p 8080:80 myimage 9.12 ARG Instruction Syntax:\nARG \u0026lt;name\u0026gt; ARG \u0026lt;name\u0026gt;=\u0026lt;default\u0026gt; Purpose: Build-time variables (not available at runtime)\nExamples:\n# Define build argument ARG VERSION=1.0 ARG BUILD_DATE # Use in Dockerfile RUN echo \u0026#34;Building version $VERSION\u0026#34; LABEL build.date=$BUILD_DATE Pass during build:\ndocker build --build-arg VERSION=2.0 --build-arg BUILD_DATE=$(date) -t myapp . ARG vs ENV:\n# ARG - Only during build ARG BUILD_ENV=development RUN echo \u0026#34;Building for $BUILD_ENV\u0026#34; # Works CMD echo $BUILD_ENV # Empty! (not available at runtime) # ENV - Available at runtime ENV RUNTIME_ENV=production CMD echo $RUNTIME_ENV # Works! 9.13 LABEL Instruction Syntax:\nLABEL \u0026lt;key\u0026gt;=\u0026lt;value\u0026gt; Purpose: Add metadata to image\nExamples:\n# Single label LABEL version=\u0026#34;1.0\u0026#34; # Multiple labels LABEL version=\u0026#34;1.0\u0026#34; \\ description=\u0026#34;My application\u0026#34; \\ maintainer=\u0026#34;you@example.com\u0026#34; # Common labels LABEL org.opencontainers.image.authors=\u0026#34;Your Name\u0026#34; LABEL org.opencontainers.image.version=\u0026#34;1.0.0\u0026#34; LABEL org.opencontainers.image.created=\u0026#34;2024-01-01\u0026#34; View labels:\ndocker inspect --format=\u0026#39;{{json .Config.Labels}}\u0026#39; myimage | jq 9.14 USER Instruction Syntax:\nUSER \u0026lt;username|UID\u0026gt; Purpose: Set user for subsequent instructions and runtime\nExamples:\n# Run as non-root user RUN useradd -m appuser USER appuser # All subsequent commands run as appuser COPY . /app CMD [\u0026#34;./app\u0026#34;] Why this matters (security):\n# ❌ Bad - Runs as root (security risk) FROM ubuntu CMD [\u0026#34;./app\u0026#34;] # ✅ Good - Runs as non-root user FROM ubuntu RUN useradd -m appuser USER appuser CMD [\u0026#34;./app\u0026#34;] 9.15 VOLUME Instruction Syntax:\nVOLUME [\u0026#34;/data\u0026#34;] VOLUME /data Purpose: Create mount point for persistent data\nExamples:\n# Single volume VOLUME /data # Multiple volumes VOLUME [\u0026#34;/data\u0026#34;, \u0026#34;/logs\u0026#34;] # Example: Database FROM postgres VOLUME /var/lib/postgresql/data We\u0026rsquo;ll cover volumes in detail in Part 4!\n9.16 Complete Dockerfile Examples Example 1: Python Flask App\n# Use Python 3.11 slim image FROM python:3.11-slim # Set working directory WORKDIR /app # Copy requirements file COPY requirements.txt . # Install dependencies RUN pip install --no-cache-dir -r requirements.txt # Copy application code COPY . . # Expose port EXPOSE 5000 # Set environment variable ENV FLASK_APP=app.py # Run as non-root user RUN useradd -m appuser USER appuser # Start application CMD [\u0026#34;flask\u0026#34;, \u0026#34;run\u0026#34;, \u0026#34;--host=0.0.0.0\u0026#34;] Example 2: Node.js Express App\n# Use Node.js 18 Alpine FROM node:18-alpine # Set working directory WORKDIR /app # Copy package files COPY package*.json ./ # Install dependencies RUN npm ci --only=production # Copy source code COPY . . # Expose port EXPOSE 3000 # Set environment ENV NODE_ENV=production # Run as non-root user USER node # Start app CMD [\u0026#34;node\u0026#34;, \u0026#34;server.js\u0026#34;] Example 3: Nginx with Custom Config\n# Use nginx Alpine FROM nginx:alpine # Copy custom config COPY nginx.conf /etc/nginx/nginx.conf # Copy static files COPY ./dist /usr/share/nginx/html # Expose HTTP EXPOSE 80 # Nginx starts automatically (from base image) 9.17 Dockerfile Best Practices Summary # ✅ Good Dockerfile Structure # 1. Use specific base image tags FROM python:3.11-slim # 2. Set working directory early WORKDIR /app # 3. Copy dependency files first (better caching) COPY requirements.txt . # 4. Install dependencies RUN pip install --no-cache-dir -r requirements.txt # 5. Copy source code last COPY . . # 6. Expose ports (documentation) EXPOSE 8000 # 7. Use environment variables ENV PYTHONUNBUFFERED=1 # 8. Run as non-root user RUN useradd -m appuser USER appuser # 9. Define startup command CMD [\u0026#34;python\u0026#34;, \u0026#34;app.py\u0026#34;] 9.18 Dockerfile Instructions Reference # Base image FROM image:tag # Execute commands during build RUN command # Copy files from host to image COPY source dest # Set working directory WORKDIR /path # Default command when container starts CMD [\u0026#34;executable\u0026#34;, \u0026#34;params\u0026#34;] # Configure executable ENTRYPOINT [\u0026#34;executable\u0026#34;] # Environment variables ENV KEY=value # Document ports EXPOSE port # Build-time variables ARG name=default # Metadata LABEL key=value # Switch user USER username # Create volume mount point VOLUME /path # Health check HEALTHCHECK CMD command # Signal to stop container STOPSIGNAL signal # Set shell SHELL [\u0026#34;/bin/bash\u0026#34;, \u0026#34;-c\u0026#34;] 10. Building Custom Images 10.1 The Build Command Basic syntax:\ndocker build [OPTIONS] PATH Most common:\ndocker build -t image_name:tag . Options:\n-t - Tag (name) the image . - Build context (current directory) -f - Specify Dockerfile name --build-arg - Pass build arguments --no-cache - Don\u0026rsquo;t use cache 10.2 Build Context What is build context?\nThe directory Docker sends to the daemon for building. Everything in this directory can be copied into the image.\nExample:\n# Current directory is build context docker build -t myapp . Build context includes:\nmyproject/ ├── Dockerfile ├── app.py ← Can COPY this ├── config.json ← Can COPY this ├── data/ ← Can COPY this └── .git/ ← Sent but shouldn\u0026#39;t copy (use .dockerignore) Large context = slow builds!\n10.3 .dockerignore File Purpose: Exclude files from build context (like .gitignore)\nCreate .dockerignore file:\n# Ignore git .git .gitignore # Ignore dependencies (will be installed in image) node_modules __pycache__ *.pyc # Ignore logs *.log logs/ # Ignore development files .env .env.local *.swp .DS_Store # Ignore documentation README.md docs/ # Ignore test files tests/ *.test.js Benefits:\nFaster builds (smaller context) Smaller images (don\u0026rsquo;t copy unnecessary files) Better security (don\u0026rsquo;t copy secrets) 10.4 Building Your First Custom Image Project structure:\nmy-python-app/ ├── Dockerfile ├── app.py ├── requirements.txt └── .dockerignore app.py:\nfrom flask import Flask app = Flask(__name__) @app.route(\u0026#39;/\u0026#39;) def hello(): return \u0026#34;Hello from Docker!\u0026#34; if __name__ == \u0026#39;__main__\u0026#39;: app.run(host=\u0026#39;0.0.0.0\u0026#39;, port=5000) requirements.txt:\nflask==2.3.0 Dockerfile:\nFROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY app.py . EXPOSE 5000 CMD [\u0026#34;python\u0026#34;, \u0026#34;app.py\u0026#34;] .dockerignore:\n__pycache__ *.pyc .env Build it:\ncd my-python-app docker build -t my-flask-app:v1 . Run it:\ndocker run -d -p 5000:5000 --name flask_app my-flask-app:v1 Test:\ncurl http://localhost:5000 # Output: Hello from Docker! 10.5 Understanding Build Layers Each Dockerfile instruction creates a layer:\nFROM python:3.11-slim # Layer 1 WORKDIR /app # Layer 2 COPY requirements.txt . # Layer 3 RUN pip install -r req.txt # Layer 4 COPY app.py . # Layer 5 CMD [\u0026#34;python\u0026#34;, \u0026#34;app.py\u0026#34;] # Layer 6 (metadata, no actual layer) Layers are cached:\nFirst build: Layer 1: Download python:3.11-slim Layer 2: Create /app Layer 3: Copy requirements.txt Layer 4: Run pip install (slow!) Layer 5: Copy app.py Total time: 2 minutes Second build (only app.py changed): Layer 1: Use cache ✅ Layer 2: Use cache ✅ Layer 3: Use cache ✅ (requirements.txt didn\u0026#39;t change) Layer 4: Use cache ✅ (pip install skipped!) Layer 5: Rebuild (app.py changed) Total time: 5 seconds! 10.6 Optimizing Build Cache ❌ Bad (rebuilds everything on code change):\nFROM python:3.11-slim WORKDIR /app COPY . . # Copies everything RUN pip install -r req.txt # Runs every time code changes! CMD [\u0026#34;python\u0026#34;, \u0026#34;app.py\u0026#34;] ✅ Good (leverages cache):\nFROM python:3.11-slim WORKDIR /app COPY requirements.txt . # Copy deps first RUN pip install -r req.txt # Only runs if requirements.txt changes COPY . . # Copy code last CMD [\u0026#34;python\u0026#34;, \u0026#34;app.py\u0026#34;] Why this works:\nIf only code changes, layers 1-4 are cached Only layer 5 rebuilds Much faster! 10.7 Tagging Images Tag during build:\n# Latest tag (default) docker build -t myapp . docker build -t myapp:latest . # Specific version docker build -t myapp:v1.0 . docker build -t myapp:1.0.0 . # Multiple tags docker build -t myapp:v1.0 -t myapp:latest . # With registry docker build -t myregistry.com/myapp:v1.0 . Tag after build:\n# Tag existing image docker tag myapp:v1.0 myapp:latest docker tag myapp:v1.0 myregistry.com/myapp:v1.0 View all tags:\ndocker images myapp 10.8 Using Custom Dockerfile Name Sometimes you need multiple Dockerfiles:\nproject/ ├── Dockerfile # Production ├── Dockerfile.dev # Development └── Dockerfile.test # Testing Build with specific Dockerfile:\n# Use Dockerfile.dev docker build -f Dockerfile.dev -t myapp:dev . # Use Dockerfile.test docker build -f Dockerfile.test -t myapp:test . 10.9 Build Arguments Pass variables at build time:\nDockerfile:\nARG PYTHON_VERSION=3.11 FROM python:${PYTHON_VERSION}-slim ARG APP_ENV=production ENV APP_ENV=${APP_ENV} RUN echo \u0026#34;Building for ${APP_ENV}\u0026#34; Build:\n# Use defaults docker build -t myapp . # Override arguments docker build --build-arg PYTHON_VERSION=3.10 --build-arg APP_ENV=development -t myapp:dev . Common use cases:\n# Different base image versions docker build --build-arg NODE_VERSION=18 -t myapp . # Build date docker build --build-arg BUILD_DATE=$(date) -t myapp . # Git commit docker build --build-arg GIT_COMMIT=$(git rev-parse HEAD) -t myapp . 10.10 Build Without Cache Force rebuild everything:\ndocker build --no-cache -t myapp . When to use:\nAfter updating base image When cache seems corrupted To ensure clean build Pull latest base image before build:\ndocker build --pull -t myapp . 10.11 Viewing Build History See layers in image:\ndocker history myapp:v1 Output:\nIMAGE CREATED CREATED BY SIZE abc123... 2 hours ago CMD [\u0026#34;python\u0026#34; \u0026#34;app.py\u0026#34;] 0B def456... 2 hours ago COPY app.py . 1.5kB ghi789... 2 hours ago RUN pip install -r requirements.txt 50MB ... 10.12 Multi-Platform Builds Build for different architectures:\n# Build for AMD64 (x86_64) docker build --platform linux/amd64 -t myapp . # Build for ARM64 (Apple M1, Raspberry Pi) docker build --platform linux/arm64 -t myapp . # Build for both docker buildx build --platform linux/amd64,linux/arm64 -t myapp . 10.13 Practical Example: Node.js App Complete working example:\nProject structure:\nmy-node-app/ ├── .dockerignore ├── Dockerfile ├── package.json ├── package-lock.json └── server.js package.json:\n{ \u0026#34;name\u0026#34;: \u0026#34;my-app\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;1.0.0\u0026#34;, \u0026#34;dependencies\u0026#34;: { \u0026#34;express\u0026#34;: \u0026#34;^4.18.2\u0026#34; }, \u0026#34;scripts\u0026#34;: { \u0026#34;start\u0026#34;: \u0026#34;node server.js\u0026#34; } } server.js:\nconst express = require(\u0026#34;express\u0026#34;); const app = express(); app.get(\u0026#34;/\u0026#34;, (req, res) =\u0026gt; { res.send(\u0026#34;Hello from Dockerized Node.js!\u0026#34;); }); app.listen(3000, \u0026#34;0.0.0.0\u0026#34;, () =\u0026gt; { console.log(\u0026#34;Server running on port 3000\u0026#34;); }); Dockerfile:\n# Use Node.js 18 Alpine FROM node:18-alpine # Set working directory WORKDIR /app # Copy package files COPY package*.json ./ # Install dependencies RUN npm ci --only=production # Copy source code COPY . . # Expose port EXPOSE 3000 # Run as node user USER node # Start application CMD [\u0026#34;npm\u0026#34;, \u0026#34;start\u0026#34;] .dockerignore:\nnode_modules npm-debug.log .git .gitignore .env README.md Build:\ndocker build -t my-node-app:v1 . Run:\ndocker run -d -p 3000:3000 --name node_app my-node-app:v1 Test:\ncurl http://localhost:3000 # Output: Hello from Dockerized Node.js! Check logs:\ndocker logs node_app # Output: Server running on port 3000 10.14 Building Image Cheat Sheet # Basic build docker build -t myapp . docker build -t myapp:v1.0 . # Custom Dockerfile docker build -f Dockerfile.dev -t myapp:dev . # Multiple tags docker build -t myapp:v1 -t myapp:latest . # Build arguments docker build --build-arg VERSION=1.0 -t myapp . # No cache docker build --no-cache -t myapp . # Pull latest base image docker build --pull -t myapp . # Multi-platform docker buildx build --platform linux/amd64,linux/arm64 -t myapp . # View build history docker history myapp # Tag existing image docker tag myapp:v1 myapp:latest # Remove image docker rmi myapp:v1 # Remove unused images docker image prune docker image prune -a # Remove all unused 11. Dockerfile Best Practices 11.1 Use Specific Base Image Tags ❌ Bad:\nFROM python:latest FROM node:latest Problems:\n\u0026ldquo;latest\u0026rdquo; tag changes over time Builds not reproducible Might break unexpectedly ✅ Good:\nFROM python:3.11-slim FROM node:18-alpine FROM nginx:1.24 Benefits:\nPredictable builds Version control Easy rollback 11.2 Use Smaller Base Images Size comparison:\n# Ubuntu full (~77MB) FROM ubuntu:22.04 # Debian slim (~100MB) FROM debian:11-slim # Alpine (~7MB) - smallest! FROM alpine:3.17 # Language-specific FROM python:3.11 # ~900MB FROM python:3.11-slim # ~150MB FROM python:3.11-alpine # ~50MB Trade-offs:\nAlpine:\n✅ Smallest size ✅ Fast downloads ❌ Uses musl libc (some packages incompatible) ❌ Missing some tools Slim:\n✅ Good balance ✅ Compatible with most packages ✅ Smaller than full images When to use each:\nAlpine: Microservices, simple apps Slim: Most applications Full: When you need specific tools/packages 11.3 Minimize Layers ❌ Bad (too many layers):\nFROM ubuntu RUN apt update RUN apt install -y curl RUN apt install -y vim RUN apt install -y git RUN apt clean ✅ Good (fewer layers):\nFROM ubuntu RUN apt update \u0026amp;\u0026amp; \\ apt install -y curl vim git \u0026amp;\u0026amp; \\ apt clean \u0026amp;\u0026amp; \\ rm -rf /var/lib/apt/lists/* Why:\nEach RUN creates a layer Fewer layers = smaller image Faster builds 11.4 Optimize Layer Caching ❌ Bad (invalidates cache on any file change):\nFROM node:18-alpine WORKDIR /app COPY . . # Copies everything first RUN npm install # Runs on every change! CMD [\u0026#34;node\u0026#34;, \u0026#34;server.js\u0026#34;] ✅ Good (leverages cache):\nFROM node:18-alpine WORKDIR /app COPY package*.json ./ # Copy deps first RUN npm install # Cached unless deps change COPY . . # Copy code last CMD [\u0026#34;node\u0026#34;, \u0026#34;server.js\u0026#34;] Principle: Put things that change less frequently earlier in Dockerfile\n11.5 Use .dockerignore Create .dockerignore:\n# Dependencies (will be installed in image) node_modules __pycache__ venv/ .venv/ # Git .git .gitignore # IDE .vscode .idea *.swp # Logs *.log logs/ # Environment files .env .env.local .env.*.local # OS files .DS_Store Thumbs.db # Documentation README.md docs/ *.md # Tests tests/ **/*.test.js **/*.spec.js # Build artifacts dist/ build/ Benefits:\nSmaller build context Faster builds Don\u0026rsquo;t copy secrets Smaller images 11.6 Don\u0026rsquo;t Run as Root ❌ Bad (security risk):\nFROM ubuntu COPY app.py /app/ CMD [\u0026#34;python\u0026#34;, \u0026#34;/app/app.py\u0026#34;] # Runs as root! ✅ Good (run as non-root user):\nFROM ubuntu RUN useradd -m -u 1000 appuser WORKDIR /app COPY --chown=appuser:appuser app.py . USER appuser CMD [\u0026#34;python\u0026#34;, \u0026#34;app.py\u0026#34;] For images with existing users:\n# Node.js images have \u0026#39;node\u0026#39; user FROM node:18-alpine WORKDIR /app COPY --chown=node:node . . USER node CMD [\u0026#34;node\u0026#34;, \u0026#34;server.js\u0026#34;] # Python images don\u0026#39;t have default user, create one FROM python:3.11-slim RUN useradd -m appuser USER appuser WORKDIR /home/appuser CMD [\u0026#34;python\u0026#34;, \u0026#34;app.py\u0026#34;] 11.7 Use Multi-Stage Builds Purpose: Smaller final images by separating build and runtime\n❌ Without multi-stage (large image):\nFROM node:18 WORKDIR /app COPY package*.json ./ RUN npm install # Includes dev dependencies! COPY . . RUN npm run build CMD [\u0026#34;npm\u0026#34;, \u0026#34;start\u0026#34;] # Final image: 1.2GB (includes build tools, dev deps) ✅ With multi-stage (small image):\n# Build stage FROM node:18 AS builder WORKDIR /app COPY package*.json ./ RUN npm install # All dependencies COPY . . RUN npm run build # Production stage FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production # Only production deps COPY --from=builder /app/dist ./dist CMD [\u0026#34;npm\u0026#34;, \u0026#34;start\u0026#34;] # Final image: 200MB (no dev deps, no build tools) We\u0026rsquo;ll cover this in detail in Section 12!\n11.8 Combine Commands ❌ Bad:\nRUN apt update RUN apt install -y curl RUN curl -O https://example.com/file.tar.gz RUN tar xzf file.tar.gz RUN rm file.tar.gz ✅ Good:\nRUN apt update \u0026amp;\u0026amp; \\ apt install -y curl \u0026amp;\u0026amp; \\ curl -O https://example.com/file.tar.gz \u0026amp;\u0026amp; \\ tar xzf file.tar.gz \u0026amp;\u0026amp; \\ rm file.tar.gz \u0026amp;\u0026amp; \\ apt remove -y curl \u0026amp;\u0026amp; \\ apt autoremove -y \u0026amp;\u0026amp; \\ rm -rf /var/lib/apt/lists/* Benefits:\nSingle layer Cleanup in same layer Smaller image 11.9 Clean Up in Same Layer ❌ Bad (cache still in image):\nRUN apt update RUN apt install -y curl RUN rm -rf /var/lib/apt/lists/* # Too late! Previous layers still have cache ✅ Good (cleanup in same RUN):\nRUN apt update \u0026amp;\u0026amp; \\ apt install -y curl \u0026amp;\u0026amp; \\ rm -rf /var/lib/apt/lists/* Python example:\n# ❌ Bad RUN pip install -r requirements.txt RUN rm -rf ~/.cache/pip # ✅ Good RUN pip install --no-cache-dir -r requirements.txt Node.js example:\n# ✅ Use npm ci instead of install RUN npm ci --only=production \u0026amp;\u0026amp; \\ npm cache clean --force 11.10 Use Specific COPY ❌ Bad (copies everything):\nCOPY . . ✅ Good (copy only what\u0026rsquo;s needed):\nCOPY package*.json ./ COPY src/ ./src/ COPY public/ ./public/ Benefits:\nBetter caching Smaller images Clearer dependencies 11.11 Leverage Build Cache Order matters!\n# ✅ Optimal order FROM python:3.11-slim # 1. Things that change least WORKDIR /app # 2. Dependencies (change occasionally) COPY requirements.txt . RUN pip install -r requirements.txt # 3. Source code (changes frequently) COPY . . # 4. Metadata (doesn\u0026#39;t add layers) EXPOSE 8000 CMD [\u0026#34;python\u0026#34;, \u0026#34;app.py\u0026#34;] Rebuild simulation:\nChange 1: Only app.py changed Layer 1-4: Cached ✅ (fast!) Layer 5: Rebuild Total time: 5 seconds Change 2: Added new package Layer 1-3: Cached ✅ Layer 4: Rebuild (pip install) Layer 5: Rebuild Total time: 30 seconds Change 3: Changed base image All layers: Rebuild Total time: 2 minutes 11.12 Use HEALTHCHECK Add health check to monitor container:\nFROM nginx COPY index.html /usr/share/nginx/html/ HEALTHCHECK --interval=30s --timeout=3s --retries=3 \\ CMD curl -f http://localhost/ || exit 1 Parameters:\n--interval: How often to check (default: 30s) --timeout: How long to wait for response (default: 30s) --retries: How many failures before unhealthy (default: 3) --start-period: Grace period before checking (default: 0s) Examples:\n# HTTP health check HEALTHCHECK CMD curl -f http://localhost:3000/health || exit 1 # Database health check HEALTHCHECK CMD pg_isready -U postgres || exit 1 # Custom script HEALTHCHECK CMD /app/healthcheck.sh || exit 1 # No health check (disable inherited one) HEALTHCHECK NONE 11.13 Document with Labels Add metadata:\nLABEL org.opencontainers.image.authors=\u0026#34;you@example.com\u0026#34; LABEL org.opencontainers.image.version=\u0026#34;1.0.0\u0026#34; LABEL org.opencontainers.image.title=\u0026#34;My Application\u0026#34; LABEL org.opencontainers.image.description=\u0026#34;Description here\u0026#34; LABEL org.opencontainers.image.created=\u0026#34;2024-01-01\u0026#34; Or combined:\nLABEL org.opencontainers.image.authors=\u0026#34;you@example.com\u0026#34; \\ org.opencontainers.image.version=\u0026#34;1.0.0\u0026#34; \\ org.opencontainers.image.title=\u0026#34;My Application\u0026#34; 11.14 Complete Best Practice Example Production-ready Dockerfile:\n# Use specific version FROM python:3.11-slim # Add labels LABEL org.opencontainers.image.authors=\u0026#34;dev@example.com\u0026#34; LABEL org.opencontainers.image.version=\u0026#34;1.0.0\u0026#34; # Set working directory WORKDIR /app # Copy and install dependencies first (for caching) COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Copy source code COPY src/ ./src/ COPY config/ ./config/ # Create non-root user RUN useradd -m -u 1000 appuser \u0026amp;\u0026amp; \\ chown -R appuser:appuser /app # Switch to non-root user USER appuser # Expose port (documentation) EXPOSE 8000 # Add health check HEALTHCHECK --interval=30s --timeout=3s --retries=3 \\ CMD python -c \u0026#34;import requests; requests.get(\u0026#39;http://localhost:8000/health\u0026#39;)\u0026#34; || exit 1 # Set environment variable ENV PYTHONUNBUFFERED=1 # Start application CMD [\u0026#34;python\u0026#34;, \u0026#34;src/app.py\u0026#34;] 11.15 Best Practices Checklist ✅ Use specific base image tags (python:3.11-slim, not latest) ✅ Use smallest appropriate base image (alpine, slim) ✅ Minimize number of layers (combine RUN commands) ✅ Optimize layer caching (copy deps before code) ✅ Use .dockerignore ✅ Run as non-root user (USER directive) ✅ Use multi-stage builds (covered next section) ✅ Clean up in same layer ✅ Use specific COPY (not COPY . .) ✅ Add health checks ✅ Add labels for documentation ✅ Set WORKDIR explicitly ✅ Use ENV for configuration ✅ Expose ports for documentation ✅ One process per container 11.16 Common Mistakes to Avoid 1. Running as root:\n# ❌ Don\u0026#39;t do this FROM ubuntu CMD [\u0026#34;./app\u0026#34;] 2. Not using .dockerignore:\nSends 2GB node_modules to Docker daemon... 3. Installing unnecessary packages:\n# ❌ Don\u0026#39;t install what you don\u0026#39;t need RUN apt install -y vim git curl wget htop 4. Using latest tag:\n# ❌ Unpredictable FROM python:latest # ✅ Specific version FROM python:3.11-slim 5. Not cleaning up:\n# ❌ Cache remains in image RUN apt update \u0026amp;\u0026amp; apt install -y curl RUN rm -rf /var/lib/apt/lists/* # ✅ Cleanup in same layer RUN apt update \u0026amp;\u0026amp; apt install -y curl \u0026amp;\u0026amp; rm -rf /var/lib/apt/lists/* 12. Multi-Stage Builds 12.1 What are Multi-Stage Builds? Problem: Build process needs tools that runtime doesn\u0026rsquo;t need\nExample scenario:\nBuild: Need compilers, build tools, dev dependencies Runtime: Only need compiled app, runtime dependencies Without multi-stage:\nBuild tools + Dev dependencies + App + Runtime dependencies = Large image (1-2GB) With multi-stage:\nStage 1 (Build): Build tools + Dev deps + App Stage 2 (Runtime): Only App + Runtime deps = Small image (100-200MB) 12.2 Basic Multi-Stage Syntax # Stage 1: Build stage FROM node:18 AS builder # ... build steps ... # Stage 2: Production stage FROM node:18-alpine # Copy only what\u0026#39;s needed from builder COPY --from=builder /app/dist ./dist # ... runtime setup ... Key points:\nMultiple FROM statements Name stages with AS Copy between stages with \u0026ndash;from Only last stage becomes final image 12.3 Simple Example: Node.js App Single-stage (bad):\nFROM node:18 WORKDIR /app COPY package*.json ./ RUN npm install # Includes devDependencies! COPY . . RUN npm run build CMD [\u0026#34;npm\u0026#34;, \u0026#34;start\u0026#34;] # Result: 1.2GB image Multi-stage (good):\n# Build stage FROM node:18 AS builder WORKDIR /app COPY package*.json ./ RUN npm install # All dependencies for build COPY . . RUN npm run build # Creates /app/dist # Production stage FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production # Only production dependencies COPY --from=builder /app/dist ./dist EXPOSE 3000 CMD [\u0026#34;node\u0026#34;, \u0026#34;dist/server.js\u0026#34;] # Result: 200MB image Size comparison:\nSingle-stage: 1.2GB Multi-stage: 200MB 6x smaller! 12.4 Practical Example: React App Complete production-ready build:\n# Stage 1: Build React app FROM node:18 AS builder WORKDIR /app # Install dependencies COPY package*.json ./ RUN npm ci # Copy source and build COPY . . RUN npm run build # Creates /app/build with static files # Stage 2: Serve with Nginx FROM nginx:alpine # Copy built static files COPY --from=builder /app/build /usr/share/nginx/html # Copy custom nginx config (optional) COPY nginx.conf /etc/nginx/nginx.conf EXPOSE 80 CMD [\u0026#34;nginx\u0026#34;, \u0026#34;-g\u0026#34;, \u0026#34;daemon off;\u0026#34;] Build and run:\ndocker build -t my-react-app . docker run -d -p 80:80 my-react-app Result:\nBuild stage: 1GB+ (not in final image!) Final image: 25MB Just nginx + static files 12.5 Example: Python App with Compilation Scenario: Python app needs to compile C extensions\n# Stage 1: Build stage (with compilers) FROM python:3.11 AS builder WORKDIR /app # Install build dependencies RUN apt-get update \u0026amp;\u0026amp; \\ apt-get install -y gcc g++ \u0026amp;\u0026amp; \\ rm -rf /var/lib/apt/lists/* # Install Python packages (some need compilation) COPY requirements.txt . RUN pip install --user --no-cache-dir -r requirements.txt # Stage 2: Runtime (no compilers needed) FROM python:3.11-slim WORKDIR /app # Copy installed packages from builder COPY --from=builder /root/.local /root/.local # Copy application code COPY app.py . # Make sure scripts in .local are usable ENV PATH=/root/.local/bin:$PATH # Run as non-root user RUN useradd -m appuser USER appuser CMD [\u0026#34;python\u0026#34;, \u0026#34;app.py\u0026#34;] Benefits:\nBuild stage: Has gcc, g++ for compiling Final stage: Clean, no compilers Much smaller image 12.6 Example: Go Application Go is perfect for multi-stage builds:\n# Stage 1: Build Go binary FROM golang:1.21 AS builder WORKDIR /app # Copy go mod files COPY go.mod go.sum ./ RUN go mod download # Copy source code COPY . . # Build binary RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . # Stage 2: Minimal runtime FROM alpine:3.17 # Install ca-certificates for HTTPS RUN apk --no-cache add ca-certificates WORKDIR /root/ # Copy binary from builder COPY --from=builder /app/main . EXPOSE 8080 CMD [\u0026#34;./main\u0026#34;] Amazing result:\nBuild stage: 1GB+ Go toolchain Final image: 10-15MB! Just binary + Alpine Or even smaller with scratch:\n# ... builder stage same as above ... # Stage 2: Scratch (absolutely minimal!) FROM scratch COPY --from=builder /app/main . EXPOSE 8080 CMD [\u0026#34;./main\u0026#34;] Result: 5-7MB image! (just the binary)\n12.7 Copying from Multiple Stages You can copy from multiple named stages:\n# Frontend build FROM node:18 AS frontend-builder WORKDIR /app/frontend COPY frontend/package*.json ./ RUN npm ci COPY frontend/ . RUN npm run build # Backend build FROM golang:1.21 AS backend-builder WORKDIR /app/backend COPY backend/ . RUN go build -o server # Final stage: Combine both FROM alpine:3.17 WORKDIR /app # Copy frontend static files COPY --from=frontend-builder /app/frontend/dist ./static # Copy backend binary COPY --from=backend-builder /app/backend/server . EXPOSE 8080 CMD [\u0026#34;./server\u0026#34;] 12.8 Using External Images in Stages Copy from any image, not just build stages:\nFROM alpine:3.17 # Copy from official nginx image COPY --from=nginx:latest /etc/nginx/nginx.conf /etc/nginx/ # Copy from specific image version COPY --from=busybox:1.36 /bin/busybox /bin/ # Rest of your Dockerfile... 12.9 Development vs Production Builds Use build targets for different purposes:\n# Base stage (common for both) FROM node:18 AS base WORKDIR /app COPY package*.json ./ # Development stage FROM base AS development RUN npm install # All dependencies COPY . . CMD [\u0026#34;npm\u0026#34;, \u0026#34;run\u0026#34;, \u0026#34;dev\u0026#34;] # Build stage FROM base AS builder RUN npm ci COPY . . RUN npm run build # Production stage FROM node:18-alpine AS production WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY --from=builder /app/dist ./dist CMD [\u0026#34;node\u0026#34;, \u0026#34;dist/server.js\u0026#34;] Build specific stage:\n# Development docker build --target development -t myapp:dev . # Production docker build --target production -t myapp:prod . # Or just: docker build -t myapp:prod . # Builds to final stage by default 12.10 Real-World Full Example Production Next.js application:\n# Dependencies FROM node:18-alpine AS deps RUN apk add --no-cache libc6-compat WORKDIR /app COPY package*.json ./ RUN npm ci # Builder FROM node:18-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build # Runner FROM node:18-alpine AS runner WORKDIR /app ENV NODE_ENV production RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs # Copy necessary files COPY --from=builder /app/public ./public COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static USER nextjs EXPOSE 3000 ENV PORT 3000 CMD [\u0026#34;node\u0026#34;, \u0026#34;server.js\u0026#34;] 12.11 Debugging Multi-Stage Builds Build and inspect specific stage:\n# Build only up to builder stage docker build --target builder -t myapp:builder . # Run it to debug docker run -it myapp:builder sh # Inside container, check what was built ls -la View size of each stage:\n# Build all stages with tags docker build --target builder -t myapp:builder . docker build --target production -t myapp:production . # Compare sizes docker images | grep myapp 12.12 Multi-Stage Best Practices 1. Name your stages descriptively:\n# ✅ Good FROM node:18 AS dependencies FROM node:18 AS builder FROM node:18-alpine AS production # ❌ Bad FROM node:18 AS stage1 FROM node:18 AS stage2 2. Use smallest possible final image:\n# Builder: Can be large FROM node:18 AS builder # Final: As small as possible FROM node:18-alpine # Or even: FROM alpine:3.17 # Or: FROM scratch 3. Order stages efficiently:\n# Dependencies (changes rarely) first FROM node:18 AS deps COPY package*.json ./ RUN npm ci # Build (uses deps) FROM node:18 AS builder COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm build # Production (uses build output) FROM node:18-alpine AS production COPY --from=builder /app/dist ./dist 4. Don\u0026rsquo;t copy unnecessary files:\n# ❌ Bad - Copies everything from builder COPY --from=builder /app /app # ✅ Good - Copies only what\u0026#39;s needed COPY --from=builder /app/dist ./dist COPY --from=builder /app/package.json ./ 5. Use .dockerignore:\n# Prevents copying to builder stage node_modules .git tests/ 12.13 When to Use Multi-Stage ✅ Use multi-stage when:\nCompiled languages (Go, Rust, C++) Frontend builds (React, Vue, Angular) Apps needing build tools Want smallest possible image Separating build and runtime dependencies ❌ Don\u0026rsquo;t need multi-stage when:\nInterpreted languages with no build step Already using minimal image Simple scripts Size not a concern 12.14 Multi-Stage Cheat Sheet # Basic pattern FROM base:tag AS stage_name # Build steps FROM smaller:tag COPY --from=stage_name /path /path # Multiple stages FROM image1 AS stage1 # ... FROM image2 AS stage2 COPY --from=stage1 /path /path # ... FROM image3 COPY --from=stage1 /path1 /path1 COPY --from=stage2 /path2 /path2 # Build specific stage $ docker build --target stage_name -t image:tag . # Copy from external image COPY --from=nginx:latest /etc/nginx/nginx.conf /etc/nginx/ # Development vs Production FROM base AS development # dev stuff FROM base AS production # prod stuff $ docker build --target development -t app:dev . $ docker build --target production -t app:prod . Data Persistence, Docker Compose, Networking \u0026amp; Advanced Topics PART 4: DATA PERSISTENCE 13. Understanding Container Data 13.1 The Ephemeral Nature of Containers Key concept: By default, container data is TEMPORARY\nWhat this means:\n# Start a container docker run -d --name mydb postgres # Container creates data (inside container filesystem) # Database files, logs, etc. # Stop and remove container docker stop mydb docker rm mydb # ALL DATA IS GONE! ❌ Why this happens:\nContainers are designed to be disposable Container filesystem is isolated When container is deleted, its filesystem is deleted 13.2 Container Filesystem Layers How container filesystem works:\nImage Layers (Read-Only) ├── Layer 4: Your app ├── Layer 3: Dependencies ├── Layer 2: Runtime └── Layer 1: Base OS Container Layer (Read-Write) ← Your data goes here └── When container deleted, this layer is deleted! Example:\n# Start nginx container docker run -d --name web nginx # Create file inside container docker exec web touch /usr/share/nginx/html/test.html # File exists docker exec web ls /usr/share/nginx/html/test.html # Output: /usr/share/nginx/html/test.html # Remove container docker rm -f web # Start new container from same image docker run -d --name web nginx # File is GONE! docker exec web ls /usr/share/nginx/html/test.html # Output: No such file or directory 13.3 Three Ways to Persist Data Docker provides three mechanisms for data persistence:\n1. Volumes (Recommended)\nManaged by Docker Stored in Docker\u0026rsquo;s directory Persists after container deletion Can be shared between containers Best for databases, logs, etc. 2. Bind Mounts\nMount specific host directory into container Full control over location Good for development Source code, config files, etc. 3. tmpfs mounts (Memory only)\nStored in host memory Never written to disk Lost on container stop For sensitive temporary data 13.4 Visual Comparison VOLUMES (Docker-managed) Host: /var/lib/docker/volumes/my_volume Container: /data ✅ Persists after container deletion ✅ Docker manages storage ✅ Can share between containers BIND MOUNTS (Host path) Host: /home/user/myapp Container: /app ✅ Direct access from host ✅ You choose exact location ✅ Good for development TMPFS (Memory) Host: RAM Container: /tmp ✅ Fast (in memory) ✅ Never touches disk ❌ Lost when container stops 13.5 When to Use Each Use Volumes when:\nProduction databases Application data that must persist Sharing data between containers Backups and migrations You want Docker to manage it Use Bind Mounts when:\nDevelopment (live code editing) Sharing config files Logs accessible from host Full control over file location Use tmpfs when:\nSensitive temporary files Caching Session data Files that shouldn\u0026rsquo;t touch disk 14. Volumes 14.1 Creating Volumes Create volume explicitly:\n# Create named volume docker volume create my_volume # List volumes docker volume ls # Inspect volume docker volume inspect my_volume Output of inspect:\n[ { \u0026#34;CreatedAt\u0026#34;: \u0026#34;2024-01-01T10:00:00Z\u0026#34;, \u0026#34;Driver\u0026#34;: \u0026#34;local\u0026#34;, \u0026#34;Labels\u0026#34;: {}, \u0026#34;Mountpoint\u0026#34;: \u0026#34;/var/lib/docker/volumes/my_volume/_data\u0026#34;, \u0026#34;Name\u0026#34;: \u0026#34;my_volume\u0026#34;, \u0026#34;Options\u0026#34;: {}, \u0026#34;Scope\u0026#34;: \u0026#34;local\u0026#34; } ] 14.2 Using Volumes with Containers Mount volume to container:\n# Using -v flag docker run -d \\ --name mycontainer \\ -v my_volume:/data \\ ubuntu # Using --mount flag (more explicit, recommended) docker run -d \\ --name mycontainer \\ --mount source=my_volume,target=/data \\ ubuntu Format:\n-v volume_name:container_path --mount source=volume_name,target=container_path 14.3 Practical Example: PostgreSQL Database Problem: Database data disappears when container is removed\nSolution: Use volume for data persistence\n# Create volume for database data docker volume create postgres_data # Run PostgreSQL with volume docker run -d \\ --name postgres_db \\ -e POSTGRES_PASSWORD=secret \\ -v postgres_data:/var/lib/postgresql/data \\ -p 5432:5432 \\ postgres # Add some data docker exec -it postgres_db psql -U postgres -c \u0026#34;CREATE DATABASE myapp;\u0026#34; docker exec -it postgres_db psql -U postgres -c \u0026#34;CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(100));\u0026#34; # Stop and remove container docker stop postgres_db docker rm postgres_db # Create new container with same volume docker run -d \\ --name postgres_db_new \\ -e POSTGRES_PASSWORD=secret \\ -v postgres_data:/var/lib/postgresql/data \\ -p 5432:5432 \\ postgres # Data still exists! ✅ docker exec -it postgres_db_new psql -U postgres -c \u0026#34;\\l\u0026#34; # Shows: myapp database is still there! 14.4 Anonymous Volumes Created automatically without name:\n# Docker creates anonymous volume docker run -d -v /data nginx # List volumes - you\u0026#39;ll see random name docker volume ls # DRIVER VOLUME NAME # local abc123def456... Warning: Anonymous volumes are hard to manage and reuse!\nBetter: Always use named volumes\ndocker run -d -v my_data:/data nginx 14.5 Volume Drivers Default driver: local (host filesystem)\n# Explicitly specify driver docker volume create --driver local my_volume Other drivers (third-party):\nNFS - Network file system CIFS/SMB - Windows shares AWS EBS - Amazon cloud storage GlusterFS - Distributed storage Many more\u0026hellip; Example with NFS:\ndocker volume create --driver local \\ --opt type=nfs \\ --opt o=addr=192.168.1.100,rw \\ --opt device=:/path/to/share \\ nfs_volume 14.6 Sharing Volumes Between Containers Multiple containers can use same volume:\n# Create volume docker volume create shared_data # Container 1 writes data docker run -d \\ --name writer \\ -v shared_data:/data \\ ubuntu \\ bash -c \u0026#34;echo \u0026#39;Hello from writer\u0026#39; \u0026gt; /data/message.txt\u0026#34; # Container 2 reads data docker run --rm \\ -v shared_data:/data \\ ubuntu \\ cat /data/message.txt # Output: Hello from writer Real-world example: Web server + app server sharing uploads:\n# Create volume for uploads docker volume create uploads # App server (processes uploads) docker run -d \\ --name app \\ -v uploads:/app/uploads \\ my-app-server # Web server (serves uploads) docker run -d \\ --name web \\ -v uploads:/usr/share/nginx/html/uploads \\ nginx 14.7 Backing Up Volumes Method 1: Copy from running container:\n# Container using volume docker run -d --name db -v db_data:/var/lib/postgresql/data postgres # Create backup docker run --rm \\ -v db_data:/data \\ -v $(pwd):/backup \\ ubuntu \\ tar czf /backup/db_backup.tar.gz -C /data . # Backup file created in current directory: db_backup.tar.gz Method 2: Backup helper container:\n# Backup docker run --rm \\ -v my_volume:/source:ro \\ -v $(pwd):/backup \\ alpine \\ tar czf /backup/volume_backup.tar.gz -C /source . 14.8 Restoring Volumes From backup:\n# Create new volume docker volume create restored_volume # Restore data docker run --rm \\ -v restored_volume:/target \\ -v $(pwd):/backup \\ ubuntu \\ bash -c \u0026#34;cd /target \u0026amp;\u0026amp; tar xzf /backup/db_backup.tar.gz\u0026#34; # Use restored volume docker run -d \\ --name restored_db \\ -v restored_volume:/var/lib/postgresql/data \\ postgres 14.9 Copying Data Between Volumes # Create new volume docker volume create target_volume # Copy from source to target docker run --rm \\ -v source_volume:/source:ro \\ -v target_volume:/target \\ ubuntu \\ bash -c \u0026#34;cp -r /source/* /target/\u0026#34; 14.10 Removing Volumes Remove specific volume:\n# Stop containers using volume first docker stop mycontainer docker rm mycontainer # Remove volume docker volume rm my_volume Remove all unused volumes:\ndocker volume prune # With force (no confirmation) docker volume prune -f Warning: Removing volume deletes all data permanently!\n14.11 Inspecting Volume Data View files in volume (without container):\n# Method 1: Temporary container docker run --rm -it \\ -v my_volume:/data \\ ubuntu \\ ls -la /data # Method 2: Direct access (Linux only, requires root) sudo ls -la /var/lib/docker/volumes/my_volume/_data 14.12 Read-Only Volumes Mount volume as read-only:\n# Using -v with :ro docker run -d \\ -v my_volume:/data:ro \\ ubuntu # Using --mount docker run -d \\ --mount source=my_volume,target=/data,readonly \\ ubuntu # Container can read but not write Use case: Configuration files, shared read-only data\n14.13 Volume Cheat Sheet # Create volume docker volume create \u0026lt;name\u0026gt; # List volumes docker volume ls # Inspect volume docker volume inspect \u0026lt;name\u0026gt; # Remove volume docker volume rm \u0026lt;name\u0026gt; # Remove all unused volumes docker volume prune # Use volume in container (-v flag) docker run -v \u0026lt;volume\u0026gt;:\u0026lt;container_path\u0026gt; \u0026lt;image\u0026gt; docker run -v \u0026lt;volume\u0026gt;:\u0026lt;container_path\u0026gt;:ro \u0026lt;image\u0026gt; # Read-only # Use volume in container (--mount flag) docker run --mount source=\u0026lt;volume\u0026gt;,target=\u0026lt;path\u0026gt; \u0026lt;image\u0026gt; docker run --mount source=\u0026lt;volume\u0026gt;,target=\u0026lt;path\u0026gt;,readonly \u0026lt;image\u0026gt; # Backup volume docker run --rm \\ -v \u0026lt;volume\u0026gt;:/data \\ -v $(pwd):/backup \\ ubuntu tar czf /backup/backup.tar.gz -C /data . # Restore volume docker run --rm \\ -v \u0026lt;volume\u0026gt;:/data \\ -v $(pwd):/backup \\ ubuntu tar xzf /backup/backup.tar.gz -C /data 15. Bind Mounts 15.1 What are Bind Mounts? Definition: Mount a specific host directory/file into container\nKey difference from volumes:\nYou specify EXACT host path Not managed by Docker Direct access to host filesystem Syntax:\n# -v flag docker run -v /host/path:/container/path image # --mount flag (more explicit) docker run --mount type=bind,source=/host/path,target=/container/path image 15.2 Basic Bind Mount Example Mount current directory:\n# Create a simple file echo \u0026#34;Hello from host\u0026#34; \u0026gt; test.txt # Mount current directory to /data in container docker run --rm -it \\ -v $(pwd):/data \\ ubuntu \\ bash # Inside container cat /data/test.txt # Output: Hello from host # Create file inside container echo \u0026#34;Hello from container\u0026#34; \u0026gt; /data/from_container.txt exit # File appears on host! cat from_container.txt # Output: Hello from container 15.3 Development Workflow with Bind Mounts Live code editing without rebuilding:\nProject structure:\nmy-node-app/ ├── app.js ├── package.json └── Dockerfile app.js:\nconst express = require(\u0026#34;express\u0026#34;); const app = express(); app.get(\u0026#34;/\u0026#34;, (req, res) =\u0026gt; { res.send(\u0026#34;Hello World!\u0026#34;); }); app.listen(3000, () =\u0026gt; console.log(\u0026#34;Server running\u0026#34;)); Run with bind mount:\n# Mount source code directory docker run -d \\ --name dev_server \\ -p 3000:3000 \\ -v $(pwd):/app \\ node:18 \\ bash -c \u0026#34;cd /app \u0026amp;\u0026amp; npm install \u0026amp;\u0026amp; npm start\u0026#34; Now edit app.js on your host:\n// Change response res.send(\u0026#34;Hello Docker!\u0026#34;); Changes reflected immediately (if using nodemon/hot reload)\n15.4 Bind Mount with Docker Compose (Preview) docker-compose.yml:\nversion: \u0026#34;3\u0026#34; services: web: image: node:18 volumes: - ./src:/app # Bind mount working_dir: /app command: npm start ports: - \u0026#34;3000:3000\u0026#34; We\u0026rsquo;ll cover Docker Compose in detail in Part 5!\n15.5 Read-Only Bind Mounts Prevent container from modifying host files:\n# Read-only mount docker run --rm -it \\ -v $(pwd):/data:ro \\ ubuntu \\ bash # Inside container - can read but not write cat /data/test.txt # Works echo \u0026#34;test\u0026#34; \u0026gt; /data/new.txt # Permission denied! Use cases:\nConfiguration files Source code in production Shared read-only resources 15.6 Mounting Individual Files Mount single file instead of directory:\n# Mount specific file docker run --rm \\ -v $(pwd)/config.json:/app/config.json \\ my-app # Example: Custom nginx config docker run -d \\ -v $(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro \\ -p 80:80 \\ nginx 15.7 Bind Mount Permissions Problem: Permission mismatches between host and container\nExample:\n# On host (you are user 1000) echo \u0026#34;test\u0026#34; \u0026gt; test.txt ls -l test.txt # -rw-rw-r-- 1 user user 5 Jan 1 10:00 test.txt # In container (running as root by default) docker run --rm -v $(pwd):/data ubuntu ls -l /data/test.txt # -rw-rw-r-- 1 1000 1000 5 Jan 1 10:00 test.txt # Container sees file owned by UID 1000 Solution 1: Run container as your user:\ndocker run --rm \\ --user $(id -u):$(id -g) \\ -v $(pwd):/data \\ ubuntu \\ touch /data/newfile.txt # File created with your ownership ls -l newfile.txt # -rw-r--r-- 1 youruser yourgroup 0 Jan 1 10:00 newfile.txt Solution 2: Chown inside container:\n# In Dockerfile RUN useradd -u 1000 appuser USER appuser 15.8 Practical Examples Example 1: Development Environment\n# Web development with live reload docker run -d \\ --name web_dev \\ -p 3000:3000 \\ -v $(pwd):/app \\ -v /app/node_modules \\ # Prevent overwriting node_modules node:18 \\ bash -c \u0026#34;cd /app \u0026amp;\u0026amp; npm install \u0026amp;\u0026amp; npm run dev\u0026#34; Example 2: Database with Config File\n# Custom PostgreSQL config docker run -d \\ --name postgres_custom \\ -v postgres_data:/var/lib/postgresql/data \\ # Volume for data -v $(pwd)/postgresql.conf:/etc/postgresql/postgresql.conf:ro \\ # Bind mount for config -e POSTGRES_PASSWORD=secret \\ postgres \\ -c \u0026#39;config_file=/etc/postgresql/postgresql.conf\u0026#39; Example 3: Log Files\n# Access logs on host mkdir logs docker run -d \\ --name app \\ -v $(pwd)/logs:/var/log/app \\ my-app # Logs appear in ./logs/ on host tail -f logs/app.log 15.9 Bind Mounts vs Volumes Feature Bind Mounts Volumes Location You specify exact path Docker manages Portability Path-dependent Portable Performance Good Better (esp. on Mac/Windows) Docker management No Yes Sharing Via host path Via volume name Best for Development, configs Production data, databases 15.10 Common Bind Mount Patterns Pattern 1: Source code mounting:\n-v $(pwd)/src:/app/src Pattern 2: Config files:\n-v $(pwd)/config:/etc/config:ro Pattern 3: Log files:\n-v $(pwd)/logs:/var/log/app Pattern 4: Development with dependencies:\n-v $(pwd):/app -v /app/node_modules # Anonymous volume to preserve node_modules 15.11 Troubleshooting Bind Mounts Problem: Changes not reflected\nCheck:\nPath is correct File exists on host Application caching Need to restart process Problem: Permission denied\nSolutions:\n# Run as your user docker run --user $(id -u):$(id -g) ... # Make files readable/writable chmod -R 755 /path/to/mount Problem: Files created by container owned by root\nSolutions:\n# Run as non-root user docker run --user 1000:1000 ... # Or in Dockerfile USER appuser 15.12 Bind Mount Cheat Sheet # Basic bind mount -v /host/path:/container/path -v $(pwd):/app -v /absolute/path:/container/path # Read-only -v /host/path:/container/path:ro # Mount file (not directory) -v /host/file.txt:/container/file.txt # Using --mount (explicit) --mount type=bind,source=/host/path,target=/container/path --mount type=bind,source=/host/path,target=/container/path,readonly # Run as specific user (avoid permission issues) --user $(id -u):$(id -g) # Anonymous volume to protect directory -v /container/path # Not overwritten by bind mount # Current directory shortcuts -v $(pwd):/app # Linux/Mac -v %cd%:/app # Windows CMD -v ${PWD}:/app # Windows PowerShell 16. Volume Management 16.1 Volume Lifecycle Create → Use → Backup → Restore → Remove ↑ ↓ └──────────── Recreate ────────────┘ Full lifecycle example:\n# 1. Create docker volume create app_data # 2. Use docker run -d \\ --name app \\ -v app_data:/data \\ my-app # 3. Backup docker run --rm \\ -v app_data:/source \\ -v $(pwd):/backup \\ ubuntu tar czf /backup/backup.tar.gz -C /source . # 4. Stop app docker stop app docker rm app # 5. Remove volume docker volume rm app_data # 6. Restore docker volume create app_data_restored docker run --rm \\ -v app_data_restored:/target \\ -v $(pwd):/backup \\ ubuntu tar xzf /backup/backup.tar.gz -C /target # 7. Use restored data docker run -d \\ --name app_new \\ -v app_data_restored:/data \\ my-app 16.2 Listing and Filtering Volumes List all volumes:\ndocker volume ls Filter by dangling (unused):\ndocker volume ls -f dangling=true Filter by driver:\ndocker volume ls -f driver=local Filter by label:\ndocker volume ls -f label=project=myapp 16.3 Volume Labels Create volume with labels:\ndocker volume create \\ --label environment=production \\ --label project=myapp \\ --label backup=daily \\ prod_data Query by labels:\ndocker volume ls -f label=environment=production Use in scripts:\n# Backup all production volumes for vol in $(docker volume ls -q -f label=backup=daily); do docker run --rm \\ -v $vol:/source \\ -v $(pwd)/backups:/backup \\ ubuntu tar czf /backup/${vol}.tar.gz -C /source . done 16.4 Volume Capacity and Quotas Docker doesn\u0026rsquo;t enforce volume size limits by default\nCheck volume size:\n# Method 1: Inspect volume path docker volume inspect my_volume --format \u0026#39;{{.Mountpoint}}\u0026#39; sudo du -sh /var/lib/docker/volumes/my_volume/_data # Method 2: Via container docker run --rm \\ -v my_volume:/data \\ ubuntu \\ du -sh /data Implement soft limits (application-level):\n# Monitor volume size script #!/bin/bash MAX_SIZE=10G # 10 GB limit VOLUME=my_volume SIZE=$(docker run --rm -v $VOLUME:/data ubuntu du -s /data | awk \u0026#39;{print $1}\u0026#39;) if [ $SIZE -gt $((10*1024*1024)) ]; then echo \u0026#34;Warning: Volume exceeds limit!\u0026#34; # Send alert, cleanup, etc. fi 16.5 Migrating Volumes Between containers on same host:\n# Simply use same volume with new container docker run -d --name new_app -v old_volume:/data new_image Between Docker hosts:\n# On source host: Export docker run --rm \\ -v source_volume:/data \\ -v $(pwd):/backup \\ ubuntu tar czf /backup/volume.tar.gz -C /data . # Transfer file to new host scp volume.tar.gz user@newhost:/tmp/ # On destination host: Import docker volume create dest_volume docker run --rm \\ -v dest_volume:/data \\ -v /tmp:/backup \\ ubuntu tar xzf /backup/volume.tar.gz -C /data 16.6 Volume Monitoring Check disk space usage:\ndocker system df -v Output:\nVOLUME NAME LINKS SIZE postgres_data 1 400MB app_logs 0 2.5GB redis_data 1 150MB Monitor specific volume:\n# Watch volume size watch -n 5 \u0026#39;docker run --rm -v my_volume:/data ubuntu du -sh /data\u0026#39; 16.7 Volume Best Practices 1. Name your volumes:\n# ❌ Bad docker run -v /data myapp # ✅ Good docker volume create app_data docker run -v app_data:/data myapp 2. Use labels for organization:\ndocker volume create \\ --label env=prod \\ --label app=web \\ --label tier=database \\ prod_web_db 3. Regular backups:\n# Daily backup script #!/bin/bash DATE=$(date +%Y%m%d) VOLUMES=\u0026#34;db_data app_data config_data\u0026#34; for vol in $VOLUMES; do docker run --rm \\ -v $vol:/source \\ -v /backups:/backup \\ ubuntu tar czf /backup/${vol}_${DATE}.tar.gz -C /source . done # Keep only last 7 days find /backups -name \u0026#34;*.tar.gz\u0026#34; -mtime +7 -delete 4. Document volume requirements:\n# In docker-compose.yml or README volumes: postgres_data: # Database storage - DO NOT DELETE uploads: # User uploaded files logs: # Application logs - safe to delete 5. Cleanup unused volumes:\n# Weekly cleanup docker volume prune -f 6. Separate data by type:\n# Instead of single volume docker volume create app_data # Separate by purpose docker volume create app_database docker volume create app_uploads docker volume create app_logs 16.8 Volume Troubleshooting Problem: Volume not mounting\nCheck:\n# Verify volume exists docker volume ls | grep my_volume # Inspect volume docker volume inspect my_volume # Check container mount docker inspect container_name --format \u0026#39;{{.Mounts}}\u0026#39; Problem: Permission denied in volume\nSolutions:\n# 1. Run container as correct user docker run --user 1000:1000 -v my_volume:/data myapp # 2. Fix permissions (dangerous - be careful!) docker run --rm -v my_volume:/data ubuntu chown -R 1000:1000 /data Problem: Volume is full\nSolutions:\n# 1. Check what\u0026#39;s using space docker run --rm -v my_volume:/data ubuntu du -h /data | sort -h # 2. Clean up files docker run --rm -v my_volume:/data ubuntu \\ find /data -name \u0026#34;*.log\u0026#34; -mtime +30 -delete # 3. Expand underlying storage (host-specific) 16.9 Volume Management Cheat Sheet # Lifecycle docker volume create \u0026lt;name\u0026gt; docker volume ls docker volume inspect \u0026lt;name\u0026gt; docker volume rm \u0026lt;name\u0026gt; docker volume prune # Remove unused # With labels docker volume create --label key=value \u0026lt;name\u0026gt; docker volume ls -f label=key=value # Backup docker run --rm \\ -v \u0026lt;volume\u0026gt;:/source:ro \\ -v $(pwd):/backup \\ ubuntu tar czf /backup/backup.tar.gz -C /source . # Restore docker run --rm \\ -v \u0026lt;volume\u0026gt;:/target \\ -v $(pwd):/backup \\ ubuntu tar xzf /backup/backup.tar.gz -C /target # Size check docker system df -v docker run --rm -v \u0026lt;volume\u0026gt;:/data ubuntu du -sh /data # Copy between volumes docker run --rm \\ -v source_vol:/source:ro \\ -v dest_vol:/dest \\ ubuntu cp -a /source/. /dest/ # Inspect mount location docker volume inspect \u0026lt;name\u0026gt; --format \u0026#39;{{.Mountpoint}}\u0026#39; PART 5: DOCKER COMPOSE 17. Introduction to Docker Compose 17.1 What is Docker Compose? Problem without Compose:\n# Need to run multiple containers docker network create myapp_network docker run -d \\ --name database \\ --network myapp_network \\ -v db_data:/var/lib/postgresql/data \\ -e POSTGRES_PASSWORD=secret \\ postgres docker run -d \\ --name redis \\ --network myapp_network \\ redis docker run -d \\ --name backend \\ --network myapp_network \\ -p 3000:3000 \\ -e DATABASE_URL=postgresql://postgres:secret@database/myapp \\ -e REDIS_URL=redis://redis:6379 \\ my-backend docker run -d \\ --name frontend \\ --network myapp_network \\ -p 80:80 \\ my-frontend # Too many commands! Hard to manage! 😫 With Docker Compose:\nCreate one file docker-compose.yml:\nversion: \u0026#34;3.8\u0026#34; services: database: image: postgres volumes: - db_data:/var/lib/postgresql/data environment: POSTGRES_PASSWORD: secret redis: image: redis backend: image: my-backend ports: - \u0026#34;3000:3000\u0026#34; environment: DATABASE_URL: postgresql://postgres:secret@database/myapp REDIS_URL: redis://redis:6379 depends_on: - database - redis frontend: image: my-frontend ports: - \u0026#34;80:80\u0026#34; volumes: db_data: # One command to start everything! docker compose up -d # One command to stop everything! docker compose down Benefits:\nSingle configuration file Easy to version control Reproducible environments Simple commands Define relationships between services 17.2 Docker Compose vs Dockerfile They serve different purposes:\nDockerfile:\nBuilds a single image Defines what goes INTO a container Like a recipe for one dish Docker Compose:\nRuns multiple containers Defines how containers work TOGETHER Like a meal plan (multiple dishes) You use both together:\nDockerfile (my-app/) ├── Build image for your app │ docker-compose.yml ├── Use that image └── Plus other services (database, redis, etc.) 17.3 Installing Docker Compose Included with Docker Desktop (Windows/Mac)\nLinux install:\n# Download sudo curl -L \u0026#34;https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)\u0026#34; -o /usr/local/bin/docker-compose # Make executable sudo chmod +x /usr/local/bin/docker-compose # Verify docker-compose --version Note: Modern Docker includes docker compose (space, not hyphen)\n# Old: docker-compose docker-compose up # New: docker compose docker compose up Both work, but docker compose is the newer CLI.\n17.4 Your First docker-compose.yml Simple web server:\nversion: \u0026#34;3.8\u0026#34; services: web: image: nginx ports: - \u0026#34;8080:80\u0026#34; Run it:\n# Start docker compose up # Or in background docker compose up -d # Stop docker compose down Access: http://localhost:8080\n17.5 Basic Compose Commands # Start services docker compose up # Foreground docker compose up -d # Background (detached) # Stop services docker compose down # Stop and remove containers docker compose stop # Stop but keep containers # View running services docker compose ps # View logs docker compose logs # All services docker compose logs web # Specific service docker compose logs -f # Follow logs # Execute command in service docker compose exec web bash # Restart services docker compose restart docker compose restart web # Specific service # Build images docker compose build docker compose up --build # Build then start 18. Docker Compose Syntax 18.1 YAML Basics YAML = \u0026ldquo;YAML Ain\u0026rsquo;t Markup Language\u0026rdquo;\nKey rules:\nIndentation matters (use spaces, not tabs!) Colons separate key-value pairs Dashes create lists Comments start with # Basic syntax:\n# Comment key: value string: \u0026#34;quoted value\u0026#34; number: 42 boolean: true nested: key: value another: value list: - item1 - item2 - item3 # Or inline list2: [item1, item2, item3] 18.2 Version version: \u0026#34;3.8\u0026#34; # Compose file format version Common versions:\n3.8 (recommended - latest) 3.7, 3.6, etc. 2.x (older) Usually just use 3.8:\nversion: \u0026#34;3.8\u0026#34; 18.3 Services Services = containers you want to run\nBasic service:\nservices: myservice: image: nginx Multiple services:\nservices: web: image: nginx database: image: postgres cache: image: redis 18.4 Image Use existing image from Docker Hub:\nservices: web: image: nginx:1.24 # Specific version db: image: postgres:15-alpine cache: image: redis:7 18.5 Build Build from Dockerfile:\nservices: app: build: . # Use Dockerfile in current directory Specify Dockerfile location:\nservices: app: build: context: . # Build context dockerfile: Dockerfile # Dockerfile name With build arguments:\nservices: app: build: context: . args: NODE_VERSION: 18 BUILD_ENV: production Complete example:\nservices: frontend: build: context: ./frontend dockerfile: Dockerfile.prod args: API_URL: http://localhost:3000 ports: - \u0026#34;80:80\u0026#34; 18.6 Ports Publish ports to host:\nservices: web: image: nginx ports: - \u0026#34;8080:80\u0026#34; # host:container - \u0026#34;443:443\u0026#34; # Multiple ports Syntax variations:\nports: - \u0026#34;8080:80\u0026#34; # HOST:CONTAINER - \u0026#34;3000:3000\u0026#34; # Same port both sides - \u0026#34;127.0.0.1:8080:80\u0026#34; # Bind to specific interface - \u0026#34;8080-8085:8080-8085\u0026#34; # Port range 18.7 Volumes Named volumes:\nservices: db: image: postgres volumes: - db_data:/var/lib/postgresql/data volumes: db_data: # Declare named volume Bind mounts:\nservices: app: image: node:18 volumes: - ./src:/app/src # Bind mount (relative path) - /absolute/path:/app/data # Absolute path Multiple volumes:\nservices: app: image: myapp volumes: - app_data:/data # Named volume - ./config:/app/config:ro # Bind mount (read-only) - logs:/var/log # Another named volume volumes: app_data: logs: 18.8 Environment Variables Direct assignment:\nservices: app: image: myapp environment: NODE_ENV: production DATABASE_URL: postgres://db/myapp API_KEY: abc123 List format:\nenvironment: - NODE_ENV=production - DATABASE_URL=postgres://db/myapp From .env file:\nCreate .env file:\nNODE_ENV=production DATABASE_URL=postgres://db/myapp API_KEY=secret123 Reference in compose:\nservices: app: image: myapp env_file: - .env Mix both:\nservices: app: image: myapp env_file: - .env environment: OVERRIDE_VAR: value # Overrides .env if exists 18.9 Depends On Define startup order:\nservices: web: image: nginx depends_on: - app app: image: myapp depends_on: - database - redis database: image: postgres redis: image: redis Startup order: database \u0026amp; redis → app → web\nImportant: depends_on only waits for container to START, not for service to be READY!\nBetter: Use healthchecks (covered later)\n18.10 Networks Default: All services can talk to each other\nCustom networks:\nservices: frontend: image: my-frontend networks: - frontend-net backend: image: my-backend networks: - frontend-net - backend-net database: image: postgres networks: - backend-net networks: frontend-net: backend-net: Result:\nFrontend can talk to Backend ✅ Backend can talk to Database ✅ Frontend CANNOT talk to Database ❌ (isolated) 18.11 Container Name Set specific container name:\nservices: web: image: nginx container_name: my_web_server # Instead of auto-generated name Default: project_service_1 (e.g., myapp_web_1)\nWith container_name: my_web_server\n18.12 Restart Policy services: app: image: myapp restart: always # Always restart Options:\nno - Never restart (default) always - Always restart on-failure - Restart if exit code != 0 unless-stopped - Restart unless manually stopped 18.13 Command Override default command:\nservices: app: image: ubuntu command: sleep infinity # Keep container running python: image: python:3.11 command: python app.py Multiple commands:\nservices: app: image: node:18 command: sh -c \u0026#34;npm install \u0026amp;\u0026amp; npm start\u0026#34; 18.14 Complete docker-compose.yml Example version: \u0026#34;3.8\u0026#34; services: # Frontend frontend: build: context: ./frontend dockerfile: Dockerfile ports: - \u0026#34;80:3000\u0026#34; environment: - REACT_APP_API_URL=http://localhost:3000 depends_on: - backend networks: - app-network # Backend API backend: build: context: ./backend ports: - \u0026#34;3000:3000\u0026#34; environment: NODE_ENV: production DATABASE_URL: postgres://postgres:secret@database:5432/myapp REDIS_URL: redis://cache:6379 depends_on: - database - cache volumes: - ./backend/uploads:/app/uploads restart: unless-stopped networks: - app-network # Database database: image: postgres:15-alpine environment: POSTGRES_DB: myapp POSTGRES_USER: postgres POSTGRES_PASSWORD: secret volumes: - postgres_data:/var/lib/postgresql/data networks: - app-network # Redis Cache cache: image: redis:7-alpine networks: - app-network volumes: postgres_data: networks: app-network: driver: bridge 18.15 Compose File Reference version: \u0026#34;3.8\u0026#34; services: service_name: # Image image: image:tag # Or build build: context: ./path dockerfile: Dockerfile args: KEY: value # Ports ports: - \u0026#34;HOST:CONTAINER\u0026#34; # Volumes volumes: - volume_name:/container/path - ./host/path:/container/path # Environment environment: KEY: value env_file: - .env # Dependencies depends_on: - other_service # Networks networks: - network_name # Container settings container_name: custom_name restart: always command: custom command # Health check healthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;curl\u0026#34;, \u0026#34;-f\u0026#34;, \u0026#34;http://localhost\u0026#34;] interval: 30s timeout: 3s retries: 3 # Declare volumes volumes: volume_name: # Declare networks networks: network_name: 19. Multi-Container Applications 19.1 Full-Stack Application Example Project structure:\nmyapp/ ├── docker-compose.yml ├── frontend/ │ ├── Dockerfile │ ├── package.json │ └── src/ ├── backend/ │ ├── Dockerfile │ ├── package.json │ └── src/ └── .env docker-compose.yml:\nversion: \u0026#34;3.8\u0026#34; services: # PostgreSQL Database database: image: postgres:15-alpine environment: POSTGRES_DB: ${DB_NAME:-myapp} POSTGRES_USER: ${DB_USER:-postgres} POSTGRES_PASSWORD: ${DB_PASSWORD:?Database password required} volumes: - postgres_data:/var/lib/postgresql/data - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro healthcheck: test: [\u0026#34;CMD-SHELL\u0026#34;, \u0026#34;pg_isready -U postgres\u0026#34;] interval: 10s timeout: 5s retries: 5 networks: - backend # Redis Cache redis: image: redis:7-alpine command: redis-server --appendonly yes volumes: - redis_data:/data networks: - backend # Backend API backend: build: context: ./backend args: NODE_VERSION: 18 ports: - \u0026#34;3000:3000\u0026#34; environment: NODE_ENV: production DATABASE_URL: postgres://${DB_USER:-postgres}:${DB_PASSWORD}@database:5432/${DB_NAME:-myapp} REDIS_URL: redis://redis:6379 JWT_SECRET: ${JWT_SECRET:?JWT secret required} depends_on: database: condition: service_healthy redis: condition: service_started volumes: - ./backend/uploads:/app/uploads restart: unless-stopped networks: - frontend - backend # Frontend frontend: build: context: ./frontend ports: - \u0026#34;80:80\u0026#34; environment: VITE_API_URL: http://localhost:3000 depends_on: - backend networks: - frontend volumes: postgres_data: redis_data: networks: frontend: driver: bridge backend: driver: bridge .env file:\n# Database DB_NAME=myapp DB_USER=postgres DB_PASSWORD=supersecret # Security JWT_SECRET=your-secret-key-here Commands:\n# Start everything docker compose up -d # View logs docker compose logs -f # Check status docker compose ps # Stop everything docker compose down # Stop and remove volumes (DELETES DATA!) docker compose down -v 19.2 Development vs Production Use different compose files:\ndocker-compose.yml (base config):\nversion: \u0026#34;3.8\u0026#34; services: backend: build: context: ./backend environment: DATABASE_URL: postgres://postgres:secret@database/myapp depends_on: - database database: image: postgres:15 docker-compose.override.yml (development - loaded automatically):\nversion: \u0026#34;3.8\u0026#34; services: backend: volumes: - ./backend:/app # Live code reload ports: - \u0026#34;3000:3000\u0026#34; environment: NODE_ENV: development command: npm run dev docker-compose.prod.yml (production):\nversion: \u0026#34;3.8\u0026#34; services: backend: restart: always environment: NODE_ENV: production command: npm start Usage:\n# Development (uses override automatically) docker compose up # Production docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d 19.3 Scaling Services Run multiple instances:\n# Run 3 instances of web service docker compose up -d --scale web=3 In compose file:\nservices: web: image: nginx # Don\u0026#39;t specify ports, or use port range loadbalancer: image: nginx ports: - \u0026#34;80:80\u0026#34; volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro Load balancer config (nginx.conf):\nupstream backend { server web:80; } server { listen 80; location / { proxy_pass http://backend; } } 19.4 Health Checks in Compose Define health checks:\nservices: database: image: postgres:15 healthcheck: test: [\u0026#34;CMD-SHELL\u0026#34;, \u0026#34;pg_isready -U postgres\u0026#34;] interval: 10s timeout: 5s retries: 5 start_period: 10s backend: image: myapp healthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;curl\u0026#34;, \u0026#34;-f\u0026#34;, \u0026#34;http://localhost:3000/health\u0026#34;] interval: 30s timeout: 3s retries: 3 depends_on: database: condition: service_healthy # Wait until healthy! Health check commands by service:\n# PostgreSQL test: [\u0026#34;CMD-SHELL\u0026#34;, \u0026#34;pg_isready -U postgres\u0026#34;] # MySQL test: [\u0026#34;CMD\u0026#34;, \u0026#34;mysqladmin\u0026#34;, \u0026#34;ping\u0026#34;, \u0026#34;-h\u0026#34;, \u0026#34;localhost\u0026#34;] # Redis test: [\u0026#34;CMD\u0026#34;, \u0026#34;redis-cli\u0026#34;, \u0026#34;ping\u0026#34;] # HTTP service test: [\u0026#34;CMD\u0026#34;, \u0026#34;curl\u0026#34;, \u0026#34;-f\u0026#34;, \u0026#34;http://localhost:3000/health\u0026#34;] test: [\u0026#34;CMD\u0026#34;, \u0026#34;wget\u0026#34;, \u0026#34;--no-verbose\u0026#34;, \u0026#34;--tries=1\u0026#34;, \u0026#34;--spider\u0026#34;, \u0026#34;http://localhost:3000/health\u0026#34;] # Custom script test: [\u0026#34;CMD\u0026#34;, \u0026#34;/app/healthcheck.sh\u0026#34;] 19.5 Resource Limits Limit CPU and memory:\nservices: backend: image: myapp deploy: resources: limits: cpus: \u0026#34;0.50\u0026#34; # 50% of one CPU memory: 512M reservations: cpus: \u0026#34;0.25\u0026#34; memory: 256M Note: deploy section works with Docker Swarm. For docker-compose, use runtime flags:\ndocker run --memory=\u0026#34;512m\u0026#34; --cpus=\u0026#34;0.5\u0026#34; myapp Or use v2 syntax:\nservices: backend: image: myapp mem_limit: 512m cpus: 0.5 19.6 Practical Example: WordPress Site Complete WordPress with MySQL:\nversion: \u0026#34;3.8\u0026#34; services: wordpress: image: wordpress:latest ports: - \u0026#34;8080:80\u0026#34; environment: WORDPRESS_DB_HOST: database WORDPRESS_DB_USER: wordpress WORDPRESS_DB_PASSWORD: secret WORDPRESS_DB_NAME: wordpress volumes: - wordpress_data:/var/www/html depends_on: database: condition: service_healthy restart: unless-stopped database: image: mysql:8 environment: MYSQL_DATABASE: wordpress MYSQL_USER: wordpress MYSQL_PASSWORD: secret MYSQL_ROOT_PASSWORD: rootsecret volumes: - db_data:/var/lib/mysql healthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;mysqladmin\u0026#34;, \u0026#34;ping\u0026#34;, \u0026#34;-h\u0026#34;, \u0026#34;localhost\u0026#34;] interval: 10s timeout: 5s retries: 5 restart: unless-stopped volumes: wordpress_data: db_data: Start:\ndocker compose up -d Access: http://localhost:8080\nStop:\ndocker compose down Keep data but stop:\ndocker compose stop Remove everything including data:\ndocker compose down -v 19.7 Compose Command Cheat Sheet # Start services docker compose up # Foreground docker compose up -d # Detached (background) docker compose up --build # Rebuild images then start docker compose up --force-recreate # Recreate containers # Stop services docker compose down # Stop and remove containers docker compose down -v # Also remove volumes docker compose stop # Stop but keep containers docker compose start # Start stopped containers # Service management docker compose ps # List containers docker compose ps -a # All containers docker compose logs # View logs docker compose logs -f service # Follow specific service logs docker compose exec service bash # Execute command docker compose restart # Restart all docker compose restart service # Restart specific service # Building docker compose build # Build all images docker compose build service # Build specific service docker compose pull # Pull latest images # Scaling docker compose up -d --scale web=3 # Run 3 instances of web # Config validation docker compose config # Validate and view config docker compose config --services # List services # Cleanup docker compose rm # Remove stopped containers docker compose down --rmi all # Remove containers and images PART 6: NETWORKING 21. Docker Networks Deep Dive 21.1 Container Communication Basics By default, containers are isolated:\nContainer A Container B ❌ ←──────→ ❌ Can\u0026#39;t communicate! With networking:\nContainer A ←─ Network ─→ Container B ✅ ✅ ✅ Can communicate! 21.2 DNS Resolution Containers can reach each other by name:\nservices: web: image: nginx app: image: myapp # Can connect to web using hostname \u0026#34;web\u0026#34; environment: API_URL: http://web:80 Inside app container:\ncurl http://web # Works! Docker\u0026#39;s DNS resolves \u0026#34;web\u0026#34; to web container\u0026#39;s IP 21.3 Default Bridge Network When you run container without specifying network:\ndocker run -d --name web nginx # Uses default bridge network Limitations:\nContainers can communicate via IP only No automatic DNS resolution Not recommended for production 21.4 User-Defined Networks Better approach:\n# Create network docker network create myapp-network # Run containers on this network docker run -d --name web --network myapp-network nginx docker run -d --name app --network myapp-network myapp # Containers can reach each other by name! docker exec app curl http://web 22. Network Types 22.1 Bridge Network (Default) For standalone containers on single host\nCreate:\ndocker network create my-bridge Use:\ndocker run -d --name web --network my-bridge nginx In Compose:\nservices: web: image: nginx networks: - my-network networks: my-network: driver: bridge 22.2 Host Network Container uses host\u0026rsquo;s network directly (no isolation)\ndocker run -d --network host nginx # Container\u0026#39;s port 80 = host\u0026#39;s port 80 (no port mapping needed!) Use case:\nPerformance-critical applications Need to bind to specific host interfaces Limitations:\nLess isolated Can\u0026rsquo;t run multiple containers on same port Linux only 22.3 None Network No networking at all\ndocker run -d --network none ubuntu # Container has no network access Use case:\nMaximum isolation Containers that don\u0026rsquo;t need network 22.4 Overlay Network For multi-host networking (Docker Swarm)\ndocker network create --driver overlay my-overlay Use case:\nDistributed applications Multiple Docker hosts Docker Swarm or Kubernetes We won\u0026rsquo;t cover this in detail (advanced topic)\n23. Container Communication 23.1 Same Network Communication Containers on same network can talk:\n# Create network docker network create myapp # Start database docker run -d --name db --network myapp postgres # Start app (can connect to \u0026#34;db\u0026#34;) docker run -d --name app --network myapp \\ -e DATABASE_URL=postgres://db/myapp \\ myapp Inside app container:\ndocker exec app ping db # Works! DNS resolves \u0026#34;db\u0026#34; to database container IP 23.2 Different Network Isolation Containers on different networks can\u0026rsquo;t communicate:\n# Create two networks docker network create frontend docker network create backend # Frontend container docker run -d --name web --network frontend nginx # Backend container docker run -d --name api --network backend myapi # Can\u0026#39;t communicate! docker exec web ping api # ping: api: Name or service not known Connect to multiple networks:\n# App needs to talk to both frontend and backend docker run -d --name app \\ --network frontend \\ myapp # Connect to second network docker network connect backend app # Now app can talk to both! 23.3 Connecting Existing Container # Add container to network docker network connect myapp mycontainer # Remove from network docker network disconnect myapp mycontainer 23.4 Publishing Ports Port publishing makes container accessible from host:\nHost Container (isolated network) Port 8080 ←─────→ Port 80 published Without port publishing:\ndocker run -d --name web nginx # Can only access from other containers on same network With port publishing:\ndocker run -d --name web -p 8080:80 nginx # Accessible from host at localhost:8080 24. Custom Networks 24.1 Creating Custom Networks Create bridge network:\ndocker network create --driver bridge myapp-network With specific subnet:\ndocker network create \\ --driver bridge \\ --subnet 172.20.0.0/16 \\ --gateway 172.20.0.1 \\ myapp-network With labels:\ndocker network create \\ --label project=myapp \\ --label environment=production \\ myapp-prod-network 24.2 Network in Docker Compose Basic:\nservices: web: image: nginx networks: - frontend app: image: myapp networks: - frontend - backend database: image: postgres networks: - backend networks: frontend: backend: Result:\nweb ↔ app ✅ app ↔ database ✅ web ↔ database ❌ (isolated!) Custom configuration:\nnetworks: frontend: driver: bridge ipam: config: - subnet: 172.20.0.0/16 backend: driver: bridge internal: true # No external access 24.3 Network Inspection # List networks docker network ls # Inspect network docker network inspect myapp-network # See which containers are on network docker network inspect myapp-network --format \u0026#39;{{json .Containers}}\u0026#39; 24.4 Practical Example: 3-Tier Architecture version: \u0026#34;3.8\u0026#34; services: # Frontend (Public-facing) frontend: image: nginx ports: - \u0026#34;80:80\u0026#34; networks: - frontend-tier # Application (Middle tier) backend: image: myapp networks: - frontend-tier - backend-tier environment: DATABASE_URL: postgres://database/myapp # Database (Private) database: image: postgres networks: - backend-tier volumes: - db_data:/var/lib/postgresql/data networks: frontend-tier: driver: bridge backend-tier: driver: bridge internal: true # No external access! volumes: db_data: Network isolation:\nfrontend → backend ✅ backend → database ✅ frontend → database ❌ database → internet ❌ (internal network) 24.5 Network Cleanup # Remove network docker network rm myapp-network # Remove all unused networks docker network prune # Force remove (disconnect containers first) docker network rm -f myapp-network 24.6 Network Cheat Sheet # List networks docker network ls # Create network docker network create \u0026lt;name\u0026gt; docker network create --driver bridge \u0026lt;name\u0026gt; docker network create --subnet 172.20.0.0/16 \u0026lt;name\u0026gt; # Inspect network docker network inspect \u0026lt;name\u0026gt; # Connect container to network docker network connect \u0026lt;network\u0026gt; \u0026lt;container\u0026gt; # Disconnect container docker network disconnect \u0026lt;network\u0026gt; \u0026lt;container\u0026gt; # Remove network docker network rm \u0026lt;name\u0026gt; # Remove unused networks docker network prune # Run container on network docker run --network \u0026lt;name\u0026gt; \u0026lt;image\u0026gt; Advanced Topics, Real-World Projects \u0026amp; Reference PART 7: ADVANCED TOPICS 25. Resource Management 25.1 Why Limit Resources? Without limits:\nContainer can use ALL host CPU/RAM One container can starve others System can become unresponsive OOM (Out of Memory) kills With limits:\nPredictable performance Fair resource distribution Prevent runaway containers Better stability 25.2 Memory Limits Set memory limit:\n# Limit to 512MB docker run -d --memory 512m nginx # Limit with swap docker run -d \\ --memory 512m \\ --memory-swap 1g \\ # Total memory (RAM + swap) nginx # No swap (memory-swap = memory) docker run -d --memory 512m --memory-swap 512m nginx In Docker Compose:\nservices: app: image: myapp deploy: resources: limits: memory: 512M Or v2 syntax:\nservices: app: image: myapp mem_limit: 512m memswap_limit: 1g 25.3 CPU Limits CPU shares (relative weight):\n# Default is 1024 docker run -d --cpu-shares 512 app1 # Gets half resources docker run -d --cpu-shares 1024 app2 # Gets full resources CPUs (hard limit):\n# Use at most 50% of one CPU docker run -d --cpus 0.5 myapp # Use at most 2 CPUs docker run -d --cpus 2 myapp CPU period and quota:\n# 50% of CPU time docker run -d \\ --cpu-period 100000 \\ --cpu-quota 50000 \\ myapp In Compose:\nservices: app: image: myapp deploy: resources: limits: cpus: \u0026#34;0.50\u0026#34; reservations: cpus: \u0026#34;0.25\u0026#34; 25.4 Monitoring Resource Usage Real-time monitoring:\n# All containers docker stats # Specific containers docker stats container1 container2 # Format output docker stats --format \u0026#34;table {{.Name}}\\t{{.CPUPerc}}\\t{{.MemUsage}}\u0026#34; # One-time snapshot (not continuous) docker stats --no-stream Output:\nCONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % a1b2c3d4e5f6 web 0.50% 50MiB / 512MiB 9.77% f6e5d4c3b2a1 db 2.30% 400MiB / 2GiB 19.53% 25.5 Practical Resource Allocation Example: Development Environment\nversion: \u0026#34;3.8\u0026#34; services: # Frontend (lightweight) frontend: image: my-frontend deploy: resources: limits: cpus: \u0026#34;0.25\u0026#34; memory: 256M # Backend API (moderate) backend: image: my-backend deploy: resources: limits: cpus: \u0026#34;1.0\u0026#34; memory: 1G # Database (resource-intensive) database: image: postgres deploy: resources: limits: cpus: \u0026#34;2.0\u0026#34; memory: 2G reservations: cpus: \u0026#34;0.5\u0026#34; memory: 512M # Cache (minimal) redis: image: redis:alpine deploy: resources: limits: cpus: \u0026#34;0.25\u0026#34; memory: 128M 25.6 OOM (Out of Memory) Handling By default: Linux kills container when out of memory\nDisable OOM killer:\ndocker run -d --oom-kill-disable myapp ⚠️ Warning: Only use with memory limits! Otherwise can freeze system.\nCheck if container was OOM killed:\ndocker inspect --format=\u0026#39;{{.State.OOMKilled}}\u0026#39; container_name # Output: true (if killed) or false 26. Health Checks 26.1 Why Health Checks? Problem:\nContainer is \u0026ldquo;running\u0026rdquo; but app crashed App started but not ready yet Service degraded but still running Solution: Health checks monitor app state\n26.2 Health Check in Dockerfile FROM nginx HEALTHCHECK --interval=30s --timeout=3s --retries=3 \\ CMD curl -f http://localhost/ || exit 1 Parameters:\n--interval: How often to check (default: 30s) --timeout: Max time for check (default: 30s) --retries: Failures before unhealthy (default: 3) --start-period: Grace period before checking (default: 0s) Examples:\n# HTTP health check HEALTHCHECK CMD curl -f http://localhost:3000/health || exit 1 # Database check HEALTHCHECK CMD pg_isready -U postgres || exit 1 # Custom script HEALTHCHECK CMD /app/healthcheck.sh # More frequent check HEALTHCHECK --interval=10s --timeout=2s --retries=5 \\ CMD curl -f http://localhost/api/health || exit 1 # Disable inherited health check HEALTHCHECK NONE 26.3 Health Check at Runtime docker run -d \\ --health-cmd \u0026#34;curl -f http://localhost/ || exit 1\u0026#34; \\ --health-interval 30s \\ --health-timeout 3s \\ --health-retries 3 \\ nginx 26.4 Health Check in Docker Compose services: web: image: nginx healthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;curl\u0026#34;, \u0026#34;-f\u0026#34;, \u0026#34;http://localhost\u0026#34;] interval: 30s timeout: 3s retries: 3 start_period: 10s database: image: postgres healthcheck: test: [\u0026#34;CMD-SHELL\u0026#34;, \u0026#34;pg_isready -U postgres\u0026#34;] interval: 10s timeout: 5s retries: 5 backend: image: myapp healthcheck: test: [ \u0026#34;CMD\u0026#34;, \u0026#34;wget\u0026#34;, \u0026#34;--no-verbose\u0026#34;, \u0026#34;--tries=1\u0026#34;, \u0026#34;--spider\u0026#34;, \u0026#34;http://localhost:3000/health\u0026#34;, ] interval: 30s depends_on: database: condition: service_healthy # Wait for healthy database! 26.5 Health Check States Three states:\nstarting: Grace period, not checking yet healthy: Check passed unhealthy: Check failed multiple times View health status:\n# In docker ps docker ps # Shows health status in STATUS column # Detailed check docker inspect --format=\u0026#39;{{.State.Health.Status}}\u0026#39; container_name # Full health history docker inspect --format=\u0026#39;{{json .State.Health}}\u0026#39; container_name | jq 26.6 Creating Health Check Endpoint Example: Express.js health endpoint\n// server.js const express = require(\u0026#34;express\u0026#34;); const app = express(); // Health check endpoint app.get(\u0026#34;/health\u0026#34;, async (req, res) =\u0026gt; { // Check database connection const dbOk = await checkDatabase(); // Check Redis connection const redisOk = await checkRedis(); if (dbOk \u0026amp;\u0026amp; redisOk) { res.status(200).json({ status: \u0026#34;healthy\u0026#34; }); } else { res.status(503).json({ status: \u0026#34;unhealthy\u0026#34;, database: dbOk ? \u0026#34;ok\u0026#34; : \u0026#34;failed\u0026#34;, redis: redisOk ? \u0026#34;ok\u0026#34; : \u0026#34;failed\u0026#34;, }); } }); async function checkDatabase() { try { await db.query(\u0026#34;SELECT 1\u0026#34;); return true; } catch (err) { return false; } } async function checkRedis() { try { await redis.ping(); return true; } catch (err) { return false; } } app.listen(3000); Dockerfile with health check:\nFROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . HEALTHCHECK --interval=30s --timeout=3s --retries=3 --start-period=40s \\ CMD node healthcheck.js || exit 1 CMD [\u0026#34;node\u0026#34;, \u0026#34;server.js\u0026#34;] healthcheck.js:\nconst http = require(\u0026#34;http\u0026#34;); const options = { host: \u0026#34;localhost\u0026#34;, port: 3000, path: \u0026#34;/health\u0026#34;, timeout: 2000, }; const request = http.request(options, (res) =\u0026gt; { if (res.statusCode === 200) { process.exit(0); // Healthy } else { process.exit(1); // Unhealthy } }); request.on(\u0026#34;error\u0026#34;, () =\u0026gt; { process.exit(1); // Unhealthy }); request.end(); 26.7 Health Check Best Practices 1. Keep checks fast (\u0026lt; 3 seconds)\n# ✅ Good - Simple ping HEALTHCHECK CMD curl -f http://localhost/ping # ❌ Bad - Complex database query HEALTHCHECK CMD curl -f http://localhost/full-system-test 2. Check critical dependencies\n// Check database, cache, external APIs app.get(\u0026#39;/health\u0026#39;, async (req, res) =\u0026gt; { const checks = await Promise.all([ checkDatabase(), checkRedis(), checkExternalAPI() ]); const allHealthy = checks.every(c =\u0026gt; c === true); res.status(allHealthy ? 200 : 503).json({ ... }); }); 3. Use start period for slow-starting apps\n# Give app 60 seconds to start before checking HEALTHCHECK --start-period=60s --interval=30s \\ CMD curl -f http://localhost/health || exit 1 4. Return proper exit codes\n# 0 = healthy # 1 = unhealthy # Exit code determines health status! 27. Security Best Practices 27.1 Don\u0026rsquo;t Run as Root ❌ Bad (runs as root):\nFROM ubuntu COPY app.py /app/ CMD [\u0026#34;python\u0026#34;, \u0026#34;/app/app.py\u0026#34;] ✅ Good (runs as non-root user):\nFROM ubuntu # Create non-root user RUN useradd -m -u 1000 appuser # Set ownership WORKDIR /app COPY --chown=appuser:appuser app.py . # Switch to non-root user USER appuser CMD [\u0026#34;python\u0026#34;, \u0026#34;app.py\u0026#34;] For official images with existing users:\n# Node.js images have \u0026#39;node\u0026#39; user FROM node:18-alpine USER node # Python images don\u0026#39;t, create one FROM python:3.11-slim RUN useradd -m appuser USER appuser 27.2 Use Official Images ✅ Trusted sources:\nDocker Official Images (verified) Verified Publishers Well-known organizations ❌ Avoid:\nRandom user images Unmaintained images Images with no documentation Check image:\n# Pull only from trusted registry docker pull nginx # Official docker pull bitnami/nginx # Verified publisher # Check image signature docker trust inspect --pretty nginx:latest 27.3 Keep Images Updated Regularly update base images:\n# Pull latest docker pull python:3.11-slim # Rebuild images docker compose build --pull In Dockerfile, use specific versions:\n# ❌ Bad FROM python:latest # ✅ Good FROM python:3.11.5-slim # ⚡ Better - use digest for immutability FROM python@sha256:abc123... 27.4 Scan for Vulnerabilities Docker Scout (built-in):\n# Scan image docker scout cves nginx # Quick health check docker scout quickview nginx Trivy (third-party):\n# Install trivy # Scan image trivy image nginx 27.5 Limit Container Capabilities Containers run with many Linux capabilities by default\nDrop all, add only needed:\ndocker run -d \\ --cap-drop=ALL \\ --cap-add=NET_BIND_SERVICE \\ nginx In Compose:\nservices: web: image: nginx cap_drop: - ALL cap_add: - NET_BIND_SERVICE 27.6 Read-Only Root Filesystem Prevent file modifications:\ndocker run -d --read-only nginx Problem: App needs to write temp files!\nSolution: Mount writable tmpfs:**\ndocker run -d \\ --read-only \\ --tmpfs /tmp \\ --tmpfs /var/run \\ nginx In Compose:\nservices: web: image: nginx read_only: true tmpfs: - /tmp - /var/run 27.7 Use Secrets (Don\u0026rsquo;t Hardcode) ❌ Bad:\nENV DATABASE_PASSWORD=supersecret ENV API_KEY=abc123xyz ✅ Good - Use environment variables:\ndocker run -e DATABASE_PASSWORD=$DB_PASS myapp ✅ Better - Use Docker secrets (Swarm):\nservices: app: image: myapp secrets: - db_password secrets: db_password: file: ./secrets/db_password.txt ✅ Best - Use secrets manager:\nAWS Secrets Manager HashiCorp Vault Azure Key Vault 27.8 Limit Network Exposure Only expose necessary ports:\nservices: database: image: postgres # Don\u0026#39;t expose database to host! # Only accessible from other containers backend: image: myapp # Only backend talks to database frontend: image: nginx ports: - \u0026#34;80:80\u0026#34; # Only frontend exposed Bind to localhost only:\n# Instead of 0.0.0.0:8080 docker run -p 127.0.0.1:8080:80 nginx 27.9 Security Cheat Sheet # Secure Dockerfile Template # Use specific version FROM python:3.11.5-slim # Don\u0026#39;t run as root RUN useradd -m -u 1000 appuser # Set working directory WORKDIR /app # Copy with correct ownership COPY --chown=appuser:appuser requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Copy app COPY --chown=appuser:appuser . . # Switch to non-root user USER appuser # Use non-root port EXPOSE 8000 # Run app CMD [\u0026#34;python\u0026#34;, \u0026#34;app.py\u0026#34;] # Secure docker-compose.yml template version: \u0026#34;3.8\u0026#34; services: app: build: . read_only: true cap_drop: - ALL tmpfs: - /tmp environment: - SECRET=${SECRET} # From .env networks: - internal deploy: resources: limits: cpus: \u0026#34;0.50\u0026#34; memory: 512M networks: internal: internal: true # No external access 28. Docker Registry \u0026amp; Hub 28.1 Docker Hub Basics Docker Hub = GitHub for Docker images\nPublic images:\nFree Anyone can pull Great for open source Private images:\nLimited free private repos Paid plans for more 28.2 Pushing to Docker Hub 1. Create account at hub.docker.com\n2. Login:\ndocker login # Enter username and password 3. Tag image with username:\n# Build image docker build -t myapp . # Tag for Docker Hub docker tag myapp:latest username/myapp:latest docker tag myapp:latest username/myapp:v1.0 4. Push:\ndocker push username/myapp:latest docker push username/myapp:v1.0 5. Pull from anywhere:\ndocker pull username/myapp:latest 28.3 Private Registry Run your own registry:\n# Start registry docker run -d \\ -p 5000:5000 \\ --name registry \\ -v registry_data:/var/lib/registry \\ registry:2 # Tag image for private registry docker tag myapp localhost:5000/myapp # Push docker push localhost:5000/myapp # Pull docker pull localhost:5000/myapp 28.4 Complete Push/Pull Example # Build docker build -t my-web-app . # Tag for different registries docker tag my-web-app:latest username/my-web-app:latest # Docker Hub docker tag my-web-app:latest myregistry.com/my-web-app:latest # Private docker tag my-web-app:latest localhost:5000/my-web-app:latest # Local # Push docker push username/my-web-app:latest docker push myregistry.com/my-web-app:latest docker push localhost:5000/my-web-app:latest # Pull on another machine docker pull username/my-web-app:latest docker run -d -p 80:80 username/my-web-app:latest 29. Optimization Techniques 29.1 Image Size Optimization Technique 1: Use Alpine base images\n# Before (900MB) FROM python:3.11 # After (50MB) FROM python:3.11-alpine Technique 2: Multi-stage builds\n# Build stage (large) FROM node:18 AS builder WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build # Production stage (small) FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY --from=builder /app/dist ./dist CMD [\u0026#34;node\u0026#34;, \u0026#34;dist/server.js\u0026#34;] Technique 3: Minimize layers\n# Before (multiple layers) RUN apt update RUN apt install -y curl RUN apt install -y vim RUN apt clean # After (single layer) RUN apt update \u0026amp;\u0026amp; \\ apt install -y curl vim \u0026amp;\u0026amp; \\ apt clean \u0026amp;\u0026amp; \\ rm -rf /var/lib/apt/lists/* Technique 4: Use .dockerignore\nnode_modules .git .env tests/ *.md .dockerignore Dockerfile Technique 5: Clean up in same layer\n# ❌ Bad - Cache remains in image RUN apt update RUN apt install -y curl RUN rm -rf /var/lib/apt/lists/* # ✅ Good - Clean in same RUN RUN apt update \u0026amp;\u0026amp; \\ apt install -y curl \u0026amp;\u0026amp; \\ rm -rf /var/lib/apt/lists/* 29.2 Build Speed Optimization Order matters for caching:\n# ✅ Optimal order FROM node:18-alpine WORKDIR /app # 1. Copy dependency files (change rarely) COPY package*.json ./ # 2. Install dependencies (cached unless package.json changes) RUN npm ci # 3. Copy source code (changes frequently) COPY . . # 4. Build RUN npm run build CMD [\u0026#34;npm\u0026#34;, \u0026#34;start\u0026#34;] Use BuildKit:\n# Enable BuildKit export DOCKER_BUILDKIT=1 # Or for single build DOCKER_BUILDKIT=1 docker build -t myapp . Parallel builds:\n# Build multiple images in parallel docker compose build --parallel 29.3 Runtime Optimization Use health checks:\nHEALTHCHECK --interval=30s CMD curl -f http://localhost/health || exit 1 Set resource limits:\nservices: app: image: myapp deploy: resources: limits: cpus: \u0026#34;1.0\u0026#34; memory: 1G Use restart policies:\nservices: app: image: myapp restart: unless-stopped PART 8: REAL-WORLD PROJECTS 30. Project 1: Simple Web Application Goal: Containerize a basic web application\nProject Structure simple-web-app/ ├── docker-compose.yml ├── Dockerfile ├── app.py ├── requirements.txt └── templates/ └── index.html app.py from flask import Flask, render_template import os app = Flask(__name__) @app.route(\u0026#39;/\u0026#39;) def home(): return render_template(\u0026#39;index.html\u0026#39;, hostname=os.environ.get(\u0026#39;HOSTNAME\u0026#39;, \u0026#39;unknown\u0026#39;)) @app.route(\u0026#39;/health\u0026#39;) def health(): return {\u0026#39;status\u0026#39;: \u0026#39;healthy\u0026#39;}, 200 if __name__ == \u0026#39;__main__\u0026#39;: app.run(host=\u0026#39;0.0.0.0\u0026#39;, port=5000) requirements.txt flask==2.3.0 templates/index.html \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;Docker Web App\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;Hello from Docker!\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;Container ID: {{ hostname }}\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Dockerfile FROM python:3.11-slim WORKDIR /app # Install dependencies COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Copy app COPY . . # Create non-root user RUN useradd -m appuser \u0026amp;\u0026amp; chown -R appuser:appuser /app USER appuser # Health check HEALTHCHECK --interval=30s --timeout=3s --retries=3 \\ CMD python -c \u0026#34;import requests; requests.get(\u0026#39;http://localhost:5000/health\u0026#39;)\u0026#34; || exit 1 EXPOSE 5000 CMD [\u0026#34;python\u0026#34;, \u0026#34;app.py\u0026#34;] docker-compose.yml version: \u0026#34;3.8\u0026#34; services: web: build: . ports: - \u0026#34;5000:5000\u0026#34; environment: - FLASK_ENV=production restart: unless-stopped Running the Project # Build and start docker compose up -d # View logs docker compose logs -f # Test curl http://localhost:5000 # Scale to 3 instances docker compose up -d --scale web=3 # Stop docker compose down 31. Project 2: Full-Stack MERN App Goal: Complete application with MongoDB, Express, React, Node.js\nProject Structure mern-app/ ├── docker-compose.yml ├── backend/ │ ├── Dockerfile │ ├── package.json │ └── server.js ├── frontend/ │ ├── Dockerfile │ ├── package.json │ └── src/ └── .env docker-compose.yml version: \u0026#34;3.8\u0026#34; services: # MongoDB Database mongodb: image: mongo:6 environment: MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER} MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD} volumes: - mongo_data:/data/db networks: - backend healthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;mongosh\u0026#34;, \u0026#34;--eval\u0026#34;, \u0026#34;db.adminCommand(\u0026#39;ping\u0026#39;)\u0026#34;] interval: 10s timeout: 5s retries: 5 # Backend API backend: build: context: ./backend ports: - \u0026#34;3001:3001\u0026#34; environment: - NODE_ENV=production - MONGO_URL=mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongodb:27017/myapp?authSource=admin - PORT=3001 depends_on: mongodb: condition: service_healthy networks: - frontend - backend healthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;curl\u0026#34;, \u0026#34;-f\u0026#34;, \u0026#34;http://localhost:3001/health\u0026#34;] interval: 30s timeout: 3s retries: 3 restart: unless-stopped # Frontend frontend: build: context: ./frontend ports: - \u0026#34;3000:80\u0026#34; environment: - REACT_APP_API_URL=http://localhost:3001 depends_on: - backend networks: - frontend restart: unless-stopped volumes: mongo_data: networks: frontend: driver: bridge backend: driver: bridge backend/Dockerfile FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY --from=builder /app . RUN addgroup -g 1001 -S nodejs \u0026amp;\u0026amp; \\ adduser -S nodejs -u 1001 \u0026amp;\u0026amp; \\ chown -R nodejs:nodejs /app USER nodejs EXPOSE 3001 CMD [\u0026#34;node\u0026#34;, \u0026#34;server.js\u0026#34;] backend/server.js const express = require(\u0026#34;express\u0026#34;); const mongoose = require(\u0026#34;mongoose\u0026#34;); const cors = require(\u0026#34;cors\u0026#34;); const app = express(); app.use(cors()); app.use(express.json()); // Connect to MongoDB mongoose .connect(process.env.MONGO_URL) .then(() =\u0026gt; console.log(\u0026#34;Connected to MongoDB\u0026#34;)) .catch((err) =\u0026gt; console.error(\u0026#34;MongoDB connection error:\u0026#34;, err)); // Simple schema const ItemSchema = new mongoose.Schema({ name: String, createdAt: { type: Date, default: Date.now }, }); const Item = mongoose.model(\u0026#34;Item\u0026#34;, ItemSchema); // Routes app.get(\u0026#34;/health\u0026#34;, (req, res) =\u0026gt; { res.json({ status: \u0026#34;healthy\u0026#34; }); }); app.get(\u0026#34;/api/items\u0026#34;, async (req, res) =\u0026gt; { const items = await Item.find(); res.json(items); }); app.post(\u0026#34;/api/items\u0026#34;, async (req, res) =\u0026gt; { const item = new Item({ name: req.body.name }); await item.save(); res.json(item); }); const PORT = process.env.PORT || 3001; app.listen(PORT, \u0026#34;0.0.0.0\u0026#34;, () =\u0026gt; { console.log(`Server running on port ${PORT}`); }); frontend/Dockerfile # Build stage FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # Production stage FROM nginx:alpine COPY --from=builder /app/build /usr/share/nginx/html COPY nginx.conf /etc/nginx/nginx.conf EXPOSE 80 CMD [\u0026#34;nginx\u0026#34;, \u0026#34;-g\u0026#34;, \u0026#34;daemon off;\u0026#34;] .env MONGO_USER=admin MONGO_PASSWORD=secretpassword Running the Project # Start everything docker compose up -d # Check status docker compose ps # View logs docker compose logs -f backend # Stop docker compose down # Stop and remove data docker compose down -v 32. Project 3: Microservices Architecture Goal: Multiple services with service discovery\nProject Structure microservices/ ├── docker-compose.yml ├── api-gateway/ ├── user-service/ ├── product-service/ └── nginx.conf docker-compose.yml version: \u0026#34;3.8\u0026#34; services: # API Gateway (Nginx) gateway: image: nginx:alpine ports: - \u0026#34;80:80\u0026#34; volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro depends_on: - user-service - product-service networks: - microservices # User Service user-service: build: ./user-service environment: - SERVICE_NAME=user-service - DB_URL=mongodb://mongodb:27017/users depends_on: - mongodb networks: - microservices deploy: replicas: 2 resources: limits: cpus: \u0026#34;0.5\u0026#34; memory: 512M # Product Service product-service: build: ./product-service environment: - SERVICE_NAME=product-service - DB_URL=mongodb://mongodb:27017/products depends_on: - mongodb networks: - microservices deploy: replicas: 2 resources: limits: cpus: \u0026#34;0.5\u0026#34; memory: 512M # Shared Database mongodb: image: mongo:6 volumes: - mongo_data:/data/db networks: - microservices # Redis Cache redis: image: redis:alpine networks: - microservices volumes: mongo_data: networks: microservices: driver: bridge nginx.conf events { worker_connections 1024; } http { upstream user-service { server user-service:3000; } upstream product-service { server product-service:3000; } server { listen 80; location /api/users { proxy_pass http://user-service; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /api/products { proxy_pass http://product-service; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /health { return 200 \u0026#34;OK\\n\u0026#34;; add_header Content-Type text/plain; } } } 33. Project 4: Development Environment Goal: Complete dev environment with hot reload\ndocker-compose.dev.yml version: \u0026#34;3.8\u0026#34; services: # Database postgres: image: postgres:15-alpine ports: - \u0026#34;5432:5432\u0026#34; environment: POSTGRES_DB: dev_db POSTGRES_USER: dev POSTGRES_PASSWORD: devpass volumes: - postgres_dev_data:/var/lib/postgresql/data # Backend with hot reload backend: build: context: ./backend target: development ports: - \u0026#34;3000:3000\u0026#34; - \u0026#34;9229:9229\u0026#34; # Debugger port environment: NODE_ENV: development DATABASE_URL: postgres://dev:devpass@postgres:5432/dev_db volumes: - ./backend:/app # Live code sync - /app/node_modules # Preserve node_modules command: npm run dev depends_on: - postgres # Frontend with hot reload frontend: build: context: ./frontend target: development ports: - \u0026#34;8080:8080\u0026#34; environment: - CHOKIDAR_USEPOLLING=true # For hot reload volumes: - ./frontend:/app - /app/node_modules command: npm run dev # Adminer (Database UI) adminer: image: adminer ports: - \u0026#34;8081:8080\u0026#34; depends_on: - postgres # Mailhog (Email testing) mailhog: image: mailhog/mailhog ports: - \u0026#34;1025:1025\u0026#34; # SMTP - \u0026#34;8025:8025\u0026#34; # Web UI volumes: postgres_dev_data: backend/Dockerfile (Multi-stage for dev/prod) # Development stage FROM node:18-alpine AS development WORKDIR /app COPY package*.json ./ RUN npm install # All dependencies including devDependencies COPY . . EXPOSE 3000 9229 CMD [\u0026#34;npm\u0026#34;, \u0026#34;run\u0026#34;, \u0026#34;dev\u0026#34;] # Production stage FROM node:18-alpine AS production WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . USER node EXPOSE 3000 CMD [\u0026#34;npm\u0026#34;, \u0026#34;start\u0026#34;] Running Development Environment # Start dev environment docker compose -f docker-compose.dev.yml up # Access services: # - Backend: http://localhost:3000 # - Frontend: http://localhost:8080 # - Database UI: http://localhost:8081 # - Email UI: http://localhost:8025 # Code changes auto-reload! # Stop docker compose -f docker-compose.dev.yml down # Clean up (remove volumes) docker compose -f docker-compose.dev.yml down -v APPENDICES Complete Command Reference Container Commands # Run container docker run [OPTIONS] IMAGE [COMMAND] docker run -d # Detached (background) docker run -it # Interactive with terminal docker run --rm # Auto-remove when stopped docker run --name \u0026lt;n\u0026gt; # Custom name docker run -p 8080:80 # Port mapping docker run -v vol:/path # Volume mount docker run -e KEY=value # Environment variable docker run --network \u0026lt;n\u0026gt; # Connect to network docker run --restart always # Restart policy docker run --memory 512m # Memory limit docker run --cpus 0.5 # CPU limit # List containers docker ps # Running docker ps -a # All docker ps -q # IDs only docker ps --filter \u0026#34;status=exited\u0026#34; # Filter # Control containers docker start \u0026lt;c\u0026gt; # Start stopped docker stop \u0026lt;c\u0026gt; # Stop (graceful) docker restart \u0026lt;c\u0026gt; # Restart docker kill \u0026lt;c\u0026gt; # Force stop docker pause \u0026lt;c\u0026gt; # Pause docker unpause \u0026lt;c\u0026gt; # Unpause docker rm \u0026lt;c\u0026gt; # Remove docker rm -f \u0026lt;c\u0026gt; # Force remove # Execute in container docker exec \u0026lt;c\u0026gt; \u0026lt;command\u0026gt; # Run command docker exec -it \u0026lt;c\u0026gt; bash # Interactive shell # Logs docker logs \u0026lt;c\u0026gt; # View logs docker logs -f \u0026lt;c\u0026gt; # Follow docker logs --tail 100 \u0026lt;c\u0026gt; # Last 100 lines docker logs --since 30m \u0026lt;c\u0026gt; # Last 30 minutes # Copy files docker cp \u0026lt;c\u0026gt;:/path /local # From container docker cp /local \u0026lt;c\u0026gt;:/path # To container # Inspect docker inspect \u0026lt;c\u0026gt; # Full details docker top \u0026lt;c\u0026gt; # Processes docker stats \u0026lt;c\u0026gt; # Resource usage docker port \u0026lt;c\u0026gt; # Port mappings # Cleanup docker container prune # Remove stopped docker rm $(docker ps -a -q) # Remove all Image Commands # Build image docker build -t \u0026lt;name\u0026gt; . # Basic build docker build -t \u0026lt;name\u0026gt;:\u0026lt;tag\u0026gt; . # With tag docker build -f Dockerfile.dev . # Custom Dockerfile docker build --no-cache . # No cache docker build --build-arg KEY=val . # Build arguments # List images docker images # All images docker images -q # IDs only docker images --filter \u0026#34;dangling=true\u0026#34; # Untagged # Tag image docker tag \u0026lt;image\u0026gt; \u0026lt;new-name\u0026gt;:\u0026lt;tag\u0026gt; # Push/Pull docker push \u0026lt;image\u0026gt; # Push to registry docker pull \u0026lt;image\u0026gt; # Pull from registry # Remove images docker rmi \u0026lt;image\u0026gt; # Remove image docker rmi -f \u0026lt;image\u0026gt; # Force remove docker image prune # Remove dangling docker image prune -a # Remove all unused # Inspect docker history \u0026lt;image\u0026gt; # Layer history docker inspect \u0026lt;image\u0026gt; # Details Volume Commands # Create volume docker volume create \u0026lt;n\u0026gt; # Create docker volume create --label key=val \u0026lt;n\u0026gt; # With label # List volumes docker volume ls # All docker volume ls -q # IDs only docker volume ls -f dangling=true # Unused # Inspect docker volume inspect \u0026lt;n\u0026gt; # Remove docker volume rm \u0026lt;n\u0026gt; # Remove docker volume prune # Remove unused # Use volume docker run -v \u0026lt;vol\u0026gt;:\u0026lt;path\u0026gt; \u0026lt;image\u0026gt; # Mount volume docker run -v $(pwd):\u0026lt;path\u0026gt; \u0026lt;image\u0026gt; # Bind mount docker run -v \u0026lt;path\u0026gt; \u0026lt;image\u0026gt; # Anonymous volume # Backup/Restore docker run --rm \\ -v \u0026lt;vol\u0026gt;:/source \\ -v $(pwd):/backup \\ ubuntu tar czf /backup/backup.tar.gz -C /source . docker run --rm \\ -v \u0026lt;vol\u0026gt;:/target \\ -v $(pwd):/backup \\ ubuntu tar xzf /backup/backup.tar.gz -C /target Network Commands # Create network docker network create \u0026lt;n\u0026gt; # Basic docker network create --driver bridge \u0026lt;n\u0026gt; docker network create --subnet 172.20.0.0/16 \u0026lt;n\u0026gt; # List networks docker network ls # Inspect docker network inspect \u0026lt;n\u0026gt; # Connect/Disconnect docker network connect \u0026lt;n\u0026gt; \u0026lt;c\u0026gt; # Connect container docker network disconnect \u0026lt;n\u0026gt; \u0026lt;c\u0026gt; # Disconnect # Remove docker network rm \u0026lt;n\u0026gt; docker network prune # Remove unused # Use network docker run --network \u0026lt;n\u0026gt; \u0026lt;image\u0026gt; Docker Compose Commands # Start services docker compose up # Foreground docker compose up -d # Detached docker compose up --build # Build first docker compose up --force-recreate # Recreate containers # Stop services docker compose down # Stop and remove docker compose down -v # Also remove volumes docker compose stop # Stop only docker compose start # Start stopped # Service management docker compose ps # List docker compose logs # Logs docker compose logs -f \u0026lt;s\u0026gt; # Follow service docker compose exec \u0026lt;s\u0026gt; \u0026lt;cmd\u0026gt; # Execute docker compose restart # Restart all docker compose restart \u0026lt;s\u0026gt; # Restart service # Building docker compose build # Build all docker compose build \u0026lt;s\u0026gt; # Build service docker compose pull # Pull images # Scaling docker compose up -d --scale web=3 # Run 3 instances # Config docker compose config # Validate docker compose config --services # List services # Cleanup docker compose rm # Remove stopped docker compose down --rmi all # Remove images too System Commands # System info docker version # Version docker info # System info # Disk usage docker system df # Disk usage docker system df -v # Verbose # Cleanup docker system prune # Remove unused docker system prune -a # Remove all unused docker system prune -a --volumes # Also volumes # Events docker events # Monitor events docker events --filter type=container Troubleshooting Guide Container Won\u0026rsquo;t Start Problem: Container exits immediately\nSolutions:\n# 1. Check logs docker logs \u0026lt;container\u0026gt; # 2. Run without -d to see output docker run -it \u0026lt;image\u0026gt; # 3. Check exit code docker ps -a # Look at STATUS column # 4. Override entrypoint to debug docker run -it --entrypoint bash \u0026lt;image\u0026gt; Can\u0026rsquo;t Connect to Container Problem: Cannot access containerized app\nCheck:\n# 1. Container is running docker ps # 2. Port mapping is correct docker port \u0026lt;container\u0026gt; # 3. App is listening on 0.0.0.0, not 127.0.0.1 docker exec \u0026lt;container\u0026gt; netstat -tlnp # 4. Firewall allows port # 5. Test from inside container docker exec \u0026lt;container\u0026gt; curl http://localhost:80 Volume Data Lost Problem: Data disappears\nCauses:\n# 1. Used anonymous volume (random name) docker volume ls # Check for random names # 2. Removed with -v flag docker compose down -v # This removes volumes! # 3. Used container filesystem instead of volume Prevention:\n# Always use named volumes docker volume create my_data docker run -v my_data:/data \u0026lt;image\u0026gt; # In Compose volumes: my_data: # Declare named volume Out of Disk Space Problem: \u0026ldquo;No space left on device\u0026rdquo;\nSolutions:\n# 1. Check usage docker system df # 2. Remove unused docker system prune -a docker volume prune # 3. Remove specific resources docker container prune docker image prune -a # 4. Find large images docker images --format \u0026#34;{{.Repository}}:{{.Tag}} {{.Size}}\u0026#34; Permission Denied Problem: Cannot access files in volume\nSolutions:\n# Run as your user docker run --user $(id -u):$(id -g) ... # Or in Dockerfile RUN useradd -u 1000 appuser USER appuser # Fix permissions in volume docker run --rm -v my_vol:/data ubuntu chown -R 1000:1000 /data Network Issues Problem: Containers can\u0026rsquo;t communicate\nCheck:\n# 1. Same network? docker inspect \u0026lt;container\u0026gt; --format \u0026#39;{{.NetworkSettings.Networks}}\u0026#39; # 2. DNS resolution docker exec container1 ping container2 # 3. Firewall rules # 4. Network exists docker network ls Build Fails Problem: Docker build errors\nSolutions:\n# 1. Check Dockerfile syntax docker build -t test . # 2. Build with no cache docker build --no-cache -t test . # 3. Check .dockerignore cat .dockerignore # 4. Check build context size du -sh . # 5. Increase build memory (Docker Desktop) # Settings → Resources → Memory Common Patterns \u0026amp; Solutions Pattern: Development with Live Reload version: \u0026#34;3.8\u0026#34; services: app: build: . ports: - \u0026#34;3000:3000\u0026#34; volumes: - ./src:/app/src # Live code sync - /app/node_modules # Preserve dependencies environment: - NODE_ENV=development command: npm run dev Pattern: Database with Initialization version: \u0026#34;3.8\u0026#34; services: database: image: postgres volumes: - db_data:/var/lib/postgresql/data - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro environment: POSTGRES_PASSWORD: secret volumes: db_data: Pattern: Wait for Service Ready services: database: image: postgres healthcheck: test: [\u0026#34;CMD-SHELL\u0026#34;, \u0026#34;pg_isready\u0026#34;] interval: 10s timeout: 5s retries: 5 app: image: myapp depends_on: database: condition: service_healthy # Wait for healthy! Pattern: Secrets Management # Use .env file version: \u0026#34;3.8\u0026#34; services: app: image: myapp environment: - DB_PASSWORD=${DB_PASSWORD} env_file: - .env Pattern: Multi-Stage Build # Build stage FROM node:18 AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # Production stage FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY --from=builder /app/dist ./dist CMD [\u0026#34;node\u0026#34;, \u0026#34;dist/server.js\u0026#34;] Glossary Image: Read-only template containing application and dependencies\nContainer: Running instance of an image\nDockerfile: Text file with instructions to build an image\nVolume: Persistent data storage managed by Docker\nBind Mount: Direct mount of host directory into container\nNetwork: Virtual network connecting containers\nRegistry: Storage for Docker images (e.g., Docker Hub)\nLayer: One instruction in Dockerfile (cached for efficiency)\nTag: Version label for images (e.g., nginx:1.24)\nCompose: Tool for defining multi-container applications\nService: Container definition in docker-compose.yml\nHealth Check: Test to verify container is healthy\nMulti-Stage Build: Dockerfile with multiple FROM statements\nBridge Network: Default network type for containers\nPort Mapping: Exposing container port to host\nEnvironment Variable: Configuration passed to container\nEntrypoint: Main executable run when container starts\nCMD: Default arguments for entrypoint\n🎉 Congratulations! You\u0026rsquo;ve completed the comprehensive Docker guide!\nYou now know:\n✅ Docker fundamentals and concepts ✅ Working with containers and images ✅ Creating Dockerfiles and building images ✅ Data persistence with volumes ✅ Multi-container apps with Docker Compose ✅ Networking and container communication ✅ Security best practices ✅ Optimization techniques ✅ Real-world project patterns Next Steps:\nPractice with the projects in Part 8 Dockerize your own applications Explore Docker Swarm or Kubernetes Contribute to open source Docker projects Resources:\nDocker Documentation: docs.docker.com Docker Hub: hub.docker.com Play with Docker: labs.play-with-docker.com Keep Learning! 🚀\n","permalink":"/explore/docker-101/","summary":"\u003cp\u003e\u003cstrong\u003eA Comprehensive, Self-Learning Docker Resource\u003c/strong\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"-table-of-contents\"\u003e📑 Table of Contents\u003c/h2\u003e\n\u003ch3 id=\"part-1-foundations\"\u003e\u003cstrong\u003ePART 1: FOUNDATIONS\u003c/strong\u003e\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e\u003ca href=\"#1-introduction-to-docker\"\u003eIntroduction to Docker\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#2-core-concepts\"\u003eCore Concepts\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#3-installation--setup\"\u003eInstallation \u0026amp; Setup\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#4-your-first-container\"\u003eYour First Container\u003c/a\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"part-2-working-with-containers\"\u003e\u003cstrong\u003ePART 2: WORKING WITH CONTAINERS\u003c/strong\u003e\u003c/h3\u003e\n\u003col start=\"5\"\u003e\n\u003cli\u003e\u003ca href=\"#5-container-lifecycle\"\u003eContainer Lifecycle\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#6-container-management\"\u003eContainer Management\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#7-port-mapping--networking\"\u003ePort Mapping \u0026amp; Networking\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#8-container-logs--debugging\"\u003eContainer Logs \u0026amp; Debugging\u003c/a\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"part-3-creating-images\"\u003e\u003cstrong\u003ePART 3: CREATING IMAGES\u003c/strong\u003e\u003c/h3\u003e\n\u003col start=\"9\"\u003e\n\u003cli\u003e\u003ca href=\"#9-understanding-dockerfiles\"\u003eUnderstanding Dockerfiles\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#10-building-custom-images\"\u003eBuilding Custom Images\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#11-dockerfile-best-practices\"\u003eDockerfile Best Practices\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#12-multi-stage-builds\"\u003eMulti-Stage Builds\u003c/a\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"part-4-data-persistence\"\u003e\u003cstrong\u003ePART 4: DATA PERSISTENCE\u003c/strong\u003e\u003c/h3\u003e\n\u003col start=\"13\"\u003e\n\u003cli\u003e\u003ca href=\"#13-understanding-container-data\"\u003eUnderstanding Container Data\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#14-volumes\"\u003eVolumes\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#15-bind-mounts\"\u003eBind Mounts\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#16-volume-management\"\u003eVolume Management\u003c/a\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"part-5-docker-compose\"\u003e\u003cstrong\u003ePART 5: DOCKER COMPOSE\u003c/strong\u003e\u003c/h3\u003e\n\u003col start=\"17\"\u003e\n\u003cli\u003e\u003ca href=\"#17-introduction-to-docker-compose\"\u003eIntroduction to Docker Compose\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#18-docker-compose-syntax\"\u003eDocker Compose Syntax\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#19-multi-container-applications\"\u003eMulti-Container Applications\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#20-environment-variables--secrets\"\u003eEnvironment Variables \u0026amp; Secrets\u003c/a\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"part-6-networking\"\u003e\u003cstrong\u003ePART 6: NETWORKING\u003c/strong\u003e\u003c/h3\u003e\n\u003col start=\"21\"\u003e\n\u003cli\u003e\u003ca href=\"#21-docker-networks-deep-dive\"\u003eDocker Networks Deep Dive\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#22-network-types\"\u003eNetwork Types\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#23-container-communication\"\u003eContainer Communication\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#24-custom-networks\"\u003eCustom Networks\u003c/a\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"part-7-advanced-topics\"\u003e\u003cstrong\u003ePART 7: ADVANCED TOPICS\u003c/strong\u003e\u003c/h3\u003e\n\u003col start=\"25\"\u003e\n\u003cli\u003e\u003ca href=\"#25-resource-management\"\u003eResource Management\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#26-health-checks\"\u003eHealth Checks\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#27-security-best-practices\"\u003eSecurity Best Practices\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#28-docker-registry--hub\"\u003eDocker Registry \u0026amp; Hub\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#29-optimization-techniques\"\u003eOptimization Techniques\u003c/a\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"part-8-real-world-projects\"\u003e\u003cstrong\u003ePART 8: REAL-WORLD PROJECTS\u003c/strong\u003e\u003c/h3\u003e\n\u003col start=\"30\"\u003e\n\u003cli\u003e\u003ca href=\"#30-project-1-simple-web-application\"\u003eProject 1: Simple Web Application\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#31-project-2-full-stack-mern-app\"\u003eProject 2: Full-Stack MERN App\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#32-project-3-microservices-architecture\"\u003eProject 3: Microservices Architecture\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#33-project-4-development-environment\"\u003eProject 4: Development Environment\u003c/a\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"appendices\"\u003e\u003cstrong\u003eAPPENDICES\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#complete-command-reference\"\u003eComplete Command Reference\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#troubleshooting-guide\"\u003eTroubleshooting Guide\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#common-patterns--solutions\"\u003eCommon Patterns \u0026amp; Solutions\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#glossary\"\u003eGlossary\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch1 id=\"part-1-foundations-1\"\u003ePART 1: FOUNDATIONS\u003c/h1\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-introduction-to-docker\"\u003e1. Introduction to Docker\u003c/h2\u003e\n\u003ch3 id=\"11-what-is-docker\"\u003e1.1 What is Docker?\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eSimple Definition:\u003c/strong\u003e\nDocker is a platform that packages applications and their dependencies into containers - portable, isolated environments that run consistently across any computer.\u003c/p\u003e","title":"🐳 Complete Docker Guide: From Basic to Advanced"},{"content":"Complete Notes — From Zero to Solving Problems How to use these notes: Read each section once top to bottom. Then close the notes and try to recall the patterns. Come back only to verify. Retention comes from recall, not re-reading.\n1. The Mindset This is not a general C++ course. The goal is one thing:\nAlgorithm idea → immediate, clean C++ implementation\nEvery concept in these notes exists to eliminate the friction between knowing how to solve a problem and actually writing the code. If a topic does not appear in DSA solutions, it is not here.\nTwo rules to follow while studying:\nAfter reading a concept, type it out yourself. Don\u0026rsquo;t copy-paste. Ever. When you see a new pattern in someone else\u0026rsquo;s solution, immediately connect it back to a section here. 2. Boilerplate Every competitive programming solution starts with the same skeleton. Memorize this. It should take under 10 seconds to type from scratch.\n#include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); // your code here return 0; } Why each line exists:\n#include \u0026lt;bits/stdc++.h\u0026gt; — includes every standard library header at once. You never need to include \u0026lt;vector\u0026gt;, \u0026lt;map\u0026gt;, \u0026lt;algorithm\u0026gt; separately. Only works in GCC (standard in competitive programming).\nusing namespace std; — lets you write vector instead of std::vector, cout instead of std::cout everywhere.\nios::sync_with_stdio(false) — by default, C++ syncs its I/O with C\u0026rsquo;s I/O. This disables that sync. Result: cin and cout become 5–10x faster. Critical for problems with large input.\ncin.tie(nullptr) — unties cin from cout. Without this, every time you use cin, it first flushes cout. Disabling this saves unnecessary flushes.\nOne rule: After adding these lines, never mix scanf/printf with cin/cout. Pick one system and stay with it.\nUseful constants — define these at the top:\nconst int INF = 1e9; // safe \u0026#34;infinity\u0026#34; for int const int MOD = 1e9 + 7; // standard modulo in problems const int MAXN = 1e5 + 5; // max array size + buffer // When int isn\u0026#39;t enough long long big = 1e18; // ~9.2 × 10^18 3. Block 1 — C++ Features That Reduce Friction These are not extra features. They are the default way experienced programmers write C++ for DSA. You will use every single one of these in almost every solution you write.\n3.1 auto — Stop Writing Long Type Names auto tells the compiler to figure out the type. The type is still there — you just don\u0026rsquo;t have to type it.\nauto x = 5; // int auto y = 3.14; // double auto n = v.size(); // size_t — you don\u0026#39;t care what this is // Where it really helps — long iterator types map\u0026lt;string, vector\u0026lt;int\u0026gt;\u0026gt;::iterator it = m.begin(); // painful auto it = m.begin(); // clean The rule: Use auto whenever writing the type is longer than writing auto or makes the code harder to read.\nThe trap: auto without \u0026amp; makes a copy. Always think about whether you need a reference.\nfor (auto x : v) // copies each element — fine for int for (auto\u0026amp; x : v) // reference — use for objects, strings, pairs 3.2 Range-Based For Loop — Iterate Without Indices When you don\u0026rsquo;t need the index, this is cleaner than a traditional loop.\nvector\u0026lt;int\u0026gt; v = {1, 2, 3, 4, 5}; // Traditional — use when you need the index for (int i = 0; i \u0026lt; v.size(); i++) cout \u0026lt;\u0026lt; v[i] \u0026lt;\u0026lt; \u0026#34; \u0026#34;; // Range-based — use when you just need each value for (auto\u0026amp; x : v) cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#34; \u0026#34;; // Read-only — when you won\u0026#39;t modify elements for (const auto\u0026amp; x : v) cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#34; \u0026#34;; The \u0026amp; rule — applies everywhere, not just here:\nYou want Write Read and modify auto\u0026amp; x Read only, no copy const auto\u0026amp; x A copy you can modify auto x (but ask yourself why) For anything larger than a primitive (int, char, bool), always use \u0026amp;. Copying strings, pairs, vectors inside a loop is a silent performance killer.\n3.3 Structured Bindings — Unpack Pairs with Real Names pair.first and pair.second tell you nothing about what those values mean. Structured bindings let you unpack a pair into named variables.\npair\u0026lt;int,int\u0026gt; p = {5, 3}; // Old way — what does first and second mean here? cout \u0026lt;\u0026lt; p.first \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; p.second; // Structured binding — meaning is clear auto\u0026amp; [distance, node] = p; cout \u0026lt;\u0026lt; distance \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; node; Most common use — looping over maps and vectors of pairs:\nmap\u0026lt;string, int\u0026gt; scores = {{\u0026#34;alice\u0026#34;, 90}, {\u0026#34;bob\u0026#34;, 85}}; // Old way for (auto\u0026amp; e : scores) cout \u0026lt;\u0026lt; e.first \u0026lt;\u0026lt; \u0026#34;: \u0026#34; \u0026lt;\u0026lt; e.second \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // With structured bindings — immediately clear for (auto\u0026amp; [name, score] : scores) cout \u0026lt;\u0026lt; name \u0026lt;\u0026lt; \u0026#34;: \u0026#34; \u0026lt;\u0026lt; score \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; With priority queue in Dijkstra — the clearest form:\nauto [dist, u] = pq.top(); pq.pop(); for (auto\u0026amp; [v, w] : adj[u]) { if (dist + w \u0026lt; d[v]) { ... } } The \u0026amp; rule again: Write auto\u0026amp; [a, b] by default. Without \u0026amp; you get copies.\n3.4 References — The Most Important Concept for Performance A reference is an alias. It is not a copy. It refers to the original variable.\nint x = 5; int\u0026amp; ref = x; // ref is x — same memory location ref = 10; cout \u0026lt;\u0026lt; x; // prints 10 — x changed through ref Why this matters in DSA — function parameters:\nEvery time you pass a container to a function, C++ copies it by default. A vector of 100,000 integers copied on every recursive DFS call = guaranteed TLE.\n// Copies the entire vector every call — O(n) per call void dfs(int node, vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; adj) // ❌ // Passes reference — zero copy cost void dfs(int node, vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; adj) // ✅ The three patterns you will write constantly:\n// Modify the container inside the function void fill(vector\u0026lt;int\u0026gt;\u0026amp; v) // Read-only — protect from accidental modification void print(const vector\u0026lt;int\u0026gt;\u0026amp; v) // Return by reference (careful — don\u0026#39;t return local variable references) int\u0026amp; getElement(vector\u0026lt;int\u0026gt;\u0026amp; v, int i) { return v[i]; } The one mistake that crashes programs:\nint\u0026amp; bad() { int x = 5; return x; // ❌ x dies when function returns — dangling reference } Never return a reference to a local variable. The variable dies when the function ends. The reference becomes garbage.\n3.5 const — Two Uses That Matter in DSA Forget everything about const except these two patterns:\n// Pattern 1 — fixed values (use these in almost every solution) const int INF = 1e9; const int MOD = 1e9 + 7; // Pattern 2 — read-only function parameters void solve(const vector\u0026lt;int\u0026gt;\u0026amp; v) { v[0] = 5; // ❌ compiler error — cannot modify cout \u0026lt;\u0026lt; v[0]; // ✅ reading is fine } const in a function parameter says: \u0026ldquo;I promise not to modify this. I just need to read it.\u0026rdquo; It\u0026rsquo;s both documentation and a safeguard against bugs.\n3.6 pair and tuple — Bundling Values DSA constantly involves values that come in twos or threes: edges (u, v, weight), coordinates (row, col), frequencies (value, count). pair and tuple bundle these into a single unit.\npair — for two values:\npair\u0026lt;int, int\u0026gt; p = {3, 7}; p.first; // 3 p.second; // 7 // In vectors — very common vector\u0026lt;pair\u0026lt;int,int\u0026gt;\u0026gt; edges; edges.push_back({0, 1}); edges.push_back({1, 2}); // Comparison is lexicographic — first element compared first pair\u0026lt;int,int\u0026gt; a = {1, 5}; pair\u0026lt;int,int\u0026gt; b = {1, 3}; a \u0026gt; b; // true — first elements equal, 5 \u0026gt; 3 tuple — for three or more values:\n// Edge with weight — 3 values tuple\u0026lt;int,int,int\u0026gt; edge = {weight, u, v}; // Unpack with structured binding auto [w, u, v] = edge; // Or access by index (uglier) get\u0026lt;0\u0026gt;(edge); // weight get\u0026lt;1\u0026gt;(edge); // u get\u0026lt;2\u0026gt;(edge); // v The classic DSA pattern — value with original index:\nvector\u0026lt;int\u0026gt; nums = {40, 10, 30, 20}; vector\u0026lt;pair\u0026lt;int,int\u0026gt;\u0026gt; indexed; for (int i = 0; i \u0026lt; nums.size(); i++) indexed.push_back({nums[i], i}); // {value, original_index} sort(indexed.begin(), indexed.end()); // Now sorted by value, but you still know original positions 3.7 Lambda Expressions — Inline Logic for Comparators A lambda is a function you define right where you need it. In DSA, they appear almost exclusively as custom comparators for sort and priority_queue.\nAnatomy:\n[capture](parameters) { body } // Examples: auto add = [](int a, int b) { return a + b; }; cout \u0026lt;\u0026lt; add(3, 4); // 7 The only part you need to understand for DSA right now — the comparator rule:\nA comparator returns true if a should come before b. That\u0026rsquo;s the entire rule.\n// Sort ascending (default behavior) sort(v.begin(), v.end(), [](auto\u0026amp; a, auto\u0026amp; b) { return a \u0026lt; b; }); // Sort descending sort(v.begin(), v.end(), [](auto\u0026amp; a, auto\u0026amp; b) { return a \u0026gt; b; }); // Sort pairs by second element ascending sort(v.begin(), v.end(), [](auto\u0026amp; a, auto\u0026amp; b) { return a.second \u0026lt; b.second; }); // Sort by multiple keys — grade descending, name ascending on tie sort(v.begin(), v.end(), [](auto\u0026amp; a, auto\u0026amp; b) { if (a.grade != b.grade) return a.grade \u0026gt; b.grade; return a.name \u0026lt; b.name; }); Capture brackets — when your lambda needs an outside variable:\nint threshold = 5; // [\u0026amp;] — capture all outside variables by reference auto isAbove = [\u0026amp;](int x) { return x \u0026gt; threshold; }; Capture Meaning [] capture nothing [\u0026amp;] all outside variables by reference [=] all by value (copy) [\u0026amp;x, y] x by ref, y by value The fatal comparator mistake — never use \u0026gt;= or \u0026lt;=:\n// ❌ undefined behavior — strict weak ordering violated sort(v.begin(), v.end(), [](auto\u0026amp; a, auto\u0026amp; b) { return a \u0026gt;= b; }); // ✅ always strict: \u0026lt; or \u0026gt; sort(v.begin(), v.end(), [](auto\u0026amp; a, auto\u0026amp; b) { return a \u0026gt; b; }); 4. Block 2 — Core STL Containers These are the data structures you will use in every problem. For each container: understand what problem it solves, how to use it, and when to prefer it over alternatives.\n4.1 vector — Your Default Array A vector is a dynamic array. It handles its own sizing. Use it everywhere you would use a plain array.\nDeclaring and initializing:\nvector\u0026lt;int\u0026gt; v; // empty vector\u0026lt;int\u0026gt; v(5); // [0, 0, 0, 0, 0] vector\u0026lt;int\u0026gt; v(5, 3); // [3, 3, 3, 3, 3] vector\u0026lt;int\u0026gt; v = {1, 2, 3, 4, 5}; // initializer list // 2D vector — replaces int grid[MAXN][MAXN] vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; grid(rows, vector\u0026lt;int\u0026gt;(cols, 0)); Core operations:\nv.push_back(x); // add to end — O(1) amortized v.pop_back(); // remove from end — O(1) v[i]; // access by index — O(1) v.size(); // number of elements — O(1) v.empty(); // true if size == 0 v.clear(); // remove all elements // Safe size usage — avoids sign comparison warning int n = v.size(); // store as int, not auto for (int i = 0; i \u0026lt; n; i++) { ... } Passing to functions — always by reference:\nvoid process(vector\u0026lt;int\u0026gt;\u0026amp; v) // modify void process(const vector\u0026lt;int\u0026gt;\u0026amp; v) // read only reserve — avoid reallocations when size is known:\nvector\u0026lt;int\u0026gt; v; v.reserve(100000); // allocate space upfront for (int i = 0; i \u0026lt; 100000; i++) v.push_back(i); // no reallocation happens Without reserve, a vector doubles its capacity every time it fills up, triggering a copy of all elements. With reserve, you pay the cost once.\n4.2 stack — Last In, First Out Use a stack when you need to process the most recently added item first.\nProblems that need a stack: valid parentheses, next greater element, undo operations, DFS iterative.\nstack\u0026lt;int\u0026gt; st; st.push(10); // add to top st.push(20); st.top(); // see top — 20 (does not remove) st.pop(); // remove top st.empty(); // true if empty st.size(); // number of elements Critical rule: Always check !st.empty() before calling top() or pop(). Calling either on an empty stack is undefined behavior (crash).\nif (!st.empty()) cout \u0026lt;\u0026lt; st.top(); // safe Classic pattern — valid parentheses:\nstack\u0026lt;char\u0026gt; st; for (char c : s) { if (c == \u0026#39;(\u0026#39; || c == \u0026#39;{\u0026#39; || c == \u0026#39;[\u0026#39;) { st.push(c); } else { if (st.empty()) return false; char top = st.top(); st.pop(); if (c == \u0026#39;)\u0026#39; \u0026amp;\u0026amp; top != \u0026#39;(\u0026#39;) return false; if (c == \u0026#39;}\u0026#39; \u0026amp;\u0026amp; top != \u0026#39;{\u0026#39;) return false; if (c == \u0026#39;]\u0026#39; \u0026amp;\u0026amp; top != \u0026#39;[\u0026#39;) return false; } } return st.empty(); 4.3 queue — First In, First Out Use a queue when you process items in arrival order. The primary use in DSA is BFS.\nqueue\u0026lt;int\u0026gt; q; q.push(10); // add to back q.front(); // see front — does not remove q.back(); // see back — does not remove q.pop(); // remove from front q.empty(); q.size(); The most important distinction: stack uses top(), queue uses front(). Mixing these up is one of the most common bugs.\n4.4 deque — Double-Ended Queue Like a vector and a queue combined. Efficient push/pop at both ends.\ndeque\u0026lt;int\u0026gt; dq; dq.push_back(x); // add to back dq.push_front(x); // add to front dq.pop_back(); // remove from back dq.pop_front(); // remove from front dq.front(); dq.back(); dq[i]; // random access like vector When to use: Sliding window maximum problem — you need to add to one end and remove from both ends efficiently. That\u0026rsquo;s the main DSA use case.\n4.5 priority_queue — Always Get the Most Important Element A priority queue (heap) lets you always extract the maximum (or minimum) element in O(log n), regardless of insertion order.\nProblems: Dijkstra, k largest elements, greedy algorithms, merge k sorted lists.\n// Max-heap — default, largest element on top priority_queue\u0026lt;int\u0026gt; pq; pq.push(3); pq.push(1); pq.push(4); pq.top(); // 4 (largest) pq.pop(); // removes 4 Min-heap — smallest element on top:\npriority_queue\u0026lt;int, vector\u0026lt;int\u0026gt;, greater\u0026lt;int\u0026gt;\u0026gt; pq; // Same API — just smallest comes out first With pairs — extremely common:\n// Max-heap of pairs — compares by first element by default priority_queue\u0026lt;pair\u0026lt;int,int\u0026gt;\u0026gt; pq; // Min-heap of pairs — for Dijkstra priority_queue\u0026lt;pair\u0026lt;int,int\u0026gt;, vector\u0026lt;pair\u0026lt;int,int\u0026gt;\u0026gt;, greater\u0026lt;pair\u0026lt;int,int\u0026gt;\u0026gt;\u0026gt; pq; pq.push({dist, node}); auto [d, u] = pq.top(); pq.pop(); The three differences from stack:\nstack priority_queue Order LIFO By priority Access top() — most recent top() — highest priority Insertion push() push() Both use top() and pop(). The difference is only in what top() returns.\n4.6 set — Sorted Unique Elements A set stores elements in sorted order with no duplicates. Internal structure is a balanced BST (red-black tree).\nUse when: You need to know if something exists, get unique elements, or iterate in sorted order.\nset\u0026lt;int\u0026gt; s; s.insert(3); s.insert(1); s.insert(4); s.insert(1); // duplicate — silently ignored // s = {1, 3, 4} — sorted, no duplicates s.count(3); // 1 if exists, 0 if not — use for existence check s.find(3); // iterator to element, or s.end() if not found s.erase(3); // remove element s.size(); // Build from vector — instant dedup + sort set\u0026lt;int\u0026gt; s(v.begin(), v.end()); Existence check — two ways:\n// count — simpler for existence only if (s.count(x)) { /* exists */ } // find — when you need to do something with the element too auto it = s.find(x); if (it != s.end()) { /* *it is the element */ } One critical rule: You cannot modify an element in a set. Set elements are immutable. If you need to change a value, erase it and insert the new value.\ns.erase(3); s.insert(5); // replace 3 with 5 4.7 multiset — Sorted Elements with Duplicates Like set but allows duplicates. Use when you need a sorted structure where you insert and delete individual occurrences.\nmultiset\u0026lt;int\u0026gt; ms; ms.insert(3); ms.insert(3); // kept — ms = {3, 3} ms.insert(1); // ms = {1, 3, 3} // ⚠️ THE CLASSIC BUG: ms.erase(3); // ❌ removes ALL 3s — ms = {1} // ✅ CORRECT — remove only one occurrence: ms.erase(ms.find(3)); // removes one 3 — ms = {1, 3} Burn ms.erase(ms.find(x)) into memory. It is the only correct way to remove a single element from a multiset.\n4.8 unordered_set — Fast Existence Check, No Order Same as set but uses hashing internally instead of a BST. No sorted order. O(1) average for all operations.\nunordered_set\u0026lt;int\u0026gt; us; us.insert(x); us.count(x); // 0 or 1 us.erase(x); us.find(x); // same as set When to use which:\nNeed Use Fast existence check, don\u0026rsquo;t need order unordered_set Sorted iteration or range queries set Allow duplicates multiset Default choice for existence checking: unordered_set. Only switch to set if you need sorted traversal.\n4.9 map — Key-Value Lookup A map stores key-value pairs, sorted by key. Like an array but the index can be anything — string, char, pair, etc.\nProblems: Frequency counting, grouping elements, memoization, any \u0026ldquo;look up X, return Y\u0026rdquo; scenario.\nmap\u0026lt;string, int\u0026gt; freq; freq[\u0026#34;apple\u0026#34;] = 3; // insert/update freq[\u0026#34;apple\u0026#34;]++; // increment (creates with 0 if missing) // ⚠️ Critical: m[key] CREATES the key if it doesn\u0026#39;t exist // Never use m[key] to check existence — use count() freq.count(\u0026#34;apple\u0026#34;); // 1 if exists, 0 if not ✅ freq[\u0026#34;banana\u0026#34;]; // creates \u0026#34;banana\u0026#34; with value 0 ❌ for checking auto it = freq.find(\u0026#34;apple\u0026#34;); if (it != freq.end()) { cout \u0026lt;\u0026lt; it-\u0026gt;first \u0026lt;\u0026lt; \u0026#34;: \u0026#34; \u0026lt;\u0026lt; it-\u0026gt;second; } freq.erase(\u0026#34;apple\u0026#34;); freq.size(); Frequency counting — the most common map pattern:\nvector\u0026lt;int\u0026gt; nums = {1, 3, 2, 1, 3, 3}; map\u0026lt;int, int\u0026gt; freq; for (auto x : nums) freq[x]++; // auto-initializes to 0, then increments for (auto\u0026amp; [val, cnt] : freq) cout \u0026lt;\u0026lt; val \u0026lt;\u0026lt; \u0026#34; appears \u0026#34; \u0026lt;\u0026lt; cnt \u0026lt;\u0026lt; \u0026#34; times\\n\u0026#34;; // Output is in sorted key order Map always keeps keys sorted. Iterating a map gives you pairs in ascending key order. This is useful when you need sorted output.\n4.10 unordered_map — Fast Map, No Order Same as map but O(1) average instead of O(log n). Identical API.\nunordered_map\u0026lt;int, int\u0026gt; um; um[key]++; um.count(key); um.find(key); // everything same as map When to use which:\nSituation Use Need sorted keys map Just need fast lookup/counting unordered_map Not sure unordered_map (it\u0026rsquo;s faster) One warning: In rare contest scenarios with adversarial inputs, unordered_map can degrade to O(n) per operation due to hash collisions. If you\u0026rsquo;re getting TLE with unordered_map, switch to map.\n4.11 string — Character Sequences Strings in C++ behave like vectors of characters with extra operations.\nCore operations:\nstring s = \u0026#34;helloworld\u0026#34;; s.length(); // 10, same as s.size() s[i]; // access character by index s[0] = \u0026#39;H\u0026#39;; // modify character s.push_back(\u0026#39;!\u0026#39;); // append character s.pop_back(); // remove last character s + \u0026#34; world\u0026#34;; // concatenation (creates new string) substr(start, length) — second argument is LENGTH, not end index:\nstring s = \u0026#34;helloworld\u0026#34;; // 0123456789 s.substr(5); // \u0026#34;world\u0026#34; — from index 5 to end s.substr(0, 5); // \u0026#34;hello\u0026#34; — from 0, length 5 s.substr(2, 3); // \u0026#34;llo\u0026#34; — from 2, length 3 // ⚠️ Common mistake: s.substr(2, 5); // \u0026#34;llowo\u0026#34; — NOT s[2..5] // Second arg is how many chars to take, not where to stop find — locate a substring:\nint pos = s.find(\u0026#34;world\u0026#34;); // returns index, or string::npos if not found if (s.find(\u0026#34;world\u0026#34;) != string::npos) cout \u0026lt;\u0026lt; \u0026#34;found at \u0026#34; \u0026lt;\u0026lt; pos \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // string::npos is a special sentinel value meaning \u0026#34;not found\u0026#34; // Just like map.end() means \u0026#34;key not found\u0026#34; Splitting a string by spaces:\nstring sentence = \u0026#34;the cat sat on the mat\u0026#34;; string word; vector\u0026lt;string\u0026gt; words; stringstream ss(sentence); while (ss \u0026gt;\u0026gt; word) words.push_back(word); // words = {\u0026#34;the\u0026#34;, \u0026#34;cat\u0026#34;, \u0026#34;sat\u0026#34;, \u0026#34;on\u0026#34;, \u0026#34;the\u0026#34;, \u0026#34;mat\u0026#34;} stringstream treats a string like cin. The \u0026gt;\u0026gt; operator reads one whitespace-delimited token at a time. This pattern appears in almost every string manipulation problem.\nCharacter frequency — the c - 'a' trick:\n// Maps lowercase letters to indices 0-25 // \u0026#39;a\u0026#39; - \u0026#39;a\u0026#39; = 0, \u0026#39;b\u0026#39; - \u0026#39;a\u0026#39; = 1, ..., \u0026#39;z\u0026#39; - \u0026#39;a\u0026#39; = 25 int freq[26] = {0}; for (char c : s) freq[c - \u0026#39;a\u0026#39;]++; // Reverse: index back to character char ch = (char)(\u0026#39;a\u0026#39; + i); This avoids a map for simple character frequency problems. O(1) access instead of O(log n).\nString ↔ number conversion:\nstring s = \u0026#34;42\u0026#34;; int n = stoi(s); // string to int long long n = stoll(s); // string to long long double d = stod(s); // string to double int n = 42; string s = to_string(n); // int to string Comparison — lexicographic by default:\nstring a = \u0026#34;apple\u0026#34;, b = \u0026#34;banana\u0026#34;; a == b; // false a \u0026lt; b; // true — \u0026#39;a\u0026#39; \u0026lt; \u0026#39;b\u0026#39; alphabetically Sorting a vector\u0026lt;string\u0026gt; with sort works automatically because \u0026lt; is defined on strings.\n5. Block 3 — Essential STL Algorithms All STL algorithms operate on ranges defined by two iterators: begin() and end(). Think of begin() as a pointer to the first element, and end() as a pointer to one-past-the-last element.\nsort(v.begin(), v.end()); // ^^^^^^^^^^ ^^^^^^^^ // start here stop before here 5.1 sort — The Most Used Algorithm // Ascending (default) sort(v.begin(), v.end()); // Descending — using built-in greater sort(v.begin(), v.end(), greater\u0026lt;int\u0026gt;()); // Custom — using lambda sort(v.begin(), v.end(), [](auto\u0026amp; a, auto\u0026amp; b) { return a.second \u0026lt; b.second; // by second element of pair }); // Sort only part of the vector sort(v.begin(), v.begin() + k); // sort first k elements sort(v.begin() + l, v.begin() + r + 1); // sort range [l, r] sort is O(n log n). It uses introsort internally (hybrid of quicksort, heapsort, insertion sort).\nstable_sort — same signature, preserves relative order of equal elements. O(n log n) but slightly slower. Use only when the problem explicitly requires stability.\n5.2 binary_search, lower_bound, upper_bound All three require a sorted array. Always sort first.\nbinary_search — does value exist?\nbool found = binary_search(v.begin(), v.end(), target); // Returns true or false only. Does not give position. lower_bound — first position where value could be inserted (first element \u0026gt;= target):\nvector\u0026lt;int\u0026gt; v = {1, 2, 4, 4, 5, 6}; // 0 1 2 3 4 5 auto it = lower_bound(v.begin(), v.end(), 4); int idx = it - v.begin(); // → 2 (first index of 4) upper_bound — position after all occurrences (first element \u0026gt; target):\nauto it = upper_bound(v.begin(), v.end(), 4); int idx = it - v.begin(); // → 4 (one past last 4) Together — count occurrences:\nauto lo = lower_bound(v.begin(), v.end(), x); auto hi = upper_bound(v.begin(), v.end(), x); int count = hi - lo; // number of times x appears This is O(log n) and replaces a linear scan for counting in sorted arrays.\nConverting iterator to index:\n// Subtract begin() from any iterator to get the index auto it = lower_bound(v.begin(), v.end(), x); int index = it - v.begin(); 5.3 Other Algorithms reverse:\nreverse(v.begin(), v.end()); // reverses vector in place reverse(s.begin(), s.end()); // reverses string in place min_element and max_element:\nauto it = min_element(v.begin(), v.end()); cout \u0026lt;\u0026lt; *it; // value — dereference the iterator int idx = it - v.begin(); // index of minimum auto it = max_element(v.begin(), v.end()); cout \u0026lt;\u0026lt; *it; accumulate — sum (or any reduction):\n#include \u0026lt;numeric\u0026gt; // or just use bits/stdc++.h int sum = accumulate(v.begin(), v.end(), 0); // Third argument is the starting value — almost always 0 next_permutation — generate all permutations:\nvector\u0026lt;int\u0026gt; v = {1, 2, 3}; sort(v.begin(), v.end()); // must start sorted to get all permutations do { for (int x : v) cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } while (next_permutation(v.begin(), v.end())); // Prints all 6 permutations of {1, 2, 3} in lexicographic order 6. Block 4 — Structures for DSA 6.1 struct — Custom Data Bundling When you need more than two related values, use a struct instead of a pair.\nstruct Edge { int from, to, weight; }; // Create and use Edge e = {0, 1, 5}; cout \u0026lt;\u0026lt; e.from \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; e.to \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; e.weight; // Vector of structs vector\u0026lt;Edge\u0026gt; edges; edges.push_back({0, 1, 4}); edges.push_back({1, 2, 3}); Functions inside struct:\nstruct Student { string name; int grade; bool isPass() { return grade \u0026gt;= 75; } }; Student s = {\u0026#34;alice\u0026#34;, 80}; s.isPass(); // true 6.2 Constructor and Initializer List A constructor is a special function that runs automatically when you create an object.\nstruct ListNode { int val; ListNode* next; // Constructor ListNode(int x) : val(x), next(nullptr) {} // ^^^^^^ ^^^^^^^^^^^^ // set val=x set next=nullptr }; // Usage ListNode* node = new ListNode(5); // node-\u0026gt;val = 5, node-\u0026gt;next = nullptr — set by constructor The : val(x), next(nullptr) part is the initializer list — it sets member variables during construction. It is exactly equivalent to writing this inside the {}:\nListNode(int x) { val = x; next = nullptr; } The initializer list version is preferred — it\u0026rsquo;s more compact and slightly more efficient.\n6.3 operator\u0026lt; Overloading — Custom Default Sort Order When you call sort on a vector of your custom struct, C++ doesn\u0026rsquo;t know how to compare your structs. You either provide a lambda comparator, or you define operator\u0026lt; inside the struct.\nDefining operator\u0026lt; means: \u0026ldquo;when C++ needs to know if a \u0026lt; b, use this logic.\u0026rdquo;\nstruct Edge { int from, to, weight; bool operator\u0026lt;(const Edge\u0026amp; other) const { return weight \u0026lt; other.weight; // sort by weight ascending } // `other` = the right side of \u0026lt; // `this` = the left side of \u0026lt; }; sort(edges.begin(), edges.end()); // uses operator\u0026lt; — no lambda needed Once operator\u0026lt; is defined, sort, set, map, and priority_queue all use it automatically.\nLambda vs operator\u0026lt; — when to use which:\nSituation Use One-off sort in one place Lambda comparator Same struct sorted the same way everywhere operator\u0026lt; Need to put struct in a set or map operator\u0026lt; required 6.4 Node Structures — Linked List and Tree These exact structs appear in LeetCode/competitive programming problems. You need to be able to write them from memory.\nLinked List Node:\nstruct ListNode { int val; ListNode* next; ListNode(int x) : val(x), next(nullptr) {} }; // Build 1 -\u0026gt; 2 -\u0026gt; 3 ListNode* head = new ListNode(1); head-\u0026gt;next = new ListNode(2); head-\u0026gt;next-\u0026gt;next = new ListNode(3); Binary Tree Node:\nstruct TreeNode { int val; TreeNode* left; TreeNode* right; TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; // Build: // 1 // / \\ // 2 3 TreeNode* root = new TreeNode(1); root-\u0026gt;left = new TreeNode(2); root-\u0026gt;right = new TreeNode(3); Graph Edge Struct:\nstruct Edge { int to, weight; }; vector\u0026lt;vector\u0026lt;Edge\u0026gt;\u0026gt; adj(n); // adjacency list with weights adj[u].push_back({v, w}); 7. Block 5 — Pointers and Memory Pointers are required for linked lists, trees, and any dynamically allocated node-based structure. Understanding them is non-negotiable for DSA.\n7.1 What is a Pointer? Every variable lives at some address in memory. A pointer is a variable that stores that address.\nint x = 42; // x lives at address 1000 (example) // memory at address 1000 holds the value 42 int* p = \u0026amp;x; // p holds the value 1000 (the address of x) // the * in declaration means \u0026#34;this is a pointer to int\u0026#34; // the \u0026amp; in \u0026amp;x means \u0026#34;give me the address of x\u0026#34; 7.2 The Two Uses of * and \u0026amp; — Read This Carefully Both symbols have two completely different meanings depending on context. This is the single most confusing thing about pointers for beginners.\n* has two meanings:\nint* p = \u0026amp;x; // Meaning 1: DECLARATION — \u0026#34;p is a pointer to int\u0026#34; cout \u0026lt;\u0026lt; *p; // Meaning 2: DEREFERENCE — \u0026#34;value at the address p holds\u0026#34; \u0026amp; has two meanings:\nint\u0026amp; ref = x; // Meaning 1: DECLARATION — \u0026#34;ref is a reference to int\u0026#34; int* p = \u0026amp;x; // Meaning 2: ADDRESS-OF — \u0026#34;give me the address of x\u0026#34; In summary:\nSymbol In a declaration (int* p, int\u0026amp; r) On a variable (*p, \u0026amp;x) * \u0026ldquo;this is a pointer to\u0026hellip;\u0026rdquo; \u0026ldquo;value at address\u0026rdquo; (dereference) \u0026amp; \u0026ldquo;this is a reference to\u0026hellip;\u0026rdquo; \u0026ldquo;address of this variable\u0026rdquo; Putting it together:\nint x = 42; int* p = \u0026amp;x; // p = address of x (say, 1000) cout \u0026lt;\u0026lt; p; // prints 1000 — the address (rarely useful) cout \u0026lt;\u0026lt; *p; // prints 42 — the value at that address *p = 99; // change the value at that address cout \u0026lt;\u0026lt; x; // prints 99 — x changed because p pointed to it 7.3 new — Allocating on the Heap // Stack allocation — automatic cleanup when scope ends int x = 5; // lives until } of current scope // Heap allocation — lives until you delete it int* p = new int(5); // allocate one int, initialize to 5 int* arr = new int[100]; // allocate array of 100 ints For DSA, you mainly use new to create nodes:\nListNode* node = new ListNode(5); // Allocates a ListNode on the heap // node holds the address of that node // node-\u0026gt;val = 5, node-\u0026gt;next = nullptr In competitive programming, you almost never call delete. Memory leaks don\u0026rsquo;t matter in contest submissions — the process ends and the OS reclaims everything. In production code, you would always delete what you new.\n7.4 nullptr — The Empty Pointer nullptr means \u0026ldquo;this pointer points to nothing.\u0026rdquo; Like null in Java or None in Python.\nListNode* p = nullptr; // p points to nothing // Always check before dereferencing if (p != nullptr) { cout \u0026lt;\u0026lt; p-\u0026gt;val; // safe } // Shorter — pointers evaluate to false when null if (p) { cout \u0026lt;\u0026lt; p-\u0026gt;val; // same thing } Accessing a nullptr pointer is undefined behavior — on most systems, an immediate crash. This is the most common runtime error in pointer code.\n7.5 The -\u0026gt; Operator When you have a pointer to a struct, use -\u0026gt; to access members. When you have the struct directly, use .\nListNode node(5); // direct struct cout \u0026lt;\u0026lt; node.val; // use dot ListNode* p = \u0026amp;node; // pointer to struct cout \u0026lt;\u0026lt; p-\u0026gt;val; // use arrow // -\u0026gt; is just shorthand: p-\u0026gt;val == (*p).val // The arrow dereferences and accesses in one step Memory rule: Pointer → use -\u0026gt;. Direct object → use .\n7.6 Linked List Patterns The linked list traversal template. Write this until it is automatic:\n// Traverse — print all values ListNode* curr = head; while (curr != nullptr) { // condition: curr not null cout \u0026lt;\u0026lt; curr-\u0026gt;val \u0026lt;\u0026lt; \u0026#34; \u0026#34;; curr = curr-\u0026gt;next; // move to next node } Why curr != nullptr and NOT curr-\u0026gt;next != nullptr:\ncurr != nullptr — stops AFTER the last node (processes every node including last). curr-\u0026gt;next != nullptr — stops AT the last node (misses the last node\u0026rsquo;s value).\nAlways use curr != nullptr for traversal.\nReverse a linked list — the canonical interview problem:\nListNode* reverse(ListNode* head) { ListNode* prev = nullptr; ListNode* curr = head; ListNode* next = nullptr; while (curr != nullptr) { next = curr-\u0026gt;next; // 1. save next before we overwrite it curr-\u0026gt;next = prev; // 2. flip the pointer prev = curr; // 3. advance prev curr = next; // 4. advance curr } return prev; // prev is the new head } The four steps in the loop — in order, every time: save, flip, advance prev, advance curr.\n7.7 Tree Traversals Inorder (left → root → right) — gives sorted order for BST:\nvoid inorder(TreeNode* root) { if (root == nullptr) return; // base case inorder(root-\u0026gt;left); cout \u0026lt;\u0026lt; root-\u0026gt;val \u0026lt;\u0026lt; \u0026#34; \u0026#34;; inorder(root-\u0026gt;right); } Preorder (root → left → right):\nvoid preorder(TreeNode* root) { if (!root) return; cout \u0026lt;\u0026lt; root-\u0026gt;val \u0026lt;\u0026lt; \u0026#34; \u0026#34;; preorder(root-\u0026gt;left); preorder(root-\u0026gt;right); } Postorder (left → right → root):\nvoid postorder(TreeNode* root) { if (!root) return; postorder(root-\u0026gt;left); postorder(root-\u0026gt;right); cout \u0026lt;\u0026lt; root-\u0026gt;val \u0026lt;\u0026lt; \u0026#34; \u0026#34;; } The pattern: always check if (!root) return; first. This is the base case that stops recursion.\n8. Block 6 — Performance Knowledge You don\u0026rsquo;t need to understand computer architecture. You need to know which operations are fast and which are slow.\n8.1 STL Complexity Reference Container Operation Complexity vector push_back O(1) amortized vector insert at middle O(n) — avoid in hot loops vector operator[] O(1) stack / queue push, pop, top/front O(1) priority_queue push, pop O(log n) priority_queue top O(1) set / map insert, find, erase O(log n) unordered_set / unordered_map insert, find, erase O(1) avg sort — O(n log n) binary_search — O(log n) lower_bound / upper_bound — O(log n) The critical comparison: If you\u0026rsquo;re doing 100,000 lookups, unordered_map takes 100,000 × O(1) = O(n). map takes 100,000 × O(log n) = O(n log n). For n = 10^5, that\u0026rsquo;s the difference between 10^5 and ~1.7 × 10^6 operations.\n8.2 Avoiding Unnecessary Copies Copies are silent performance killers. They don\u0026rsquo;t cause errors — they just make your code slower.\n// ❌ All of these make copies for (auto v : matrix) // copies each row for (string s : words) // copies each string void solve(vector\u0026lt;int\u0026gt; v) // copies entire vector // ✅ References — no copy for (auto\u0026amp; v : matrix) for (auto\u0026amp; s : words) void solve(vector\u0026lt;int\u0026gt;\u0026amp; v) Rule of thumb: If the object is larger than a pointer (8 bytes on 64-bit), use a reference.\n8.3 Move Semantics — One Paragraph When you return a vector from a function, you might expect it to be copied. In modern C++ (C++11 and later), the compiler applies Return Value Optimization (RVO) — it constructs the vector directly in the caller\u0026rsquo;s memory, with zero copies. If RVO doesn\u0026rsquo;t apply, the compiler uses move semantics: instead of copying all the data, it \u0026ldquo;moves\u0026rdquo; ownership (just copies a pointer). The result: returning large containers from functions is cheap. Write clean code; don\u0026rsquo;t sacrifice clarity to avoid returning vectors.\nvector\u0026lt;int\u0026gt; buildResult() { vector\u0026lt;int\u0026gt; result; // ... fill result return result; // zero or near-zero cost — compiler optimizes this } 9. Block 7 — Coding Templates These are the exact skeletons you will use in real problems. Learn the shape of each one. When you encounter a problem, recognize which template fits, then fill in the logic.\n9.1 Frequency Counting unordered_map\u0026lt;int, int\u0026gt; freq; for (auto x : nums) freq[x]++; // Find most frequent int maxFreq = 0; int result = -1; for (auto\u0026amp; [val, cnt] : freq) { if (cnt \u0026gt; maxFreq) { maxFreq = cnt; result = val; } } 9.2 Two Pointers Use when: array is sorted and you\u0026rsquo;re looking for pairs satisfying a condition.\nsort(nums.begin(), nums.end()); // sort first if not already sorted int l = 0, r = n - 1; while (l \u0026lt; r) { int sum = nums[l] + nums[r]; if (sum == target) { // found a pair l++; r--; } else if (sum \u0026lt; target) { l++; // need larger sum } else { r--; // need smaller sum } } 9.3 Sliding Window — Fixed Size Use when: you need maximum/minimum/sum of all subarrays of exactly size k.\nint win = 0, best = 0; // Build first window for (int i = 0; i \u0026lt; k; i++) win += nums[i]; best = win; // Slide — add right element, remove left element for (int i = k; i \u0026lt; n; i++) { win += nums[i]; // add new right element win -= nums[i - k]; // remove element that left the window best = max(best, win); } 9.4 Sliding Window — Variable Size Use when: you need the longest/shortest subarray satisfying a condition.\nint l = 0, best = 0; unordered_map\u0026lt;int, int\u0026gt; window; for (int r = 0; r \u0026lt; n; r++) { window[nums[r]]++; // expand right while (/* condition violated */) { window[nums[l]]--; // shrink left if (window[nums[l]] == 0) window.erase(nums[l]); l++; } best = max(best, r - l + 1); // window size = r - l + 1 } 9.5 Binary Search Template int l = 0, r = n - 1; while (l \u0026lt;= r) { // note: \u0026lt;=, not \u0026lt; int mid = l + (r - l) / 2; // avoids integer overflow if (nums[mid] == target) { return mid; } else if (nums[mid] \u0026lt; target) { l = mid + 1; } else { r = mid - 1; } } return -1; // not found Why l + (r - l) / 2 instead of (l + r) / 2: If l and r are both close to INT_MAX, their sum overflows. The first form never overflows.\n9.6 Prefix Sum Use when: you need to answer multiple range sum queries efficiently.\nint n = nums.size(); vector\u0026lt;int\u0026gt; prefix(n + 1, 0); // Build — O(n) for (int i = 0; i \u0026lt; n; i++) prefix[i + 1] = prefix[i] + nums[i]; // Query sum from index l to r inclusive — O(1) int rangeSum = prefix[r + 1] - prefix[l]; Without prefix sum: each range query is O(n). With prefix sum: O(1) per query after O(n) preprocessing.\n9.7 DFS — Recursive Graph Traversal vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; adj(n); // build this from input edges vector\u0026lt;bool\u0026gt; visited(n, false); void dfs(int u) { visited[u] = true; // process u here (before neighbors = preorder) for (auto v : adj[u]) { if (!visited[v]) { dfs(v); } } // process u here (after neighbors = postorder) } // Call from main dfs(startNode); 9.8 BFS — Level-Order Graph Traversal vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; adj(n); vector\u0026lt;bool\u0026gt; visited(n, false); vector\u0026lt;int\u0026gt; dist(n, -1); // optional: track distance from source queue\u0026lt;int\u0026gt; q; q.push(start); visited[start] = true; dist[start] = 0; while (!q.empty()) { int u = q.front(); q.pop(); for (auto v : adj[u]) { if (!visited[v]) { visited[v] = true; dist[v] = dist[u] + 1; // one step further q.push(v); } } } BFS gives shortest path (in number of edges) in unweighted graphs.\n9.9 Graph — Building Adjacency List int n, m; // n nodes (0-indexed), m edges cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; adj(n); // unweighted for (int i = 0; i \u0026lt; m; i++) { int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; adj[u].push_back(v); adj[v].push_back(u); // remove for directed graph } // Weighted graph vector\u0026lt;vector\u0026lt;pair\u0026lt;int,int\u0026gt;\u0026gt;\u0026gt; adj(n); // adj[u] = {v, weight} int u, v, w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; adj[u].push_back({v, w}); adj[v].push_back({u, w}); 9.10 Dijkstra\u0026rsquo;s Shortest Path For weighted graphs — finds shortest distance from source to all nodes.\nvector\u0026lt;vector\u0026lt;pair\u0026lt;int,int\u0026gt;\u0026gt;\u0026gt; adj(n); // adj[u] = {v, weight} vector\u0026lt;int\u0026gt; dist(n, INT_MAX); priority_queue\u0026lt;pair\u0026lt;int,int\u0026gt;, vector\u0026lt;pair\u0026lt;int,int\u0026gt;\u0026gt;, greater\u0026lt;pair\u0026lt;int,int\u0026gt;\u0026gt;\u0026gt; pq; // min-heap dist[src] = 0; pq.push({0, src}); // {distance, node} while (!pq.empty()) { auto [d, u] = pq.top(); pq.pop(); if (d \u0026gt; dist[u]) continue; // outdated entry — skip for (auto [v, w] : adj[u]) { if (dist[u] + w \u0026lt; dist[v]) { dist[v] = dist[u] + w; pq.push({dist[v], v}); } } } // dist[i] = shortest distance from src to i // INT_MAX means unreachable 9.11 Memoization (Top-Down DP) unordered_map\u0026lt;int, long long\u0026gt; memo; long long solve(int n) { // Base case if (n \u0026lt;= 1) return n; // Return cached result if already computed if (memo.count(n)) return memo[n]; // Compute, store, return return memo[n] = solve(n - 1) + solve(n - 2); } The three-step pattern: base case → check cache → compute and store.\n9.12 Sorting Pairs and Structs // Sort vector of pairs by second element descending sort(v.begin(), v.end(), [](auto\u0026amp; a, auto\u0026amp; b) { return a.second \u0026gt; b.second; }); // Sort structs with multiple keys sort(students.begin(), students.end(), [](auto\u0026amp; a, auto\u0026amp; b) { if (a.grade != b.grade) return a.grade \u0026gt; b.grade; // grade descending return a.name \u0026lt; b.name; // name ascending on tie }); // Sort edges by weight (using operator\u0026lt;) sort(edges.begin(), edges.end()); // uses operator\u0026lt; if defined 10. Common Mistakes Quick Reference These are mistakes that produce incorrect output or crashes with no compiler error. Silent bugs.\nMistake Wrong Correct Map existence check if (m[key]) — creates key if (m.count(key)) Multiset erase one ms.erase(x) — removes ALL ms.erase(ms.find(x)) Linked list loop curr-\u0026gt;next != nullptr — misses last curr != nullptr Comparator return a \u0026gt;= b — UB return a \u0026gt; b (strict) Priority queue access pq.front() — no such function pq.top() substr second arg s.substr(2, 5) — means length 5 second arg is length Loop copy for (auto x : v) for strings/pairs for (auto\u0026amp; x : v) Sign comparison for (int i = 0; i \u0026lt; v.size(); ...) int n = v.size(); i \u0026lt; n Overflow (l + r) / 2 l + (r - l) / 2 Null dereference p-\u0026gt;val without checking if (p) p-\u0026gt;val Redundant includes #include \u0026lt;bits/stdc++.h\u0026gt; + others only bits/stdc++.h Slow output cout \u0026lt;\u0026lt; endl in tight loop cout \u0026lt;\u0026lt; \u0026quot;\\n\u0026quot; endl vs \u0026quot;\\n\u0026quot;: endl flushes the output buffer every call. \u0026quot;\\n\u0026quot; does not. In problems with 100,000+ output lines, using endl can cause TLE by itself. Always use \u0026quot;\\n\u0026quot;.\n11. Complexity Cheat Sheet Big-O intuition for contest constraints:\nn (input size) Max acceptable complexity n ≤ 10 O(n!) — permutations fine n ≤ 20 O(2^n) — bitmask DP fine n ≤ 500 O(n³) n ≤ 5,000 O(n²) n ≤ 10^6 O(n log n) n ≤ 10^8 O(n) Container operations:\nvector push_back O(1) access O(1) insert-mid O(n) stack push/pop O(1) top O(1) queue push/pop O(1) front O(1) pq push/pop O(logn) top O(1) set/map insert O(logn) find O(logn) erase O(logn) uo_set/map insert O(1)* find O(1)* erase O(1)* sort O(n logn) bin_search O(logn) *average case, O(n) worst case for unordered containers\nThese notes cover everything required to implement standard DSA topics in C++ cleanly and quickly. The templates in Block 7 are the payoff — the rest of the notes exist to make those templates readable and writable without hesitation.\n","permalink":"/explore/cpp-notes/","summary":"\u003ch3 id=\"complete-notes--from-zero-to-solving-problems\"\u003eComplete Notes — From Zero to Solving Problems\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eHow to use these notes:\u003c/strong\u003e Read each section once top to bottom. Then close the notes and try to recall the patterns. Come back only to verify. Retention comes from recall, not re-reading.\u003c/p\u003e\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-the-mindset\"\u003e1. The Mindset\u003c/h2\u003e\n\u003cp\u003eThis is not a general C++ course. The goal is one thing:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eAlgorithm idea → immediate, clean C++ implementation\u003c/strong\u003e\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003eEvery concept in these notes exists to eliminate the friction between knowing how to solve a problem and actually writing the code. If a topic does not appear in DSA solutions, it is not here.\u003c/p\u003e","title":"C++ for DSA \u0026 Competitive Programming"},{"content":"This is a very naive and beginner way to just get the thing done in the most easy and raw way possible, I won\u0026rsquo;t be using any specific library or complex code for this project, this is just an exploration code.\nOk, so I am a beginner coder, currently in my sophomore year of college, and recently I got super obsessed with running local AI models on my laptop using Ollama.\nBut there is a huge, annoying problem with local LLMs: They are basically goldfish.\nEvery time you restart your python script, the AI forgets who you are, what you like, and what you talked about yesterday. I wanted to code a simple, human-like memory for my AI from scratch.\nI started with a really basic idea, hit a bunch of walls, and finally built something that actually works like a human brain. Here is my exact thinking process, all the logic explained simply, and the raw Python code to do it yourself.\nThe Brainstorming \u0026amp; Fixing My Dumb Logic Alright, so let\u0026rsquo;s begin with the thinking. My very first draft was simple: Just maintain a JSON file, dump all the chat history in it, and send the whole file to the AI every time I type a message.\nQ: Wait, that sounds easy! Why is that a bad idea? A: Because of the Token Limit (Context Window). Imagine the AI\u0026rsquo;s brain has a tiny RAM size of 4,000 words. If I keep shoving our entire 6-month chat history into every single prompt, the AI will crash or completely forget the actual question I just asked.\nQ: Okay, so we can\u0026rsquo;t save the whole chat. What if we divide it? Keep old chats in a folder, and only give the AI the current chat? A: Better! But what if I ask, \u0026ldquo;Hey, what was that movie I told you I liked last week?\u0026rdquo; If the AI only has the current chat in its RAM, it can\u0026rsquo;t answer.\nQ: So how do human brains do it? A: Exactly! Humans don\u0026rsquo;t remember every single word of a conversation. We remember Facts, Intents, and Emotions. If I tell you, \u0026ldquo;I went to the park yesterday and I was looking at a golden retriever and suddenly some stupid just entered the park with his motor bike and almost hit me, I got startled and jumped in the water\u0026rdquo; you don\u0026rsquo;t memorize the sentence. Your brain just extracts: [friend jumped in pool when a bike came unexpectedly to hit him in the park yesterday].\nWe also forget things over time to keep our brains fast and clutter-free.\nSo, my final logic for the JSON file was just a list of extracted Facts with a Strength Score.\nThe Raw Code \u0026amp; Core Logic Let\u0026rsquo;s build the basic engine\u0026hellip;\nFirst, let\u0026rsquo;s setup our brain.json structure. It looks like this:\n{ \u0026#34;short_term\u0026#34;: [], \u0026#34;long_term\u0026#34;: [ { \u0026#34;memory\u0026#34;: \u0026#34;Eshan loves coding in Python\u0026#34;, \u0026#34;strength\u0026#34;: 10, \u0026#34;last_mentioned\u0026#34;: \u0026#34;2024-10-25\u0026#34; } ] } Now, let\u0026rsquo;s write the code to talk to Ollama and inject only the relevant memories.\nimport json import requests from datetime import datetime import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) BRAIN_FILE = os.path.join(BASE_DIR, \u0026#39;brain.json\u0026#39;) def load_brain(): try: with open(BRAIN_FILE, \u0026#39;r\u0026#39;) as f: return json.load(f) except FileNotFoundError: return {\u0026#34;short_term\u0026#34;: [], \u0026#34;long_term\u0026#34;: []} def save_brain(brain_data): with open(BRAIN_FILE, \u0026#39;w\u0026#39;) as f: json.dump(brain_data, f, indent=4) def ask_ollama(prompt, system_prompt=\u0026#34;You are a helpful AI.\u0026#34;): url = \u0026#34;http://localhost:11434/api/generate\u0026#34; payload = { \u0026#34;model\u0026#34;: \u0026#34;gemma2:2b\u0026#34;, \u0026#34;prompt\u0026#34;: prompt, \u0026#34;system\u0026#34;: system_prompt, \u0026#34;stream\u0026#34;: False } response = requests.post(url, json=payload) return response.json()[\u0026#39;response\u0026#39;] def chat_with_ai(user_input): brain = load_brain() # --- Build context from long term memory --- # Just grab all memories and pass them so AI knows who user is long_term_text = \u0026#34;\u0026#34; if brain[\u0026#34;long_term\u0026#34;]: long_term_text = \u0026#34;What you remember about this person:\\n\u0026#34; for m in brain[\u0026#34;long_term\u0026#34;]: long_term_text += f\u0026#34;- {m[\u0026#39;memory\u0026#39;]}\\n\u0026#34; # --- Build context from short term (current session conversation) --- # This is so AI remembers what was JUST said in this conversation short_term_text = \u0026#34;\u0026#34; for turn in brain[\u0026#34;short_term\u0026#34;]: short_term_text += f\u0026#34;User: {turn[\u0026#39;user\u0026#39;]}\\nAI: {turn[\u0026#39;ai\u0026#39;]}\\n\u0026#34; system_prompt = f\u0026#34;\u0026#34;\u0026#34;You are a personal AI assistant who remembers the user. {long_term_text} Conversation so far today: {short_term_text}\u0026#34;\u0026#34;\u0026#34; # Get AI response ai_response = ask_ollama(user_input, system_prompt) # Save this turn to short term brain[\u0026#34;short_term\u0026#34;].append({ \u0026#34;user\u0026#34;: user_input, \u0026#34;ai\u0026#34;: ai_response }) save_brain(brain) print(\u0026#34;AI:\u0026#34;, ai_response) Q: Wait, where does the long_term list come from? How does the AI extract the facts? A: I\u0026rsquo;m so glad you asked. That brings us to the next level.\nThe \u0026ldquo;Sleep Cycle\u0026rdquo; \u0026amp; Forgetting Curve If we force the AI to extract facts while we are chatting, the chat will lag. It will take 10 seconds to reply.\nQ: So when does it learn? A: When it sleeps! Or, in computer terms, when we stop typing.\nI wrote a background function called consolidate_memory(). You run this function when the chat session ends (like when the user types \u0026ldquo;bye\u0026rdquo;). It takes the short_term buffer, asks Ollama to extract the hard facts, moves them to long_term, and clears the buffer.\ndef consolidate_memory(): brain = load_brain() if len(brain[\u0026#34;short_term\u0026#34;]) == 0: print(\u0026#34;Nothing to remember. Goodnight!\u0026#34;) return print(\u0026#34;Processing memories...\u0026#34;) today = str(datetime.now().date()) for turn in brain[\u0026#34;short_term\u0026#34;]: user_message = turn[\u0026#34;user\u0026#34;] # IMPROVEMENT 1: Added explicit instructions to ignore questions prompt = f\u0026#34;\u0026#34;\u0026#34;Extract facts about the user from this message. Rules: 1. If the message is a question, contains no facts, or is conversational filler, return: NOTHING 2. Only extract stated personal facts (hobbies, preferences, name, job, etc). 3. Do not output sentences, just raw facts joined by |||. Message: \u0026#34;{user_message}\u0026#34; Output:\u0026#34;\u0026#34;\u0026#34; result = ask_ollama(prompt, \u0026#34;You are a data extractor. Return \u0026#39;NOTHING\u0026#39; for questions or filler.\u0026#34;) result = result.strip() if not result or result.upper() == \u0026#34;NOTHING\u0026#34; or len(result) \u0026lt; 5: continue facts = [f.strip() for f in result.split(\u0026#34;|||\u0026#34;) if len(f.strip()) \u0026gt; 5] for fact in facts: # IMPROVEMENT 2: Check for duplicates to increase strength found_existing = False for existing in brain[\u0026#34;long_term\u0026#34;]: # Logic for overlap detection existing_words = set(existing[\u0026#34;memory\u0026#34;].lower().split()) new_words = set(fact.lower().split()) overlap = len(existing_words \u0026amp; new_words) # If they are very similar (more than 60% overlap) if overlap / max(len(new_words), 1) \u0026gt; 0.6: existing[\u0026#34;strength\u0026#34;] += 2 print(f\u0026#34; Strengthened: {existing[\u0026#39;memory\u0026#39;]} (Strength: {existing[\u0026#39;strength\u0026#39;]})\u0026#34;) found_existing = True break # IMPROVEMENT 3: Only add new if it wasn\u0026#39;t found if not found_existing: brain[\u0026#34;long_term\u0026#34;].append( {\u0026#34;memory\u0026#34;: fact, \u0026#34;last_mentioned\u0026#34;: today, \u0026#34;strength\u0026#34;: 10} ) print(f\u0026#34; New memory saved: {fact}\u0026#34;) brain[\u0026#34;short_term\u0026#34;] = [] save_brain(brain) print(\u0026#34;Done! Goodnight!\u0026#34;) Q: That’s so cool! But wait, you said human memory forgets stuff to keep the brain fast. How does the AI forget? A: Good catch! That’s exactly why we added that \u0026quot;strength\u0026quot;: 10 variable in the JSON. Right now, our code only adds memories. If we don\u0026rsquo;t delete the useless ones, our JSON file will explode, and the token limit will crash again.\nHere is the raw logic for forgetting: Every time you run the python script, a small function checks the dates. For every day that passes, the memory loses 1 strength point. If the strength hits 0, it vanishes (or gets moved to a cold_storage.json just in case we ever want to read old logs).\nBut, if you mention the memory again during a chat, remember in Level 2 we wrote item[\u0026quot;strength\u0026quot;] += 2? That pushes the memory back up! Just like real life: if you don’t play guitar for a year, you forget it. If you practice every day, the memory stays strong.\nLet\u0026rsquo;s write that quick forgetting function:\ndef clean_up_memories(): \u0026#34;\u0026#34;\u0026#34;Prunes memories based on strength decay over time.\u0026#34;\u0026#34;\u0026#34; brain = load_brain() today = datetime.now().date() surviving_memories = [] for item in brain[\u0026#34;long_term\u0026#34;]: # Ensure last_mentioned exists, fallback to today if missing last_date_str = item.get(\u0026#34;last_mentioned\u0026#34;, str(today)) last_date = datetime.strptime(last_date_str, \u0026#34;%Y-%m-%d\u0026#34;).date() days_passed = (today - last_date).days # Reduce strength by days passed current_strength = item[\u0026#34;strength\u0026#34;] - days_passed if current_strength \u0026gt; 0: item[\u0026#34;strength\u0026#34;] = current_strength item[\u0026#34;last_mentioned\u0026#34;] = str(today) surviving_memories.append(item) else: print(f\u0026#34; Memory faded away: {item[\u0026#39;memory\u0026#39;]}\u0026#34;) brain[\u0026#34;long_term\u0026#34;] = surviving_memories save_brain(brain) print(\u0026#34; Memory cleanup complete.\u0026#34;) Touching Perfection (And The Future) Q: Okay, so this is perfect now? A: Well, it\u0026rsquo;s perfect for a beginner project, but there is one final hurdle if you use this for years.\nRight now, my logic is super raw: I am just dumping the entire long_term list into every prompt. As the list grows, we\u0026rsquo;ll hit the token limit again!\nTo fix that, you\u0026rsquo;d eventually need to write a search function to only grab relevant memories (like if word in user_input:). But basic keyword searching is flawed. If I type \u0026ldquo;I love dogs,\u0026rdquo; a keyword search looks for the exact word \u0026ldquo;dogs.\u0026rdquo; What if I type \u0026ldquo;I love puppies\u0026rdquo;? The code won\u0026rsquo;t find the \u0026ldquo;dogs\u0026rdquo; memory. Humans don\u0026rsquo;t work like that. We understand vibes and context, not just exact words.\nThe Level-Up: Vector Databases (ChromaDB) To make this 100% perfect, you would replace the JSON search with something called a Vector Database (like ChromaDB). I won\u0026rsquo;t code it here to keep things simple, but basically, ChromaDB turns words into numbers. So \u0026ldquo;Dog\u0026rdquo; and \u0026ldquo;Puppy\u0026rdquo; are stored right next to each other mathematically.\nIf you use ChromaDB, you don\u0026rsquo;t search by words, you search by meaning. That\u0026rsquo;s how ChatGPT\u0026rsquo;s actual memory works.\nFinal Thoughts This whole project started because I wanted my AI to feel a little more human. I wanted it to say, \u0026ldquo;Hey Sam, how was your Python exam yesterday?\u0026rdquo; without me having to remind it that I even had an exam.\nBy just using a few basic lists, a JSON file, and some clever background prompts, we built a digital brain. It has a short-term memory (active chat), a sleep cycle (extraction), and a forgetting curve (strength decay).\nThe best part? It runs completely offline on your own laptop with Ollama. No subscriptions, no data stealing, just you and your code.\nIf you want to try this, copy the functions above, put them in a main.py file, loop an input() for the chat, and let me know on my GitHub what the first thing your AI remembered about you was!\nPeace out, keep coding. ✌️\nHere is the complete JSON-based code so far.\nThe Complete JSON Memory Script (Version 1.0) If you just want the raw, working code we’ve built so far using just JSON and Python, here is the entire script. Put this in a file called main.py, make sure Ollama is running in the background, and run it!\nimport json import requests from datetime import datetime import os # --- Configuration --- BASE_DIR = os.path.dirname(os.path.abspath(__file__)) BRAIN_FILE = os.path.join(BASE_DIR, \u0026#39;brain.json\u0026#39;) # --- Memory File Management --- def load_brain(): try: with open(BRAIN_FILE, \u0026#39;r\u0026#39;) as f: return json.load(f) except FileNotFoundError: # Automatically creates the corrrect structure if file doesn\u0026#39;t exist return {\u0026#34;short_term\u0026#34;: [], \u0026#34;long_term\u0026#34;:[]} def save_brain(brain_data): with open(BRAIN_FILE, \u0026#39;w\u0026#39;) as f: json.dump(brain_data, f, indent=4) # --- Core AI Communication --- def ask_ollama(prompt, system_prompt=\u0026#34;You are a helpful AI.\u0026#34;): url = \u0026#34;http://localhost:11434/api/generate\u0026#34; payload = { \u0026#34;model\u0026#34;: \u0026#34;gemma2:2b\u0026#34;, # Make sure you have this model pulled in Ollama! \u0026#34;prompt\u0026#34;: prompt, \u0026#34;system\u0026#34;: system_prompt, \u0026#34;stream\u0026#34;: False } try: response = requests.post(url, json=payload) return response.json()[\u0026#39;response\u0026#39;] except requests.exceptions.ConnectionError: return \u0026#34;Error: Could not connect to Ollama. Is it running?\u0026#34; # --- Chat Logic --- def chat_with_ai(user_input): brain = load_brain() # --- Build context from long term memory --- long_term_text = \u0026#34;\u0026#34; if brain[\u0026#34;long_term\u0026#34;]: long_term_text = \u0026#34;What you remember about this person:\\n\u0026#34; for m in brain[\u0026#34;long_term\u0026#34;]: long_term_text += f\u0026#34;- {m[\u0026#39;memory\u0026#39;]}\\n\u0026#34; # --- Build context from short term (current session conversation) --- short_term_text = \u0026#34;\u0026#34; for turn in brain[\u0026#34;short_term\u0026#34;]: short_term_text += f\u0026#34;User: {turn[\u0026#39;user\u0026#39;]}\\nAI: {turn[\u0026#39;ai\u0026#39;]}\\n\u0026#34; system_prompt = f\u0026#34;\u0026#34;\u0026#34;You are a personal AI assistant who remembers the user. {long_term_text} Conversation so far today: {short_term_text}\u0026#34;\u0026#34;\u0026#34; # Get AI response ai_response = ask_ollama(user_input, system_prompt) # Save this turn to short term buffer brain[\u0026#34;short_term\u0026#34;].append({ \u0026#34;user\u0026#34;: user_input, \u0026#34;ai\u0026#34;: ai_response }) save_brain(brain) print(\u0026#34;\\nAI:\u0026#34;, ai_response) # --- Sleep Cycle: Memory Consolidation --- def consolidate_memory(): brain = load_brain() if len(brain[\u0026#34;short_term\u0026#34;]) == 0: print(\u0026#34;Nothing to remember. Goodnight!\u0026#34;) return print(\u0026#34;\\nProcessing memories...\u0026#34;) today = str(datetime.now().date()) for turn in brain[\u0026#34;short_term\u0026#34;]: user_message = turn[\u0026#34;user\u0026#34;] prompt = f\u0026#34;\u0026#34;\u0026#34;Extract facts about the user from this message. Rules: 1. If the message is a question, contains no facts, or is conversational filler, return: NOTHING 2. Only extract stated personal facts (hobbies, preferences, name, job, etc). 3. Do not output sentences, just raw facts joined by |||. Message: \u0026#34;{user_message}\u0026#34; Output:\u0026#34;\u0026#34;\u0026#34; result = ask_ollama(prompt, \u0026#34;You are a data extractor. Return \u0026#39;NOTHING\u0026#39; for questions or filler.\u0026#34;) result = result.strip() if not result or result.upper() == \u0026#34;NOTHING\u0026#34; or len(result) \u0026lt; 5: continue facts =[f.strip() for f in result.split(\u0026#34;|||\u0026#34;) if len(f.strip()) \u0026gt; 5] for fact in facts: found_existing = False for existing in brain[\u0026#34;long_term\u0026#34;]: # Logic for overlap detection existing_words = set(existing[\u0026#34;memory\u0026#34;].lower().split()) new_words = set(fact.lower().split()) overlap = len(existing_words \u0026amp; new_words) # If they are very similar (more than 60% overlap) if overlap / max(len(new_words), 1) \u0026gt; 0.6: existing[\u0026#34;strength\u0026#34;] += 2 print(f\u0026#34; Strengthened: {existing[\u0026#39;memory\u0026#39;]} (Strength: {existing[\u0026#39;strength\u0026#39;]})\u0026#34;) found_existing = True break # Add new memory if it wasn\u0026#39;t found (Using the \u0026#39;last_mentioned\u0026#39; fix) if not found_existing: brain[\u0026#34;long_term\u0026#34;].append( {\u0026#34;memory\u0026#34;: fact, \u0026#34;last_mentioned\u0026#34;: today, \u0026#34;strength\u0026#34;: 10} ) print(f\u0026#34; New memory saved: {fact}\u0026#34;) # Clear the buffer brain[\u0026#34;short_term\u0026#34;] =[] save_brain(brain) print(\u0026#34;Done! Goodnight!\u0026#34;) # --- Forgetting Curve: Memory Cleanup --- def clean_up_memories(): \u0026#34;\u0026#34;\u0026#34;Prunes memories based on strength decay over time.\u0026#34;\u0026#34;\u0026#34; brain = load_brain() today = datetime.now().date() surviving_memories = [] for item in brain[\u0026#34;long_term\u0026#34;]: last_date_str = item.get(\u0026#34;last_mentioned\u0026#34;, str(today)) last_date = datetime.strptime(last_date_str, \u0026#34;%Y-%m-%d\u0026#34;).date() days_passed = (today - last_date).days # Reduce strength by days passed current_strength = item[\u0026#34;strength\u0026#34;] - days_passed if current_strength \u0026gt; 0: item[\u0026#34;strength\u0026#34;] = current_strength item[\u0026#34;last_mentioned\u0026#34;] = str(today) surviving_memories.append(item) else: print(f\u0026#34; Memory faded away: {item[\u0026#39;memory\u0026#39;]}\u0026#34;) brain[\u0026#34;long_term\u0026#34;] = surviving_memories save_brain(brain) print(\u0026#34;Memory cleanup complete.\u0026#34;) # --- Main Application Loop --- def main(): print(\u0026#34;--- Waking up AI ---\u0026#34;) clean_up_memories() print(\u0026#34;AI is ready! (Type \u0026#39;bye\u0026#39; to sleep and save memoriies)\\n\u0026#34;) while True: user_input = input(\u0026#34;You: \u0026#34;) if user_input.lower() in [\u0026#34;bye\u0026#34;, \u0026#34;exit\u0026#34;, \u0026#34;quit\u0026#34;]: print(\u0026#34;\\nAI: Goodbye! Going to sleep now...\u0026#34;) consolidate_memory() break chat_with_ai(user_input) if __name__ == \u0026#34;__main__\u0026#34;: main() ","permalink":"/projects/ai_memory/","summary":"\u003cp\u003eThis is a very naive and beginner way to just get the thing done in the most easy and raw way possible, I won\u0026rsquo;t be using any specific library or complex code for this project, this is just an exploration code.\u003c/p\u003e\n\u003cp\u003eOk, so I am a beginner coder, currently in my sophomore year of college, and recently I got super obsessed with running local AI models on my laptop using Ollama.\u003c/p\u003e","title":"How I built a Human-Like Memory for My Local LLM Without Using Any Special Library"},{"content":" $ whoami\nHey, I’m Eshan, a CSE student.\nThis blog is basically my learning log.\nRight now I’m focused mostly on full-stack development, so most of what gets posted here will revolve around things I’m learning, building, or figuring out along the way.\nOccasionally there might be something outside of that, some exploration, a random thing I came across, or an idea worth writing down.\nThose will stay separate though. I like things organised, and this blog reflects that.\nIf you\u0026rsquo;re into web development, or you simply like following someone\u0026rsquo;s learning journey, you’re welcome here.\nThe site will grow as I grow.\nI also keep a non-technical blog.\nMy Blog → A Quite Introduction\n","permalink":"/about/","summary":"\u003cstyle\u003e\n@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono\u0026display=swap');\n\n.hacker-about {\n  font-family: 'Share Tech Mono', monospace;\n  color: #9aff9a;\n  background: #0b0f0b;\n  padding: 1.5rem;\n  border-radius: 6px;\n  border: 1px solid #1f3b1f;\n  line-height: 1.7;\n}\n\n.hacker-about h1 {\n  color: #c7ffc7;\n  font-weight: normal;\n  margin-bottom: 1rem;\n}\n\n.hacker-about a {\n  color: #7cff7c;\n  text-decoration: none;\n  border-bottom: 1px dotted #7cff7c;\n}\n\n.hacker-about a:hover {\n  color: #caffca;\n}\n\n.hacker-about .prompt {\n  color: #6aff6a;\n}\n\u003c/style\u003e\n\u003cdiv class=\"hacker-about\"\u003e\n\u003cp\u003e\u003cspan class=\"prompt\"\u003e$ whoami\u003c/span\u003e\u003c/p\u003e\n\u003cp\u003eHey, I’m \u003cstrong\u003eEshan\u003c/strong\u003e, a CSE student.\u003c/p\u003e\n\u003cp\u003eThis blog is basically my \u003cstrong\u003elearning log\u003c/strong\u003e.\u003c/p\u003e","title":"About"}]