<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: toothbrush</title>
    <description>The latest articles on DEV Community by toothbrush (@toothbrush).</description>
    <link>https://dev.to/toothbrush</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4035967%2F0196b8d7-f16c-43aa-9e34-a4d6be6d5cf5.png</url>
      <title>DEV Community: toothbrush</title>
      <link>https://dev.to/toothbrush</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/toothbrush"/>
    <language>en</language>
    <item>
      <title>Linux Performance Troubleshooting: The 60-Second Check and Beyond</title>
      <dc:creator>toothbrush</dc:creator>
      <pubDate>Mon, 20 Jul 2026 00:49:47 +0000</pubDate>
      <link>https://dev.to/toothbrush/linux-performance-troubleshooting-the-60-second-check-and-beyond-193f</link>
      <guid>https://dev.to/toothbrush/linux-performance-troubleshooting-the-60-second-check-and-beyond-193f</guid>
      <description>&lt;p&gt;When a production server starts misbehaving — latency spikes, dropped requests, OOM kills — you don't have time to read man pages. You need a systematic approach that rules out the most common culprits in under a minute, then drills into the root cause with targeted tools. This is exactly how I debug Linux performance issues on production systems, and it works whether you're on a bare-metal box, a VM, or a container host.&lt;/p&gt;

&lt;p&gt;This guide covers the full troubleshooting workflow. Here's what we'll walk through: &lt;strong&gt;the 60-second checklist&lt;/strong&gt; (uptime, dmesg, vmstat, mpstat, pidstat, iostat, free, ss, sar), &lt;strong&gt;CPU hot-spot analysis&lt;/strong&gt; with perf and flamegraphs, &lt;strong&gt;memory pressure&lt;/strong&gt; including OOM and swap, &lt;strong&gt;disk I/O latency&lt;/strong&gt; with iostat and blktrace, &lt;strong&gt;network troubleshooting&lt;/strong&gt; with ss, tc, and tcpdump, and finally a &lt;strong&gt;decision tree&lt;/strong&gt; to choose the right tool for any symptom.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The 60-Second Checklist
&lt;/h2&gt;

&lt;p&gt;When you first SSH into a struggling box, run these commands in order. Each takes one look at a different subsystem. By the time you're done, you'll know &lt;em&gt;which&lt;/em&gt; resource is the problem.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ uptime                        # load averages — is the system under pressure?
$ dmesg -T | tail -20           # kernel messages — OOM kills, TCP drops, I/O errors
$ vmstat -SM 1 3                # system-wide CPU, memory, and I/O overview
$ mpstat -P ALL 1 1             # per-CPU balance — one core pegged or evenly spread?
$ pidstat 1 3                   # per-process CPU usage — who's burning cycles?
$ iostat -xz 1 1                # disk I/O — await, svctm, %util
$ free -m                       # memory usage — total, available, cached, swap
$ ss -tupwn                     # socket connections — listening, established, TIME_WAIT
$ sar -n DEV 1 1                # network throughput — packets/sec, errors, drops
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;I have an alias called &lt;code&gt;perf60&lt;/code&gt; that runs all of these. The point isn't to memorize every flag — it's to have a checklist you &lt;em&gt;always&lt;/em&gt; run before diagnosing deeper. Skipping this step is how people spend an hour debugging a memory leak that the OOM killer already logged in dmesg.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Pro tip: Save these commands as a shell script or alias and scp it to every new server you provision. Your future on-call self will thank you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  2. CPU — Finding the Hot Spots
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;uptime&lt;/code&gt; shows load averages significantly higher than the number of CPUs, and &lt;code&gt;mpstat&lt;/code&gt; shows high %user or %system across cores, you have a CPU-bound issue. The next step is figuring out &lt;em&gt;what&lt;/em&gt; is burning those cycles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-Process with pidstat
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pidstat -p ALL -u 1 3
Linux 6.8.0 (web-02)  07/20/2026  _x86_64_  (8 CPU)

02:14:33   UID   PID    %usr  %system  %guest  %CPU   CPU  Command
02:14:34  1001  1234   85.2    2.1     0.0     87.3   3    nginx
02:14:34     0  4567    0.3   12.4     0.0     12.7   7    kworker/u16:1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Here, nginx worker 1234 is using 87% of a single core. That's suspicious — either it's stuck in an infinite loop or it's genuinely serving heavy traffic. The high &lt;code&gt;%system&lt;/code&gt; on the kworker suggests something happening at the kernel level — check disk or network interrupts next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Profiling with perf and Flamegraphs
&lt;/h3&gt;

&lt;p&gt;When you need to know &lt;em&gt;which function&lt;/em&gt; is hot, nothing beats &lt;code&gt;perf&lt;/code&gt;. It uses hardware performance counters and stack sampling with negligible overhead — safe for production:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Record CPU samples for 30 seconds
$ perf record -F 99 -a -g -- sleep 30

# Generate the report
$ perf report --stdio --sort comm,dso,symbol
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;-F 99&lt;/code&gt; flag samples at 99 Hz (below 100 to avoid aliasing with common timer frequencies). The &lt;code&gt;-g&lt;/code&gt; flag captures call graphs. Pipe the output into Brendan Gregg's flamegraph scripts for a visual:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ perf script | ./stackcollapse-perf.pl | ./flamegraph.pl &amp;gt; cpu-flame.svg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;A flamegraph immediately shows you the hot code paths. Wide bars at the top are functions spending the most CPU time. If you see &lt;code&gt;__do_fault&lt;/code&gt; or &lt;code&gt;page_fault&lt;/code&gt; near the top, your application is page-faulting heavily — not a CPU problem at all, but a memory one.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Memory — OOM, Swap, and Page Cache
&lt;/h2&gt;

&lt;p&gt;Memory issues show up in three flavors: &lt;strong&gt;OOM kills&lt;/strong&gt; (the kernel murders a process), &lt;strong&gt;swap thrashing&lt;/strong&gt; (the system is paging like crazy), and &lt;strong&gt;page cache pressure&lt;/strong&gt; (the filesystem cache is starving applications).&lt;/p&gt;

&lt;h3&gt;
  
  
  Check OOM Kills First
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ dmesg -T | grep -i "oom&amp;amp;#124;killed"
[Jul 20 01:23:45] mysqld invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE)
[Jul 20 01:23:45] mysqld: cpuset=/ mems_allowed=0
[Jul 20 01:23:45] Tasks state (memory reserved: 0K, swap_total: 0K)
[Jul 20 01:23:45] [ 1234] mysqld 1001 12345678 23456789  12345      0             0 mysqld
[Jul 20 01:23:45] Out of memory: Killed process 1234 (mysqld) total-vm:12345678kB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The OOM killer logs every candidate process and the final victim. If you see your application here, the fix is not "add more RAM" (well, sometimes it is) — it's first checking for a memory leak.&lt;/p&gt;

&lt;h3&gt;
  
  
  Read the Numbers Correctly
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;free -m&lt;/code&gt; output can be misleading. The &lt;code&gt;available&lt;/code&gt; field is what matters:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ free -m
               total  used  free  shared  buff/cache  available
Mem:           15950  8940  1020     420       5990       6010
Swap:           2048   120  1928
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Linux uses free RAM for disk cache aggressively. The &lt;code&gt;available&lt;/code&gt; field (6010 MB) estimates how much memory is truly free for new allocations — it includes reclaimable cache. If &lt;code&gt;available&lt;/code&gt; is below 10% of total, you're in danger territory.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vmstat -SM 1&lt;/code&gt; shows swap activity. If &lt;code&gt;si&lt;/code&gt; (swap in) and &lt;code&gt;so&lt;/code&gt; (swap out) are consistently non-zero, the system is paging — and performance will be terrible:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ vmstat -SM 1
procs  -----------memory----------  ---swap--  -----io----  --system--  ------cpu-----
 r  b   swpd   free   buff   cache   si   so   bi    bo    in    cs  us  sy  id  wa  st
 2  1   120   1020    420   5990   50   45  1200   80   1500  2000  12   8  60  20   0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;si&lt;/code&gt;/&lt;code&gt;so&lt;/code&gt; of 50/45 MB/s means 95 MB/s of swap traffic. That's disk-bound paging. Every page fault now waits for disk I/O. The &lt;code&gt;wa&lt;/code&gt; column (20%) confirms it — the CPU is waiting on I/O.&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding Memory Leaks
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;pidstat -r 1&lt;/code&gt; to watch per-process RSS growth over time:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pidstat -r 1 10
02:20:00   UID   PID  minflt/s  majflt/s  VSZ    RSS    %MEM  Command
02:20:01  1001  1234  12345      0       2.5G   1.8G   11.5  java
02:20:02  1001  1234  12350      0       2.5G   1.8G   11.5  java
02:20:03  1001  1234  14420      0       2.7G   2.0G   12.6  java   ← growing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If RSS keeps climbing without stabilizing, you have a leak. For Java, run &lt;code&gt;jmap -histo:live&lt;/code&gt; to dump heap. For other apps, &lt;code&gt;/proc/PID/smaps&lt;/code&gt; gives the full picture of memory mappings.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Disk I/O — Finding the Slow Storage
&lt;/h2&gt;

&lt;p&gt;High &lt;code&gt;wa&lt;/code&gt; in vmstat or &lt;code&gt;iowait&lt;/code&gt; in mpstat points to disk contention. The &lt;code&gt;iostat -xz 1&lt;/code&gt; command reveals which device and which operation is slow:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ iostat -xz 1
Device    r/s    w/s    rkB/s    wkB/s  await  svctm  %util
sda      500     0    81920       0     24.0   0.40   20.0
sdb        0   120        0    30720    180.0  1.20   14.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Key fields to interpret:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;await&lt;/strong&gt; (ms) — average time for an I/O request to complete. Modern SSDs should be under 2 ms. Above 10 ms means something is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;svctm&lt;/strong&gt; (ms) — the actual service time for a single I/O. If await &amp;gt;&amp;gt; svctm, the requests are queuing (too much concurrency for the device).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;%util&lt;/strong&gt; — percentage of time the device was busy. Important caveat: modern NVMe SSDs can parallelize I/O deeply, so %util can hit 100% even though the device isn't saturated. Use &lt;code&gt;iostat -x 1 -m&lt;/code&gt; and watch &lt;code&gt;aqu-sz&lt;/code&gt; (average queue size) instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here sdb has an await of 180 ms — terrible. It's a write-heavy workload on what is likely a slow HDD or a network filesystem. The queue is deep: svctm is 1.2 ms while await is 180 ms, meaning requests are spending 99% of their time waiting in line.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trace Specific I/O with blktrace
&lt;/h3&gt;

&lt;p&gt;When you need to know &lt;em&gt;which process&lt;/em&gt; is thrashing the disk:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Trace sdb for 5 seconds
$ blktrace -d /dev/sdb -o - | blkparse -i - -o /tmp/blk-trace.txt
$ grep -E " D  | A  " /tmp/blk-trace.txt | awk '{print $12}' | sort | uniq -c | sort -rn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This shows you which process names are doing the most I/O. Combine with &lt;code&gt;iotop -oP&lt;/code&gt; for a live view.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Network — Connections, Drops, and Latency
&lt;/h2&gt;

&lt;p&gt;Network issues masquerade as application slowness all the time. Before blaming the code, rule out the wire.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connection State with ss
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ss -s
Total: 1234 (kernel 1567)
TCP:   567 (estab 234, closed 300, orphaned 3, synrecv 0, timewait 156/0), ports 45

Transport Total     IP        IPv6
*         1567      -         -
RAW       0         0         0
UDP       12        10        2
TCP       567       523       44
INET      579       533       46
FRAG      0         0         0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;156 TIME_WAIT connections isn't a problem by itself. But if &lt;code&gt;ss -t state time-wait&lt;/code&gt; shows thousands, you're exhausting ephemeral ports — typically caused by HTTP/1.0 short connections or a misconfigured proxy pool.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Check listen backlog drops
$ ss -ltp | grep -E "^(LISTEN|State)"
$ netstat -s | grep -i "listen.*overflowed&amp;amp;#124;LISTEN.*overflows"
    1234 times the listen queue of a socket overflowed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;A listen backlog overflow means the application isn't accepting connections fast enough. Increase &lt;code&gt;net.core.somaxconn&lt;/code&gt; and the application's backlog parameter. But this is a band-aid — the real fix is making your app accept connections faster (thread pool sizing, event loop tuning).&lt;/p&gt;

&lt;h3&gt;
  
  
  Packet Drops and Retransmits
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ netstat -s | egrep "segments retransmited|packets pruned|TCPLoss"
    8923 segments retransmited
    156 packets pruned from receive queue
    78 TCP sockets finished time wait in fast timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Retransmits above 0.1% of total segments mean packet loss on the network. Use &lt;code&gt;tcpdump -i eth0 host $UPSTREAM_HOST&lt;/code&gt; to capture and look for duplicate ACKs. It's often a saturated upstream switch port or a duplex mismatch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bandwidth and Queue Drops
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ sar -n DEV 1 1 | grep eth0
02:30:01     eth0    65000.00    8000.00    78901.00     6789.00     0.00     0.00     0.00

$ tc -s qdisc show dev eth0
qdisc pfifo_fast 0: root refcnt 2 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
 Sent 123456789 bytes 98765 pkt (dropped 456, overlimits 0)
 backlog 0b 0p requeues 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;dropped 456&lt;/code&gt; in tc output means the transmit queue is overflowing. Your application is sending data faster than the NIC can transmit it. Either increase the txqueuelen (&lt;code&gt;ip link set eth0 txqueuelen 10000&lt;/code&gt;) or reduce the application burst rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Decision Tree — Which Tool for Which Symptom
&lt;/h2&gt;

&lt;p&gt;Here's a quick reference to jump directly to the right tool based on what you observe:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;First Command&lt;/th&gt;
&lt;th&gt;Deep Dive&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Load average high, CPU idle low&lt;/td&gt;
&lt;td&gt;mpstat -P ALL 1&lt;/td&gt;
&lt;td&gt;perf top / flamegraphs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load high, CPU idle also high&lt;/td&gt;
&lt;td&gt;vmstat 1 (check b column)&lt;/td&gt;
&lt;td&gt;iostat -xz 1, blktrace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application slow, no obvious resource issue&lt;/td&gt;
&lt;td&gt;ss -tupn (check TIME_WAIT)&lt;/td&gt;
&lt;td&gt;tcpdump, strace -c -p PID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server unreachable or timeout&lt;/td&gt;
&lt;td&gt;dmesg -T&lt;/td&gt;
&lt;td&gt;tail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OOM kills in logs&lt;/td&gt;
&lt;td&gt;free -m, pidstat -r 1&lt;/td&gt;
&lt;td&gt;pmap -x PID, /proc/PID/smaps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk latency high (wa &amp;gt; 10%)&lt;/td&gt;
&lt;td&gt;iostat -xz 1&lt;/td&gt;
&lt;td&gt;iotop -oP, blktrace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network retransmits &amp;gt; 0.1%&lt;/td&gt;
&lt;td&gt;sar -n ETCP 1&lt;/td&gt;
&lt;td&gt;tcpdump -w /tmp/dump.pcap&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;Effective Linux performance troubleshooting is a skill you build by doing — but always start with the 60-second checklist. It forces you to check CPU, memory, disk, and network &lt;em&gt;before&lt;/em&gt; jumping to conclusions. I've lost count of how many "application bugs" turned out to be a server running out of memory or a saturated network link.&lt;/p&gt;

&lt;p&gt;The tools here (&lt;code&gt;perf&lt;/code&gt;, &lt;code&gt;vmstat&lt;/code&gt;, &lt;code&gt;iostat&lt;/code&gt;, &lt;code&gt;ss&lt;/code&gt;, &lt;code&gt;blktrace&lt;/code&gt;, &lt;code&gt;tcpdump&lt;/code&gt;) are available on every Linux distribution and have zero dependencies. Learn to read their output fluently, and you'll be the person in the war room who can say "it's the disk queue depth, not the application code" with confidence.&lt;/p&gt;

&lt;p&gt;One final rule: &lt;strong&gt;instrument before you need it.&lt;/strong&gt; Set up &lt;code&gt;sar&lt;/code&gt; data collection (&lt;code&gt;sysstat&lt;/code&gt; package), configure kernel audit logging for OOM events, and deploy a metrics agent that keeps historical data. When the next incident hits — and it will — you'll have the baseline to know whether things are actually worse than normal.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>REST API Design Best Practices: A Practical Guide for 2026</title>
      <dc:creator>toothbrush</dc:creator>
      <pubDate>Sun, 19 Jul 2026 02:53:51 +0000</pubDate>
      <link>https://dev.to/toothbrush/rest-api-design-best-practices-a-practical-guide-for-2026-2eb0</link>
      <guid>https://dev.to/toothbrush/rest-api-design-best-practices-a-practical-guide-for-2026-2eb0</guid>
      <description>&lt;p&gt;Every team builds APIs. Few build ones that survive their second rewrite. After inheriting three different REST APIs in as many years — one with endpoints named &lt;code&gt;/getAllUsers&lt;/code&gt;, another that returned &lt;code&gt;{ "status": "ok" }&lt;/code&gt; for both success &lt;em&gt;and&lt;/em&gt; 500 errors — I started keeping a list of the practices that actually distinguish robust APIs from ones that generate PagerDuty alerts at 3 AM.&lt;/p&gt;

&lt;p&gt;This guide distills six rules I've validated across production services handling tens of millions of requests. None are theoretical. All come with working code.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Resource-Oriented Naming — Not Action-Oriented
&lt;/h2&gt;

&lt;p&gt;The single biggest smell in a REST API is action verbs in URLs:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Bad — these are RPC, not REST
GET  /api/getUser?id=42
POST /api/createUser
POST /api/deleteUser/42
POST /api/activateUserSubscription
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Resources are nouns, not verbs. The HTTP method &lt;em&gt;is&lt;/em&gt; the verb:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Good
GET    /api/users/42
POST   /api/users
DELETE /api/users/42
POST   /api/users/42/subscriptions   # nested resource
DELETE /api/users/42/subscriptions/active
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Key conventions that have held across every production API I've consulted on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plural nouns&lt;/strong&gt; : &lt;code&gt;/users&lt;/code&gt;, not &lt;code&gt;/user&lt;/code&gt;. Consistency with list endpoints (&lt;code&gt;GET /users&lt;/code&gt; = a list) makes singular feel like a bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kebab-case for multi-word resources&lt;/strong&gt; : &lt;code&gt;/order-items&lt;/code&gt;, not &lt;code&gt;/orderItems&lt;/code&gt; or &lt;code&gt;/order_items&lt;/code&gt;. It's URL-safe and matches what browsers expect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nest at most two levels&lt;/strong&gt; : &lt;code&gt;/users/42/orders/7&lt;/code&gt; is fine. &lt;code&gt;/users/42/orders/7/items/3/addresses/9&lt;/code&gt; is a cry for help. At that point, use a query parameter: &lt;code&gt;/items?order_id=7&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use query params for filtering, not path segments&lt;/strong&gt; : &lt;code&gt;/users?status=active&amp;amp;role=admin&lt;/code&gt;, not &lt;code&gt;/users/active/admins&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Consistent Error Responses — The Contract People Actually Rely On
&lt;/h2&gt;

&lt;p&gt;Most API errors are parsable only by humans staring at a screen. That's a bug. Every error response should follow the same schema so clients can handle them programmatically:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "User with id 42 was not found.",
    "details": {
      "resource": "users",
      "identifier": "42"
    },
    "request_id": "req_a1b2c3d4"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;code&lt;/code&gt; field is the key — it's a machine-readable string clients can switch on. Never change the meaning of a code after release. The &lt;code&gt;request_id&lt;/code&gt; is a trace identifier your logs and your users' bug reports can share, so debugging a production error doesn't start with "which request was that."&lt;/p&gt;

&lt;p&gt;Here's a FastAPI middleware that enforces this consistently:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI()

class APIException(Exception):
    def __init__(self, code: str, message: str, status: int, details: dict = None):
        self.code = code
        self.message = message
        self.status = status
        self.details = details or {}

@app.exception_handler(APIException)
async def api_error_handler(request: Request, exc: APIException):
    return JSONResponse(
        status_code=exc.status,
        content={
            "error": {
                "code": exc.code,
                "message": exc.message,
                "details": exc.details,
                "request_id": request.headers.get("x-request-id", "unknown"),
            }
        },
    )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;With this in place, your HTTP status codes still mean what HTTP says they mean (4xx vs 5xx), but the &lt;code&gt;code&lt;/code&gt; field gives clients an actionable discriminator. Stripe pioneered this pattern and it's stood the test of a decade.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Pagination That Doesn't Surprise
&lt;/h2&gt;

&lt;p&gt;Cursor-based pagination beats offset-based for production APIs. Here's why: if a new record is inserted before page 2, offset pagination shifts every subsequent page — users see duplicates or miss records. Cursor pagination uses a stable pointer:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /users?cursor=eyJpZCI6IDQyfQ==&amp;amp;limit=20

Response:
{
  "data": [ ... ],
  "pagination": {
    "next_cursor": "eyJpZCI6IDg0fQ==",
    "has_more": true,
    "limit": 20
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The cursor is an opaque base64-encoded token, usually containing the last seen record's ID and a timestamp. Clients don't decode it — just pass it to the next request. Implementation is straightforward with SQL's &lt;code&gt;WHERE id &amp;gt; :cursor ORDER BY id&lt;/code&gt; pattern:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT id, name, email FROM users
WHERE id &amp;gt; :cursor
ORDER BY id ASC
LIMIT 21   -- fetch one extra to detect has_more
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The extra row tells you &lt;code&gt;has_more = rows_returned == limit + 1&lt;/code&gt;. Drop that row from the response and encode its id as the next cursor. This pattern handles millions of rows without the performance cliff that &lt;code&gt;OFFSET&lt;/code&gt; hits past page 50.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. API Versioning — Accept or Header, Not URL Path
&lt;/h2&gt;

&lt;p&gt;The old orthodoxy was &lt;code&gt;/api/v1/users&lt;/code&gt;. This commits you to maintaining parallel URL trees forever. A better approach: use the &lt;code&gt;Accept&lt;/code&gt; header (content negotiation):&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Request
GET /api/users/42
Accept: application/vnd.devopsdaily.v2+json

# Response
Content-Type: application/vnd.devopsdaily.v2+json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Why this wins: the same URL always works. New clients get v2, old clients get v1, your infrastructure stays simple. When v3 ships, you don't redeploy — you update a default-version config. Stripe and GitHub both moved to this model for good reason.&lt;/p&gt;

&lt;p&gt;The practical downside is discoverability — a developer can't glance at the URL and see "this is v2." Mitigate by documenting version defaults in your API reference and returning the version in response headers:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X-API-Version: 2
Deprecated: true
Sunset: Sat, 19 Dec 2026 00:00:00 GMT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If you absolutely must use URL versioning (perhaps your consumers are curl-in-a-shell-script types), stick to the simple &lt;code&gt;/v1/&lt;/code&gt; prefix — but deprecate it in favor of the &lt;code&gt;Accept&lt;/code&gt; header on your next major release.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Rate Limiting That Tells You When to Retry
&lt;/h2&gt;

&lt;p&gt;Rate limiting without feedback headers is just annoying. Every response — successful or not — should carry:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 483
X-RateLimit-Reset: 1721414400
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;And when the client exceeds the limit:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP/1.1 429 Too Many Requests
Retry-After: 42
Content-Type: application/json

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded. Retry after 42 seconds.",
    "details": {
      "limit": 1000,
      "reset_at": "2026-07-19T14:00:00Z"
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;Retry-After&lt;/code&gt; header is the most important part — without it, clients have to guess, and they'll either hammer you (making things worse) or back off too conservatively (starving your own metrics). If you're using a token bucket algorithm (and you should be), the reset time is simply the bucket's next refill tick.&lt;/p&gt;

&lt;p&gt;Here's a minimal implementation using sliding window counters in Python — no external dependency:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import time
from collections import defaultdict

class SlidingWindowRateLimiter:
    def __init__(self, max_requests: int, window_seconds: int = 60):
        self.max_requests = max_requests
        self.window = window_seconds
        self.requests = defaultdict(list)  # key -&amp;gt; [timestamps]

    def check(self, key: str) -&amp;gt; tuple[bool, int, int]:
        now = time.time()
        cutoff = now - self.window
        self.requests[key] = [t for t in self.requests[key] if t &amp;gt; cutoff]

        remaining = self.max_requests - len(self.requests[key])
        if remaining &amp;gt; 0:
            self.requests[key].append(now)
            return True, remaining, int(cutoff + self.window)
        return False, 0, int(cutoff + self.window)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;For production, swap this for Redis-backed counters — but the API contract stays identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Idempotency and Safe Retries
&lt;/h2&gt;

&lt;p&gt;Network failures happen. When a client sends &lt;code&gt;POST /api/payments&lt;/code&gt; and gets a timeout, was the payment charged or not? Without idempotency, the safe answer is "I don't know" — which is the wrong answer.&lt;/p&gt;

&lt;p&gt;Use idempotency keys. The client generates a unique key (UUID) and sends it on every mutation request:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /api/payments
Idempotency-Key: 7c7a3e81-3a1e-4b1e-8e1e-3e1e7c7a3e81
Content-Type: application/json

{
  "amount": 2999,
  "currency": "usd",
  "source": "tok_visa"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The server caches the response keyed by &lt;code&gt;(Idempotency-Key, endpoint)&lt;/code&gt;. If it sees the same key again, it returns the cached response instead of executing the operation twice:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Fast enough for most use cases — a simple dict
# For production: Redis with TTL matching your max retry window
idempotency_store: dict[str, dict] = {}

@app.post("/api/payments")
async def create_payment(request: Request):
    idem_key = request.headers.get("Idempotency-Key")
    if not idem_key:
        raise APIException("IDEMPOTENCY_KEY_REQUIRED", ...)

    cached = idempotency_store.get(idem_key)
    if cached:
        return JSONResponse(content=cached, status_code=200)

    result = await process_payment(request.json())
    idempotency_store[idem_key] = result
    return JSONResponse(content=result, status_code=201)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Stripe's idempotency layer handles billions of dollars. The pattern has been battle-tested. Copy it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;A well-designed REST API is a contract. The six practices above — resource naming, structured errors, cursor pagination, header-based versioning, informative rate limits, and idempotency keys — form the foundation of APIs that developers enjoy integrating with.&lt;/p&gt;

&lt;p&gt;The next time you're building an endpoint, skip the framework boilerplate and start with these. Your consumers (and your on-call rotation) will thank you.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Built a QuickBooks Automation Tool in a Weekend</title>
      <dc:creator>toothbrush</dc:creator>
      <pubDate>Sun, 19 Jul 2026 02:52:48 +0000</pubDate>
      <link>https://dev.to/toothbrush/i-built-a-quickbooks-automation-tool-in-a-weekend-3723</link>
      <guid>https://dev.to/toothbrush/i-built-a-quickbooks-automation-tool-in-a-weekend-3723</guid>
      <description>&lt;p&gt;Last Thursday I spent 36 minutes typing invoice data into QuickBooks. Supplier name. Date. Amount. Line items. Category. Attach PDF. Next invoice. Repeat. I'm a developer — I automate things for a living — and here I was, doing data entry like it was 1998.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;InvoiceFlow&lt;/strong&gt; — a tool that reads your invoice emails and enters the data into QuickBooks automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;5 million QuickBooks Online users. Many are small business owners — restaurants, contractors, freelancers. They don't have accounting departments. They ARE the accounting department. Every month they process 20-50 supplier invoices. At 3-5 minutes each, that's &lt;strong&gt;2-4 hours per month&lt;/strong&gt; of pure data entry. Enterprise tools exist (Bill.com, Stampli) but start at $45+/month and assume you have an AP team. Small businesses get left with Copy → Paste → Cry.&lt;/p&gt;

&lt;h2&gt;
  
  
  What InvoiceFlow Does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Forward the invoice email&lt;/strong&gt; — you get a personal InvoiceFlow address. Supplier sends invoice → you forward it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. AI parses everything&lt;/strong&gt; — vendor name, date, amount, line items extracted from any PDF format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Appears in QuickBooks&lt;/strong&gt; — auto-entered as a Bill. Categorized, dated, PDF attached. Confirmation email sent.&lt;/p&gt;

&lt;p&gt;Your time: 3 seconds (hit Forward). Processing: ~30 seconds. Done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Pain Points (from Reddit &amp;amp; Forums)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"I spend 3 hours every Sunday just entering invoices. I hate it." — Restaurant owner, r/smallbusiness&lt;/p&gt;

&lt;p&gt;"My bookkeeper charges $300/month and 80% of that is literally typing numbers from PDFs into QuickBooks." — Contractor, r/QuickBooks&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Email ingestion via IMAP — watches for forwarded invoices&lt;/li&gt;
&lt;li&gt;AI vision models for PDF parsing — handles any invoice layout&lt;/li&gt;
&lt;li&gt;QuickBooks OAuth2 API — creates Bills via REST&lt;/li&gt;
&lt;li&gt;Error detection — flags mismatched totals, duplicates before posting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No blockchain. No microservices. No Kubernetes. Just a focused tool that solves one boring problem well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;$29/month&lt;/strong&gt;. Unlimited invoices. Cancel anytime. 14-day free trial — no credit card. Compare: part-time bookkeeper = $200-400/month. Enterprise tools = $45-100+/month with limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Want early access?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Join the waitlist — 50% off for life when we launch.&lt;/p&gt;

&lt;p&gt;&lt;a href="//../invoice-flow/"&gt;Get Early Access →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>VS Code Extensions You're Not Using (But Should Be)</title>
      <dc:creator>toothbrush</dc:creator>
      <pubDate>Sun, 19 Jul 2026 02:47:20 +0000</pubDate>
      <link>https://dev.to/toothbrush/vs-code-extensions-youre-not-using-but-should-be-kih</link>
      <guid>https://dev.to/toothbrush/vs-code-extensions-youre-not-using-but-should-be-kih</guid>
      <description>&lt;p&gt;Everyone has Prettier and ESLint. But there are extensions hiding in the marketplace that will genuinely change how you code. Here are the ones that deserve a spot in your setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error Lens — Errors Where They Happen
&lt;/h2&gt;

&lt;p&gt;Instead of hovering over squiggly lines to see what's wrong, Error Lens shows errors, warnings, and hints inline — right at the end of the offending line. You'll spot bugs 3x faster because you don't have to move your eyes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Better Comments — Comments That Work For You
&lt;/h2&gt;

&lt;p&gt;Prefix comments with special characters and they highlight in different colors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;!&lt;/code&gt; — Red for critical notes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;?&lt;/code&gt; — Blue for questions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TODO&lt;/code&gt; — Orange for todos&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;*&lt;/code&gt; — Green for highlights&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your brain processes color faster than text. Use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitLens — Git Superpowers
&lt;/h2&gt;

&lt;p&gt;Blame annotations on every line, file history at a click, and side-by-side diff comparisons without leaving your editor. GitLens adds 20+ features to VS Code's built-in Git — the free version alone is a massive upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thunder Client — API Testing Without Postman
&lt;/h2&gt;

&lt;p&gt;Lightweight REST client built directly into VS Code's sidebar. No separate app, no account required. Create collections, environment variables, and test APIs without leaving your editor. Faster than Postman for everyday use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Manager — Switch Contexts Instantly
&lt;/h2&gt;

&lt;p&gt;If you juggle 5+ projects, you need this. Save projects with &lt;code&gt;Cmd+Shift+P&lt;/code&gt; and switch between them in one click. Each project remembers its own window state, open files, and terminal sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bookmarks — Never Lose Your Place Again
&lt;/h2&gt;

&lt;p&gt;Mark specific lines and jump between them with keyboard shortcuts. When you're jumping between 3 different sections of a 2000-line file, bookmarks are a lifesaver. Works across files too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turbo Console Log — Logging at Key Press
&lt;/h2&gt;

&lt;p&gt;Select a variable, press &lt;code&gt;Ctrl+Alt+L&lt;/code&gt;, and a &lt;code&gt;console.log&lt;/code&gt; with the variable name and value is inserted on the next line. Tiny feature, massive time saver when debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Install all 7. Give each one a day of use. By the end of the week, you'll wonder how you ever coded without them.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>5 Git Tricks That Make You Look Like a Senior Dev</title>
      <dc:creator>toothbrush</dc:creator>
      <pubDate>Sun, 19 Jul 2026 02:46:00 +0000</pubDate>
      <link>https://dev.to/toothbrush/5-git-tricks-that-make-you-look-like-a-senior-dev-30j4</link>
      <guid>https://dev.to/toothbrush/5-git-tricks-that-make-you-look-like-a-senior-dev-30j4</guid>
      <description>&lt;p&gt;Most developers use about 20% of Git's power. These 5 commands are the other 80%. Master them and you'll solve problems in seconds that others spend hours on.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Interactive Rebase — Clean Commit History
&lt;/h2&gt;

&lt;p&gt;Never push a messy branch again. Interactive rebase lets you squash, reorder, and edit commits before anyone sees them.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git rebase -i HEAD~4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This opens an editor with your last 4 commits. Change &lt;code&gt;pick&lt;/code&gt; to &lt;code&gt;squash&lt;/code&gt; to combine commits, or &lt;code&gt;reword&lt;/code&gt; to fix a message. Your PR reviewer will thank you.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Git Bisect — Find the Bug in O(log n)
&lt;/h2&gt;

&lt;p&gt;A bug was introduced sometime in the last 100 commits. Instead of checking them one by one, let Git binary-search for you:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git bisect start
git bisect bad HEAD        # current state is broken
git bisect good v1.0.0     # last known good state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Git checks out a commit in the middle. Test it, mark it &lt;code&gt;git bisect good&lt;/code&gt; or &lt;code&gt;git bisect bad&lt;/code&gt;. In 7 steps, you'll find the exact commit among 100. Then &lt;code&gt;git bisect reset&lt;/code&gt; to return.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Worktrees — Work on Two Branches Simultaneously
&lt;/h2&gt;

&lt;p&gt;Need to switch branches but don't want to stash your changes? Worktrees let you check out multiple branches into separate directories — all from one repo.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git worktree add ../feature-branch feature-branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now you have two working directories: your current one and &lt;code&gt;../feature-branch&lt;/code&gt;. Make changes in either, they share the same &lt;code&gt;.git&lt;/code&gt; folder. Perfect for hotfixes while deep in a feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Reflog — Your Undo Safety Net
&lt;/h2&gt;

&lt;p&gt;Accidentally deleted a branch? Force-pushed the wrong thing? &lt;code&gt;git reflog&lt;/code&gt; shows every action Git has taken in the last 90 days:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git reflog
# abc1234 HEAD@{0}: commit: fix login bug
# def5678 HEAD@{1}: reset: moving to HEAD~3
# ghi9012 HEAD@{2}: commit: add payment feature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Find the commit you lost and do &lt;code&gt;git checkout ghi9012&lt;/code&gt; to recover it. The reflog has saved more careers than any StackOverflow answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Partial Staging — Commit Specific Lines
&lt;/h2&gt;

&lt;p&gt;You made 5 changes in one file but only want to commit 2 of them. Don't copy-paste into a new file. Use interactive staging:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git add -p
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Git shows each chunk and asks what to do. Press &lt;code&gt;y&lt;/code&gt; to stage it, &lt;code&gt;n&lt;/code&gt; to skip, or &lt;code&gt;s&lt;/code&gt; to split a large chunk into smaller ones. Commit exactly what you mean to commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Start with &lt;code&gt;git add -p&lt;/code&gt; and &lt;code&gt;git reflog&lt;/code&gt; — you'll use those weekly. Then add &lt;code&gt;rebase -i&lt;/code&gt; for your next PR. Bisect and worktrees are your secret weapons for the tough debugging sessions.&lt;/p&gt;

&lt;p&gt;Senior developers don't memorize more commands. They know fewer commands that solve problems faster.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>10 Docker Commands Every Developer Should Know in 2026</title>
      <dc:creator>toothbrush</dc:creator>
      <pubDate>Sun, 19 Jul 2026 02:42:19 +0000</pubDate>
      <link>https://dev.to/toothbrush/10-docker-commands-every-developer-should-know-in-2026-5f26</link>
      <guid>https://dev.to/toothbrush/10-docker-commands-every-developer-should-know-in-2026-5f26</guid>
      <description>&lt;p&gt;Docker has been around for a decade, yet most developers still use the same 3 commands. Here are 10 commands that will make you faster, fix production issues quicker, and actually understand what's happening inside your containers.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. docker exec -it — Your Debug Entry Point
&lt;/h2&gt;

&lt;p&gt;The classic. But most people don't use it right. Always specify the shell explicitly:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker exec -it my-container /bin/bash
# Alpine containers don't have bash:
docker exec -it my-container /bin/sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Bonus: pipe commands directly without entering the container:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker exec my-container cat /etc/nginx/nginx.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  2. docker logs — Read the Room
&lt;/h2&gt;

&lt;p&gt;Don't SSH into a container to read logs. Use the tools built for it:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker logs --tail 100 -f my-container   # last 100 lines, follow
docker logs --since 10m my-container       # last 10 minutes
docker logs --timestamps my-container      # show when each line happened
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  3. docker system df — Where Did My Disk Go?
&lt;/h2&gt;

&lt;p&gt;This single command tells you exactly how much space Docker is eating:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker system df
# TYPE           TOTAL   ACTIVE   SIZE    RECLAIMABLE
# Images         12      5        2.3GB   1.1GB (47%)
# Containers     8       3        150MB   90MB (60%)
# Volumes        4       2        500MB   200MB (40%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Follow up with &lt;code&gt;docker system prune -a&lt;/code&gt; when you need to clean house. The &lt;code&gt;-a&lt;/code&gt; flag also removes unused images, not just dangling ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. docker inspect — The Truth Is in the JSON
&lt;/h2&gt;

&lt;p&gt;Every Docker object has a complete JSON manifest. &lt;code&gt;inspect&lt;/code&gt; reveals it. Use Go templates to extract exactly what you need:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker inspect -f '{{.NetworkSettings.IPAddress}}' my-container
docker inspect -f '{{range .Mounts}}{{.Source}} -&amp;gt; {{.Destination}}{{println}}{{end}}' my-container
docker inspect -f '{{.State.StartedAt}}' my-container
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  5. docker stats — Real-Time Resource Monitor
&lt;/h2&gt;

&lt;p&gt;Like &lt;code&gt;top&lt;/code&gt; but for containers. Shows CPU, memory, network, and disk I/O — live:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker stats --no-stream          # snapshot
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If a container is using 2GB of RAM but your app only needs 200MB, you'll spot it here.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. docker history — What's Actually In That Image?
&lt;/h2&gt;

&lt;p&gt;Before pulling a 1.5GB image from Docker Hub, check what's inside:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker history node:20-alpine
# IMAGE          CREATED       SIZE    COMMENT
# abc123         2 days ago    5.6MB   /bin/sh -c apk add --no-cache ...
# def456         2 days ago    112MB   /bin/sh -c mkdir /app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This shows every layer and its size. You'll often find 500MB of build dependencies that should've been cleaned up in the Dockerfile.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. docker cp — Move Files In and Out
&lt;/h2&gt;

&lt;p&gt;No need to mount volumes for one-off file transfers:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker cp ./config.json my-container:/app/config.json     # host → container
docker cp my-container:/var/log/app.log ./app.log          # container → host
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  8. docker commit — Snapshot a Running Container
&lt;/h2&gt;

&lt;p&gt;Made changes inside a container and want to save them as an image? Don't rebuild — snapshot it:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker commit my-container my-debug-image:v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Useful for debugging, but don't build production images this way. It captures everything including temp files and secrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. docker network — See Who's Talking to Whom
&lt;/h2&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker network ls                          # list all networks
docker network inspect bridge              # see connected containers
docker network create --driver bridge my-net  # custom network
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Containers on the same custom network can resolve each other by name. No more hardcoding IPs.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. docker events — Watch Everything Happen
&lt;/h2&gt;

&lt;p&gt;Real-time stream of every Docker event — container starts, stops, health checks, image pulls:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker events --since 5m
docker events --filter 'event=die' --filter 'container=my-app'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Pipe this into your monitoring stack. When a container unexpectedly dies at 3 AM, you'll know exactly when.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;You don't need to memorize all 10. Start with &lt;code&gt;inspect&lt;/code&gt; and &lt;code&gt;system df&lt;/code&gt; — you'll use them weekly. Add &lt;code&gt;logs --since&lt;/code&gt; for production debugging and &lt;code&gt;events&lt;/code&gt; for monitoring. The rest will become muscle memory over time.&lt;/p&gt;

&lt;p&gt;The gap between "I use Docker" and "I understand Docker" is exactly these 10 commands.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>7 AI Coding Tools That Actually Work in 2026: An Honest Comparison</title>
      <dc:creator>toothbrush</dc:creator>
      <pubDate>Sun, 19 Jul 2026 02:40:05 +0000</pubDate>
      <link>https://dev.to/toothbrush/7-ai-coding-tools-that-actually-work-in-2026-an-honest-comparison-21e8</link>
      <guid>https://dev.to/toothbrush/7-ai-coding-tools-that-actually-work-in-2026-an-honest-comparison-21e8</guid>
      <description>&lt;p&gt;AI coding assistants went from novelty to necessity in 18 months. Every developer I know has at least one running. The problem: there are now &lt;strong&gt;dozens&lt;/strong&gt; of them, and they all claim to be the best. This is the comparison I wish I'd had before burning through trial periods and switching tools five times.&lt;/p&gt;

&lt;p&gt;I tested seven tools over two weeks — real projects, not toy examples. Here is what actually works.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. GitHub Copilot — The Default Choice That Earned It
&lt;/h2&gt;

&lt;p&gt;Copilot is the incumbent for a reason. It integrates with VS Code, JetBrains, and Neovim. It autocompletes entire functions. It understands your project context — imports, neighboring files, even your test suite. The June 2026 update added multi-file editing and a chat panel that can refactor across modules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Developers who want zero configuration and broad IDE support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weakness:&lt;/strong&gt; No free tier (unlike most competitors). $10/month or $100/year.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Cursor — The IDE That Replaced VS Code
&lt;/h2&gt;

&lt;p&gt;Cursor is not a plugin — it is a full IDE, forked from VS Code. That means it controls the entire editing experience. The key feature: &lt;strong&gt;Cmd+K inline editing&lt;/strong&gt;. Highlight code, describe the change in plain English, and Cursor rewrites it. It also has a sidebar agent that can read your entire codebase and make changes across files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Developers willing to switch editors for a massive productivity jump.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weakness:&lt;/strong&gt; No JetBrains support. The agent sometimes over-engineers simple fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Codeium / Windsurf — The Free Powerhouse
&lt;/h2&gt;

&lt;p&gt;Codeium's free tier is genuinely unlimited. Autocomplete, chat, context-aware suggestions — all free for individual developers. Windsurf, their standalone IDE, adds agentic features similar to Cursor. For students and open-source maintainers, this is the obvious starting point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Cost-conscious developers and open-source contributors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weakness:&lt;/strong&gt; Paid enterprise features lag behind Copilot's offerings.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Cody by Sourcegraph — The Codebase Whisperer
&lt;/h2&gt;

&lt;p&gt;Cody's killer feature: &lt;strong&gt;it indexes your entire repository&lt;/strong&gt; and answers questions about your codebase. Ask "where is authentication logic implemented?" and it finds the exact files — not just grep results, but actual understanding. It also generates unit tests, explains complex functions, and can fix bugs with natural language prompts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Large codebases and onboarding new team members.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weakness:&lt;/strong&gt; Free tier limits context size. Indexing takes minutes on first run.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Amazon CodeWhisperer — The AWS-Native Option
&lt;/h2&gt;

&lt;p&gt;If you live in the AWS ecosystem, CodeWhisperer is the obvious pick. It suggests IAM policies, CloudFormation snippets, and Lambda handlers that follow AWS best practices. It also scans for security vulnerabilities — flagging hardcoded credentials and overly permissive policies inline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; AWS developers who want infrastructure-as-code suggestions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weakness:&lt;/strong&gt; Outside AWS, suggestions are less accurate than Copilot.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Tabnine — The Privacy-First Alternative
&lt;/h2&gt;

&lt;p&gt;Tabnine runs models locally — your code never leaves your machine. For companies with strict compliance requirements (finance, healthcare, government), this is non-negotiable. The local models are smaller than cloud-hosted ones, so suggestions are slightly less creative, but they are fast — sub-100ms completion times.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Enterprise teams with data privacy requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weakness:&lt;/strong&gt; Local model quality is noticeably behind cloud-hosted competitors.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Continue — The Open-Source Wildcard
&lt;/h2&gt;

&lt;p&gt;Continue is an open-source VS Code/JetBrains extension that lets you plug in &lt;strong&gt;any LLM&lt;/strong&gt; — OpenAI, Anthropic, local Ollama models, or even your own self-hosted API. You can chain models: use a fast local model for autocomplete and a cloud model for chat. The configuration is a single JSON file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Developers who want full control over models and pricing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weakness:&lt;/strong&gt; Requires setup. Not "install and forget" like Copilot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;If you want zero friction, &lt;strong&gt;GitHub Copilot&lt;/strong&gt; is still the answer — $10/month, works everywhere, 90% accurate suggestions. If you are willing to switch editors, &lt;strong&gt;Cursor&lt;/strong&gt; gives you the biggest productivity boost per dollar. &lt;strong&gt;Codeium&lt;/strong&gt; is the best free option by a wide margin. And if you manage a team shipping to AWS, &lt;strong&gt;CodeWhisperer&lt;/strong&gt; saves you hours of reading documentation.&lt;/p&gt;

&lt;p&gt;The one tool I would &lt;em&gt;avoid&lt;/em&gt; : any AI assistant that does not integrate with your existing tools. Switching workflows is the hidden cost nobody talks about.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>10 Python Libraries That Will Save You Hours in 2026</title>
      <dc:creator>toothbrush</dc:creator>
      <pubDate>Sun, 19 Jul 2026 02:36:36 +0000</pubDate>
      <link>https://dev.to/toothbrush/10-python-libraries-that-will-save-you-hours-in-2026-4ig0</link>
      <guid>https://dev.to/toothbrush/10-python-libraries-that-will-save-you-hours-in-2026-4ig0</guid>
      <description>&lt;p&gt;Every year, the Python ecosystem churns out hundreds of libraries. Most fade away. A few become indispensable. Here are 10 libraries that will genuinely cut your development time in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. httpx — HTTP for Humans, Now With Async
&lt;/h2&gt;

&lt;p&gt;Requests is great. &lt;code&gt;httpx&lt;/code&gt; is Requests with async support and HTTP/2 built in. If you're still writing synchronous HTTP calls in 2026, you're leaving performance on the table.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import httpx

async with httpx.AsyncClient() as client:
    r = await client.get('https://api.example.com')
    print(r.json())
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Drop-in replacement for Requests, but async-first. The migration is trivial.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. rich — Terminal Output That Doesn't Suck
&lt;/h2&gt;

&lt;p&gt;Progress bars, syntax-highlighted tracebacks, tables, markdown rendering — all in your terminal. &lt;code&gt;rich&lt;/code&gt; replaces a dozen small utility libraries.&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from rich.console import Console&lt;br&gt;
from rich.table import Table

&lt;p&gt;console = Console()&lt;br&gt;
table = Table(title="Server Status")&lt;br&gt;
table.add_column("Host")&lt;br&gt;
table.add_column("CPU")&lt;br&gt;
table.add_row("prod-1", "42%")&lt;br&gt;
console.print(table)&lt;br&gt;
&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  

&lt;ol&gt;
&lt;li&gt;pydantic v2 — Fast Data Validation
&lt;/li&gt;
&lt;/ol&gt;
&lt;/h2&gt;


&lt;p&gt;If you work with APIs, configs, or any structured data, Pydantic v2 is ~30x faster than v1 and now written in Rust under the hood. Define a model, get validation, serialization, and JSON Schema for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. typer — CLI Apps in Minutes
&lt;/h2&gt;

&lt;p&gt;Built on top of Click, &lt;code&gt;typer&lt;/code&gt; uses type hints to auto-generate CLI interfaces. Write a function, add type annotations, and you have a production-ready CLI with help text and error handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. polars — The Pandas Killer
&lt;/h2&gt;

&lt;p&gt;Pandas is slow with large datasets. &lt;code&gt;polars&lt;/code&gt; is written in Rust, uses Apache Arrow, and runs 10-100x faster. If you're processing more than a few million rows, switch now.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. textual — TUI Apps Like Never Before
&lt;/h2&gt;

&lt;p&gt;Want to build a terminal dashboard? &lt;code&gt;textual&lt;/code&gt; gives you a React-like component model for terminals. Build TUIs with CSS-like styling and async event handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. duckdb — SQLite for Analytics
&lt;/h2&gt;

&lt;p&gt;Run SQL directly on CSV, Parquet, and Pandas DataFrames. Zero setup, zero server. &lt;code&gt;duckdb&lt;/code&gt; is faster than Spark for datasets under 10GB.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. structlog — Structured Logging Done Right
&lt;/h2&gt;

&lt;p&gt;Stop grepping through log files. &lt;code&gt;structlog&lt;/code&gt; outputs JSON logs that you can pipe into any observability stack. Attach context to every log message automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. ruff — Linting at Rust Speed
&lt;/h2&gt;

&lt;p&gt;Replaces Flake8, isort, pyupgrade, and a dozen other linting tools — all in one binary, 100x faster. Written in Rust, integrates with pyproject.toml.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. litestar — FastAPI Without the Bloat
&lt;/h2&gt;

&lt;p&gt;FastAPI is great, but &lt;code&gt;litestar&lt;/code&gt; is leaner, more modular, and community-driven. Better dependency injection, Class-Based Views, and first-class async support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;You don't need to adopt all 10. Pick 3 that solve your current pain points and integrate them into one project. The time savings compound fast.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>GitHub Actions CI/CD Pipeline: From Zero to Production</title>
      <dc:creator>toothbrush</dc:creator>
      <pubDate>Sun, 19 Jul 2026 02:34:50 +0000</pubDate>
      <link>https://dev.to/toothbrush/github-actions-cicd-pipeline-from-zero-to-production-3f18</link>
      <guid>https://dev.to/toothbrush/github-actions-cicd-pipeline-from-zero-to-production-3f18</guid>
      <description>&lt;p&gt;Most teams start with GitHub Actions because it's already there — your code is on GitHub, so the CI runner is a checkbox away. But a real production pipeline is more than running &lt;code&gt;npm test&lt;/code&gt; on every push. It needs caching, Docker builds, multi-stage environments, artifact management, and deployment that doesn't wake you up at 3 AM.&lt;/p&gt;

&lt;p&gt;I've built and broken enough pipelines to know what works. Here's a production-grade GitHub Actions workflow — explained piece by piece so you understand &lt;em&gt;why&lt;/em&gt; each part exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Workflow Skeleton
&lt;/h2&gt;

&lt;p&gt;Every GitHub Actions workflow lives in &lt;code&gt;.github/workflows/&lt;/code&gt;. We'll build a single file called &lt;code&gt;ci-cd.yml&lt;/code&gt; that handles test, build, and deploy for a Node.js + Docker application.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: CI/CD Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;on.push&lt;/code&gt; trigger runs on main-branch commits. The &lt;code&gt;pull_request&lt;/code&gt; trigger runs on PRs &lt;em&gt;targeting&lt;/em&gt; main — so you catch failures before merge. The &lt;code&gt;env&lt;/code&gt; block defines shared variables so you don't repeat yourself in every step.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Job 1: Lint &amp;amp; Test (Fast Feedback)
&lt;/h2&gt;

&lt;p&gt;Your first job should finish in under 2 minutes. Speed matters because this is what runs on every PR commit — slow tests kill developer productivity.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jobs:
  lint-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci
      - run: npm run lint
      - run: npm test -- --coverage

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: coverage-report
          path: coverage/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Key details that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;npm ci&lt;/code&gt;&lt;/strong&gt; — not &lt;code&gt;npm install&lt;/code&gt;. &lt;code&gt;ci&lt;/code&gt; uses the lockfile exactly, fails if lockfile ≠ package.json, and is 2-3x faster. Never use &lt;code&gt;npm install&lt;/code&gt; in CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cache: 'npm'&lt;/code&gt;&lt;/strong&gt; — built into &lt;code&gt;setup-node&lt;/code&gt;. Caches &lt;code&gt;~/.npm&lt;/code&gt; based on &lt;code&gt;package-lock.json&lt;/code&gt;. Cuts install time from 45s to 8s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;upload-artifact&lt;/code&gt;&lt;/strong&gt; with &lt;code&gt;if: always()&lt;/code&gt; — coverage reports are useful even when tests fail. You can browse them in the Actions UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separation of concerns&lt;/strong&gt; — this job has nothing to do with Docker or deployment. It fails fast and tells the developer "your code is broken" before anything expensive runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Job 2: Docker Build &amp;amp; Scan
&lt;/h2&gt;

&lt;p&gt;The second job builds the Docker image. Crucially, it &lt;em&gt;waits&lt;/em&gt; for lint-test to pass. GitHub Actions uses &lt;code&gt;needs&lt;/code&gt; for this.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  docker-build:
    needs: lint-test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Three things here that beginners get wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two tags&lt;/strong&gt; — &lt;code&gt;latest&lt;/code&gt; lets you always pull the newest. The &lt;code&gt;sha&lt;/code&gt; tag lets you deploy a specific version and roll back. Without the SHA tag, you can't tell &lt;em&gt;which&lt;/em&gt; commit is running in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cache-from/cache-to: type=gha&lt;/code&gt;&lt;/strong&gt; — this is GitHub Actions' magic cache for Docker layers. Without it, every build starts from scratch. With it, only changed layers rebuild. A 3-minute build drops to 40 seconds on the second run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;permissions: packages: write&lt;/code&gt;&lt;/strong&gt; — the default GITHUB_TOKEN can't push packages. You must explicitly grant write access to the GitHub Container Registry.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Job 3: Security Scan
&lt;/h2&gt;

&lt;p&gt;Don't deploy vulnerable images. Add a scan step between build and deploy:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  security-scan:
    needs: docker-build
    runs-on: ubuntu-latest
    steps:
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}'
          format: 'sarif'
          output: 'trivy-results.sarif'

      - name: Upload Trivy results to GitHub Security
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: 'trivy-results.sarif'
          category: 'trivy'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Trivy scans your image against known CVE databases. If someone added a vulnerable dependency, you'll see it in GitHub's Security tab before it ever hits production. I've caught 4 critical CVEs this way — including an &lt;code&gt;undici&lt;/code&gt; HTTP request smuggling vulnerability that wasn't flagged by npm audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Job 4: Deploy
&lt;/h2&gt;

&lt;p&gt;Deployment is the most environment-specific part. I'll show a generic SSH-based deploy to a VPS, which applies to most small-to-mid-size setups:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  deploy:
    needs: [security-scan]
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          script: |
            cd /opt/my-app
            docker compose pull
            docker compose up -d --remove-orphans
            docker system prune -af --filter "until=24h"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This runs &lt;code&gt;docker compose pull&lt;/code&gt; on your server (it fetches the updated image you just pushed), recreates containers, and cleans up old images that are more than 24 hours old. The &lt;code&gt;--remove-orphans&lt;/code&gt; flag removes containers defined in the compose file that are no longer running — prevents stale container drift.&lt;/p&gt;

&lt;p&gt;The deploy job only runs on the main branch (&lt;code&gt;if: github.ref == 'refs/heads/main'&lt;/code&gt;). PR builds stop at security-scan. This is deliberate — you want to know the image is safe before you deploy, but the actual deploy only happens when code merges.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Environment Protection Rules
&lt;/h2&gt;

&lt;p&gt;Don't deploy to production just because someone pushed to main. Set up environment protection in your repo settings:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      - name: Deploy to Production
        uses: appleboy/ssh-action@v1.0.3
        with:
          # ...
        environment: production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then in GitHub → Settings → Environments → production, require:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Required reviewers&lt;/strong&gt; — a senior dev must approve the deploy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait timer&lt;/strong&gt; — 5 minutes. Gives you a window to cancel if the pipeline just ran on the wrong branch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment branches&lt;/strong&gt; — only &lt;code&gt;main&lt;/code&gt; can deploy to production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This single setting has saved me from deploying broken code more times than I'd like to admit. The 5-minute wait is surprisingly effective — it forces you to sit and watch the first few seconds of deployment logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Caching: The Single Biggest Speed Win
&lt;/h2&gt;

&lt;p&gt;A pipeline without caching is a slow pipeline. Beyond Docker layer caching, use GitHub's generic cache action for everything else:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      - name: Cache node_modules
        uses: actions/cache@v4
        with:
          path: |
            ~/.npm
            node_modules
            .next/cache
          key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-npm-
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This caches npm dependencies AND the Next.js (or similar) build cache. Combined with Docker layer caching, a full pipeline that took 8 minutes now runs in under 3. Your team will stop complaining about slow CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Matrix Builds (Bonus)
&lt;/h2&gt;

&lt;p&gt;If your app supports multiple runtimes, use a matrix strategy to test them all in parallel:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    strategy:
      matrix:
        node-version: [18, 20, 22]

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Three parallel runners, each testing a different Node version. They all run simultaneously and the &lt;code&gt;needs&lt;/code&gt; condition for downstream jobs only waits for &lt;em&gt;this&lt;/em&gt; job to succeed on &lt;em&gt;all&lt;/em&gt; matrix entries. If Node 22 has a regression, you'll know before your users do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;The final workflow has four sequential stages:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lint-test → docker-build → security-scan → deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Each stage gates the next. If linting fails, nothing builds. If the image has critical CVEs, nothing deploys. If the deploy fails, the previous version keeps running because &lt;code&gt;docker compose up&lt;/code&gt; only restarts containers when the image actually changes.&lt;/p&gt;

&lt;p&gt;The total file is about 90 lines. Copy it into &lt;code&gt;.github/workflows/ci-cd.yml&lt;/code&gt;, replace the deploy script with whatever your server needs, and you have a production CI/CD pipeline that handles caching, security scanning, and safe deployment.&lt;/p&gt;

&lt;p&gt;One final thing: &lt;strong&gt;test your pipeline on a branch before pushing to main.&lt;/strong&gt; Every time I skip this, I end up fixing YAML indentation via 20 commits to a broken &lt;code&gt;main&lt;/code&gt;. Push your workflow to a feature branch first, open a PR, and watch the pipeline run before you merge.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ci</category>
      <category>cd</category>
      <category>github</category>
    </item>
  </channel>
</rss>
