<?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: Mohamed Idris</title>
    <description>The latest articles on DEV Community by Mohamed Idris (@edriso).</description>
    <link>https://dev.to/edriso</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F542036%2F3d38f955-495f-4ee8-9445-db0d27f2fd7b.png</url>
      <title>DEV Community: Mohamed Idris</title>
      <link>https://dev.to/edriso</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/edriso"/>
    <language>en</language>
    <item>
      <title>Learning MongoDB As If You Built It Yourself</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Thu, 21 May 2026 13:08:00 +0000</pubDate>
      <link>https://dev.to/edriso/learning-mongodb-as-if-you-built-it-yourself-4djp</link>
      <guid>https://dev.to/edriso/learning-mongodb-as-if-you-built-it-yourself-4djp</guid>
      <description>&lt;p&gt;If you have ever tried to fit a messy real world thing into rigid SQL tables, you remember the feeling. A user has zero, one, or many addresses. Each address has a label, sometimes a unit number, sometimes notes. So you make an &lt;code&gt;addresses&lt;/code&gt; table. Now every read needs a join. Then a customer feature ships that needs "favorite delivery instructions per address per day of the week", and your diagram grows three more boxes.&lt;/p&gt;

&lt;p&gt;The shape of your data changed faster than your schema could keep up. Every change meant a migration, a deploy, and a small panic.&lt;/p&gt;

&lt;p&gt;That is the gap MongoDB fills.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is MongoDB, really
&lt;/h2&gt;

&lt;p&gt;Think of MongoDB as &lt;strong&gt;a giant filing cabinet of folders&lt;/strong&gt;. Each folder is a complete dossier on one thing. A user folder contains the user's name, their preferences, all their addresses, even their three latest orders if you want them right there. You do not have to open six folders and staple them together to learn about the user. Everything that belongs together lives together.&lt;/p&gt;

&lt;p&gt;The folders are JSON like documents. The cabinet drawers are called &lt;strong&gt;collections&lt;/strong&gt;. The cabinet itself is the &lt;strong&gt;database&lt;/strong&gt;. There are no tables, no rigid columns, and no joins required for the common case. You ask for the folder, you get the folder.&lt;/p&gt;

&lt;p&gt;The price for that flexibility is responsibility: the database is not going to enforce your schema for you unless you ask it to, and if you store data sloppily, you will read it back sloppily.&lt;/p&gt;

&lt;p&gt;That is the whole vibe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's pretend we are building one
&lt;/h2&gt;

&lt;p&gt;We want a database that stores data the way our app already thinks about it (as nested objects), scales out across many machines, and lets us evolve schemas without crying. We will call it &lt;strong&gt;MongoDB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For the running example, we are building a tiny &lt;strong&gt;recipe app&lt;/strong&gt;. Recipes have ingredients, steps, tags, comments. Perfect for showing off documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: The unit of data is a document, not a row
&lt;/h2&gt;

&lt;p&gt;A document is a JSON object. (Internally, it is &lt;strong&gt;BSON&lt;/strong&gt;, a binary form of JSON that adds types like &lt;code&gt;Date&lt;/code&gt;, &lt;code&gt;ObjectId&lt;/code&gt;, &lt;code&gt;Decimal128&lt;/code&gt;, and binary blobs.) It can have arrays. It can have nested objects. It can be thirty fields deep. It is not a row.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nc"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;65fa1c...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mochi Cookies&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mia&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dessert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;japanese&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;easy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;ingredients&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rice flour&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sugar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;milk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ml&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mix dry ingredients.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Add milk slowly while whisking.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cook on medium heat until smooth.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;prepMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nc"&gt;ISODate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-04-01T10:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth pausing on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every document has an &lt;code&gt;_id&lt;/code&gt;.&lt;/strong&gt; If you do not provide one, the driver creates an &lt;code&gt;ObjectId&lt;/code&gt; for you. It is sortable by time and unique across the cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schemas are flexible.&lt;/strong&gt; Two recipes in the same collection do not need identical fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arrays are first class.&lt;/strong&gt; No "join table" needed for tags, steps, or ingredients on a recipe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There is no enforced schema by default.&lt;/strong&gt; This is a feature and a footgun. We will add validation in a moment.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision 2: Four verbs, just like SQL
&lt;/h2&gt;

&lt;p&gt;The mental model is familiar. We just talk to documents instead of rows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Insert
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mochi Cookies&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mia&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dessert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;japanese&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;prepMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertMany&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Miso Soup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;prepMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Onigiri&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="na"&gt;prepMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Find (the workhorse)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dessert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;65fa1c...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;projection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;prepMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// only those fields&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;prepMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$lte&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;prepMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;find&lt;/code&gt; returns a cursor. You stream from it. &lt;code&gt;findOne&lt;/code&gt; returns one document or &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Update
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;65fa1c...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prepMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;japanese&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$addToSet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;asian&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the dollar prefixed operators. They are how you describe &lt;strong&gt;what&lt;/strong&gt; to do, not the literal new document. If you write &lt;code&gt;{ $set: { x: 1 } }&lt;/code&gt;, you set field &lt;code&gt;x&lt;/code&gt;. If you forget the operator and write just &lt;code&gt;{ x: 1 }&lt;/code&gt;, you replace the entire document with &lt;code&gt;{ x: 1 }&lt;/code&gt;. Yes, that bug stings the first time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Delete
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;65fa1c...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;spam&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Decision 3: Query operators, the dollar sign menu
&lt;/h2&gt;

&lt;p&gt;Inside a query, MongoDB has a small language of operators. The greatest hits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;prepMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$gt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;$lte&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dessert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;snack&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$ne&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Anonymous&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$gte&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ISODate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-01-01&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$exists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comparison operators: &lt;code&gt;$eq&lt;/code&gt;, &lt;code&gt;$ne&lt;/code&gt;, &lt;code&gt;$gt&lt;/code&gt;, &lt;code&gt;$gte&lt;/code&gt;, &lt;code&gt;$lt&lt;/code&gt;, &lt;code&gt;$lte&lt;/code&gt;, &lt;code&gt;$in&lt;/code&gt;, &lt;code&gt;$nin&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Logical operators (when AND of fields is not enough):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;$or&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prepMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$lt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;easy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Querying inside arrays is shockingly useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// any ingredient with name "sugar"&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ingredients.name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sugar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// at least one ingredient with name "sugar" AND amount &amp;gt; 50, on the same element&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;ingredients&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$elemMatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sugar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$gt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// a recipe with all of these tags&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$all&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;easy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dessert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update operators are a parallel menu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;$set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prepMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;$inc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;views&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;$push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;popular&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;$addToSet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;popular&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;        &lt;span class="c1"&gt;// push but only if not already there&lt;/span&gt;
  &lt;span class="na"&gt;$pull&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;$unset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;temporaryNote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;$rename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;oldField&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;newField&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you remember three operators, remember &lt;code&gt;$set&lt;/code&gt;, &lt;code&gt;$inc&lt;/code&gt;, and &lt;code&gt;$addToSet&lt;/code&gt;. You will reach for them every day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 4: Schema design, the part that actually matters
&lt;/h2&gt;

&lt;p&gt;Here is the truth that catches every newcomer: in MongoDB, &lt;strong&gt;schema design is 90% of your job&lt;/strong&gt;, even though there is no enforced schema. The flexibility is real, but the wrong shape is still going to hurt you.&lt;/p&gt;

&lt;p&gt;The single most important decision is &lt;strong&gt;embed or reference&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Embed when data is owned and read together
&lt;/h3&gt;

&lt;p&gt;A recipe owns its ingredients. They are not shared with other recipes. They are read every time you read the recipe. So embed them right inside the recipe document.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mochi Cookies&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ingredients&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rice flour&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sugar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One read, all the data. No joins. This is MongoDB's superpower.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reference when data is shared or grows without bound
&lt;/h3&gt;

&lt;p&gt;If a recipe is part of a cookbook, and a cookbook can have hundreds of recipes, you do not embed all the recipes inside the cookbook. You store the recipe ids:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// cookbooks&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;c1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Tiny Asian Bites&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;recipeIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;r1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;r2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// recipes&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;r1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mochi Cookies&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you fetch the recipes separately (or with &lt;code&gt;$lookup&lt;/code&gt;, see the aggregation pipeline soon).&lt;/p&gt;

&lt;h3&gt;
  
  
  The rule of thumb
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One to few&lt;/strong&gt;: embed. (A user with 3 addresses.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One to many but bounded and small&lt;/strong&gt;: embed. (A blog post with 50 comments? Probably embed. With 5,000? Reference.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One to many, unbounded or shared&lt;/strong&gt;: reference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Many to many&lt;/strong&gt;: reference, with arrays of ids on one side or both.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also a maximum document size: &lt;strong&gt;16 MB&lt;/strong&gt;. If a document is approaching that limit, your model is wrong.&lt;/p&gt;

&lt;p&gt;A few more design patterns that show up in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bucket pattern&lt;/strong&gt;: instead of one document per sensor reading per second, store one document per minute (or hour) holding an array of readings. Way fewer documents, better cache use, much faster aggregations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Computed pattern&lt;/strong&gt;: precompute aggregates and store them on the parent (&lt;code&gt;commentCount&lt;/code&gt;, &lt;code&gt;totalSpentCents&lt;/code&gt;). Update them on writes. The read becomes free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extended reference&lt;/strong&gt;: when you reference, copy the few fields you almost always need (a recipe stores &lt;code&gt;author: { _id, name, avatarUrl }&lt;/code&gt;). Saves a join. Trade off: you must update the copy when the source changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema versioning&lt;/strong&gt;: store a &lt;code&gt;schemaVersion: 2&lt;/code&gt; field on every document. Migrating becomes "any document still on v1 gets transformed lazily on read".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These patterns are why senior MongoDB feels different from senior SQL. You design for the read, not the write.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 5: Validate at the door anyway
&lt;/h2&gt;

&lt;p&gt;Schemaless feels great until a typo in your code writes &lt;code&gt;{ tile: "Mochi" }&lt;/code&gt; instead of &lt;code&gt;{ title: "Mochi" }&lt;/code&gt; for three weeks before anyone notices. So MongoDB lets you bolt on &lt;strong&gt;JSON Schema validation&lt;/strong&gt; at the collection level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCollection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;recipes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;$jsonSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;bsonType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;createdAt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bsonType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;prepMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bsonType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;int&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minimum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bsonType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bsonType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bsonType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;date&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;validationLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;strict&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;validationAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now writes that violate the schema are rejected. You keep the flexibility, you remove the typos.&lt;/p&gt;

&lt;p&gt;In a real Node.js app, you usually go one level higher: define the schema once with &lt;strong&gt;Zod&lt;/strong&gt; (or use &lt;strong&gt;Mongoose&lt;/strong&gt; if you want a fuller ODM), and let it both validate at the API boundary and produce your TypeScript types.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 6: Indexes work just like SQL, but read the rules
&lt;/h2&gt;

&lt;p&gt;Without indexes, every query is a full collection scan. Same as SQL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;               &lt;span class="c1"&gt;// ascending&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// compound&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Special index types you will actually use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multikey&lt;/strong&gt; (automatic): if a field is an array, the index covers each element. &lt;code&gt;db.recipes.createIndex({ tags: 1 })&lt;/code&gt; lets you find recipes by any tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text&lt;/strong&gt;: full text search. &lt;code&gt;db.recipes.createIndex({ title: "text", description: "text" })&lt;/code&gt;. Then &lt;code&gt;find({ $text: { $search: "mochi" } })&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geospatial&lt;/strong&gt; (&lt;code&gt;2dsphere&lt;/code&gt;): for "find places near me" queries on GeoJSON points.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTL&lt;/strong&gt;: documents auto delete after a deadline. Great for sessions, magic links, signed urls. &lt;code&gt;db.sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 })&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial&lt;/strong&gt;: index only the matching subset. &lt;code&gt;{ partialFilterExpression: { active: true } }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wildcard&lt;/strong&gt;: index unknown fields. Use sparingly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To see what a query is doing, ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dessert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;explain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;executionStats&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at &lt;code&gt;winningPlan&lt;/code&gt; and the &lt;code&gt;totalDocsExamined&lt;/code&gt; vs &lt;code&gt;nReturned&lt;/code&gt;. If they are wildly different, you are probably scanning when you could be using an index.&lt;/p&gt;

&lt;p&gt;The compound index ordering rule is the same as SQL: &lt;strong&gt;the order of fields matters&lt;/strong&gt;. &lt;code&gt;{ author: 1, createdAt: -1 }&lt;/code&gt; helps queries that filter by &lt;code&gt;author&lt;/code&gt; (alone) or by &lt;code&gt;author&lt;/code&gt; plus &lt;code&gt;createdAt&lt;/code&gt;. It does not help a query that only filters by &lt;code&gt;createdAt&lt;/code&gt;. This is sometimes called the &lt;strong&gt;ESR rule&lt;/strong&gt; in MongoDB land: index keys should generally appear in the order &lt;strong&gt;Equality, Sort, Range&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 7: The aggregation pipeline, the real power tool
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;find&lt;/code&gt; is fine for "give me documents that match". The moment you need to group, transform, join, project, or compute, you graduate to the &lt;strong&gt;aggregation pipeline&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You think of it as Unix pipes. Each stage takes documents in and emits documents out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aggregate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;placedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$gte&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ISODate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-04-01&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$customerId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$sum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;revenueCents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$sum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$totalCents&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$sort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;revenueCents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stages you will use most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;$match&lt;/code&gt;&lt;/strong&gt;: filter, like &lt;code&gt;WHERE&lt;/code&gt;. Put it first when you can to use indexes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;$project&lt;/code&gt;&lt;/strong&gt;: shape the output, pick fields, compute new ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;$group&lt;/code&gt;&lt;/strong&gt;: collapse documents by a key, with accumulators (&lt;code&gt;$sum&lt;/code&gt;, &lt;code&gt;$avg&lt;/code&gt;, &lt;code&gt;$min&lt;/code&gt;, &lt;code&gt;$max&lt;/code&gt;, &lt;code&gt;$push&lt;/code&gt;, &lt;code&gt;$addToSet&lt;/code&gt;, &lt;code&gt;$first&lt;/code&gt;, &lt;code&gt;$last&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;$sort&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;$limit&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;$skip&lt;/code&gt;&lt;/strong&gt;: classic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;$unwind&lt;/code&gt;&lt;/strong&gt;: turn an array field into one document per element. Magic for "explode tags".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;$lookup&lt;/code&gt;&lt;/strong&gt;: yes, &lt;strong&gt;MongoDB does joins&lt;/strong&gt;. Same idea as SQL &lt;code&gt;LEFT JOIN&lt;/code&gt;, with a slightly different API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;$facet&lt;/code&gt;&lt;/strong&gt;: run multiple sub pipelines on the same input, return them side by side. Great for dashboards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;$set&lt;/code&gt; / &lt;code&gt;$addFields&lt;/code&gt;&lt;/strong&gt;: add computed fields without losing the existing ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;$merge&lt;/code&gt;&lt;/strong&gt; / &lt;strong&gt;&lt;code&gt;$out&lt;/code&gt;&lt;/strong&gt;: write the result back into a collection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A &lt;code&gt;$lookup&lt;/code&gt; example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aggregate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dessert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$lookup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;localField&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authorId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;foreignField&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;as&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;author&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$unwind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$author&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;                  &lt;span class="c1"&gt;// flatten to a single object&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;author.name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;prepMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a join, an unwind, and a projection in one query. Once the pipeline clicks, you stop reaching for application code to do post processing. You let the database do it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Senior tip: aggregations can use indexes, but only on the early &lt;code&gt;$match&lt;/code&gt; and &lt;code&gt;$sort&lt;/code&gt; stages. Filter early, then transform.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Decision 8: Transactions, when you really need them
&lt;/h2&gt;

&lt;p&gt;For most workloads, the trick is to design your documents so a single write changes everything that needs to change. One document update is atomic by itself. That is enough for 90% of cases.&lt;/p&gt;

&lt;p&gt;When you really do need to write across multiple documents or collections atomically, MongoDB has &lt;strong&gt;multi document transactions&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$inc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$inc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;transfers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;amountCents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ACID is real here, just like in SQL. The catches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transactions need a &lt;strong&gt;replica set&lt;/strong&gt; (which Atlas always gives you, and most production MongoDBs run as).&lt;/li&gt;
&lt;li&gt;They are slower than single document writes. Reach for them only when you need them.&lt;/li&gt;
&lt;li&gt;Long transactions hurt throughput. Keep them short.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The right design instinct in MongoDB is "make this one document update", and the second instinct is "okay then, transactions". Not the other way around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 9: Scaling, replication, and sharding
&lt;/h2&gt;

&lt;p&gt;Three things you will hear about, in order of importance for most apps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Replica set&lt;/strong&gt;: a primary plus secondaries that follow along. Reads can go to secondaries with the right read preference. Writes go to the primary. If the primary dies, a secondary is elected. This gives you durability and high availability and is the default in Atlas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read concern and write concern&lt;/strong&gt;: you choose how many nodes must acknowledge a write (&lt;code&gt;w: "majority"&lt;/code&gt; is the safe default), and how recent your reads must be (&lt;code&gt;readConcern: "majority"&lt;/code&gt; for the safest reads). These two knobs are the trade off between durability and latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sharding&lt;/strong&gt;: when one machine cannot hold the data, you split the collection across many. You pick a &lt;strong&gt;shard key&lt;/strong&gt; that determines which documents live where. Pick wisely: an &lt;code&gt;_id&lt;/code&gt; or random shard key spreads writes evenly, but lookups by user are then scattered across all shards. There is no perfect shard key, only the one that fits your access patterns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most apps under "huge" scale never need sharding. Replica sets are enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 10: The modern stack
&lt;/h2&gt;

&lt;p&gt;In 2026, the typical MongoDB setup looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Atlas&lt;/strong&gt; (the official managed service) for production. Free tier for prototypes. Handles replica sets, backups, point in time restore, monitoring, and Atlas Search.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atlas Search&lt;/strong&gt; for full text search backed by Lucene. It is wildly more capable than the built in &lt;code&gt;$text&lt;/code&gt; operator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atlas Vector Search&lt;/strong&gt; for semantic search and RAG (retrieval augmented generation) for AI apps. Vectors live next to your documents. One database, both kinds of search.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Official drivers&lt;/strong&gt; for every language. In Node.js, the modern choice is the official &lt;code&gt;mongodb&lt;/code&gt; driver, often paired with &lt;strong&gt;Zod&lt;/strong&gt; for validation, or the &lt;strong&gt;Mongoose&lt;/strong&gt; ODM if you want a more opinionated layer with hooks and middleware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compass&lt;/strong&gt; (the GUI) for poking at data and prototyping aggregations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;mongosh&lt;/code&gt;&lt;/strong&gt; (the new shell) for command line work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are starting a new app, the lean default is: official driver + Zod schemas + Atlas. Reach for Mongoose only if you genuinely want its conveniences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 11: Pitfalls you only learn the hard way
&lt;/h2&gt;

&lt;p&gt;A short list of senior level traps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting &lt;code&gt;$set&lt;/code&gt; in updates&lt;/strong&gt; replaces the document. Always include the operator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storing a stringly typed &lt;code&gt;_id&lt;/code&gt;&lt;/strong&gt; when you could have used &lt;code&gt;ObjectId&lt;/code&gt;. ObjectIds are sortable by creation time, smaller, and indexed by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Querying with the wrong type&lt;/strong&gt; silently returns nothing. &lt;code&gt;{ price: "10" }&lt;/code&gt; will not match &lt;code&gt;{ price: 10 }&lt;/code&gt;. Types matter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Letting documents grow unbounded.&lt;/strong&gt; A recipe with 50,000 comments is a problem. Bucket or reference them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating Mongo like SQL.&lt;/strong&gt; If your queries are full of &lt;code&gt;$lookup&lt;/code&gt; joins, you may be designing as if you are still in SQL. Reconsider whether the data should be embedded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writing &lt;code&gt;find()&lt;/code&gt; and forgetting the projection.&lt;/strong&gt; You ship the entire 8KB document over the wire when you only needed the title.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping indexes "for now".&lt;/strong&gt; "For now" becomes "in production" faster than you think.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring the &lt;code&gt;_id&lt;/code&gt; in &lt;code&gt;$group&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;_id: null&lt;/code&gt; groups everything together. &lt;code&gt;_id: "$field"&lt;/code&gt; groups by that field. This is the most confusing line in the aggregation pipeline for newcomers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not using &lt;code&gt;.lean()&lt;/code&gt; (Mongoose) or projection (driver) when you do not need full hydrated objects.&lt;/strong&gt; Slower otherwise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating capped collections like queues.&lt;/strong&gt; Use a real queue.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A peek under the hood
&lt;/h2&gt;

&lt;p&gt;What really happens when you run a query:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The driver opens a connection (pooled) to the cluster and figures out the topology (which node is primary, which are secondaries).&lt;/li&gt;
&lt;li&gt;Your query goes to the appropriate node based on read preference.&lt;/li&gt;
&lt;li&gt;The query planner picks an execution plan, ideally using an index.&lt;/li&gt;
&lt;li&gt;The storage engine (WiredTiger, by default) reads pages from the cache or from disk. Documents are stored as compressed BSON.&lt;/li&gt;
&lt;li&gt;Results stream back through the driver. Cursors fetch in batches by default (100 documents or 1MB), so a &lt;code&gt;find()&lt;/code&gt; over a million docs does not blow up memory.&lt;/li&gt;
&lt;li&gt;Writes go to the primary, get applied in memory, then flushed to the on disk &lt;strong&gt;journal&lt;/strong&gt;, then replicated to secondaries based on your write concern.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The mental model is: WiredTiger is the SQLite or InnoDB underneath, BSON is the row format, and the replica set is the durability story. Once you know that, debugging slow queries and odd behavior gets much easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tiny tips that will save you later
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design for the read.&lt;/strong&gt; What does your most common query look like? Shape documents to make it one read, no joins.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pick &lt;code&gt;ObjectId&lt;/code&gt; for &lt;code&gt;_id&lt;/code&gt; unless you have a reason not to.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always set &lt;code&gt;w: "majority"&lt;/code&gt;&lt;/strong&gt; in production. Default is fine for development.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use Atlas Search for anything past trivial text search.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add indexes before they bite, not after.&lt;/strong&gt; Watch slow query logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the aggregation pipeline.&lt;/strong&gt; It is faster than fetching documents to your app and looping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep documents under a megabyte where you can.&lt;/strong&gt; 16 MB is the hard limit, but cache and replication love smaller documents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate at the boundary with Zod&lt;/strong&gt; (or the collection validator). Schemaless does not mean schema free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;.explain("executionStats")&lt;/code&gt; like you would &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;.&lt;/strong&gt; It is the same skill in a different costume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Back it up. Test the restore.&lt;/strong&gt; Atlas does this for you. If you self host, you are on the hook.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;So that is the whole story. We were tired of forcing nested, evolving data into rigid tables. We built a database that stores documents, the same shape your app already passes around. We grouped them into collections, indexed them, and gave them a powerful aggregation pipeline so we did not have to do post processing in app code.&lt;/p&gt;

&lt;p&gt;We accepted the trade off: schemaless is freedom, and freedom needs discipline. We designed documents around the reads we cared about, embedded what was owned, referenced what was shared, and added validation to keep typos from rotting our data. We learned that ACID is not just a SQL thing: single document writes are atomic, and full transactions are there when we need them.&lt;/p&gt;

&lt;p&gt;Once that map is in your head, MongoDB stops feeling like SQL with weird syntax and starts feeling like the data model your app was always trying to be.&lt;/p&gt;

&lt;p&gt;Happy modeling, and save a cookie for me.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>mongodb</category>
      <category>database</category>
    </item>
    <item>
      <title>Learning SQL As If You Built It Yourself</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Wed, 20 May 2026 13:08:00 +0000</pubDate>
      <link>https://dev.to/edriso/learning-sql-as-if-you-built-it-yourself-3fig</link>
      <guid>https://dev.to/edriso/learning-sql-as-if-you-built-it-yourself-3fig</guid>
      <description>&lt;p&gt;If you have ever tried to keep your app's data in a JSON file or in memory, you know how that story ends. At first it works. Two users? Easy. A hundred users with orders, addresses, and a history of returns? Now your "database" is a 40MB file you are scared to open, and looking up a single record takes a full scan of the disk.&lt;/p&gt;

&lt;p&gt;You start writing helper functions. Find a user by email. Find all their orders. Make sure no order points at a missing user. After a week of this, you have invented a worse, slower database, and you still cannot answer "what were our top 5 selling books last month?".&lt;/p&gt;

&lt;p&gt;That is the gap SQL fills.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is SQL, really
&lt;/h2&gt;

&lt;p&gt;Think of SQL as &lt;strong&gt;a giant spreadsheet on steroids, with a strict librarian living inside&lt;/strong&gt;. Your data goes into tables (sheets). Each row is a record. Each column has a type and rules. The librarian enforces the rules: no missing required fields, no duplicate ids, no orders pointing to a user that does not exist.&lt;/p&gt;

&lt;p&gt;You do not poke around the sheets yourself. You write a polite question in a special language, and the librarian goes off, walks the shelves, and brings back exactly the rows you asked for. The question is &lt;strong&gt;declarative&lt;/strong&gt;. You say what you want, not how to get it. The librarian (the query planner) figures out the fastest path.&lt;/p&gt;

&lt;p&gt;That is the whole vibe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's pretend we are building one
&lt;/h2&gt;

&lt;p&gt;We want a way to store structured data, enforce rules between pieces of data, and answer questions about it without writing custom code every time. We will call the language &lt;strong&gt;SQL&lt;/strong&gt; (Structured Query Language) and the engine behind it a &lt;strong&gt;relational database&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For our running example, we are opening a tiny online bookstore. Books, authors, customers, orders. We will model the whole thing and learn to ask it useful questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: Data lives in tables, with a strict shape
&lt;/h2&gt;

&lt;p&gt;A table has a fixed set of columns. Each column has a type. Each row must obey the shape. No "this row has an extra field" surprises.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;authors&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt;    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;        &lt;span class="nb"&gt;TEXT&lt;/span&gt;         &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;born_year&lt;/span&gt;   &lt;span class="nb"&gt;INT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt;    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt;       &lt;span class="nb"&gt;TEXT&lt;/span&gt;         &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;author_id&lt;/span&gt;   &lt;span class="nb"&gt;BIGINT&lt;/span&gt;       &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;authors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;price_cents&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;          &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price_cents&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;published&lt;/span&gt;   &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;  &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;     &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt;  &lt;span class="nb"&gt;TEXT&lt;/span&gt;      &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;   &lt;span class="nb"&gt;TEXT&lt;/span&gt;      &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt;    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;       &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;book_id&lt;/span&gt;     &lt;span class="nb"&gt;BIGINT&lt;/span&gt;       &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;qty&lt;/span&gt;         &lt;span class="nb"&gt;INT&lt;/span&gt;          &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;placed_at&lt;/span&gt;   &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth pausing on, because they show up everywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PRIMARY KEY&lt;/code&gt;&lt;/strong&gt; is the row's unique identity. Every table has one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;REFERENCES other_table(id)&lt;/code&gt;&lt;/strong&gt; is a &lt;strong&gt;foreign key&lt;/strong&gt;. The librarian will refuse to insert an order for a customer that does not exist, and refuse to delete a customer who still has orders (unless you say &lt;code&gt;ON DELETE CASCADE&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;NOT NULL&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;UNIQUE&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;CHECK&lt;/code&gt;&lt;/strong&gt; are constraints that bake business rules into the data, not into the app code. The database becomes the last line of defense, even if six different apps write to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DEFAULT&lt;/code&gt;&lt;/strong&gt; lets the database fill in values for you (timestamps, ids, version numbers).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This shape is the &lt;strong&gt;schema&lt;/strong&gt;. The whole database is just schemas plus rows.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Quick sanity rule: if your data has a stable shape and relationships matter, use SQL. If your data is messy, document like, and varies row to row, that is what we will discuss in the MongoDB post.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Decision 2: Four verbs, infinite combinations
&lt;/h2&gt;

&lt;p&gt;Every interaction with SQL is one of four verbs. Learn these and you have learned 70% of the language.&lt;/p&gt;

&lt;h3&gt;
  
  
  Insert
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;authors&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;born_year&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Saint-Exupery'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1900&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price_cents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;published&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'The Little Prince'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'1943-04-06'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Night Flight'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1199&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'1931-01-01'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Select (the workhorse)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price_cents&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;books&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;price_cents&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;published&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt;  &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it like English: pick these columns, from this table, where this is true, sorted this way, give me the first 10.&lt;/p&gt;

&lt;h3&gt;
  
  
  Update
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;    &lt;span class="n"&gt;price_cents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1099&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you forget the &lt;code&gt;WHERE&lt;/code&gt;, &lt;strong&gt;you update every row&lt;/strong&gt;. Yes, every senior has done it once. Wrap big updates in a transaction, we will see how soon.&lt;/p&gt;

&lt;h3&gt;
  
  
  Delete
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same warning as &lt;code&gt;UPDATE&lt;/code&gt;. No &lt;code&gt;WHERE&lt;/code&gt;, no rows left.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 3: Filtering rows the librarian can understand
&lt;/h2&gt;

&lt;p&gt;Inside &lt;code&gt;WHERE&lt;/code&gt; you can stack conditions. A short list of the most useful operators:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;price_cents&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;       &lt;span class="k"&gt;ILIKE&lt;/span&gt; &lt;span class="s1"&gt;'%prince%'&lt;/span&gt;        &lt;span class="c1"&gt;-- case insensitive in Postgres&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;author_id&lt;/span&gt;   &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;published&lt;/span&gt;  &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;published&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2000-01-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The classic gotcha: &lt;code&gt;NULL&lt;/code&gt; is not equal to anything, not even itself. &lt;code&gt;WHERE col = NULL&lt;/code&gt; always returns no rows. Use &lt;code&gt;IS NULL&lt;/code&gt; and &lt;code&gt;IS NOT NULL&lt;/code&gt;. Always.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LIKE 'foo%'&lt;/code&gt; matches the prefix &lt;code&gt;foo&lt;/code&gt;. &lt;code&gt;LIKE '%foo'&lt;/code&gt; matches the suffix. &lt;code&gt;LIKE '%foo%'&lt;/code&gt; is the full text contains. The percent sign is the wildcard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 4: Joining tables, the actual superpower
&lt;/h2&gt;

&lt;p&gt;Splitting data into multiple tables only pays off if we can stitch them back together at query time. That is what &lt;strong&gt;joins&lt;/strong&gt; do.&lt;/p&gt;

&lt;p&gt;The four flavors that matter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- INNER JOIN: only rows that match in both tables&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt;   &lt;span class="n"&gt;authors&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- LEFT JOIN: every row from the left, with NULLs where the right has nothing&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;authors&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- An author with zero books still appears, with title = NULL.&lt;/span&gt;

&lt;span class="c1"&gt;-- RIGHT JOIN: mirror image of LEFT JOIN. Rarely used (just flip the tables).&lt;/span&gt;

&lt;span class="c1"&gt;-- FULL OUTER JOIN: every row from both sides, NULL where the other is missing.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ON&lt;/code&gt; clause says how to match. The &lt;code&gt;WHERE&lt;/code&gt; clause filters the joined result.&lt;/p&gt;

&lt;p&gt;Two senior level rules that catch newcomers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;LEFT JOIN&lt;/code&gt; plus a &lt;code&gt;WHERE&lt;/code&gt; on the right table acts like an &lt;code&gt;INNER JOIN&lt;/code&gt;.&lt;/strong&gt; If you write &lt;code&gt;LEFT JOIN books WHERE books.published IS NOT NULL&lt;/code&gt;, you have just thrown away the unmatched rows. Put filters that should keep the unmatched rows inside the &lt;code&gt;ON&lt;/code&gt; clause: &lt;code&gt;LEFT JOIN books b ON b.author_id = a.id AND b.published IS NOT NULL&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always alias your tables&lt;/strong&gt; in non trivial queries. &lt;code&gt;b.title&lt;/code&gt; is much easier to read (and grep) than &lt;code&gt;books.title&lt;/code&gt; repeated five times.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Decision 5: Aggregations, the "summarize this" verbs
&lt;/h2&gt;

&lt;p&gt;When you do not want individual rows but a roll up, you reach for &lt;code&gt;GROUP BY&lt;/code&gt; and the aggregate functions: &lt;code&gt;COUNT&lt;/code&gt;, &lt;code&gt;SUM&lt;/code&gt;, &lt;code&gt;AVG&lt;/code&gt;, &lt;code&gt;MIN&lt;/code&gt;, &lt;code&gt;MAX&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Top selling books last month&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sold&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt;   &lt;span class="n"&gt;books&lt;/span&gt;  &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;placed_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;sold&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt;  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules to internalize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every column in the &lt;code&gt;SELECT&lt;/code&gt; must either be in the &lt;code&gt;GROUP BY&lt;/code&gt; or wrapped in an aggregate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;HAVING&lt;/code&gt; filters groups.&lt;/strong&gt; &lt;code&gt;WHERE&lt;/code&gt; filters rows before grouping. Do not confuse them.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;orders_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;-- only customers with 3+ orders&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Decision 6: When one query is not enough, build it in pieces
&lt;/h2&gt;

&lt;p&gt;For complex queries, stacking joins and aggregations gets unreadable fast. We added two tools for that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Subqueries
&lt;/h3&gt;

&lt;p&gt;A query inside a query. Treat its result as a virtual table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;books&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;author_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;authors&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;born_year&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1950&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Common Table Expressions (CTEs)
&lt;/h3&gt;

&lt;p&gt;Same idea, named, top to bottom. Far more readable when the query has multiple stages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;top_books&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;book_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sold&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;placed_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;book_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sold&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;top_books&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt;   &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sold&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt;  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CTEs can be &lt;strong&gt;recursive&lt;/strong&gt;, which is how you walk tree shaped data (categories with subcategories, employees with managers, comments with replies):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;descendants&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
  &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt;   &lt;span class="n"&gt;descendants&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;descendants&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CTEs read top to bottom like a small program. Use them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 7: Window functions, the senior superpower
&lt;/h2&gt;

&lt;p&gt;Sometimes you want a per row calculation that "sees" other rows around it without collapsing into groups. That is what &lt;strong&gt;window functions&lt;/strong&gt; do.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Rank books by sales within each author&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;RANK&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author_id&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rank_for_author&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;OVER(...)&lt;/code&gt; says "do this calculation across this window". &lt;code&gt;PARTITION BY&lt;/code&gt; is the group, &lt;code&gt;ORDER BY&lt;/code&gt; is the order within the group. You get one output row per input row, plus the calculated value.&lt;/p&gt;

&lt;p&gt;Common ones to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ROW_NUMBER()&lt;/code&gt; for "give each row a serial number".&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RANK()&lt;/code&gt; and &lt;code&gt;DENSE_RANK()&lt;/code&gt; for tied ranking.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LAG(col)&lt;/code&gt;, &lt;code&gt;LEAD(col)&lt;/code&gt; for "value of the previous/next row in the window".&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SUM(col) OVER (ORDER BY ...)&lt;/code&gt; for running totals.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once these click, a whole class of "I had to do this in app code" problems become one liners.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 8: Indexes, the secret to fast queries
&lt;/h2&gt;

&lt;p&gt;A table without indexes is a phone book printed in random order. Finding "Alice" means starting at page one. With an index, the librarian builds an alphabetical map: "Alice is around page 47, go straight there".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;books_author_id_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;author_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;orders_customer_placed_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;placed_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;customers_email_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The senior level rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Primary keys and unique constraints already create indexes.&lt;/strong&gt; You get those free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Index columns you filter or join on&lt;/strong&gt;, not columns you only display.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composite index column order matters.&lt;/strong&gt; &lt;code&gt;(customer_id, placed_at)&lt;/code&gt; helps queries that filter by &lt;code&gt;customer_id&lt;/code&gt; (alone) or by &lt;code&gt;customer_id&lt;/code&gt; plus &lt;code&gt;placed_at&lt;/code&gt;. It does &lt;strong&gt;not&lt;/strong&gt; help a query that only filters by &lt;code&gt;placed_at&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres has special index types&lt;/strong&gt;: GIN (for arrays and JSONB), BRIN (for huge time series), partial (&lt;code&gt;WHERE active = true&lt;/code&gt;), expression (&lt;code&gt;LOWER(email)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indexes are not free.&lt;/strong&gt; Every write has to update them. Do not index every column "just in case". Profile first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To find out what the database is actually doing for a query, ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;author_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That returns the &lt;strong&gt;query plan&lt;/strong&gt;. Look for "Index Scan" (good) versus "Seq Scan" (full table read, fine for small tables, scary for big ones), and look at the actual time per step.&lt;/p&gt;

&lt;p&gt;Reading query plans is the single skill that separates "writes SQL" from "writes fast SQL". Spend time on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 9: Transactions, all or nothing
&lt;/h2&gt;

&lt;p&gt;Sometimes a single user action needs multiple statements, and "half done" is the worst possible state. Money moved out of one account but never into the other. An order created without payment.&lt;/p&gt;

&lt;p&gt;We wrap the statements in a &lt;strong&gt;transaction&lt;/strong&gt;. Either every statement commits, or none of them do.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;-- or ROLLBACK if anything went wrong&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Transactions guarantee four properties together known as &lt;strong&gt;ACID&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Atomicity&lt;/strong&gt;: all or nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt;: constraints stay valid before and after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolation&lt;/strong&gt;: concurrent transactions do not see each other's half done work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durability&lt;/strong&gt;: once committed, it survives a crash.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Isolation has levels (read committed, repeatable read, serializable). The default in Postgres is &lt;code&gt;READ COMMITTED&lt;/code&gt;, which is fine for most apps. Reach for &lt;code&gt;SERIALIZABLE&lt;/code&gt; when you have a critical multi step invariant and you would rather have the database retry than risk anomalies. Read about phantom reads and lost updates once, then you will recognize the bug shapes when you see them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 10: Views and materialized views
&lt;/h2&gt;

&lt;p&gt;If a complex query is something you ask all the time, give it a name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;top_books_30d&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sold&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt;   &lt;span class="n"&gt;books&lt;/span&gt;  &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;placed_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;top_books_30d&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;sold&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;view&lt;/strong&gt; is just a saved query. It runs every time you &lt;code&gt;SELECT&lt;/code&gt; from it.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;materialized view&lt;/strong&gt; stores the result physically and refreshes on demand. Great for expensive analytics that do not need to be real time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;top_books_30d&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;
&lt;span class="n"&gt;REFRESH&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;top_books_30d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Decision 11: Set operations
&lt;/h2&gt;

&lt;p&gt;Less famous, but very handy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Books either by author 1 or with sales (and both)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;author_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;UNION&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;book_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Same idea but keep duplicates&lt;/span&gt;
&lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;

&lt;span class="c1"&gt;-- In A but not in B&lt;/span&gt;
&lt;span class="k"&gt;EXCEPT&lt;/span&gt;

&lt;span class="c1"&gt;-- In both&lt;/span&gt;
&lt;span class="k"&gt;INTERSECT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;UNION&lt;/code&gt; deduplicates and is slower. &lt;code&gt;UNION ALL&lt;/code&gt; keeps everything and is faster. Pick on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 12: Talking to SQL safely from your app
&lt;/h2&gt;

&lt;p&gt;This is the one rule you must never break.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never build a query by string concatenation with user input.&lt;/strong&gt; That is how you get SQL injection, the most embarrassing bug in our profession.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// NEVER&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`SELECT * FROM users WHERE email = '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ALWAYS&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SELECT * FROM users WHERE email = $1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use parameterized queries. Every modern client supports them. The driver sends the SQL and the values separately, so the database never confuses one for the other.&lt;/p&gt;

&lt;p&gt;The other classic app side trap is the &lt;strong&gt;N+1 query problem&lt;/strong&gt;. You fetch a list of authors, then for each one, you fire a separate query for their books. 1 query becomes 1 + N queries. The fix is one query with a &lt;code&gt;JOIN&lt;/code&gt;, or batch loading by id list (&lt;code&gt;WHERE author_id IN (...)&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 13: Designing schemas that age well
&lt;/h2&gt;

&lt;p&gt;The art is in the schema, not the queries. Tables you regret are tables you fight forever. A short senior level checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Normalize first, denormalize on purpose.&lt;/strong&gt; A normalized schema (no duplicated facts) is easier to keep correct. You can always add a denormalized cache column later if profiling demands it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick the right primary key.&lt;/strong&gt; &lt;code&gt;BIGSERIAL&lt;/code&gt; (auto increment) is fine for most things. UUIDs are great for distributed systems and for not exposing row counts in URLs. Use &lt;code&gt;UUID v7&lt;/code&gt; if you need sortable UUIDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always store timestamps with timezone.&lt;/strong&gt; &lt;code&gt;TIMESTAMPTZ&lt;/code&gt;, not &lt;code&gt;TIMESTAMP&lt;/code&gt;. Future you will thank present you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Money in integer cents, not floats.&lt;/strong&gt; &lt;code&gt;1.10 + 2.20&lt;/code&gt; in floats is heartbreak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Booleans are fine.&lt;/strong&gt; Do not use a &lt;code&gt;CHAR(1)&lt;/code&gt; "Y"/"N". This is not 1995.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add indexes for foreign keys.&lt;/strong&gt; The database does not always do this for you, and queries that join on them will sit and cry without one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Soft delete with a &lt;code&gt;deleted_at TIMESTAMPTZ&lt;/code&gt; column&lt;/strong&gt; if you need recoverable deletes. Then add &lt;code&gt;WHERE deleted_at IS NULL&lt;/code&gt; to your reads (or a partial index).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migrations are version controlled SQL.&lt;/strong&gt; Tools like Flyway, Sqitch, Prisma Migrate, Alembic. Never edit the production schema by hand.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision 14: A peek under the hood
&lt;/h2&gt;

&lt;p&gt;What really happens when you run a query:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The client sends the SQL to the server (bind parameters separately if you used a parameterized query).&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;parser&lt;/strong&gt; turns the text into a syntax tree.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;planner / optimizer&lt;/strong&gt; considers many possible execution plans (full scan, index scan, different join orders, hash join vs merge join vs nested loop). It uses statistics about the table sizes and column distributions to pick the cheapest plan it can find.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;executor&lt;/strong&gt; runs the chosen plan, reading pages from disk or the buffer cache, joining, filtering, aggregating.&lt;/li&gt;
&lt;li&gt;Results stream back to the client. Transactions wrap all of this in WAL (write ahead log) entries for durability.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That little planner is the brain of the whole thing. It is also why the same query can be fast on Tuesday and slow on Wednesday: the data shape changed, and the planner picked a different plan. Keep statistics fresh (&lt;code&gt;ANALYZE&lt;/code&gt; in Postgres) and you will rarely be surprised.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short guide to dialects
&lt;/h2&gt;

&lt;p&gt;SQL is a standard. Real engines extend and tweak it. The big three you will see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt;: the senior favorite. Best in class JSON support, window functions, CTEs, generated columns, partial indexes, &lt;code&gt;RETURNING&lt;/code&gt;, extensions. Default choice for most new apps in 2026.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL / MariaDB&lt;/strong&gt;: massive ecosystem, fine performance, weaker default constraints (set &lt;code&gt;STRICT_ALL_TABLES&lt;/code&gt;), historically weaker on JSON and window features (now caught up).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite&lt;/strong&gt;: a single file database, perfect for embedded use, tests, mobile, and surprisingly capable for small to mid sized apps. Great for prototypes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Almost every example in this post is portable. The places you will find dialect differences: identifier quoting, auto increment syntax, JSON functions, full text search, and date functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tiny tips that will save you later
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SELECT *&lt;/code&gt; is fine in the editor, dangerous in production code.&lt;/strong&gt; List the columns you need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always run an &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt; as a &lt;code&gt;SELECT&lt;/code&gt; first.&lt;/strong&gt; Same &lt;code&gt;WHERE&lt;/code&gt;, same eyes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrap risky changes in a transaction&lt;/strong&gt; with &lt;code&gt;BEGIN; ... ROLLBACK;&lt;/code&gt; until you are certain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;COUNT(*)&lt;/code&gt; counts rows. &lt;code&gt;COUNT(col)&lt;/code&gt; skips NULLs.&lt;/strong&gt; They are different.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer &lt;code&gt;EXISTS&lt;/code&gt; over &lt;code&gt;IN&lt;/code&gt; for subqueries&lt;/strong&gt; that may grow large.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter early.&lt;/strong&gt; Push &lt;code&gt;WHERE&lt;/code&gt; and joins as low as possible so fewer rows climb the pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the query plan before optimizing.&lt;/strong&gt; Guessing is a waste of an evening.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection pool, do not connect per request.&lt;/strong&gt; Use PgBouncer, the language client's built in pool, or your framework's helper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put long names into a style guide.&lt;/strong&gt; &lt;code&gt;snake_case&lt;/code&gt; for tables and columns, plural for tables (&lt;code&gt;books&lt;/code&gt;, not &lt;code&gt;book&lt;/code&gt;). Pick one and never argue about it again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Back up the database. Test the restore.&lt;/strong&gt; A backup you have never restored is a wish, not a backup.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;So that is the whole story. We were tired of inventing tiny worse databases out of files and helper functions. We built one engine that holds structured data, enforces relationships, and answers questions in a declarative language we agreed to call SQL.&lt;/p&gt;

&lt;p&gt;We taught the engine four verbs: &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;. We let it stitch tables back together with joins, summarize with aggregations, and walk windows with window functions. We added CTEs so complex queries read top to bottom. We made it fast with indexes, and gave it transactions and ACID guarantees so half done work is never visible. We baked rules into the schema with constraints, so the data stays sane even when buggy apps try to mess it up.&lt;/p&gt;

&lt;p&gt;Once that map is in your head, every database tutorial, blog post, and codebase starts to feel familiar. SQL stops feeling like a foreign language and starts feeling like a calm conversation with a very picky librarian who is, secretly, on your side.&lt;/p&gt;

&lt;p&gt;Happy querying, and put a bookmark in chapter two for me.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>database</category>
      <category>sql</category>
    </item>
    <item>
      <title>Learning Frontend Testing As If You Built It Yourself</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Tue, 19 May 2026 13:07:00 +0000</pubDate>
      <link>https://dev.to/edriso/learning-frontend-testing-as-if-you-built-it-yourself-3ko6</link>
      <guid>https://dev.to/edriso/learning-frontend-testing-as-if-you-built-it-yourself-3ko6</guid>
      <description>&lt;p&gt;If you have ever pushed a "tiny" change to production on a Friday and watched the bug reports roll in over the weekend, you know why testing exists. Manual checking does not scale. A teammate refactors a util, your screen still looks fine, and three pages you never opened are quietly broken.&lt;/p&gt;

&lt;p&gt;Tests are how you stop being scared of your own code.&lt;/p&gt;

&lt;p&gt;That is the gap testing fills.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is frontend testing, really
&lt;/h2&gt;

&lt;p&gt;Think of tests as &lt;strong&gt;a tiny robot that opens your app for you, faster and more thorough than you ever could&lt;/strong&gt;. The robot clicks, types, waits, asserts. It does this on every commit. When something breaks, the robot tells you within seconds.&lt;/p&gt;

&lt;p&gt;There are three sizes of robot, and a senior frontend engineer uses all three on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unit tests&lt;/strong&gt; poke a single function. Tiny, fast, run by the thousands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Component / integration tests&lt;/strong&gt; mount one component (or a small tree) and interact with it. Real DOM, real events, mocked network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;End to end tests&lt;/strong&gt; drive a real browser through a real app. Slow, expensive, the closest thing to a user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 2026 sweet spot in the React world: &lt;strong&gt;Vitest&lt;/strong&gt; for the first two, &lt;strong&gt;Playwright&lt;/strong&gt; for the third. &lt;strong&gt;React Testing Library&lt;/strong&gt; for the assertions and queries inside Vitest.&lt;/p&gt;

&lt;p&gt;That is the whole vibe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's pretend we are building one
&lt;/h2&gt;

&lt;p&gt;We want a frontend testing setup that is fast in development, accurate in CI, and gives us confidence to refactor without panic. We will not build it from scratch. We will assemble it from three modern tools.&lt;/p&gt;

&lt;p&gt;For the running example, we are testing pieces of an "Adopt a cat" app: a &lt;code&gt;formatPrice&lt;/code&gt; helper, an &lt;code&gt;AdoptForm&lt;/code&gt; component, and the full happy path of finding and adopting a cat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: Three layers, three tools
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────┐
│  E2E tests           Playwright          ~few   │
│  ──────────────                                  │
│  Component tests     Vitest + RTL        ~many  │
│  ──────────────                                  │
│  Unit tests          Vitest              ~lots  │
└─────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The number of tests should look roughly like that triangle. Lots of cheap unit tests, a healthy middle of component tests, a small number of expensive E2E tests for the critical paths.&lt;/p&gt;

&lt;p&gt;The trap senior engineers warn about: a flat shape, where you have hundreds of E2E tests and almost no unit or component tests. Your CI takes 40 minutes, every flaky test wastes the team's morning, and a small refactor breaks 60 tests at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: Unit tests with Vitest
&lt;/h2&gt;

&lt;p&gt;Install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-D&lt;/span&gt; vitest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add to &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vitest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:run"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vitest run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:cov"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vitest run --coverage"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A unit test is a function that calls another function and asserts the result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/format-price.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`$&lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;cents&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/format-price.test.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;formatPrice&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./format-price&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;formatPrice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;formats whole dollars&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;formatPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$5.00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;formats cents with two decimals&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;formatPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1234&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$12.34&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;handles zero&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;formatPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$0.00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;describe&lt;/code&gt;&lt;/strong&gt; groups related tests, &lt;strong&gt;&lt;code&gt;it&lt;/code&gt;&lt;/strong&gt; (or &lt;code&gt;test&lt;/code&gt;) is one test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;expect(x).toBe(y)&lt;/code&gt;&lt;/strong&gt; checks strict equality. &lt;strong&gt;&lt;code&gt;.toEqual(y)&lt;/code&gt;&lt;/strong&gt; for deep equality. &lt;strong&gt;&lt;code&gt;.toBeTruthy()&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;.toContain()&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;.toThrow()&lt;/code&gt;&lt;/strong&gt; for the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vitest uses Vite's config&lt;/strong&gt;, so your aliases, plugins, and TypeScript setup work for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests run in parallel by default&lt;/strong&gt;, so write them isolated. No shared global state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The senior level habits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test the contract, not the implementation.&lt;/strong&gt; Same input, same output. Refactors should not break unit tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One assertion per test in spirit.&lt;/strong&gt; A test that checks five different things is five tests in a trench coat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name tests as sentences.&lt;/strong&gt; "formats whole dollars" reads better than "test1".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip happy path snapshots.&lt;/strong&gt; A snapshot of a function output is fine. A snapshot of a 200 line HTML tree is a tax you will pay forever.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision 3: Component tests with React Testing Library
&lt;/h2&gt;

&lt;p&gt;Install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-D&lt;/span&gt; @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure Vitest for the DOM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vitest.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vitest/config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;react&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@vitejs/plugin-react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;react&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jsdom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;setupFiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./test/setup.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;globals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// test/setup.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@testing-library/jest-dom/vitest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can mount components and interact with them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/components/AdoptForm.tsx&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AdoptForm&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;onAdopt&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;onAdopt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;onAdopt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Cat name&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Adopt&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/components/AdoptForm.test.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@testing-library/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@testing-library/user-event&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AdoptForm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./AdoptForm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AdoptForm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;disables the submit button when the name is empty&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AdoptForm&lt;/span&gt; &lt;span class="na"&gt;onAdopt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/adopt/i&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;toBeDisabled&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;calls onAdopt with the typed name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onAdopt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AdoptForm&lt;/span&gt; &lt;span class="na"&gt;onAdopt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onAdopt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/cat name/i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mochi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/adopt/i&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onAdopt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mochi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The single most important rule of React Testing Library:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Find elements the way a user would.&lt;/strong&gt; By role, by label, by visible text. Almost never by class name or test id.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The query priority, from best to worst:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;getByRole&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;button&lt;/code&gt;, &lt;code&gt;heading&lt;/code&gt;, &lt;code&gt;textbox&lt;/code&gt;, &lt;code&gt;link&lt;/code&gt;, ...). Tests both rendering and accessibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;getByLabelText&lt;/code&gt;&lt;/strong&gt; for form fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;getByPlaceholderText&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;getByText&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;getByDisplayValue&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;getByAltText&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;getByTitle&lt;/code&gt;&lt;/strong&gt; for images and tooltips.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;getByTestId&lt;/code&gt;&lt;/strong&gt; as a last resort.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your tests cannot find a button by its role, that is a sign the markup is not accessible. Tests guide you toward better HTML.&lt;/p&gt;

&lt;p&gt;The four flavors of query:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;getBy...&lt;/code&gt;&lt;/strong&gt; throws if not found. Use for things that should be there now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;queryBy...&lt;/code&gt;&lt;/strong&gt; returns null if not found. Use for asserting absence: &lt;code&gt;expect(queryByText("Loading")).toBeNull()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;findBy...&lt;/code&gt;&lt;/strong&gt; is async, retries until it appears. Use for things that show up after a state change or a fetch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;...All&lt;/code&gt;&lt;/strong&gt; versions return arrays for matching multiple.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision 4: Mocking the network, the right way
&lt;/h2&gt;

&lt;p&gt;Real components fetch data. In tests you do not want to hit a real API. Two modern choices:&lt;/p&gt;

&lt;h3&gt;
  
  
  MSW (Mock Service Worker), the senior favorite
&lt;/h3&gt;

&lt;p&gt;MSW intercepts fetch calls at the network layer, so your component code uses real &lt;code&gt;fetch&lt;/code&gt; but the response comes from your handlers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// test/mocks.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;msw&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;setupServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;msw/node&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handlers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/cats&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mochi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}])&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/adopt/:id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setupServer&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// test/setup.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./mocks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;beforeAll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;afterAll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;afterEach&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;beforeAll&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;onUnhandledRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nf"&gt;afterEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resetHandlers&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nf"&gt;afterAll&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now any &lt;code&gt;fetch("/api/cats")&lt;/code&gt; in any test returns the mocked data. You can override per test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;msw&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../test/mocks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shows an error when the API fails&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/cats&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;})));&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MSW is the closest thing to "real network" you can get without one. The same handlers work in dev (with the browser worker), in tests (with the Node server), and in Storybook.&lt;/p&gt;

&lt;h3&gt;
  
  
  Module mocks for everything else
&lt;/h3&gt;

&lt;p&gt;For non network dependencies, use Vitest's &lt;code&gt;vi.mock&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;vi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;mockResolvedValue&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use sparingly. A test full of mocks tests the mocks, not the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 5: End to end tests with Playwright
&lt;/h2&gt;

&lt;p&gt;Install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm init playwright@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Playwright spins up real Chromium, Firefox, and WebKit, drives them through your app, and asserts what the user sees. It is the gold standard for E2E in 2026.&lt;/p&gt;

&lt;p&gt;A single test of the adoption flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// e2e/adopt.spec.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a user can adopt a cat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/cats&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;heading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/cats looking for a home/i&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Adopt Mochi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mochi is going home&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;cats&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;adopted/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The senior level habits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use the same query priorities as RTL.&lt;/strong&gt; &lt;code&gt;getByRole&lt;/code&gt;, &lt;code&gt;getByLabel&lt;/code&gt;, &lt;code&gt;getByText&lt;/code&gt;. Skip CSS selectors when you can.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web first assertions auto retry.&lt;/strong&gt; &lt;code&gt;await expect(locator).toBeVisible()&lt;/code&gt; keeps trying until the timeout. Almost no &lt;code&gt;waitForSelector&lt;/code&gt; needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the critical path, not every page.&lt;/strong&gt; 20 to 30 well chosen E2E tests is plenty for most apps. The unit and component layers cover the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run against a real build (&lt;code&gt;next build &amp;amp;&amp;amp; next start&lt;/code&gt;)&lt;/strong&gt; in CI, not the dev server. You catch problems specific to production builds (missing &lt;code&gt;"use client"&lt;/code&gt;, env var issues, route handler bugs).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shard E2E in CI.&lt;/strong&gt; Playwright supports &lt;code&gt;--shard=1/4&lt;/code&gt; natively. Splitting the suite across four parallel runners turns a 12 minute run into 3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trace on failure.&lt;/strong&gt; &lt;code&gt;npx playwright test --trace=retain-on-failure&lt;/code&gt; records a video, screenshots, and network for every failing test. Debugging gets ten times faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few useful Playwright extras:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input[name=name]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mochi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// fill a field&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Adopt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// click&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                       &lt;span class="c1"&gt;// wait for navigation&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fail.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;               &lt;span class="c1"&gt;// screenshot on demand&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Loading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toHaveCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// assert it disappeared&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toContainText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Saved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For visual regression, Playwright has a built in &lt;code&gt;toHaveScreenshot()&lt;/code&gt; that diffs against a stored baseline. Great for design system primitives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 6: A modern test pyramid for a real React app
&lt;/h2&gt;

&lt;p&gt;A repeatable target for any new project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                &lt;span class="s"&gt;lots                tests/lib/**/*.test.ts&lt;/span&gt;
&lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;           &lt;span class="s"&gt;per feature         tests/components/**/*.test.tsx&lt;/span&gt;
&lt;span class="na"&gt;e2e&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                 &lt;span class="s"&gt;critical paths      e2e/**/*.spec.ts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What to test where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pure functions, formatters, parsers, validators&lt;/strong&gt;: unit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single component behavior with props and events&lt;/strong&gt;: component.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A page or a feature working end to end (auth, checkout, signup, search)&lt;/strong&gt;: E2E.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server actions, API routes, server queries&lt;/strong&gt;: integration tests with a real test database (or a transactional rollback per test). Use Vitest with &lt;code&gt;node&lt;/code&gt; environment, not jsdom.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip&lt;/strong&gt;: testing internal state, testing your component library, testing TypeScript types, testing third party hooks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision 7: Make tests easy to read
&lt;/h2&gt;

&lt;p&gt;The single best style guide for tests, in one rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A failing test should tell you what is wrong without you reading the code.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two patterns help:&lt;/p&gt;

&lt;h3&gt;
  
  
  Arrange / Act / Assert
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;calls onAdopt with the typed name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Arrange&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onAdopt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AdoptForm&lt;/span&gt; &lt;span class="nx"&gt;onAdopt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onAdopt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;
  &lt;span class="c1"&gt;// Act&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/cat name/i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mochi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/adopt/i&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="c1"&gt;// Assert&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onAdopt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mochi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Custom render helpers
&lt;/h3&gt;

&lt;p&gt;Wrap once, reuse everywhere. Providers (router, query client, theme, i18n) live in one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// test/render.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@testing-library/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;QueryClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;QueryClientProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renderWithProviders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;qc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QueryClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;defaultOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;QueryClientProvider&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;qc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;QueryClientProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every component test imports &lt;code&gt;renderWithProviders&lt;/code&gt; and the noise stays out of the test body.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 8: Testing async UI without flakes
&lt;/h2&gt;

&lt;p&gt;Async tests fail intermittently when they assert at the wrong moment. The fix is to ask the testing library to wait for the right thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// wait for a thing to appear&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Adopted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// wait for a thing to disappear&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitForElementToBeRemoved&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queryByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Loading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// wait for any condition&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitFor&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onAdopt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalled&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two anti patterns to avoid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;setTimeout&lt;/code&gt; or hand spun delays.&lt;/strong&gt; Always flaky.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing &lt;code&gt;useEffect&lt;/code&gt; directly.&lt;/strong&gt; Test the user visible result, not the hook internals.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision 9: CI integration and speed
&lt;/h2&gt;

&lt;p&gt;A CI pipeline that respects developers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/test.yml (sketch)&lt;/span&gt;
&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run lint&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run test:run -- --reporter=github&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright install --with-deps&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright test --shard=${{ matrix.shard }}/4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order matters: lint first (cheapest), unit and component tests next (fast), build, then E2E (slowest, sharded).&lt;/p&gt;

&lt;p&gt;The senior level habits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fail fast.&lt;/strong&gt; If lint fails, do not run E2E.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache dependencies.&lt;/strong&gt; &lt;code&gt;actions/setup-node&lt;/code&gt; plus &lt;code&gt;cache: npm&lt;/code&gt; shaves real time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload Playwright traces&lt;/strong&gt; as artifacts on failure. Future you will thank present you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ban &lt;code&gt;.only&lt;/code&gt; and &lt;code&gt;.skip&lt;/code&gt;&lt;/strong&gt; in CI with a lint rule. They sneak in.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision 10: Senior level moves and pitfalls
&lt;/h2&gt;

&lt;p&gt;A short list of habits that separate "writes tests" from "writes good tests":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test behavior, not structure.&lt;/strong&gt; A test that breaks when you change the markup but not the behavior is testing too much.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid &lt;code&gt;data-testid&lt;/code&gt; unless you must.&lt;/strong&gt; They drift from the user experience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the test first when the bug is hard.&lt;/strong&gt; A failing test pinpoints the problem and stays as a regression guard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never test third party libraries.&lt;/strong&gt; They have their own tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the boundary, not the internals.&lt;/strong&gt; A component takes props, renders, and emits events. Test those.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep tests independent.&lt;/strong&gt; Order should not matter. No shared mutable state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mock at the network, not at every module.&lt;/strong&gt; MSW &amp;gt; a forest of &lt;code&gt;vi.mock&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run a real database in integration tests.&lt;/strong&gt; SQLite in memory, Postgres in Docker, Mongo Memory Server. Mocked databases lie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage is not a goal.&lt;/strong&gt; 80 high quality tests beat 200 noisy ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete tests that no longer earn their keep.&lt;/strong&gt; A test you mute every other week is a tax, not an asset.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A peek under the hood
&lt;/h2&gt;

&lt;p&gt;What really happens when you run &lt;code&gt;vitest&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Vitest reads your &lt;code&gt;vite.config&lt;/code&gt; and starts a Vite dev server.&lt;/li&gt;
&lt;li&gt;Vite transforms your source on the fly using esbuild or SWC.&lt;/li&gt;
&lt;li&gt;Tests run in worker threads, in parallel.&lt;/li&gt;
&lt;li&gt;JSDOM provides a fake DOM. React renders into it. Events go through React's synthetic event system.&lt;/li&gt;
&lt;li&gt;Assertions run, results stream to the reporter.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What really happens when you run Playwright:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Playwright launches a real browser binary (Chromium by default).&lt;/li&gt;
&lt;li&gt;It opens your test URL in a fresh context (cookies, storage are isolated).&lt;/li&gt;
&lt;li&gt;Each &lt;code&gt;await&lt;/code&gt; is an action recorded in a trace, with snapshots and network logs.&lt;/li&gt;
&lt;li&gt;Tests run in parallel across browser contexts within a single browser instance.&lt;/li&gt;
&lt;li&gt;On failure, traces are saved, screenshots are taken, the next test still runs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two consequences for your time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vitest is fast because Vite is fast.&lt;/strong&gt; Cold start in milliseconds, hot reload of tests as you edit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playwright is slow because browsers are slow.&lt;/strong&gt; Use it sparingly, run in parallel, shard in CI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tiny tips that will save you later
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;screen.debug()&lt;/code&gt;&lt;/strong&gt; prints the current DOM. Use it when you cannot find a node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;logRoles(container)&lt;/code&gt;&lt;/strong&gt; lists every accessible role. Pick the one your test should query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;userEvent&lt;/code&gt;, not &lt;code&gt;fireEvent&lt;/code&gt;.&lt;/strong&gt; It simulates real user actions including focus, keypresses, and accessible interactions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reset MSW handlers between tests.&lt;/strong&gt; Otherwise tests bleed into each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run Playwright in &lt;code&gt;--ui&lt;/code&gt; mode&lt;/strong&gt; locally. The time travel debugger is magical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot only stable, narrow output.&lt;/strong&gt; A function output, a serialized state. Not a 5000 character DOM string.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write a small &lt;code&gt;renderWithProviders&lt;/code&gt;&lt;/strong&gt; on day one. Tests stay lean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep one test file per source file&lt;/strong&gt;, named the same way (&lt;code&gt;Foo.tsx&lt;/code&gt; paired with &lt;code&gt;Foo.test.tsx&lt;/code&gt;). Easy to find.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;So that is the whole story. We were tired of being scared of our own code. We built a three layer testing pyramid: lots of unit tests with Vitest, a healthy middle of component tests with React Testing Library, a small number of E2E tests with Playwright. We mocked the network with MSW, queried elements the way a user would, asserted behavior instead of internals, and let CI catch what we missed.&lt;/p&gt;

&lt;p&gt;We learned to write tests as sentences, structure them as Arrange / Act / Assert, keep them independent, and delete the ones that stopped earning their keep. We chose tools that pay back our time: Vitest for speed, RTL for accessibility friendly queries, Playwright for real browser confidence, MSW for sane network mocks.&lt;/p&gt;

&lt;p&gt;Once that map is in your head, tests stop feeling like a tax and start feeling like a backup brain that catches the regressions a tired Friday you would have shipped. You start refactoring without flinching. You start sleeping better.&lt;/p&gt;

&lt;p&gt;Happy testing, and may your suite stay green.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>frontend</category>
      <category>testing</category>
    </item>
    <item>
      <title>Learning Web Performance As If You Built It Yourself</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Mon, 18 May 2026 13:06:00 +0000</pubDate>
      <link>https://dev.to/edriso/learning-web-performance-as-if-you-built-it-yourself-29fn</link>
      <guid>https://dev.to/edriso/learning-web-performance-as-if-you-built-it-yourself-29fn</guid>
      <description>&lt;p&gt;If you have ever opened a beautiful new website on your phone and watched the layout shift around for two seconds while you tried to tap a button, you have met bad web performance. You are not alone. According to the 2025 Web Almanac, only 48% of mobile pages and 56% of desktop pages pass all three Core Web Vitals.&lt;/p&gt;

&lt;p&gt;Performance is the gap between "the design looks great" and "people actually use it". A 24% lower bounce rate is the kind of number a CEO will print on a poster. It comes from making the page feel fast.&lt;/p&gt;

&lt;p&gt;That is the gap web performance fills.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is web performance, really
&lt;/h2&gt;

&lt;p&gt;Think of web performance as &lt;strong&gt;measuring three feelings the user has, not measuring how clever your code is&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Did the page show up?"&lt;/strong&gt; (LCP, Largest Contentful Paint)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Does it respond when I tap?"&lt;/strong&gt; (INP, Interaction to Next Paint)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Is it staying still or jumping around?"&lt;/strong&gt; (CLS, Cumulative Layout Shift)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Google calls these &lt;strong&gt;Core Web Vitals&lt;/strong&gt;. They are user perception metrics, not synthetic numbers. Google measures them on real users. They feed search rankings. Browsers expose them via the Performance API for free.&lt;/p&gt;

&lt;p&gt;The 2026 thresholds you must memorize:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Good&lt;/th&gt;
&lt;th&gt;Needs work&lt;/th&gt;
&lt;th&gt;Poor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LCP&lt;/td&gt;
&lt;td&gt;≤ 2.5 s&lt;/td&gt;
&lt;td&gt;2.5 to 4.0 s&lt;/td&gt;
&lt;td&gt;&amp;gt; 4.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INP&lt;/td&gt;
&lt;td&gt;≤ 200 ms&lt;/td&gt;
&lt;td&gt;200 to 500 ms&lt;/td&gt;
&lt;td&gt;&amp;gt; 500 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLS&lt;/td&gt;
&lt;td&gt;≤ 0.1&lt;/td&gt;
&lt;td&gt;0.1 to 0.25&lt;/td&gt;
&lt;td&gt;&amp;gt; 0.25&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;INP replaced FID (First Input Delay) in March 2024 and is the strictest Core Web Vital. &lt;strong&gt;43% of sites fail the 200ms INP threshold&lt;/strong&gt;. Most of the work in 2026 is here.&lt;/p&gt;

&lt;p&gt;That is the whole vibe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's pretend we are building one
&lt;/h2&gt;

&lt;p&gt;We want a way to make websites feel fast on real devices, on real networks, in real users' hands. We will not invent the metrics. The browser already gives them to us. We just need to learn to read them and to fix what they reveal.&lt;/p&gt;

&lt;p&gt;For the running example, we are speeding up a tiny &lt;strong&gt;online bakery&lt;/strong&gt; landing page: a hero image, a list of products, a "buy" button. We will improve each metric in turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: Measure first, optimize second
&lt;/h2&gt;

&lt;p&gt;Before you change anything, measure. Three places to measure, in order:&lt;/p&gt;

&lt;h3&gt;
  
  
  Lighthouse / PageSpeed Insights
&lt;/h3&gt;

&lt;p&gt;Open Chrome DevTools, hit the Lighthouse tab, run a report. Or paste your URL into &lt;a href="https://pagespeed.web.dev" rel="noopener noreferrer"&gt;pagespeed.web.dev&lt;/a&gt;. You get scores for each metric, plus specific suggestions ranked by impact.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;lab data&lt;/strong&gt;, run on a simulated mobile device. Useful for catching regressions, but does not match what real users see.&lt;/p&gt;

&lt;h3&gt;
  
  
  CrUX (Chrome User Experience Report)
&lt;/h3&gt;

&lt;p&gt;Real anonymized data from real Chrome users. The PageSpeed report shows it at the top under "field data". This is the data Google uses for ranking. Trust this number more than the Lighthouse one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real User Monitoring (RUM)
&lt;/h3&gt;

&lt;p&gt;Send your own metrics from production using the &lt;code&gt;web-vitals&lt;/code&gt; library. Two minutes of setup gives you the truth, on every device, every connection, every page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onLCP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onINP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onCLS&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;web-vitals&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ship to your analytics endpoint&lt;/span&gt;
  &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendBeacon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/vitals&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;onLCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;onINP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;onCLS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The senior level rule: &lt;strong&gt;Lighthouse for trends, CrUX for the truth, RUM for debugging.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: Improve LCP, the "did the page show up" metric
&lt;/h2&gt;

&lt;p&gt;LCP measures the time until the largest visible element finishes rendering. Almost always: a hero image, a hero &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;, or a big text block. If LCP is slow, the page feels broken even if everything else is fine.&lt;/p&gt;

&lt;p&gt;The four highest impact fixes, in order:&lt;/p&gt;

&lt;h3&gt;
  
  
  Preload the LCP image
&lt;/h3&gt;

&lt;p&gt;Tell the browser the most important image early, while it is still parsing HTML.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt;
  &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preload"&lt;/span&gt;
  &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"image"&lt;/span&gt;
  &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/hero-bakery.jpg"&lt;/span&gt;
  &lt;span class="na"&gt;fetchpriority=&lt;/span&gt;&lt;span class="s"&gt;"high"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Next.js, &lt;code&gt;priority&lt;/code&gt; on &lt;code&gt;&amp;lt;Image&amp;gt;&lt;/code&gt; does the same thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/hero-bakery.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Use modern image formats and responsive sizes
&lt;/h3&gt;

&lt;p&gt;AVIF is roughly half the size of JPEG. WebP is roughly 25% smaller. Browsers support them. Serve them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;picture&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"/hero.avif"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/avif"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"/hero.webp"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/webp"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/hero.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1200"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"800"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/picture&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always serve a size that fits the viewport. A phone does not need a 4K hero. Use &lt;code&gt;srcset&lt;/code&gt; with descriptors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/hero-1200.jpg"&lt;/span&gt;
  &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"/hero-400.jpg 400w, /hero-800.jpg 800w, /hero-1600.jpg 1600w"&lt;/span&gt;
  &lt;span class="na"&gt;sizes=&lt;/span&gt;&lt;span class="s"&gt;"(max-width: 600px) 100vw, 1200px"&lt;/span&gt;
  &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A tool like Sharp or a CDN like Cloudinary or Vercel Image Optimization gives you these sizes automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inline critical CSS, defer the rest
&lt;/h3&gt;

&lt;p&gt;Render blocking CSS delays paint. Inline the styles your above-the-fold content needs in &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt;, then load the full stylesheet &lt;code&gt;&amp;lt;link rel="preload" as="style"&amp;gt;&lt;/code&gt; and apply asynchronously.&lt;/p&gt;

&lt;p&gt;Most teams do not do this by hand. Frameworks handle it. If yours does not, run a tool like Critters or use a service that does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server side render the first paint
&lt;/h3&gt;

&lt;p&gt;Static HTML (SSG, ISR, RSC, plain server rendering) puts visible content on the screen before any JavaScript executes. That is the single biggest win you will ever ship.&lt;/p&gt;

&lt;p&gt;If your app is a Vite SPA where everything renders inside &lt;code&gt;&amp;lt;div id="root"&amp;gt;&amp;lt;/div&amp;gt;&lt;/code&gt; on the client, your LCP is at the mercy of the JS bundle. If you can move to Next.js, Astro, Remix, or a framework that ships HTML, do.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self host fonts and use &lt;code&gt;font-display: swap&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;A custom font that takes 800ms to load and blocks text with &lt;code&gt;font-display: block&lt;/code&gt; will torpedo your LCP. The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@font-face&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"Inter"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url("/fonts/inter.woff2")&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;"woff2"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;font-display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;swap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="m"&gt;900&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;/* variable font, one file for all weights */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;font-display: swap&lt;/code&gt; shows fallback text immediately and swaps to the custom font when it loads. Then preload the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preload"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"font"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"font/woff2"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/fonts/inter.woff2"&lt;/span&gt; &lt;span class="na"&gt;crossorigin&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better yet, use &lt;code&gt;next/font&lt;/code&gt; (in Next.js) or &lt;code&gt;unplugin-fonts&lt;/code&gt; (everywhere else). They handle subsetting, preloading, and &lt;code&gt;size-adjust&lt;/code&gt; to prevent CLS when the font swaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 3: Improve INP, the "does it respond" metric
&lt;/h2&gt;

&lt;p&gt;INP is the time between a user interaction (tap, click, keypress) and the next paint after the resulting work. If your button takes 600ms to react, INP fails.&lt;/p&gt;

&lt;p&gt;The pain almost always comes from a &lt;strong&gt;long task&lt;/strong&gt; on the main thread blocking the browser from painting. JavaScript is single threaded. While it is busy, nothing else happens.&lt;/p&gt;

&lt;p&gt;The senior level fixes:&lt;/p&gt;

&lt;h3&gt;
  
  
  Break long tasks into chunks
&lt;/h3&gt;

&lt;p&gt;A 200ms &lt;code&gt;for&lt;/code&gt; loop blocks the main thread for 200ms. Split it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processInChunks&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CHUNK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;CHUNK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;CHUNK&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// yield to the browser&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use the modern API directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;yieldToMain&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scheduler&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yield&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside long work, &lt;code&gt;await yieldToMain()&lt;/code&gt; periodically. The browser gets a chance to paint and respond.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use React's transitions for non urgent updates
&lt;/h3&gt;

&lt;p&gt;Sometimes the work is React rendering. &lt;code&gt;useTransition&lt;/code&gt; marks an update as low priority so the browser can paint the urgent stuff first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startTransition&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTransition&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                          &lt;span class="c1"&gt;// urgent: input updates immediately&lt;/span&gt;
  &lt;span class="nf"&gt;startTransition&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt; &lt;span class="c1"&gt;// not urgent&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The input feels instant even when the result list is heavy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Move heavy work off the main thread
&lt;/h3&gt;

&lt;p&gt;If a function takes 400ms, it should not run on the UI thread. Use a &lt;strong&gt;Web Worker&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// worker.ts&lt;/span&gt;
&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;expensiveCompute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// main.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./worker.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;module&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tools like &lt;code&gt;comlink&lt;/code&gt; make this much friendlier (the worker exposes a function, you &lt;code&gt;await&lt;/code&gt; it from the main thread).&lt;/p&gt;

&lt;h3&gt;
  
  
  Cut your JavaScript bundle
&lt;/h3&gt;

&lt;p&gt;The fastest function is the one that does not run. Common wins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code split by route.&lt;/strong&gt; Next.js does this automatically. In Vite SPAs, &lt;code&gt;React.lazy&lt;/code&gt; plus a route loader does the trick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lazy load heavy widgets.&lt;/strong&gt; Maps, charts, rich text editors should load only when used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tree shake your icon libraries.&lt;/strong&gt; Importing a single icon from &lt;code&gt;lucide-react&lt;/code&gt; is fine. Importing the whole namespace is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace heavy dependencies.&lt;/strong&gt; &lt;code&gt;date-fns/locale/en&lt;/code&gt; over &lt;code&gt;moment&lt;/code&gt;. &lt;code&gt;nanoid&lt;/code&gt; over &lt;code&gt;uuid&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the Coverage tab in DevTools&lt;/strong&gt; to see how much of your shipped JS is actually used. The number will surprise you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stop unnecessary re renders
&lt;/h3&gt;

&lt;p&gt;A React component that re renders on every keystroke when it does not need to is INP poison. Tools to reach for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;React.memo&lt;/code&gt; on heavy components passed stable props.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useMemo&lt;/code&gt; for expensive computations.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useCallback&lt;/code&gt; only when the callback is a dependency of a memoed child.&lt;/li&gt;
&lt;li&gt;The React Compiler (in 2026) handles most of this for you, but it is not magic. Inspect with the React DevTools Profiler.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision 4: Improve CLS, the "is it staying still" metric
&lt;/h2&gt;

&lt;p&gt;CLS measures unexpected layout shifts: the page jumping while the user is reading or about to tap. Three causes account for almost every shift in the wild:&lt;/p&gt;

&lt;h3&gt;
  
  
  Images without dimensions
&lt;/h3&gt;

&lt;p&gt;The browser does not know how big the image will be until it loads, so it reserves zero space. When the image arrives, everything below it shifts.&lt;/p&gt;

&lt;p&gt;The fix is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/cookie.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"640"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"480"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or in CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;aspect-ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser uses &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; to compute an aspect ratio and reserves space immediately. This is a free CLS win that almost no one does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Late loading fonts
&lt;/h3&gt;

&lt;p&gt;A custom font usually has different metrics from the fallback. When it swaps in, every line of text shifts.&lt;/p&gt;

&lt;p&gt;Mitigation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;font-display: optional&lt;/code&gt; to skip the swap entirely on slow connections.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;size-adjust&lt;/code&gt;, &lt;code&gt;ascent-override&lt;/code&gt;, &lt;code&gt;descent-override&lt;/code&gt; in &lt;code&gt;@font-face&lt;/code&gt; to make the fallback look like the custom font.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;next/font&lt;/code&gt; package and the modern Fontsource setup handle this for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Dynamic content (ads, embeds, banners) injected without reserved space
&lt;/h3&gt;

&lt;p&gt;The fix is to &lt;strong&gt;always reserve the space&lt;/strong&gt; before the content arrives. A skeleton, a placeholder div with a min height, an explicit &lt;code&gt;aspect-ratio&lt;/code&gt;. Anything that holds the slot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.ad-slot&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.embed-yt&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;aspect-ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the dimensions are unknown, do not inject content above the user's viewport at all. Inject below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 5: The network is the bottleneck
&lt;/h2&gt;

&lt;p&gt;A perfectly written app on a slow network is still slow. Two layers of fixes:&lt;/p&gt;

&lt;h3&gt;
  
  
  Compress everything
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# nginx (or your CDN equivalent)&lt;/span&gt;
&lt;span class="k"&gt;gzip&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;# compresses text by 70%+&lt;/span&gt;
&lt;span class="k"&gt;brotli&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;# compresses 20-25% better than gzip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most CDNs and platforms do this by default. Confirm in DevTools (Network tab, Headers, look for &lt;code&gt;content-encoding&lt;/code&gt;). If you see &lt;code&gt;text/css&lt;/code&gt; files at 100KB uncompressed, something is misconfigured.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cache aggressively, invalidate precisely
&lt;/h3&gt;

&lt;p&gt;Static assets (JS bundles, CSS, fonts, images) should be cached for a year, with a hash in the filename so a deploy busts the cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cache-Control: public, max-age=31536000, immutable
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HTML responses should be revalidated each time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cache-Control: no-cache
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most build tools (Vite, Next.js, Astro) handle hash naming automatically. Your CDN (Vercel, Netlify, Cloudflare) sets the headers.&lt;/p&gt;

&lt;p&gt;For dynamic content, use &lt;strong&gt;stale-while-revalidate&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cache-Control: public, max-age=60, stale-while-revalidate=600
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reads:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Trust this for 60 seconds. After that, keep serving the stale copy for up to 10 minutes while you fetch a fresh one in the background.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That single header is the secret to APIs that feel instant under load.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use HTTP/2 or HTTP/3
&lt;/h3&gt;

&lt;p&gt;If your server still answers in HTTP/1.1, you are wasting connections. Modern hosting gives you HTTP/2 or HTTP/3 (QUIC) for free. Multiplexing means dozens of small requests share a single connection, so you can stop bundling 200 modules into a single mega bundle out of fear.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preconnect and DNS prefetch
&lt;/h3&gt;

&lt;p&gt;For domains you know the page will hit (analytics, fonts, CDN, API), tell the browser early:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preconnect"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://api.example.com"&lt;/span&gt; &lt;span class="na"&gt;crossorigin&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"dns-prefetch"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.example.com"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Saves 100 to 500 ms on the first request to that origin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 6: Make the rest of the page feel fast
&lt;/h2&gt;

&lt;p&gt;A few moves that do not show up on Lighthouse but real users feel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Skeletons over spinners.&lt;/strong&gt; A grey placeholder shaped like the content feels faster than a centered spinner. The page looks alive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic UI for mutations.&lt;/strong&gt; When the user clicks "Like", increase the count immediately. Roll back if the request fails. (See &lt;code&gt;useOptimistic&lt;/code&gt; in React 19.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefetch on hover or intent.&lt;/strong&gt; Next.js's &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; does this automatically. For other apps, use &lt;code&gt;fetchpriority="low"&lt;/code&gt; on speculative requests, or a library like Quicklink.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Defer below the fold.&lt;/strong&gt; Anything not visible can wait. &lt;code&gt;&amp;lt;img loading="lazy"&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;iframe loading="lazy"&amp;gt;&lt;/code&gt;, &lt;code&gt;IntersectionObserver&lt;/code&gt; for components.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid &lt;code&gt;display: none&lt;/code&gt; for things you will show in 50ms.&lt;/strong&gt; Build them off screen, animate in. The user perceives the action faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision 7: A practical performance budget
&lt;/h2&gt;

&lt;p&gt;A budget is a number you commit to and let your CI enforce. Without one, performance rots over time.&lt;/p&gt;

&lt;p&gt;A starting budget for most apps:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Asset&lt;/th&gt;
&lt;th&gt;Budget&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Main bundle JS&lt;/td&gt;
&lt;td&gt;&amp;lt; 100 KB gzipped on the entry route&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS&lt;/td&gt;
&lt;td&gt;&amp;lt; 50 KB gzipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Images on first paint&lt;/td&gt;
&lt;td&gt;&amp;lt; 200 KB total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fonts&lt;/td&gt;
&lt;td&gt;1 to 2 weights, woff2, &amp;lt; 100 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total page weight&lt;/td&gt;
&lt;td&gt;&amp;lt; 500 KB on first paint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LCP&lt;/td&gt;
&lt;td&gt;&amp;lt; 2.5 s on a slow 4G mid range phone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INP&lt;/td&gt;
&lt;td&gt;&amp;lt; 200 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLS&lt;/td&gt;
&lt;td&gt;&amp;lt; 0.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plug a tool like &lt;code&gt;lighthouse-ci&lt;/code&gt;, &lt;code&gt;bundlesize&lt;/code&gt;, or &lt;code&gt;size-limit&lt;/code&gt; into CI. If a PR pushes the bundle past the budget, the build fails. The PR explains itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 8: Devtools you should know
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lighthouse panel&lt;/strong&gt; for one-off audits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance panel&lt;/strong&gt; to record a real interaction and see where time goes. The flame graph is your best friend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network panel&lt;/strong&gt; with throttling. Set "Fast 3G", reload, watch what happens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage panel&lt;/strong&gt; to find unused JS and CSS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebPageTest&lt;/strong&gt; (&lt;a href="https://www.webpagetest.org" rel="noopener noreferrer"&gt;webpagetest.org&lt;/a&gt;) for a deeper, scriptable analysis. The waterfall view is iconic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;bundle-analyzer&lt;/code&gt;&lt;/strong&gt; plugins for Vite, Webpack, and Next.js. See what is in your bundle, by file, sorted by size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;web-vitals&lt;/code&gt; extension&lt;/strong&gt; to see real metrics in the browser bar as you click around.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A peek under the hood
&lt;/h2&gt;

&lt;p&gt;What really happens between the click and the pixels:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;DNS lookup&lt;/strong&gt; for the domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TCP / TLS handshake&lt;/strong&gt; with the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP request&lt;/strong&gt; for the HTML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTML streams&lt;/strong&gt; to the browser. The parser starts immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS in the head blocks rendering&lt;/strong&gt; until it parses. This is why critical CSS matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags&lt;/strong&gt; without &lt;code&gt;defer&lt;/code&gt;/&lt;code&gt;async&lt;/code&gt; block the parser. This is why script placement matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layout&lt;/strong&gt; computes the size and position of every element.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paint&lt;/strong&gt; fills in pixels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composite&lt;/strong&gt; stacks layers and shows the final frame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript hydrates&lt;/strong&gt; any framework on the page, attaching event listeners.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two practical consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anything that delays steps 4 to 8 hurts LCP.&lt;/strong&gt; Fonts, images, render blocking CSS, server time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything that runs on the main thread after step 10 hurts INP.&lt;/strong&gt; Heavy hydration, large JS bundles, third party scripts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That mental model is enough to debug almost any performance issue you will hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tiny tips that will save you later
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test on a real low end phone.&lt;/strong&gt; Your MacBook does not represent your users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throttle to slow 4G in DevTools&lt;/strong&gt; before believing your local times.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; on every image and iframe.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;loading="lazy"&lt;/code&gt;&lt;/strong&gt; on images below the fold.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Self host fonts. Preload one. Use &lt;code&gt;font-display: swap&lt;/code&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code split by route.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove dependencies you do not use.&lt;/strong&gt; &lt;code&gt;npx depcheck&lt;/code&gt; finds them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit third party scripts.&lt;/strong&gt; Analytics, tag managers, chat widgets are often the slowest thing on a page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run Lighthouse in CI.&lt;/strong&gt; Performance regresses silently otherwise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track INP in production.&lt;/strong&gt; Most regressions live in JavaScript work, not in the network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache HTML for short windows with &lt;code&gt;stale-while-revalidate&lt;/code&gt;&lt;/strong&gt; to absorb traffic spikes without losing freshness.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;So that is the whole story. We were tired of building beautiful sites that felt slow. We learned that the user only cares about three things: did the page show up, does it respond when I tap, is it staying still. Google bottled those into LCP, INP, and CLS. We measured with Lighthouse, CrUX, and &lt;code&gt;web-vitals&lt;/code&gt;. We fixed LCP with preloading, modern image formats, server rendering, and font tactics. We fixed INP by yielding to the main thread, splitting bundles, cutting work, and moving heavy compute to workers. We fixed CLS with image dimensions, font metric overrides, and reserved space.&lt;/p&gt;

&lt;p&gt;We taught our network to compress, cache, preconnect, and prefetch. We set a budget, plugged it into CI, and stopped letting bundle size sneak upward.&lt;/p&gt;

&lt;p&gt;Once that map is in your head, web performance stops feeling like a dark art and starts feeling like a small set of repeatable habits. You ship fast pages on purpose, not by accident.&lt;/p&gt;

&lt;p&gt;Happy optimizing, and may your Vitals always be green.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Learning the Web Platform APIs As If You Built Them Yourself</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Sun, 17 May 2026 13:05:00 +0000</pubDate>
      <link>https://dev.to/edriso/learning-the-web-platform-apis-as-if-you-built-them-yourself-3o14</link>
      <guid>https://dev.to/edriso/learning-the-web-platform-apis-as-if-you-built-them-yourself-3o14</guid>
      <description>&lt;p&gt;If you have ever reached for a library to do something the browser already does, you have met the gap this post is about. Need a debounce? You wrote a &lt;code&gt;useEffect&lt;/code&gt;. Need to detect when a card scrolls into view? You added a 300 line library. Need offline support? You shrugged and gave up.&lt;/p&gt;

&lt;p&gt;The browser has changed. Modern browsers ship a generous, capable platform. Most of the libraries we still install in 2026 were written when the platform was missing the feature. They are not missing anymore.&lt;/p&gt;

&lt;p&gt;A senior frontend engineer knows what the browser already gives them, and reaches for it before reaching for npm.&lt;/p&gt;

&lt;p&gt;That is the gap this post fills.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the web platform, really
&lt;/h2&gt;

&lt;p&gt;Think of the browser as &lt;strong&gt;a small operating system that runs in a tab&lt;/strong&gt;. It has storage, a network stack, threads, scheduling, sensors, sometimes even a file system and Bluetooth. Each capability has an API. Many of them are excellent. Many of them are five lines of code instead of a 12KB dependency.&lt;/p&gt;

&lt;p&gt;Two ideas drive the whole thing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The platform is doing more than you think.&lt;/strong&gt; Capabilities ship every six weeks across all the major browsers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Promises and observers are the shape.&lt;/strong&gt; Most modern APIs are either &lt;code&gt;await&lt;/code&gt;able or "watch this thing and call me back".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the whole vibe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's pretend we are building one
&lt;/h2&gt;

&lt;p&gt;We are not building the platform. We are doing a senior level tour of it. For the running example, we will sprinkle a tiny &lt;strong&gt;read later&lt;/strong&gt; notes app with native APIs as we go. Save notes offline, fetch articles, observe the page, schedule work, broadcast across tabs.&lt;/p&gt;

&lt;h2&gt;
  
  
  API 1: &lt;code&gt;fetch&lt;/code&gt; (and AbortController), the network everyone forgets has features
&lt;/h2&gt;

&lt;p&gt;Every frontend engineer knows &lt;code&gt;fetch&lt;/code&gt;. Half of them only use 20% of it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/notes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Read later&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;include&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// send cookies on cross origin requests&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// override default&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cors&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// also "same-origin", "no-cors"&lt;/span&gt;
  &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="nx"&gt;ctrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// AbortController&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The features senior engineers actually use:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;AbortController&lt;/code&gt; for cancelling
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ctrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 5s timeout&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ctrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same &lt;code&gt;AbortController&lt;/code&gt; works with &lt;code&gt;addEventListener&lt;/code&gt;, with most modern Promise APIs, and with React Query's queries. Make it your default mental model: any long lived async work should be cancellable.&lt;/p&gt;

&lt;p&gt;The shortcut for "fetch with timeout":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Streaming responses
&lt;/h3&gt;

&lt;p&gt;The body is a &lt;code&gt;ReadableStream&lt;/code&gt;. You can consume it as it arrives, perfect for AI streaming or large downloads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeThrough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoderStream&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;getReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chunk:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;Response&lt;/code&gt; and &lt;code&gt;Request&lt;/code&gt; are real classes
&lt;/h3&gt;

&lt;p&gt;You can build them, store them, clone them. Service Workers rely on this. The same &lt;code&gt;Response&lt;/code&gt; API you receive from &lt;code&gt;fetch&lt;/code&gt; is the one you return from a Service Worker.&lt;/p&gt;

&lt;h2&gt;
  
  
  API 2: Storage, three flavors
&lt;/h2&gt;

&lt;p&gt;The browser has three storage mechanisms. Pick on purpose.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;localStorage&lt;/code&gt; and &lt;code&gt;sessionStorage&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Tiny key value store. Synchronous. Strings only. Good for: small UI state (theme, last opened tab), tiny preferences. Bad for: anything large, anything secret, anything that should sync.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sessionStorage&lt;/code&gt; is the same, scoped to the tab. Cleared on close.&lt;/p&gt;

&lt;p&gt;A senior level rule: &lt;strong&gt;never store auth tokens in &lt;code&gt;localStorage&lt;/code&gt;&lt;/strong&gt;. JavaScript can read it, which means every third party script can too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cookies
&lt;/h3&gt;

&lt;p&gt;Sent automatically with every same origin request. The browser respects &lt;code&gt;HttpOnly&lt;/code&gt;, &lt;code&gt;Secure&lt;/code&gt;, &lt;code&gt;SameSite&lt;/code&gt;. We covered them in the auth post. Use them for sessions and CSRF tokens.&lt;/p&gt;

&lt;h3&gt;
  
  
  IndexedDB, the actual database
&lt;/h3&gt;

&lt;p&gt;A real client side database. Asynchronous. Stores anything structurable, including blobs. Indexed for fast queries. Quotas in the megabytes to gigabytes range.&lt;/p&gt;

&lt;p&gt;The native API is famously verbose. Use a tiny wrapper like &lt;strong&gt;&lt;code&gt;idb-keyval&lt;/code&gt;&lt;/strong&gt; for simple cases or &lt;strong&gt;Dexie.js&lt;/strong&gt; for richer querying.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;del&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idb-keyval&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;note:42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Read later&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;note&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;note:42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;note:42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use IndexedDB for: offline caches, drafts, full text search indexes, large user data, anything you need to survive a refresh and a flaky network. It is the storage that makes serious offline apps possible.&lt;/p&gt;

&lt;p&gt;In 2026 there is also an exciting trend: &lt;strong&gt;SQLite in the browser&lt;/strong&gt;, via wasm libraries like &lt;code&gt;sql.js&lt;/code&gt; and &lt;code&gt;wa-sqlite&lt;/code&gt;, often backed by IndexedDB or the &lt;strong&gt;Origin Private File System (OPFS)&lt;/strong&gt;. Real SQL queries on the client. Worth knowing about for offline first apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  API 3: Service Workers, the engine of offline
&lt;/h2&gt;

&lt;p&gt;A Service Worker is a script that runs in the background, separate from any page, and can intercept network requests for a whole origin. It is how websites become installable apps that work offline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/main.ts&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;serviceWorker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/sw.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// public/sw.js&lt;/span&gt;
&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;install&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addAll&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/styles.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/app.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What that gives you: the page loads from cache instantly, even offline.&lt;/p&gt;

&lt;p&gt;The senior level patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cache first&lt;/strong&gt; for static assets that change rarely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network first, cache fallback&lt;/strong&gt; for HTML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stale while revalidate&lt;/strong&gt; for API responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a tool, do not hand roll.&lt;/strong&gt; &lt;strong&gt;Workbox&lt;/strong&gt; (Google) and &lt;strong&gt;Vite PWA Plugin&lt;/strong&gt; scaffold a sane Service Worker in a few lines. They handle caching strategies, precaching, runtime caching, navigation fallback, and updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full PWA story (offline + installable + push notifications) is a real thing in 2026. Browsers across Mac, Windows, Linux, Android, and even iOS support it. For an app that runs daily, "Add to Home Screen" is real distribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  API 4: Web Workers, threads for compute
&lt;/h2&gt;

&lt;p&gt;JavaScript runs on a single thread. Long work blocks the page. A &lt;strong&gt;Web Worker&lt;/strong&gt; runs a script on a separate thread, communicating by message passing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// worker.ts&lt;/span&gt;
&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;expensiveCompute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// main.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./worker.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;module&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The friendlier modern API: &lt;strong&gt;Comlink&lt;/strong&gt; turns the worker into a proxy you can &lt;code&gt;await&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// worker.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expose&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;comlink&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;expose&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nf"&gt;parseMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;slowParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;md&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// main.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;wrap&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;comlink&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wrap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;parseMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./worker.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;module&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use Web Workers for: heavy parsing (Markdown, syntax highlighting, JSON), image processing, data transforms, anything more than 50ms of compute. The page stays smooth, INP stays under 200ms.&lt;/p&gt;

&lt;p&gt;A close cousin: &lt;strong&gt;&lt;code&gt;requestIdleCallback&lt;/code&gt;&lt;/strong&gt; runs a function when the browser is idle. Great for non urgent work like analytics flushing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;requestIdleCallback&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sendQueuedAnalytics&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  API 5: IntersectionObserver, "tell me when this is on screen"
&lt;/h2&gt;

&lt;p&gt;Before this API, "is the element visible" was solved by listening to &lt;code&gt;scroll&lt;/code&gt; and reading layout in a tight loop. It was awful.&lt;/p&gt;

&lt;p&gt;Now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;obs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntersectionObserver&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isIntersecting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visible:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rootMargin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;200px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.lazy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Uses for senior frontends:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lazy load images and components&lt;/strong&gt; before they are needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infinite scroll&lt;/strong&gt;: observe a sentinel at the bottom of the list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trigger animations&lt;/strong&gt; when an element scrolls into view.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track impressions&lt;/strong&gt; for analytics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;rootMargin&lt;/code&gt; lets you start the work early, so the user never sees the loading state.&lt;/p&gt;

&lt;p&gt;A close cousin: &lt;strong&gt;&lt;code&gt;ResizeObserver&lt;/code&gt;&lt;/strong&gt; fires when an element changes size. Great for charts, tables, and components that need to re-render on layout changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ResizeObserver&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;drawChart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chartEl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;strong&gt;&lt;code&gt;MutationObserver&lt;/code&gt;&lt;/strong&gt; for "tell me when the DOM changed". Niche but lifesaving for browser extensions and integrations with markup you do not control.&lt;/p&gt;

&lt;h2&gt;
  
  
  API 6: BroadcastChannel, talking across tabs
&lt;/h2&gt;

&lt;p&gt;If a user has your app open in three tabs and logs out in one, the others should know. &lt;strong&gt;&lt;code&gt;BroadcastChannel&lt;/code&gt;&lt;/strong&gt; posts messages across same origin tabs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BroadcastChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;logout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;logout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;goToLogin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five lines, no library. Use it for: logout sync, cache invalidation across tabs, "this item just changed" notifications.&lt;/p&gt;

&lt;p&gt;For more general cross window coordination, the &lt;strong&gt;Web Locks API&lt;/strong&gt; ensures only one tab does a piece of work at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync-notes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;syncNotesWithServer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other tabs that try to acquire the lock will queue. Brilliant for "only one tab should be syncing".&lt;/p&gt;

&lt;h2&gt;
  
  
  API 7: Clipboard, Share, File access, the human integration layer
&lt;/h2&gt;

&lt;p&gt;These are the APIs that make a web app feel native.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clipboard
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readText&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modern, async, permission gated. The old &lt;code&gt;document.execCommand("copy")&lt;/code&gt; is deprecated. For images and other rich data, use &lt;code&gt;navigator.clipboard.write&lt;/code&gt; with &lt;code&gt;ClipboardItem&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Web Share API
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;share&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;share&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mochi's Blog&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A great post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On mobile, this opens the system share sheet. On desktop, it falls back gracefully. One line replaces a custom share modal.&lt;/p&gt;

&lt;h3&gt;
  
  
  File System Access
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showSaveFilePicker&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;suggestedName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notes.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JSON&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;writable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createWritable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;writable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;writable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real "save as" dialog, real file. Limited to Chromium browsers in 2026 but excellent for productivity apps.&lt;/p&gt;

&lt;p&gt;The simpler alternative for downloads is the venerable &lt;code&gt;&amp;lt;a download&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;download&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notes.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revokeObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  API 8: View Transitions, navigation feels smooth
&lt;/h2&gt;

&lt;p&gt;A genuinely magical API. Animate between any two DOM states with one line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startViewTransition&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// any DOM mutation here&lt;/span&gt;
  &lt;span class="nf"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser captures a snapshot before, applies your changes, captures after, and animates between them. With CSS &lt;code&gt;view-transition-name&lt;/code&gt; on shared elements, you get FLIP-style transitions for free.&lt;/p&gt;

&lt;p&gt;In 2026, the cross document version (&lt;code&gt;@view-transition&lt;/code&gt;) lets multi page apps animate between full page navigations as smoothly as SPAs. This is one of the reasons SPAs are no longer the only path to good UX.&lt;/p&gt;

&lt;h2&gt;
  
  
  API 9: Scheduler, Idle Detection, and modern timing
&lt;/h2&gt;

&lt;p&gt;The scheduler family of APIs, increasingly supported, gives you fine grained control over priority:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// new in 2024+ browsers&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;        &lt;span class="c1"&gt;// give the browser a frame&lt;/span&gt;
&lt;span class="nx"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;work&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;background&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;scheduler.yield()&lt;/code&gt; is not available, the polyfill is a &lt;code&gt;setTimeout(0)&lt;/code&gt; plus a microtask check.&lt;/p&gt;

&lt;p&gt;The classic timing tools still matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt;&lt;/strong&gt; for any visual update tied to the next frame. Always use it for animations driven by JS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;performance.now()&lt;/code&gt;&lt;/strong&gt; for high resolution timing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PerformanceObserver&lt;/code&gt;&lt;/strong&gt; to subscribe to LCP, INP, CLS, long tasks, navigation timing, resource timing. This is what &lt;code&gt;web-vitals&lt;/code&gt; is built on.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PerformanceObserver&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntries&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;entryTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;longtask&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;layout-shift&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;largest-contentful-paint&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop into devtools when you want to see what is actually slow.&lt;/p&gt;

&lt;h2&gt;
  
  
  API 10: Notifications, Push, Background Sync
&lt;/h2&gt;

&lt;p&gt;For "this app feels real" features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Notifications&lt;/strong&gt;: &lt;code&gt;new Notification("Hi")&lt;/code&gt; with permission. Used as the visual layer for Push.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push&lt;/strong&gt;: a Service Worker can receive pushes from your server even when the page is closed. Requires the user to grant permission and your server to send via the Web Push protocol.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background Sync&lt;/strong&gt;: the browser will retry a queued task when the user comes back online. Perfect for "send this comment whenever there is connectivity".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are senior level features. Only enable them where they pay off. Most users have notification fatigue, so ask politely or not at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  API 11: The "knows about the user" APIs (use sparingly)
&lt;/h2&gt;

&lt;p&gt;The platform has a long tail of "tell me about the device" APIs. Senior engineers know they exist and use them with care.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;matchMedia("(prefers-color-scheme: dark)").matches&lt;/code&gt;&lt;/strong&gt; for theme detection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;matchMedia("(prefers-reduced-motion: reduce)").matches&lt;/code&gt;&lt;/strong&gt; to respect motion preferences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;navigator.connection&lt;/code&gt;&lt;/strong&gt; (Network Information API) for adaptive loading. Slow 3G? Send fewer images.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;document.visibilityState&lt;/code&gt;&lt;/strong&gt; to pause work when the tab is hidden.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;navigator.locks&lt;/code&gt;&lt;/strong&gt; (already mentioned).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Battery&lt;/code&gt;, &lt;code&gt;Bluetooth&lt;/code&gt;, &lt;code&gt;USB&lt;/code&gt;, &lt;code&gt;Serial&lt;/code&gt;, &lt;code&gt;Geolocation&lt;/code&gt;&lt;/strong&gt;: each is a permissioned API, useful for very specific apps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The principle: &lt;strong&gt;respect the user, ask before you peek&lt;/strong&gt;. Permission prompts kill conversion if used carelessly.&lt;/p&gt;

&lt;h2&gt;
  
  
  API 12: Modern selection, drag, paste, undo
&lt;/h2&gt;

&lt;p&gt;A handful of small APIs that delete entire libraries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;getSelection()&lt;/code&gt;&lt;/strong&gt; for the current text selection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;document.execCommand&lt;/code&gt; is deprecated&lt;/strong&gt;. For rich text, use &lt;code&gt;contenteditable&lt;/code&gt; plus a library like &lt;strong&gt;TipTap&lt;/strong&gt; or &lt;strong&gt;Lexical&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;InputEvent&lt;/code&gt;&lt;/strong&gt; with &lt;code&gt;inputType&lt;/code&gt; ("insertText", "deleteContentBackward", etc.) for fine grained input handling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;HTMLDialogElement.showModal()&lt;/code&gt;&lt;/strong&gt; for native modals (we covered this in the HTML post).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt;&lt;/strong&gt; for native disclosures, no JS needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;input type="search"&amp;gt;&lt;/code&gt;, &lt;code&gt;type="date"&lt;/code&gt;, &lt;code&gt;type="time"&lt;/code&gt;, &lt;code&gt;type="color"&lt;/code&gt;&lt;/strong&gt; for native pickers on mobile.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A surprising amount of "I need a library for this" turns into "the browser already does it" once you check first.&lt;/p&gt;

&lt;h2&gt;
  
  
  API 13: WebRTC, WebSockets, SSE, beyond request/response
&lt;/h2&gt;

&lt;p&gt;Three transports for real time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebSockets&lt;/strong&gt; for bidirectional persistent connections. We mentioned this in the HTTP post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-Sent Events&lt;/strong&gt; (&lt;code&gt;EventSource&lt;/code&gt;) for one way streaming from server to client. Simpler than WebSockets, plays nicely with HTTP infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebRTC&lt;/strong&gt; for peer to peer audio, video, and data. The transport behind every browser based video call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most apps need SSE for live updates and never touch the others. Reach for them when you have a specific need (real time collaboration, video, gaming).&lt;/p&gt;

&lt;h2&gt;
  
  
  A peek under the hood
&lt;/h2&gt;

&lt;p&gt;What really happens when you call a platform API:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;JavaScript calls into the browser's C++ implementation.&lt;/li&gt;
&lt;li&gt;The browser does the work (often on another thread or in another process).&lt;/li&gt;
&lt;li&gt;The result comes back as a Promise resolution, an event, or a callback.&lt;/li&gt;
&lt;li&gt;Your code runs in the JS event loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two consequences for senior engineers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Native APIs are usually faster than userland equivalents&lt;/strong&gt; because the heavy lifting happens off the main thread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser support varies.&lt;/strong&gt; Use &lt;a href="https://caniuse.com" rel="noopener noreferrer"&gt;caniuse.com&lt;/a&gt; before committing. For features you must have everywhere, polyfills exist. For features that gracefully degrade, feature detect with &lt;code&gt;if ("foo" in window)&lt;/code&gt; and ship the better experience to capable browsers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tiny tips that will save you later
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read MDN.&lt;/strong&gt; It is the single best web platform documentation, and it is free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;caniuse.com&lt;/code&gt;&lt;/strong&gt; before adopting any platform API. Browser share matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature detect, do not user agent sniff.&lt;/strong&gt; &lt;code&gt;if ("share" in navigator)&lt;/code&gt; is the right check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cancel everything.&lt;/strong&gt; &lt;code&gt;AbortController&lt;/code&gt; works with &lt;code&gt;fetch&lt;/code&gt;, with &lt;code&gt;addEventListener&lt;/code&gt;, with most modern APIs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use &lt;code&gt;IntersectionObserver&lt;/code&gt; instead of scroll listeners.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;ResizeObserver&lt;/code&gt; instead of window resize listeners&lt;/strong&gt; when you only care about an element.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use &lt;code&gt;BroadcastChannel&lt;/code&gt; for cross tab communication.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;idb-keyval&lt;/code&gt; or Dexie for IndexedDB&lt;/strong&gt; unless you really enjoy callbacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Workbox or Vite PWA&lt;/strong&gt; for Service Workers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;view-transition&lt;/code&gt; for navigation animations.&lt;/strong&gt; Free polish.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respect &lt;code&gt;prefers-reduced-motion&lt;/code&gt; and &lt;code&gt;prefers-color-scheme&lt;/code&gt;.&lt;/strong&gt; They are one query away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep platform APIs small.&lt;/strong&gt; Wrap each in a tiny module so the call sites do not get noisy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;So that is the whole story. The web platform stopped being a dumb document viewer years ago. Modern browsers ship a small operating system: a network stack with cancellation and streaming, three storage tiers, real threads, real schedulers, observers for everything that used to need a polling loop, broadcast channels for cross tab life, file system access, share sheets, push notifications, and beautiful view transitions.&lt;/p&gt;

&lt;p&gt;A senior frontend engineer treats these as the first toolkit, not the last resort. The libraries on npm exist because the platform was missing the feature. Many features have shipped. Many libraries can come out of your &lt;code&gt;package.json&lt;/code&gt; if you check what the browser already does.&lt;/p&gt;

&lt;p&gt;Once that map is in your head, you write less code, ship smaller bundles, and build apps that feel like the device they run on.&lt;/p&gt;

&lt;p&gt;Happy platforming, and may your dependency list stay short.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>beginners</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Actually Log Out a User When You Use JWT</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Sat, 16 May 2026 16:04:34 +0000</pubDate>
      <link>https://dev.to/edriso/how-to-actually-log-out-a-user-when-you-use-jwt-2n6a</link>
      <guid>https://dev.to/edriso/how-to-actually-log-out-a-user-when-you-use-jwt-2n6a</guid>
      <description>&lt;p&gt;JWT is stateless and fast, but logout is tricky. Here is the full story with simple analogies, the jti blacklist trick, password change invalidation, refresh token rotation, and Node.js code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem in one sentence
&lt;/h2&gt;

&lt;p&gt;You log a user out, but their token still works until it expires. That is scary for any app that touches sensitive data.&lt;/p&gt;

&lt;p&gt;This post walks through why that happens and how to fix it properly. I will keep it simple and use real life analogies so it sticks in your head.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This post was inspired by a great post from Mohamed Kamal in the Node.js Egypt group. He shared a solid pattern. I am going to explain the same idea slowly, then add the parts that make it production grade.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  First, a mental model: the festival wristband
&lt;/h2&gt;

&lt;p&gt;Imagine you go to a music festival. At the entrance they give you a wristband.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The wristband has your info printed on it: your name, your ticket type (VIP or normal), and the date it stops working.&lt;/li&gt;
&lt;li&gt;The guard at every stage just &lt;strong&gt;looks&lt;/strong&gt; at your wristband. He does not call the main office to ask "is this person still allowed in?"&lt;/li&gt;
&lt;li&gt;That makes entry super fast. One guard can check thousands of people.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This wristband is a &lt;strong&gt;JWT&lt;/strong&gt; (JSON Web Token).&lt;/p&gt;

&lt;p&gt;The fact that the guard does not call the office is what we call &lt;strong&gt;stateless&lt;/strong&gt;. The server does not keep a list of who is logged in. All the info is inside the token itself, and the signature proves it is real and was not edited.&lt;/p&gt;

&lt;p&gt;This is great for speed and scaling. You can add 10 more servers and none of them need a shared "who is logged in" list. Any server can read the wristband and trust it.&lt;/p&gt;

&lt;h2&gt;
  
  
  So where is the problem?
&lt;/h2&gt;

&lt;p&gt;You leave the festival early. You go home.&lt;/p&gt;

&lt;p&gt;Your wristband is still on your wrist. It still says VIP. It still works until the festival ends.&lt;/p&gt;

&lt;p&gt;If someone cuts it off your wrist (or you gave it to a friend), they can walk back in. The guard has no idea you "left". He only checks the wristband, and the wristband is still valid.&lt;/p&gt;

&lt;p&gt;That is the JWT logout problem.&lt;/p&gt;

&lt;p&gt;When a user clicks &lt;strong&gt;Logout&lt;/strong&gt;, you usually just delete the token from the browser. But if a copy of that token was stolen earlier, it keeps working until the expiry time. The server never said "this token is dead now".&lt;/p&gt;

&lt;p&gt;For a blog, who cares. For a banking app, a health app, or anything with private data, this is a real risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive fixes (and why they are not enough)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Just make the token expire fast."&lt;/strong&gt;&lt;br&gt;
Good instinct, but if it expires every 5 minutes, the user has to log in again every 5 minutes. Annoying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Store every active token in the database and check it on every request."&lt;/strong&gt;&lt;br&gt;
This works, but now you call the database on every single request to ask "is this token still alive?". You just threw away the main benefit of JWT, which was being stateless and fast. At that point a normal session in the database is simpler.&lt;/p&gt;

&lt;p&gt;So we need a middle path. We want most requests to stay fast and stateless, but we still want the power to kill a token when we need to.&lt;/p&gt;
&lt;h2&gt;
  
  
  The real solution, step by step
&lt;/h2&gt;

&lt;p&gt;The pattern has a few moving parts. Let us build them one at a time.&lt;/p&gt;
&lt;h3&gt;
  
  
  Part 1: Two tokens, not one (access token + refresh token)
&lt;/h3&gt;

&lt;p&gt;Stop using one long living token. Use two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Access token&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Short life. Think 5 to 15 minutes.&lt;/li&gt;
&lt;li&gt;This is the festival wristband. The server trusts it without checking a database.&lt;/li&gt;
&lt;li&gt;Used on every normal request (get profile, load dashboard, etc).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Refresh token&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Long life. Think 7 to 30 days.&lt;/li&gt;
&lt;li&gt;This is like your &lt;strong&gt;ID card kept at the front desk&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;It has only one job: when your wristband expires, you show the ID card and get a fresh wristband.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Analogy: the wristband gets you into stages quickly. When it stops working after 15 minutes, you walk to the front desk, show your ID card, and they print you a new wristband. You do not need to buy a new ticket.&lt;/p&gt;

&lt;p&gt;Why this matters for logout: because the access token only lives 15 minutes, even if logout is not perfect, a stolen access token dies on its own very soon. The damage window is tiny. The real control point becomes the refresh step.&lt;/p&gt;
&lt;h3&gt;
  
  
  Part 2: Give every token an ID (the &lt;code&gt;jti&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;jti&lt;/code&gt; means &lt;strong&gt;JWT ID&lt;/strong&gt;. It is just a unique id you put inside the token when you create it. Usually a UUID.&lt;/p&gt;

&lt;p&gt;Think of it as a &lt;strong&gt;serial number&lt;/strong&gt; on the wristband.&lt;/p&gt;

&lt;p&gt;Why do we need a serial number? Because if we ever want to ban one specific wristband, we need a way to point at it and say "that one, number 8f3a..., is banned".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;v4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;uuidv4&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;uuid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jsonwebtoken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createAccessToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;jti&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uuidv4&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// the serial number&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ACCESS_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;15m&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Part 3: The blacklist (also called a denylist)
&lt;/h3&gt;

&lt;p&gt;When the user logs out, we take the token serial number (&lt;code&gt;jti&lt;/code&gt;) and put it on a &lt;strong&gt;banned list&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is exactly like the festival having a small "banned wristbands" sheet at the front desk. The guards at the stages still do not check it (they stay fast). But the front desk &lt;strong&gt;does&lt;/strong&gt; check it before giving out a new wristband.&lt;/p&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Logout&lt;/strong&gt; puts the &lt;code&gt;jti&lt;/code&gt; on the banned list.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;refresh step&lt;/strong&gt; (front desk) checks the banned list before giving a new access token.&lt;/li&gt;
&lt;li&gt;If the &lt;code&gt;jti&lt;/code&gt; is banned, refresh is refused. No new wristbands for you. And since the old access token dies in a few minutes anyway, the user is fully out very soon.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the key trick. We did &lt;strong&gt;not&lt;/strong&gt; check the blacklist on every request. We only check it at the refresh step. So normal requests stay fast and stateless, and we still get real logout.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: in the original post the blacklist was checked at refresh time. That is the smart, cheap choice &lt;strong&gt;as long as your access token is short lived&lt;/strong&gt;. If your access token lives for hours, that small window becomes a big risk. Keep access tokens short.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Part 4: Auto cleanup with a TTL index
&lt;/h3&gt;

&lt;p&gt;A banned list that grows forever is a problem. We do not need to remember a banned token after it would have expired anyway. A dead token cannot be used, banned or not.&lt;/p&gt;

&lt;p&gt;MongoDB has a feature called a &lt;strong&gt;TTL index&lt;/strong&gt; (Time To Live). You tell Mongo "delete this document automatically after this date". Mongo does the cleanup for you.&lt;/p&gt;

&lt;p&gt;Analogy: the front desk shreds old banned wristband notes at the end of each night. No one keeps paper forever.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mongoose&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mongoose&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blacklistSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;mongoose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;jti&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// when the original token would have expired anyway&lt;/span&gt;
  &lt;span class="na"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// TTL index: Mongo deletes the row when expiresAt is reached&lt;/span&gt;
&lt;span class="nx"&gt;blacklistSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expireAfterSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mongoose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BlacklistedToken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;blacklistSchema&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Part 5: Kill all old tokens when the password changes
&lt;/h3&gt;

&lt;p&gt;Here is a nasty case. A hacker stole a user token. The user feels something is wrong and changes the password. With plain JWT, the hacker token still works, because the token does not care about the password.&lt;/p&gt;

&lt;p&gt;The fix is a timestamp on the user. Call it &lt;code&gt;credentialsChangedAt&lt;/code&gt; (the original post called it &lt;code&gt;changeCredentials&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The rule is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the token was created &lt;strong&gt;before&lt;/strong&gt; the user last changed their password, the token is dead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Analogy: the festival announces "we changed the wristband color to green at 2pm. Any blue wristband is now invalid." You do not need to hunt down every blue wristband one by one. One announcement kills them all at once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// inside your auth middleware, after verifying the token signature&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credentialsChangedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iat&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credentialsChangedAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Token no longer valid, please log in again&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;iat&lt;/code&gt; is "issued at", a standard field JWT puts in automatically that says when the token was created.&lt;/p&gt;

&lt;p&gt;This one timestamp gives you a powerful "log out everywhere" button for free. Change password, all old sessions die.&lt;/p&gt;

&lt;h2&gt;
  
  
  Level up: refresh token rotation and theft detection
&lt;/h2&gt;

&lt;p&gt;This part was not in the original post, but it is the modern best practice and it is worth knowing.&lt;/p&gt;

&lt;p&gt;Right now our refresh token (the ID card) lives for many days. If someone steals it, they can keep getting fresh wristbands for days. Not good.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refresh token rotation&lt;/strong&gt; means: every time you use the refresh token to get a new access token, you also get a &lt;strong&gt;brand new refresh token&lt;/strong&gt;, and the old one is thrown away. One time use only.&lt;/p&gt;

&lt;p&gt;Analogy: every time you use your ID card at the front desk, they shred it and hand you a new ID card. The old card number is now dead.&lt;/p&gt;

&lt;p&gt;Now the clever part, &lt;strong&gt;reuse detection&lt;/strong&gt; (theft detection):&lt;/p&gt;

&lt;p&gt;If an old, already used refresh token shows up again, that is a huge red flag. It means two people have the card: the real user and a thief. A normal user never reuses an old card, because they always have the newest one.&lt;/p&gt;

&lt;p&gt;So when the server sees a used refresh token come back, it assumes theft and &lt;strong&gt;kills the entire token family&lt;/strong&gt; for that login. Both the real user and the thief are logged out. The user logs in again, the thief is locked out.&lt;/p&gt;

&lt;p&gt;Analogy: if security sees a shredded ID card number being used at the door, they lock the whole account and call you to confirm it is really you. Annoying for a second, much safer overall.&lt;/p&gt;

&lt;p&gt;Quick lifetime guide that the big providers recommend:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Token&lt;/th&gt;
&lt;th&gt;Lifetime&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Access token&lt;/td&gt;
&lt;td&gt;5 to 15 minutes&lt;/td&gt;
&lt;td&gt;Small damage window if stolen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refresh token (sensitive apps)&lt;/td&gt;
&lt;td&gt;7 to 30 days&lt;/td&gt;
&lt;td&gt;Balance of safety and not annoying the user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refresh token (single page web app)&lt;/td&gt;
&lt;td&gt;up to 24 hours&lt;/td&gt;
&lt;td&gt;Browsers are more exposed, keep it short&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where do you store these tokens in the browser?
&lt;/h2&gt;

&lt;p&gt;This part trips up almost every junior, so read slowly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;localStorage&lt;/strong&gt;: easy to use, but &lt;strong&gt;any&lt;/strong&gt; JavaScript on your page can read it. If an attacker sneaks in a script (an XSS attack), they read your token instantly. OWASP openly says do not keep session tokens in localStorage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;httpOnly cookie&lt;/strong&gt;: JavaScript &lt;strong&gt;cannot&lt;/strong&gt; read this cookie at all. Even if an attacker runs a script on your page, they cannot read the token out of it. The trade off is you must protect against CSRF (use the &lt;code&gt;SameSite&lt;/code&gt; cookie setting and CSRF tokens).&lt;/p&gt;

&lt;p&gt;The recommended hybrid setup today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Access token&lt;/strong&gt;: keep it in memory only (a variable in your app state). It is short lived, so losing it on refresh of the page is fine, you just silently get a new one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh token&lt;/strong&gt;: store it in a &lt;strong&gt;secure, httpOnly cookie&lt;/strong&gt;. Scripts cannot touch it, and it is the long lived secret you most want to protect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Analogy: you keep the cheap day pass (access token) in your pocket where it is easy to grab. You keep your passport (refresh token) in the hotel safe where no random person can reach it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting the whole flow together
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Login&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check email and password.&lt;/li&gt;
&lt;li&gt;Create an access token (15 min) with a unique &lt;code&gt;jti&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Create a refresh token (store it server side or as a signed token with its own &lt;code&gt;jti&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Send the access token to be kept in memory, and the refresh token in an httpOnly cookie.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Normal request (load dashboard, etc)&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify the access token signature. Fast. No database.&lt;/li&gt;
&lt;li&gt;Check the &lt;code&gt;credentialsChangedAt&lt;/code&gt; rule.&lt;/li&gt;
&lt;li&gt;Done. This is the stateless fast path.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Access token expired&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Browser calls the refresh endpoint. The httpOnly cookie is sent automatically.&lt;/li&gt;
&lt;li&gt;Server checks: is this refresh &lt;code&gt;jti&lt;/code&gt; on the blacklist? Was it already used (reuse detection)?&lt;/li&gt;
&lt;li&gt;If all good, issue a new access token and a new refresh token, retire the old refresh token.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Logout&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the refresh token &lt;code&gt;jti&lt;/code&gt; (and optionally the current access token &lt;code&gt;jti&lt;/code&gt;) to the blacklist with an &lt;code&gt;expiresAt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Clear the cookie.&lt;/li&gt;
&lt;li&gt;The access token dies on its own in a few minutes. The refresh token is already banned. User is fully out.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Password change&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Update &lt;code&gt;credentialsChangedAt = now&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Every token created before that instant is dead everywhere, automatically.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is a compact logout and refresh example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// LOGOUT&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/logout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;jti&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;exp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// from the verified token&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;BlacklistedToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;jti&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// matches token expiry, TTL cleans it later&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearCookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refreshToken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Logged out&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// REFRESH&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/refresh&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No refresh token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REFRESH_SECRET&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid refresh token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// is this refresh token banned (logged out or reused)?&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;banned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;BlacklistedToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;jti&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jti&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;banned&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Token revoked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User not found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// password changed after this token was made? kill it&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credentialsChangedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iat&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credentialsChangedAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please log in again&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// rotation: ban the old refresh token, issue fresh ones&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;BlacklistedToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;jti&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jti&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newAccess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createAccessToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newRefresh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRefreshToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// new jti inside&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refreshToken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newRefresh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;httpOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sameSite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;strict&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newAccess&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A note on MongoDB vs Redis for the blacklist
&lt;/h2&gt;

&lt;p&gt;The original post used MongoDB with a TTL index, and that is perfectly fine and clean. Many teams use &lt;strong&gt;Redis&lt;/strong&gt; instead because it is an in memory store built for exactly this kind of fast key lookup, and it also supports automatic expiry. If your blacklist gets very hot (checked a lot), Redis is the common choice. If you are early or your traffic is normal, MongoDB with a TTL index is a solid, simple start. Do not over engineer too early.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes juniors make
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Using one long lived token for everything. Always split into short access and longer refresh.&lt;/li&gt;
&lt;li&gt;Forgetting to revoke the refresh token on logout. If you only handle the access token, the refresh token can still mint new ones. Logout must kill the refresh token.&lt;/li&gt;
&lt;li&gt;Checking the blacklist on every single request. That kills the stateless speed benefit. Check it at the refresh step and keep access tokens short.&lt;/li&gt;
&lt;li&gt;Storing tokens in localStorage because the tutorial did. Prefer memory for access and httpOnly cookie for refresh.&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;credentialsChangedAt&lt;/code&gt;. Without it, changing the password does not actually protect a user whose token was already stolen.&lt;/li&gt;
&lt;li&gt;Letting the blacklist grow forever. Always set a TTL so dead entries clean themselves up.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The one paragraph summary
&lt;/h2&gt;

&lt;p&gt;JWT is fast because the server trusts the token without a database lookup, like a guard glancing at a wristband. The cost of that speed is that logout does not naturally kill a token. You fix it by using a short lived access token plus a longer refresh token, giving every token a unique &lt;code&gt;jti&lt;/code&gt; serial number, putting that &lt;code&gt;jti&lt;/code&gt; on a blacklist at logout and checking the blacklist only at the refresh step, auto cleaning the blacklist with a TTL index, and adding a &lt;code&gt;credentialsChangedAt&lt;/code&gt; timestamp so a password change kills all old tokens at once. Add refresh token rotation with reuse detection and store tokens safely (access in memory, refresh in an httpOnly cookie) and you have a real, production grade auth system.&lt;/p&gt;

&lt;p&gt;Authentication is not just login and register. The small security details are what separate a toy project from a real system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources and further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://supertokens.com/blog/revoking-access-with-a-jwt-blacklist" rel="noopener noreferrer"&gt;Revoke Access Using a JWT Blacklist (SuperTokens)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@ahmedosamaft/understanding-jwt-revocation-strategies-allowlist-denylist-and-jti-matcher-9d298893f8a1" rel="noopener noreferrer"&gt;Understanding JWT Revocation Strategies: Allowlist, Denylist, JTI (Medium)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://oneuptime.com/blog/post/2026-02-02-jwt-revocation/view" rel="noopener noreferrer"&gt;How to Handle JWT Revocation (OneUptime)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation" rel="noopener noreferrer"&gt;Refresh Token Rotation (Auth0 Docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.okta.com/docs/guides/refresh-tokens/main/" rel="noopener noreferrer"&gt;Refresh access tokens and rotate refresh tokens (Okta)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mihai-andrei.com/blog/refresh-token-reuse-interval-and-reuse-detection/" rel="noopener noreferrer"&gt;Secure Refresh Token Rotation with Theft Detection (Mihai Andrei)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/cotter/localstorage-vs-cookies-all-you-need-to-know-about-storing-jwt-tokens-securely-in-the-front-end-15id"&gt;LocalStorage vs Cookies: storing JWT tokens securely (DEV Community)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.wisp.blog/blog/understanding-token-storage-local-storage-vs-httponly-cookies" rel="noopener noreferrer"&gt;Understanding Token Storage: Local Storage vs HttpOnly Cookies (Wisp)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Big thanks to Mohamed Kamal for the original post in Node.js Egypt that started this. If this helped you, share it with a junior who is still using one giant token for everything.&lt;/p&gt;

</description>
      <category>jwt</category>
      <category>node</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Learning HTTP, APIs, and Auth As If You Built It Yourself</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Sat, 16 May 2026 13:05:00 +0000</pubDate>
      <link>https://dev.to/edriso/learning-http-apis-and-auth-as-if-you-built-it-yourself-ddj</link>
      <guid>https://dev.to/edriso/learning-http-apis-and-auth-as-if-you-built-it-yourself-ddj</guid>
      <description>&lt;p&gt;If you have ever built a frontend that calls a backend, you have run into all three of these. You wrote a &lt;code&gt;fetch&lt;/code&gt;. You picked a URL shape. You stuck a token somewhere and prayed. Something returned a 200 with &lt;code&gt;{ ok: false }&lt;/code&gt; in the body, and you spent the afternoon arguing in the team chat about whether that was correct.&lt;/p&gt;

&lt;p&gt;It probably was not.&lt;/p&gt;

&lt;p&gt;There is a small set of conventions that the whole web agreed on, decades ago, and most apps half know them. Frontend engineers who fully know them ship cleaner, more debuggable systems and have shorter Slack arguments.&lt;/p&gt;

&lt;p&gt;That is the gap this post fills.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are HTTP, APIs, and auth, really
&lt;/h2&gt;

&lt;p&gt;Think of the web as &lt;strong&gt;a polite postal system&lt;/strong&gt;. Your client (the browser) writes a letter (a request), the server reads it, writes back (a response), and the conversation ends. Each letter has an envelope (headers), a way of being addressed (the URL), and a body (the data). Each reply has a stamp on the front (a status code) that tells you, at a glance, what happened.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTP&lt;/strong&gt; is the postal protocol itself. Verbs, headers, status codes, caching rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;APIs&lt;/strong&gt; are the postal addresses your team agreed on. Which URLs do what, what each one expects in the body, what it sends back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth&lt;/strong&gt; is the picture ID you put in the envelope. Who are you, and are you allowed to ask for this?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A senior frontend engineer treats those three as one fluent topic, not three separate puzzles.&lt;/p&gt;

&lt;p&gt;That is the whole vibe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's pretend we are building one
&lt;/h2&gt;

&lt;p&gt;We want to design a small backend a frontend can talk to without surprises, with proper status codes, predictable URLs, and authenticated calls. We will not hand build a server. We will agree on the conventions and use any framework that respects them.&lt;/p&gt;

&lt;p&gt;For the running example, we are designing the &lt;strong&gt;Mochi Recipes API&lt;/strong&gt;: list recipes, read one, create one, log in, save a favorite. Small, but it lets us touch every important corner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: HTTP, the verbs and the rules
&lt;/h2&gt;

&lt;p&gt;Every request is one verb plus one URL. The verbs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/strong&gt;: read. Safe (no side effects). Idempotent (same request, same result).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/strong&gt;: create or trigger an action. Not idempotent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/strong&gt;: replace the resource entirely. Idempotent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PATCH&lt;/code&gt;&lt;/strong&gt;: change part of the resource. Idempotent in spirit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/strong&gt;: delete. Idempotent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;HEAD&lt;/code&gt;&lt;/strong&gt;: like GET but only the headers come back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OPTIONS&lt;/code&gt;&lt;/strong&gt;: what verbs and headers are allowed here? (Used for CORS preflights.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two things every senior should know cold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;GET&lt;/code&gt; must never modify state.&lt;/strong&gt; Browsers, proxies, and prefetchers send GETs whenever they like. A &lt;code&gt;GET /unsubscribe?id=42&lt;/code&gt; link will be deleted by Outlook's URL preview. (Yes, this is a real bug that has happened many times.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PUT&lt;/code&gt; vs &lt;code&gt;PATCH&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;PUT /recipes/42&lt;/code&gt; with a partial body replaces the recipe with that partial body and removes the missing fields. &lt;code&gt;PATCH /recipes/42&lt;/code&gt; with the same body changes only what was sent. Pick on purpose.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision 2: Status codes that say what happened
&lt;/h2&gt;

&lt;p&gt;The first three letters of the response. Memorize the pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;2xx&lt;/code&gt;&lt;/strong&gt; success

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;200 OK&lt;/code&gt;: the default success.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;201 Created&lt;/code&gt;: after a successful POST that created a thing. Include a &lt;code&gt;Location:&lt;/code&gt; header pointing at the new resource.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;204 No Content&lt;/code&gt;: success but nothing to return. Common after &lt;code&gt;DELETE&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;&lt;code&gt;3xx&lt;/code&gt;&lt;/strong&gt; redirection

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;301 Moved Permanently&lt;/code&gt;: the URL changed forever. Browsers cache this.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;302 Found&lt;/code&gt;, &lt;code&gt;307 Temporary Redirect&lt;/code&gt;: the resource is over there, for now.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;304 Not Modified&lt;/code&gt;: the cached copy is still good. The body is empty.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;&lt;code&gt;4xx&lt;/code&gt;&lt;/strong&gt; the client made a mistake

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;400 Bad Request&lt;/code&gt;: malformed input.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;401 Unauthorized&lt;/code&gt;: you are not logged in. (Misnamed, it is really "unauthenticated".)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;403 Forbidden&lt;/code&gt;: you are logged in, but not allowed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;404 Not Found&lt;/code&gt;: no such resource.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;405 Method Not Allowed&lt;/code&gt;: wrong verb for this URL.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;409 Conflict&lt;/code&gt;: your write would conflict with the current state (e.g. duplicate email).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;422 Unprocessable Entity&lt;/code&gt;: well formed but invalid (e.g. validation errors).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;429 Too Many Requests&lt;/code&gt;: slow down. Pair with &lt;code&gt;Retry-After&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;&lt;code&gt;5xx&lt;/code&gt;&lt;/strong&gt; the server made a mistake

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;500 Internal Server Error&lt;/code&gt;: something blew up.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;502 Bad Gateway&lt;/code&gt;, &lt;code&gt;503 Service Unavailable&lt;/code&gt;, &lt;code&gt;504 Gateway Timeout&lt;/code&gt;: upstream issues.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;The two anti patterns I see most often on real teams:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Returning &lt;code&gt;200 { ok: false }&lt;/code&gt; for errors.&lt;/strong&gt; This breaks every standard tool. Use the right status code. Tools, logs, monitoring, CDNs, and the browser DevTools all rely on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Returning &lt;code&gt;400&lt;/code&gt; for "not found".&lt;/strong&gt; A missing user is &lt;code&gt;404&lt;/code&gt;, not &lt;code&gt;400&lt;/code&gt;. A malformed JSON body is &lt;code&gt;400&lt;/code&gt;. Tell them apart.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision 3: REST URL design
&lt;/h2&gt;

&lt;p&gt;A URL points at a &lt;strong&gt;resource&lt;/strong&gt; (a thing). The verb says what you are doing to it. Resources are nouns, plural, lowercase.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET    /recipes              -&amp;gt; list
GET    /recipes/42           -&amp;gt; read one
POST   /recipes              -&amp;gt; create
PATCH  /recipes/42           -&amp;gt; update
DELETE /recipes/42           -&amp;gt; delete

GET    /recipes/42/comments  -&amp;gt; list comments on recipe 42
POST   /recipes/42/comments  -&amp;gt; add a comment
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few senior level rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nouns, not verbs.&lt;/strong&gt; &lt;code&gt;POST /createRecipe&lt;/code&gt; is wrong. &lt;code&gt;POST /recipes&lt;/code&gt; is right.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plural, consistently.&lt;/strong&gt; &lt;code&gt;GET /recipes&lt;/code&gt;, not &lt;code&gt;GET /recipe&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use query parameters for filtering, sorting, paging.&lt;/strong&gt; &lt;code&gt;GET /recipes?tag=dessert&amp;amp;sort=-createdAt&amp;amp;page=2&amp;amp;limit=20&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use sub paths for relationships.&lt;/strong&gt; &lt;code&gt;GET /users/42/recipes&lt;/code&gt; reads better than &lt;code&gt;GET /recipes?userId=42&lt;/code&gt; for natural ownership.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version the API in the URL or in a header.&lt;/strong&gt; &lt;code&gt;/v1/recipes&lt;/code&gt; or &lt;code&gt;Accept: application/vnd.example.v1+json&lt;/code&gt;. Pick one and never argue again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return JSON in a stable shape.&lt;/strong&gt; Never change a field type or remove a key without a version bump.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If REST does not fit your needs (lots of nested data, lots of round trips), look at &lt;strong&gt;GraphQL&lt;/strong&gt; or &lt;strong&gt;tRPC&lt;/strong&gt;. Both are excellent in 2026. Neither replaces REST for public APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 4: Headers, the metadata you cannot ignore
&lt;/h2&gt;

&lt;p&gt;Headers are the envelope of every request and response. The ones every senior should recognize:&lt;/p&gt;

&lt;h3&gt;
  
  
  Content negotiation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Content-Type:    application/json
Accept:          application/json
Accept-Language: en-US,en;q=0.9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always set &lt;code&gt;Content-Type&lt;/code&gt; on requests with a body. Always read it on responses (the body might not be JSON).&lt;/p&gt;

&lt;h3&gt;
  
  
  Auth
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Authorization: Bearer &amp;lt;token&amp;gt;
Cookie:        session=...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will dig into these in a moment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caching
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cache-Control: public, max-age=60, stale-while-revalidate=600
ETag:          "v3-abc123"
Last-Modified: Wed, 09 May 2026 10:00:00 GMT
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The single most underused tool in frontend performance is &lt;strong&gt;conditional requests&lt;/strong&gt;. A client with an &lt;code&gt;ETag&lt;/code&gt; can ask "is this still fresh?":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /recipes/42
If-None-Match: "v3-abc123"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If yes, the server replies &lt;code&gt;304 Not Modified&lt;/code&gt; with no body. Bandwidth saved, page faster.&lt;/p&gt;

&lt;h3&gt;
  
  
  CORS
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Access-Control-Allow-Origin:      https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods:     GET, POST, PATCH, DELETE
Access-Control-Allow-Headers:     Authorization, Content-Type
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CORS lives entirely on the server. It tells the browser which origins are allowed to call this API from JavaScript. It does not exist for security against attackers (curl ignores it). It exists to protect users from a malicious page calling another origin from their browser.&lt;/p&gt;

&lt;p&gt;The two CORS bugs every frontend developer hits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The preflight &lt;code&gt;OPTIONS&lt;/code&gt; request.&lt;/strong&gt; Before a "non simple" request, the browser sends an &lt;code&gt;OPTIONS&lt;/code&gt; to ask permission. Your server must respond with the right headers. Most frameworks have a one-line &lt;code&gt;cors&lt;/code&gt; middleware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;*&lt;/code&gt; and credentials do not mix.&lt;/strong&gt; If your server returns &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt;, the browser will refuse to send cookies. For credentialed requests, set the exact origin.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Rate limiting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;X-RateLimit-Limit:     100
X-RateLimit-Remaining: 23
X-RateLimit-Reset:     1715251200
Retry-After:           60
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A polite API tells the client when to back off. A polite client respects it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 5: Errors that the client can actually handle
&lt;/h2&gt;

&lt;p&gt;The body of a &lt;code&gt;4xx&lt;/code&gt; or &lt;code&gt;5xx&lt;/code&gt; response should follow a stable shape. The de facto standard is &lt;strong&gt;RFC 7807 / 9457 Problem Details&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;HTTP/&lt;/span&gt;&lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Unprocessable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Entity&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Content-Type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;application/problem+json&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/problems/validation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s2"&gt;"Invalid input"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s2"&gt;"Some fields failed validation."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"instance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/recipes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"too_short"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prepMinutes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"must_be_positive"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use the right status code first.&lt;/strong&gt; The body explains, the status decides.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Include a stable error code per problem.&lt;/strong&gt; UI code can switch on it without parsing English.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Include enough info to render a good error message&lt;/strong&gt; without leaking internals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never echo user input back in HTML errors&lt;/strong&gt; unless you escape it. That is a classic XSS path.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision 6: Authentication, the choices that actually exist
&lt;/h2&gt;

&lt;p&gt;Auth is two words pretending to be one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication (authn)&lt;/strong&gt;: who are you?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization (authz)&lt;/strong&gt;: what are you allowed to do?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The four mainstream patterns in 2026:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Session cookies (the original, still excellent)
&lt;/h3&gt;

&lt;p&gt;The server creates a session, stores it server side (in a database or Redis), and sets a cookie. The browser sends the cookie automatically on every request to the origin. The server looks up the session and knows who you are.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Set-Cookie: session=opaque_id; HttpOnly; Secure; SameSite=Lax; Path=/
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every flag matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;HttpOnly&lt;/code&gt;&lt;/strong&gt;: JavaScript cannot read it. Stops XSS from stealing the token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Secure&lt;/code&gt;&lt;/strong&gt;: only sent over HTTPS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SameSite=Lax&lt;/code&gt;&lt;/strong&gt; (or &lt;code&gt;Strict&lt;/code&gt;): only sent for same site requests, blocking most CSRF.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Path=/&lt;/code&gt;&lt;/strong&gt;: sent on every path under the origin.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The security defaults are excellent. The state lives on the server where you can revoke it instantly. Best for first party web apps where the API and the frontend share an origin.&lt;/p&gt;

&lt;p&gt;The one CSRF caveat: if you use cookies, every state changing endpoint should require either &lt;code&gt;SameSite=Strict&lt;/code&gt;, a CSRF token, or a custom header that browsers will not send cross origin without permission.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. JWT (JSON Web Tokens)
&lt;/h3&gt;

&lt;p&gt;A JWT is a self contained token that is signed by the server. It contains the user id, expiration, and any claims you want, all readable by anyone (it is base64 encoded JSON). Only the signature can be checked by the server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pros: stateless, easy to verify on any service that has the public key, popular in SPAs, mobile, and microservices.&lt;/p&gt;

&lt;p&gt;Cons: hard to revoke before expiration (you would need a blocklist, defeating the stateless point). Often misused to store sensitive data in the token (the body is readable by the client).&lt;/p&gt;

&lt;p&gt;The 2026 advice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Short lived access token (15 minutes) plus a long lived refresh token.&lt;/strong&gt; When the access token expires, the client uses the refresh token to get a new one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store tokens in &lt;code&gt;HttpOnly&lt;/code&gt; cookies, not in &lt;code&gt;localStorage&lt;/code&gt;.&lt;/strong&gt; Tokens in &lt;code&gt;localStorage&lt;/code&gt; are XSS's lunch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never use JWTs as session cookies on a single origin web app.&lt;/strong&gt; Use real session cookies. JWTs shine for cross origin or service to service.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. OAuth 2.0 / OAuth 2.1
&lt;/h3&gt;

&lt;p&gt;OAuth is not a login method. It is a protocol for your app to act on behalf of a user at another service. "Sign in with Google" is OAuth.&lt;/p&gt;

&lt;p&gt;The flow you actually need to know:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your app sends the user to Google's &lt;code&gt;/authorize&lt;/code&gt; URL.&lt;/li&gt;
&lt;li&gt;The user logs in to Google and clicks "Allow".&lt;/li&gt;
&lt;li&gt;Google redirects back to your app with an authorization code.&lt;/li&gt;
&lt;li&gt;Your server exchanges the code for an access token (and a refresh token) at Google's &lt;code&gt;/token&lt;/code&gt; endpoint.&lt;/li&gt;
&lt;li&gt;Your server uses the access token to call Google's APIs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;OAuth 2.1 (the modern consolidation) makes a few things mandatory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PKCE for all authorization code flows.&lt;/strong&gt; A short random secret the client generates and proves possession of. Closes a class of attacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No more Implicit grant.&lt;/strong&gt; It was insecure in browsers. Use authorization code with PKCE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No more Resource Owner Password grant.&lt;/strong&gt; Bad pattern, gone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exact redirect URI matching.&lt;/strong&gt; No fuzzy patterns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most frontend developers, you will not implement OAuth. You will use a library or a service: &lt;strong&gt;Auth.js / NextAuth&lt;/strong&gt;, &lt;strong&gt;Clerk&lt;/strong&gt;, &lt;strong&gt;Auth0&lt;/strong&gt;, &lt;strong&gt;WorkOS&lt;/strong&gt;, &lt;strong&gt;Stack Auth&lt;/strong&gt;. Pick one, follow its setup, get on with your day.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. API keys
&lt;/h3&gt;

&lt;p&gt;A long opaque string a service gives a developer for server to server access. Useful for partners and CLI tools. Never put one in frontend code, ever. If a key shows up in your client bundle, it is leaked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 7: Storing tokens in the browser, the only sane way
&lt;/h2&gt;

&lt;p&gt;The single most common security mistake in frontend code is putting an auth token where JavaScript can read it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// don't&lt;/span&gt;
&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// don't&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`token=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any third party script (analytics, chat widget, ad SDK) is XSS'd, your token leaks to it.&lt;/p&gt;

&lt;p&gt;The right pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The server sets an &lt;code&gt;HttpOnly&lt;/code&gt; cookie with the session or refresh token.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JavaScript never sees it.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The browser sends it automatically on calls to your API.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;For cross origin frontends, set the API on a subdomain and use &lt;code&gt;SameSite=None; Secure&lt;/code&gt; cookies, with explicit CORS allowing credentials.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Modern auth libraries default to this. If yours does not, find one that does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 8: Calling APIs from the frontend without pain
&lt;/h2&gt;

&lt;p&gt;Once the conventions are set, the client side patterns are simple.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;fetch&lt;/code&gt;, the modern default
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getRecipes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/recipes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ApiError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Recipe&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few habits to internalize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;fetch&lt;/code&gt; does not throw on &lt;code&gt;4xx&lt;/code&gt; or &lt;code&gt;5xx&lt;/code&gt;.&lt;/strong&gt; You have to check &lt;code&gt;res.ok&lt;/code&gt; yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always cancel in flight requests&lt;/strong&gt; when the user navigates away or types again. &lt;code&gt;AbortController&lt;/code&gt; is the answer:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/search?q=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ctrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// later: ctrl.abort();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wrap in a typed client.&lt;/strong&gt; Hand rolling &lt;code&gt;fetch&lt;/code&gt; with try/catch in 80 places is painful. Use &lt;strong&gt;TanStack Query&lt;/strong&gt; for caching and retries, or &lt;strong&gt;tRPC&lt;/strong&gt;, or &lt;strong&gt;Hey API&lt;/strong&gt; generated clients from an OpenAPI spec, or &lt;strong&gt;ts-rest&lt;/strong&gt; for typed contracts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;credentials: "include"&lt;/code&gt;&lt;/strong&gt; when you need cookies on cross origin requests, alongside the right CORS headers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  TanStack Query, the go to data layer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useQueryClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useRecipes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;recipes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/recipes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="na"&gt;staleTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useCreateRecipe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;qc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQueryClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;mutationFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NewRecipe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/recipes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;qc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;recipes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caching, deduping, retries, background refetching, optimistic updates, all included. We have already discussed why server data does not belong in Zustand or Redux.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 9: WebSockets and Server-Sent Events
&lt;/h2&gt;

&lt;p&gt;For real time data (chat, notifications, live dashboards), HTTP request/response is not enough. Two real options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server-Sent Events (SSE)&lt;/strong&gt;: a one way stream from server to client over HTTP. Simple. Built into the browser via &lt;code&gt;EventSource&lt;/code&gt;. Great for live updates that flow one direction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSockets&lt;/strong&gt;: a two way persistent connection. Use when both ends need to push. Heavier, more work to deploy. The browser API is &lt;code&gt;WebSocket&lt;/code&gt;. Frameworks like Socket.IO and &lt;code&gt;partykit&lt;/code&gt; make it friendlier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick SSE first. It is simpler, plays better with HTTP infrastructure (load balancers, proxies, CDNs), and covers most "live updates" needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 10: Senior level moves and pitfalls
&lt;/h2&gt;

&lt;p&gt;A short list that separates "calls APIs" from "designs and consumes APIs well":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always use HTTPS.&lt;/strong&gt; Even in development, with a self signed cert if you have to. Cookies and tokens get weird without it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate every input on the server.&lt;/strong&gt; The client is not in your control. Use Zod (or your language equivalent) at the boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never trust the client about auth.&lt;/strong&gt; Authorization happens on the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pagination by default.&lt;/strong&gt; Any list endpoint that can return more than a few hundred rows must paginate. &lt;code&gt;?page=2&amp;amp;limit=20&lt;/code&gt; or cursor based (&lt;code&gt;?after=cursor&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limit even your internal APIs.&lt;/strong&gt; A bug on the client side that hammers your endpoint will eat your database otherwise.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set up CORS once, in middleware, not per route.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Send a &lt;code&gt;Content-Length&lt;/code&gt; and a real &lt;code&gt;Content-Type&lt;/code&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use &lt;code&gt;application/json&lt;/code&gt; for JSON, not &lt;code&gt;text/json&lt;/code&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use ISO 8601 timestamps with timezone.&lt;/strong&gt; &lt;code&gt;2026-05-09T10:00:00Z&lt;/code&gt;. No "May 9 2026". No &lt;code&gt;1715251200000&lt;/code&gt; unless you also tell the consumer it is millis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Money in integer cents.&lt;/strong&gt; Same lesson as the SQL post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document with OpenAPI&lt;/strong&gt; or a similar spec. Future you, your other team, your AI tools, and your client generators all benefit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat 5xx as your fault, 4xx as the client's.&lt;/strong&gt; Logging, alerts, dashboards must respect that line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limit responses include the limit headers.&lt;/strong&gt; Do not make clients guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For long jobs, return &lt;code&gt;202 Accepted&lt;/code&gt; with a polling URL,&lt;/strong&gt; or use a webhook, or stream progress with SSE. Do not hold an HTTP request open for 30 minutes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A peek under the hood
&lt;/h2&gt;

&lt;p&gt;What really happens when your frontend calls &lt;code&gt;/api/recipes&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The browser builds a request: method, URL, headers, optional body.&lt;/li&gt;
&lt;li&gt;If cross origin and "non simple", the browser sends an &lt;code&gt;OPTIONS&lt;/code&gt; preflight first.&lt;/li&gt;
&lt;li&gt;DNS resolves, TCP and TLS handshake (kept warm if possible).&lt;/li&gt;
&lt;li&gt;The request is sent over HTTP/2 or HTTP/3, multiplexed with other requests on the same connection.&lt;/li&gt;
&lt;li&gt;The server matches the route, runs middleware (auth, rate limit, validation), runs the handler, builds a response.&lt;/li&gt;
&lt;li&gt;The response status, headers, and body stream back.&lt;/li&gt;
&lt;li&gt;The browser respects &lt;code&gt;Cache-Control&lt;/code&gt;, sets cookies (&lt;code&gt;Set-Cookie&lt;/code&gt;), updates its CORS state.&lt;/li&gt;
&lt;li&gt;Your &lt;code&gt;fetch&lt;/code&gt; resolves. Your app gets the parsed JSON.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That whole loop is what every "an API call" really is. Once you can picture it, debugging gets ten times faster, because you know where to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tiny tips that will save you later
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use the right verb and the right status code.&lt;/strong&gt; Half of all API arguments end here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set &lt;code&gt;Cache-Control&lt;/code&gt; on every response.&lt;/strong&gt; Even &lt;code&gt;no-cache&lt;/code&gt; is better than nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;ETag&lt;/code&gt;s for read heavy endpoints.&lt;/strong&gt; Free 304s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always &lt;code&gt;AbortController&lt;/code&gt; in flight requests.&lt;/strong&gt; Especially in search-as-you-type.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cookies for first party web apps. JWT for cross origin or services.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Never put tokens in &lt;code&gt;localStorage&lt;/code&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validate every body server side with Zod.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use TanStack Query (or equivalent) for any non trivial frontend.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Page everything. Sort consistently. Paginate with stable cursors when ordering matters.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document your API.&lt;/strong&gt; The OpenAPI spec pays for itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read RFCs once.&lt;/strong&gt; Especially RFC 7231 (HTTP semantics), RFC 6750 (Bearer), and RFC 9457 (Problem Details).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;So that is the whole story. The web runs on a polite postal protocol where the verbs and the stamps already mean something specific. APIs are agreements about which addresses do what. Auth is the picture ID in your envelope.&lt;/p&gt;

&lt;p&gt;We learned to use the right verbs (&lt;code&gt;GET&lt;/code&gt; is safe, &lt;code&gt;POST&lt;/code&gt; creates, &lt;code&gt;PATCH&lt;/code&gt; updates, &lt;code&gt;DELETE&lt;/code&gt; deletes), the right status codes (2xx success, 4xx your fault, 5xx mine), the right URL shapes (nouns, plural, sub paths for ownership), the right headers (Content-Type, Accept, Authorization, Cache-Control, ETag, CORS), and the right errors (Problem Details, stable codes, useful messages).&lt;/p&gt;

&lt;p&gt;We picked auth on purpose: session cookies for first party web apps, JWT in &lt;code&gt;HttpOnly&lt;/code&gt; cookies for cross origin SPAs, OAuth 2.1 for "sign in with X", API keys never in the browser. We aborted in-flight requests, cached on purpose, paginated everything, and treated client validation as cosmetic and server validation as security.&lt;/p&gt;

&lt;p&gt;Once that map is in your head, every API conversation becomes shorter. You stop arguing about whether something should return &lt;code&gt;200 { ok: false }&lt;/code&gt; because you already know the answer. You stop chasing CORS ghosts because you can read the preflight in DevTools. You stop leaking tokens because you never put them where JavaScript could see them in the first place.&lt;/p&gt;

&lt;p&gt;Happy networking, and may your &lt;code&gt;2xx&lt;/code&gt; always outnumber your &lt;code&gt;5xx&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>beginners</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Learning Next.js As If You Built It Yourself</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Fri, 15 May 2026 13:03:00 +0000</pubDate>
      <link>https://dev.to/edriso/learning-nextjs-as-if-you-built-it-yourself-16kn</link>
      <guid>https://dev.to/edriso/learning-nextjs-as-if-you-built-it-yourself-16kn</guid>
      <description>&lt;p&gt;If you have ever built a React app with plain Vite, you remember the moment. Three months in, you have hand rolled a router. You have hand rolled the loading spinner pattern. You have hand rolled metadata for SEO. You have an Express server next to it for the API, and you spend half your time keeping the two in sync. Deploying is a checklist. Production caching is a guess.&lt;/p&gt;

&lt;p&gt;You did not want to build a framework. You just wanted to ship a website.&lt;/p&gt;

&lt;p&gt;That is the gap Next.js fills.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Next.js, really
&lt;/h2&gt;

&lt;p&gt;Think of Next.js as &lt;strong&gt;a React framework with the boring parts already done&lt;/strong&gt;. Routing, data fetching, server side rendering, caching, image optimization, fonts, head tags, deployment, all of it. You write the components, Next.js wires the rest into a coherent app that runs on the server, the edge, and the browser.&lt;/p&gt;

&lt;p&gt;Two ideas drive the whole thing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server first.&lt;/strong&gt; Pages render on the server by default. Only the parts that truly need interactivity ship JavaScript to the browser. Smaller bundles, faster first paint, better SEO, secrets stay safe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The file system is your routing config.&lt;/strong&gt; A folder is a route. A &lt;code&gt;page.tsx&lt;/code&gt; inside it is the screen. A &lt;code&gt;layout.tsx&lt;/code&gt; is the shared shell. No router config file to maintain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the whole vibe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's pretend we are building one
&lt;/h2&gt;

&lt;p&gt;We want a React framework that handles routing, server rendering, mutations, and caching by default. We will call it &lt;strong&gt;Next.js&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For the running example, we are building &lt;strong&gt;Mochi's Blog&lt;/strong&gt;: posts, a single post page, an author dashboard, and a comment form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: The folder is the route
&lt;/h2&gt;

&lt;p&gt;Next.js (App Router) maps your &lt;code&gt;app/&lt;/code&gt; folder onto URLs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
  layout.tsx              -&amp;gt; shared shell for the whole site
  page.tsx                -&amp;gt; /
  about/
    page.tsx              -&amp;gt; /about
  posts/
    page.tsx              -&amp;gt; /posts
    [slug]/
      page.tsx            -&amp;gt; /posts/anything
  dashboard/
    layout.tsx            -&amp;gt; shared shell only for /dashboard/*
    page.tsx              -&amp;gt; /dashboard
    settings/
      page.tsx            -&amp;gt; /dashboard/settings
  api/
    subscribe/
      route.ts            -&amp;gt; POST/GET /api/subscribe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few rules that explain the rest of the framework:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;page.tsx&lt;/code&gt;&lt;/strong&gt; is a page. It receives &lt;code&gt;params&lt;/code&gt; and &lt;code&gt;searchParams&lt;/code&gt; props.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;layout.tsx&lt;/code&gt;&lt;/strong&gt; is a shell that wraps every page below it in the tree. Layouts compose: every nested layout wraps inside the parent's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;[slug]&lt;/code&gt;&lt;/strong&gt; is a dynamic segment. The folder name with brackets becomes a route param.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;route.ts&lt;/code&gt;&lt;/strong&gt; is a backend endpoint. Export &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt; functions and they become handlers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;loading.tsx&lt;/code&gt;&lt;/strong&gt; is the suspense fallback for that segment. Streamed in automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;error.tsx&lt;/code&gt;&lt;/strong&gt; is the error boundary for that segment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;not-found.tsx&lt;/code&gt;&lt;/strong&gt; is shown when &lt;code&gt;notFound()&lt;/code&gt; is thrown.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A minimal layout and home page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/layout.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mochi's Blog&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cats and code.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;header&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Mochi's Blog&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;header&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// app/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;HomePage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Welcome to Mochi's Blog&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a working Next.js app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: Server Components by default
&lt;/h2&gt;

&lt;p&gt;Every component you write is a &lt;strong&gt;Server Component&lt;/strong&gt; unless you opt out. Server Components run on the server, can read from a database directly, can use secrets, and ship zero JavaScript to the browser.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/posts/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PostsPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;desc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`/posts/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole page. Async function, awaits the database, returns JSX. No &lt;code&gt;useEffect&lt;/code&gt;, no loading state, no API route in between.&lt;/p&gt;

&lt;p&gt;When you do need interactivity (state, effects, event handlers, browser APIs), opt into &lt;strong&gt;Client Components&lt;/strong&gt; with &lt;code&gt;"use client"&lt;/code&gt; at the top of the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/posts/[slug]/like-button.tsx&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LikeButton&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;initial&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;♥ &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mental model: &lt;strong&gt;default to Server Components. Reach for &lt;code&gt;"use client"&lt;/code&gt; only on the interactive island.&lt;/strong&gt; The pattern is server pages with small client islands inside, not whole client trees with little server islands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 3: Server Actions for mutations
&lt;/h2&gt;

&lt;p&gt;When you need to write data, you do not have to hand build an API route. Write an async function with &lt;code&gt;"use server"&lt;/code&gt; and call it from a form or a button.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/posts/[slug]/comment-form.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addComment&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./actions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CommentForm&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;postId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;addComment&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"postId"&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;textarea&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"body"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Post comment&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// app/posts/[slug]/actions.ts&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;revalidatePath&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;postId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;revalidatePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/posts/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a full mutation. No API route. No fetch. The browser submits the form, Next.js calls the server function, the server runs it, the page revalidates, and the new comment appears.&lt;/p&gt;

&lt;p&gt;For a richer client experience (pending state, error message, optimistic UI), pair the action with React 19's &lt;code&gt;useActionState&lt;/code&gt; and &lt;code&gt;useOptimistic&lt;/code&gt; hooks. Same patterns you already know, no Next.js specific magic needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 4: Three rendering modes, one mental model
&lt;/h2&gt;

&lt;p&gt;Every route picks one of three behaviors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static&lt;/strong&gt;: built once at deploy time, cached forever, fastest possible. Default for routes with no dynamic data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic&lt;/strong&gt;: rendered on every request, can use cookies, headers, and search params freely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming&lt;/strong&gt;: starts sending HTML immediately and fills in slow parts as data arrives. Pairs with &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; and &lt;code&gt;loading.tsx&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You rarely pick these explicitly. Next.js infers from what your code does. If you read &lt;code&gt;cookies()&lt;/code&gt; or &lt;code&gt;headers()&lt;/code&gt;, the route becomes dynamic. If you wrap a slow component in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;, that part streams.&lt;/p&gt;

&lt;p&gt;For data that should not be revalidated on every request, you cache and tag it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;unstable_cache&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getPostBySlug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;unstable_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;post-by-slug&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you mutate, you invalidate by tag or by path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;revalidatePath&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// any cache tagged "posts" is dropped&lt;/span&gt;
&lt;span class="nf"&gt;revalidatePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/posts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// re-render that path on next request&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Next.js 15, the modern surface for this is the &lt;strong&gt;&lt;code&gt;use cache&lt;/code&gt;&lt;/strong&gt; directive at the top of a function or component, plus &lt;code&gt;cacheTag()&lt;/code&gt; and &lt;code&gt;cacheLife()&lt;/code&gt; to control freshness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cacheTag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cacheLife&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getRecentPosts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;cacheTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;cacheLife&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hours&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;desc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;take&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mental model: &lt;strong&gt;cache everything you can, tag what you cache, invalidate by tag when you mutate&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 5: Route handlers for the rest
&lt;/h2&gt;

&lt;p&gt;If your client (a mobile app, a third party webhook, an external integration) needs a real REST endpoint, write a &lt;code&gt;route.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/posts/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same file system pattern, same colocation. Use Server Actions for app-internal mutations and route handlers for public APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 6: Navigation and links
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; component from &lt;code&gt;next/link&lt;/code&gt; does client side navigation, prefetches in view links, and keeps your layouts mounted across navigations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Link&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/posts/mochi-stole-socks"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Read post&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`/posts/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;prefetch&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Read&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For programmatic navigation, use &lt;code&gt;useRouter()&lt;/code&gt; from &lt;code&gt;next/navigation&lt;/code&gt; (the new App Router version, not the old &lt;code&gt;next/router&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 7: Images, fonts, and metadata, the boring wins
&lt;/h2&gt;

&lt;p&gt;Next.js has built in components for the things every website gets wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Image&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Inter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/font/google&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Inter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;subsets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;latin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Hero&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;inter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/mochi.jpg"&lt;/span&gt;
        &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"A small white cat with big eyes"&lt;/span&gt;
        &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;priority&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What that gives you for free: responsive images with &lt;code&gt;srcset&lt;/code&gt;, modern formats (AVIF, WebP), lazy loading by default, layout shift prevention, font self hosting with no FOUT, font subsetting.&lt;/p&gt;

&lt;p&gt;For SEO, set &lt;code&gt;metadata&lt;/code&gt; on each route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Why my cat steals socks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A short investigation.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;openGraph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/cover.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or generate it dynamically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateMetadata&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;excerpt&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These small wins (&lt;code&gt;&amp;lt;Image&amp;gt;&lt;/code&gt;, &lt;code&gt;next/font&lt;/code&gt;, metadata API) are why most production React apps run on Next.js even when they technically could ship as a Vite SPA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 8: Middleware and edge
&lt;/h2&gt;

&lt;p&gt;Code that runs on every request, before any route, lives in &lt;code&gt;middleware.ts&lt;/code&gt; at the root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// middleware.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isAuthed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isAuthed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/dashboard/:path*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/admin/:path*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Middleware runs on the edge by default, so it is fast and globally distributed. Use it for authentication gates, redirects, A/B test bucketing, locale negotiation, bot blocking. Keep it light. It runs on every matched request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 9: Project structure that scales
&lt;/h2&gt;

&lt;p&gt;A pattern most senior teams settle on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
  (marketing)/          -&amp;gt; route group, does not appear in URLs
    page.tsx            -&amp;gt; /
    pricing/page.tsx    -&amp;gt; /pricing
  (dashboard)/
    layout.tsx          -&amp;gt; auth-protected layout
    dashboard/page.tsx  -&amp;gt; /dashboard
  api/
  layout.tsx
components/
  ui/                   -&amp;gt; design system primitives (Button, Card, Dialog)
  posts/                -&amp;gt; feature components
lib/
  db.ts                 -&amp;gt; database client
  auth.ts               -&amp;gt; auth helpers
  validators/           -&amp;gt; Zod schemas, shared between client and server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The senior level habits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Group routes with &lt;code&gt;(name)/&lt;/code&gt;&lt;/strong&gt; for sectioning without affecting URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep server only code in &lt;code&gt;lib/&lt;/code&gt;&lt;/strong&gt; and never import it from a &lt;code&gt;"use client"&lt;/code&gt; file. (Add &lt;code&gt;import "server-only"&lt;/code&gt; at the top of files that should never reach the browser.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One feature, one folder.&lt;/strong&gt; A &lt;code&gt;posts&lt;/code&gt; feature has its server queries, components, and forms colocated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the official &lt;code&gt;next.config&lt;/code&gt; minimum.&lt;/strong&gt; Resist the urge to customize Webpack until you must.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision 10: Auth, the modern story
&lt;/h2&gt;

&lt;p&gt;Next.js does not ship auth, on purpose. The two clean choices in 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auth.js (formerly NextAuth)&lt;/strong&gt;: full featured, supports OAuth providers, email magic links, credentials. Sets a session cookie. Read sessions from Server Components with &lt;code&gt;auth()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clerk / Kinde / Stack Auth / WorkOS&lt;/strong&gt;: hosted auth services with React components and a back end you do not run. Trade some money for zero auth code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roll your own session cookie + a strong library&lt;/strong&gt; like &lt;code&gt;iron-session&lt;/code&gt; or &lt;code&gt;lucia-auth&lt;/code&gt;. Reasonable for small projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whatever you pick, the rule is the same: &lt;strong&gt;read the session in middleware or in a Server Component, never trust client side flags&lt;/strong&gt;. Server Components are where authorization decisions belong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 11: Deployment and runtime
&lt;/h2&gt;

&lt;p&gt;The simplest deploy: push to a Git repo connected to Vercel. Build, deploy, edge middleware, image optimization, all included. That is the path the framework was designed for.&lt;/p&gt;

&lt;p&gt;You can also self host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;next build
next start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a Node.js server. For containerized deploys, the &lt;code&gt;output: "standalone"&lt;/code&gt; config produces a minimal output that runs anywhere Node runs.&lt;/p&gt;

&lt;p&gt;Two runtime knobs to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node.js runtime&lt;/strong&gt; (default): full Node APIs, larger cold start, runs anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge runtime&lt;/strong&gt;: a subset of APIs, V8 isolate based, low latency cold start, geographically distributed. Set per route with &lt;code&gt;export const runtime = "edge";&lt;/code&gt;. Great for middleware and small API handlers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A peek under the hood
&lt;/h2&gt;

&lt;p&gt;What really happens when a user opens your Next.js app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The request hits the edge or your Node server.&lt;/li&gt;
&lt;li&gt;Middleware runs, possibly redirecting or rewriting.&lt;/li&gt;
&lt;li&gt;Next.js matches the URL to a &lt;code&gt;page.tsx&lt;/code&gt; plus its parent layouts.&lt;/li&gt;
&lt;li&gt;Server Components render on the server, fetching data, awaiting promises.&lt;/li&gt;
&lt;li&gt;The HTML streams to the browser, with &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundaries filling in as data arrives.&lt;/li&gt;
&lt;li&gt;A small JavaScript payload hydrates the Client Components only.&lt;/li&gt;
&lt;li&gt;The browser shows pixels almost immediately, interactivity follows shortly after.&lt;/li&gt;
&lt;li&gt;Subsequent navigations are client side, prefetched, and reuse the layout tree.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two senior level consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Less JS in the browser is the headline win.&lt;/strong&gt; A typical Next.js page ships a fraction of what a typical Vite SPA ships, because Server Components stay on the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching is the second big win, and the most confusing one.&lt;/strong&gt; Next.js caches at the data level (fetch, &lt;code&gt;unstable_cache&lt;/code&gt;, &lt;code&gt;use cache&lt;/code&gt;) and the route level. Get comfortable with &lt;code&gt;revalidateTag&lt;/code&gt; and &lt;code&gt;revalidatePath&lt;/code&gt; early.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tiny tips that will save you later
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default to Server Components.&lt;/strong&gt; Add &lt;code&gt;"use client"&lt;/code&gt; only where you must.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;&amp;lt;Image&amp;gt;&lt;/code&gt;, &lt;code&gt;next/font&lt;/code&gt;, and the metadata API.&lt;/strong&gt; Free Lighthouse points.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; for internal navigation.&lt;/strong&gt; Native &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; reloads the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tag your caches.&lt;/strong&gt; It pays off the first time you ship a "show fresh data" feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep secrets server side.&lt;/strong&gt; A &lt;code&gt;process.env.SECRET&lt;/code&gt; accessed from a &lt;code&gt;"use client"&lt;/code&gt; file is shipped to the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Zod schemas in &lt;code&gt;lib/validators/&lt;/code&gt;&lt;/strong&gt; to share types between client forms and server actions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read sessions on the server.&lt;/strong&gt; Auth checks in client code are cosmetic, not security.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run &lt;code&gt;next build&lt;/code&gt; in CI.&lt;/strong&gt; Type errors and config issues show up there, not in dev.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profile with the Vercel Analytics or &lt;code&gt;next/web-vitals&lt;/code&gt;&lt;/strong&gt; to track real user metrics.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;So that is the whole story. We were tired of stitching React, a router, an API server, an image pipeline, and a deployment script together by hand. We built a framework that does it all by default. The file system is the router. Server Components do data fetching. Server Actions do mutations. Caching is built in, with tag based invalidation. Images, fonts, and metadata are first class. Middleware runs on the edge.&lt;/p&gt;

&lt;p&gt;We learned to default to the server, ship small client islands, cache aggressively, invalidate precisely, and let Next.js handle the boring parts so we can focus on the actual app.&lt;/p&gt;

&lt;p&gt;Once that map is in your head, every modern React project starts to feel familiar. Next.js stops feeling like "React with extra steps" and starts feeling like the full stack toolkit you wished you had three years ago.&lt;/p&gt;

&lt;p&gt;Happy shipping, and may your LCP always be green.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>learning</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Learning React As If You Built It Yourself</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Thu, 14 May 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/edriso/learning-react-as-if-you-built-it-yourself-3h1b</link>
      <guid>https://dev.to/edriso/learning-react-as-if-you-built-it-yourself-3h1b</guid>
      <description>&lt;p&gt;If you have ever built a webpage with vanilla JS or jQuery, you remember the dance. Click a button, find the element, change its text, also flip a class on its sibling, also hide that other thing over there, oh and update the counter in the header, do not forget the counter. Now multiply that by twenty interactions on the page, and you have a small horror movie.&lt;/p&gt;

&lt;p&gt;The bug is almost always the same: the DOM said one thing, your variables said another, and they slowly drifted apart. You spent your evening pushing pixels around by hand.&lt;/p&gt;

&lt;p&gt;That is the gap React fills.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is React, really
&lt;/h2&gt;

&lt;p&gt;Think of React as a smart artist who paints whatever you describe. You hand them a sentence: "a happy cat with a hat, named Mochi, age 2". They paint exactly that. Later, when Mochi turns 3, you do not erase and repaint the whole canvas. You just hand the artist a new sentence, and the artist quietly changes only what is different. The hat stays. The smile stays. The age becomes 3.&lt;/p&gt;

&lt;p&gt;You are no longer the painter. You are the writer. You describe &lt;strong&gt;what&lt;/strong&gt; the screen should look like for a given state. React figures out &lt;strong&gt;how&lt;/strong&gt; to make the real DOM match. That is the whole magic.&lt;/p&gt;

&lt;p&gt;This style has a name: &lt;strong&gt;declarative UI&lt;/strong&gt;. You declare the destination, React handles the path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's pretend we are building one
&lt;/h2&gt;

&lt;p&gt;We want a way to build interactive UIs without spending our lives synchronizing variables with the DOM. We will call it &lt;strong&gt;React&lt;/strong&gt;. Every decision we make will be something you see in real React code, so let's make them on purpose.&lt;/p&gt;

&lt;p&gt;For our running example, we are going to build a tiny &lt;strong&gt;Cat Adoption Wall&lt;/strong&gt;, growing it piece by piece.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: UI is built from small reusable pieces
&lt;/h2&gt;

&lt;p&gt;We will call them &lt;strong&gt;components&lt;/strong&gt;. A component is just a function that takes some inputs and returns a description of what to draw. Pure JavaScript, nothing magical.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CatCard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Mochi&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Looking for a cozy lap.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That HTML looking thing inside the function is not HTML. It is &lt;strong&gt;JSX&lt;/strong&gt;, which is just a friendlier way of writing &lt;code&gt;React.createElement(...)&lt;/code&gt;. A build tool (Vite, Next.js, whatever) turns it into real JavaScript before it ships.&lt;/p&gt;

&lt;p&gt;Two rules of JSX that bite newcomers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;className&lt;/code&gt; instead of &lt;code&gt;class&lt;/code&gt;, and &lt;code&gt;htmlFor&lt;/code&gt; instead of &lt;code&gt;for&lt;/code&gt;. Reason: &lt;code&gt;class&lt;/code&gt; and &lt;code&gt;for&lt;/code&gt; are reserved words in JS.&lt;/li&gt;
&lt;li&gt;Every element you return must be wrapped in a single parent. If you do not want an extra &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;, use the empty fragment tags: &lt;code&gt;&amp;lt;&amp;gt;...&amp;lt;/&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Components compose. A bigger component just calls smaller ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AdoptionWall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Cats looking for a home&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CatCard&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CatCard&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CatCard&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three identical cats. That is not very useful yet. Let's make them different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: Pass data in with props
&lt;/h2&gt;

&lt;p&gt;A component should be reusable, so we let callers pass values in. We will call them &lt;strong&gt;props&lt;/strong&gt; (short for "properties"). They are just function arguments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CatCard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;bio&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;small&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; years old&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;small&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AdoptionWall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Cats looking for a home&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CatCard&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Mochi"&lt;/span&gt;   &lt;span class="na"&gt;bio&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Looking for a cozy lap."&lt;/span&gt; &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CatCard&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Whiskers"&lt;/span&gt; &lt;span class="na"&gt;bio&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Loves boxes."&lt;/span&gt;           &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CatCard&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Pepper"&lt;/span&gt;   &lt;span class="na"&gt;bio&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Will steal your snack."&lt;/span&gt; &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Curly braces &lt;code&gt;{}&lt;/code&gt; inside JSX mean "pop out into JavaScript for a moment". Anything that returns a value works inside there: variables, function calls, ternaries, even other JSX.&lt;/p&gt;

&lt;p&gt;Important rule: &lt;strong&gt;props flow down, not up&lt;/strong&gt;. A parent gives data to a child. A child should not reach up and change the parent's data directly. We will see how data flows back up in a moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 3: When data changes, the screen should change too
&lt;/h2&gt;

&lt;p&gt;A static page is not a UI. We need something that, when it changes, makes React repaint the relevant parts. We will call it &lt;strong&gt;state&lt;/strong&gt;, and the way you ask for some is the &lt;code&gt;useState&lt;/code&gt; hook.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LikeButton&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;likes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLikes&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setLikes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;likes&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      Mochi has &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;likes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; likes
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that first line out loud:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Give me a piece of state called &lt;code&gt;likes&lt;/code&gt;, starting at 0, and a setter to change it."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The trick: every time you call &lt;code&gt;setLikes&lt;/code&gt;, React schedules a re-render. It calls your component function again. The function returns a fresh description of the UI based on the new value. React diffs the new description against the previous one and only touches the parts that actually changed in the real DOM.&lt;/p&gt;

&lt;p&gt;You did not move any pixels. You did not query any elements. You changed a variable, and the screen followed.&lt;/p&gt;

&lt;p&gt;A few things worth burning into memory, because they trip up everyone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;State is per component instance.&lt;/strong&gt; Two &lt;code&gt;LikeButton&lt;/code&gt;s on the page have two independent counters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Updates are not instant.&lt;/strong&gt; &lt;code&gt;setLikes(likes + 1)&lt;/code&gt; schedules an update. Right after that line, &lt;code&gt;likes&lt;/code&gt; is still the old value in the current function call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For updates that depend on the current value, use the function form&lt;/strong&gt;: &lt;code&gt;setLikes((prev) =&amp;gt; prev + 1)&lt;/code&gt;. This is safer when multiple updates queue up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat state as immutable.&lt;/strong&gt; Do not mutate arrays or objects in place. Replace them with new ones: &lt;code&gt;setCats([...cats, newCat])&lt;/code&gt;, not &lt;code&gt;cats.push(newCat)&lt;/code&gt;. React decides whether to re-render by comparing references, so a mutated array looks identical and nothing happens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last rule is the source of about half of all "why is my UI not updating" bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 4: Lists need a stable identity
&lt;/h2&gt;

&lt;p&gt;When we render an array of things, React needs to tell them apart between renders, so it knows which item moved, which got deleted, and which is new. We give every item a &lt;code&gt;key&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AdoptionWall&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;cats&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Cats looking for a home&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CatCard&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;key&lt;/code&gt; should be something stable and unique inside that list. A database id is perfect. The array index is &lt;strong&gt;not&lt;/strong&gt; good enough as soon as items can be reordered or deleted, because the index of an item changes when its neighbors change. Wrong keys lead to weird bugs where typed text jumps to a different row, or animations apply to the wrong item.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 5: Children talk back through callbacks
&lt;/h2&gt;

&lt;p&gt;Props go down. To send something back up, the parent passes a function down, and the child calls it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CatCard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onAdopt&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;onAdopt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Adopt&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AdoptionWall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCats&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initialCats&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleAdopt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setCats&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CatCard&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onAdopt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleAdopt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cat list lives in &lt;code&gt;AdoptionWall&lt;/code&gt;. The button lives in &lt;code&gt;CatCard&lt;/code&gt;. They communicate through &lt;code&gt;onAdopt&lt;/code&gt;. This pattern is called &lt;strong&gt;lifting state up&lt;/strong&gt;: when two components need to share a piece of state, you move it up to their nearest common ancestor.&lt;/p&gt;

&lt;p&gt;If lifting starts to feel painful (you are passing the same prop through five layers), that is your hint to reach for one of the tools below. Not before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 6: Sometimes you need to talk to the outside world
&lt;/h2&gt;

&lt;p&gt;State is for things React owns. But the outside world exists too: timers, websockets, the document title, browser APIs, third party widgets. We need an escape hatch to sync with those, and we call it &lt;code&gt;useEffect&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PageTitle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; cats waiting`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The function inside runs &lt;strong&gt;after&lt;/strong&gt; React updates the DOM.&lt;/li&gt;
&lt;li&gt;The array at the end is the dependency list. React reruns the effect only when one of those values changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Modern advice (this is important): &lt;code&gt;useEffect&lt;/code&gt; is for &lt;strong&gt;synchronizing with external systems&lt;/strong&gt;, not for "do this thing when the user clicks". For that, just use the click handler. A good test: if removing your effect would not silently leak something or fall out of sync with the outside world, it probably should not be an effect.&lt;/p&gt;

&lt;p&gt;The most common misuse is fetching data inside &lt;code&gt;useEffect&lt;/code&gt;. It works, but it is a road full of footguns (race conditions, cache invalidation, double fetches in dev, no caching). The much better path lives in Decision 9.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 7: Forms, the friendly version
&lt;/h2&gt;

&lt;p&gt;Forms used to be where everyone gets stuck. React 19 made them dramatically nicer with &lt;strong&gt;Actions&lt;/strong&gt; and the &lt;code&gt;useActionState&lt;/code&gt; hook. You hand the form an async function, and React handles the pending state, the result, and the form reset for you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useActionState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;adoptCat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/adopt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Could not adopt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is going home`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AdoptForm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;formAction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useActionState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;adoptCat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;formAction&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Cat name"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sending...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Adopt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;   &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;red&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;   &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;green&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single hook replaces the classic "three useStates plus a try/catch plus a loading flag" pattern. The companion hook &lt;code&gt;useFormStatus()&lt;/code&gt; works inside any descendant of the form and tells you &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;method&lt;/code&gt;, &lt;code&gt;action&lt;/code&gt;. No prop drilling needed for a submit button to know its form is busy.&lt;/p&gt;

&lt;p&gt;For richer forms with lots of fields and validation rules, reach for &lt;strong&gt;React Hook Form&lt;/strong&gt;. It is the de facto choice in 2026. It avoids unnecessary re-renders and pairs nicely with Zod for type safe validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 8: Show users instant feedback with &lt;code&gt;useOptimistic&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;When the user clicks "Adopt", we do not want to make them stare at a spinner for 400ms. We want the cat to disappear from the wall instantly, and only roll back if the server says no.&lt;/p&gt;

&lt;p&gt;React 19 ships a hook for exactly that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useOptimistic&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AdoptionWall&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;cats&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;optimisticCats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;removeOptimistic&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useOptimistic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;cats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idToRemove&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;idToRemove&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleAdopt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;removeOptimistic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// UI updates right now&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/adopt/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="c1"&gt;// when the real cats list refreshes, the optimistic state goes away&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;optimisticCats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CatCard&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onAdopt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleAdopt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the request fails, React swaps back to the real list automatically. You write the happy path, the framework handles the rollback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 9: State management, told straight
&lt;/h2&gt;

&lt;p&gt;Here is the truth that the React ecosystem learned the hard way and finally agreed on:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Server state and client state are different problems. Stop solving them with the same tool.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Server state is data that lives somewhere else (a database, an API). It needs caching, refetching, retries, deduping, and invalidation. Client state is just stuff your UI cares about: which tab is open, is the sidebar collapsed, what theme are we in.&lt;/p&gt;

&lt;p&gt;The 2026 default playbook looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kind of state&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Local component state&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;useState&lt;/code&gt; / &lt;code&gt;useReducer&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;It does not need to leave the component.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form state&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;React Hook Form&lt;/strong&gt; (+ Zod)&lt;/td&gt;
&lt;td&gt;Fewer re-renders, great validation story.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server data&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;TanStack Query&lt;/strong&gt; (React Query)&lt;/td&gt;
&lt;td&gt;Caching, refetching, loading flags, all for free.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global UI state&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Zustand&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tiny store, one hook to read and write, no boilerplate.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Atomic / fine grained&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Jotai&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;When many small bits of state should react independently.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Big enterprise app&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Redux Toolkit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Strict patterns, devtools, time travel debugging.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here is the trap to avoid: &lt;strong&gt;do not fetch data with &lt;code&gt;fetch&lt;/code&gt; inside &lt;code&gt;useEffect&lt;/code&gt; and shove the result into Zustand or Redux.&lt;/strong&gt; That is the most common anti pattern in 2026. You end up reinventing caching, you forget to invalidate, you fight stale data. Use TanStack Query for anything that came from a server.&lt;/p&gt;

&lt;p&gt;A taste of each.&lt;/p&gt;

&lt;h3&gt;
  
  
  TanStack Query for server data
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AdoptionWall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cats&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/cats&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Loading...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Something went wrong&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CatCard&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caching, retry, background refetch, dedupe across components, all included.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zustand for global UI state
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// stores/ui.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zustand&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useUIStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;toggleTheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ThemeToggle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useUIStore&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toggleTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useUIStore&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toggleTheme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;toggleTheme&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Theme: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No provider to wrap your app in, no reducers, no actions, no boilerplate. One file, one hook, done.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context for very small global stuff
&lt;/h3&gt;

&lt;p&gt;Built into React, free. Best for things that rarely change: theme, current user, locale. &lt;strong&gt;Do not use Context as a state manager for fast changing state&lt;/strong&gt;, because every consumer re-renders when the value changes. Use Zustand instead and you get cheap selector subscriptions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ThemeContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ThemeContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"dark"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Page&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ThemeContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ThemeContext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Current theme: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Decision 10: Move work to the server when you can
&lt;/h2&gt;

&lt;p&gt;For the longest time, every React app shipped a giant bundle of JavaScript, then asked the user's browser to do everything: fetch data, render markup, hydrate, run interactivity. That is a lot of work to do in the laggy place.&lt;/p&gt;

&lt;p&gt;React 19 leaned into a different idea: &lt;strong&gt;Server Components&lt;/strong&gt; and &lt;strong&gt;Server Actions&lt;/strong&gt;. A server component runs on the server, can read from a database directly, and ships only the rendered output to the browser. A server action is just an async function marked &lt;code&gt;"use server"&lt;/code&gt; that the client can call like a local function, but it actually runs on the server. No API route needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/cats/page.jsx, a Server Component (Next.js style)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AdoptButton&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./AdoptButton&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CatsPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AdoptButton&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AdoptButton.jsx, a Client Component&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;adoptCat&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./actions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AdoptButton&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;adoptCat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Adopt&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// actions.js&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;adoptCat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;adopted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mental model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Default to &lt;strong&gt;Server Components&lt;/strong&gt;. They are cheaper, they keep secrets safe, they do not send code to the browser.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;"use client"&lt;/code&gt; only on the small islands that actually need interactivity (a button, a form, a dropdown).&lt;/li&gt;
&lt;li&gt;For mutations, write a function with &lt;code&gt;"use server"&lt;/code&gt; and call it. No API route to hand roll.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not get Server Components in a plain Vite SPA. You get them in a framework like &lt;strong&gt;Next.js (App Router)&lt;/strong&gt; or &lt;strong&gt;React Router 7 (Framework Mode)&lt;/strong&gt;. For a classic single page app, Vite + React + TanStack Query + Zustand is still excellent and very common in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 11: A few more goodies in React 19
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;use()&lt;/code&gt;&lt;/strong&gt; lets you read a Promise (or a Context) inside a component and have React suspend until it resolves. Pairs beautifully with Suspense and Server Components.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;useTransition&lt;/code&gt;&lt;/strong&gt; marks an update as "not urgent". The UI stays responsive while the heavy update happens in the background, and you get a &lt;code&gt;pending&lt;/code&gt; flag for free. In React 19, the function passed to it can be async.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ref&lt;/code&gt; is now just a prop.&lt;/strong&gt; No more &lt;code&gt;forwardRef&lt;/code&gt; wrapping. You take &lt;code&gt;ref&lt;/code&gt; like any other prop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; work anywhere.&lt;/strong&gt; React hoists them into the document head for you, so SEO and head tags no longer need a separate library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better error messages&lt;/strong&gt; for hydration mismatches, plus an automatically wired Document Metadata system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not need any of this on day one. Knowing it exists means you will recognize it when you see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A peek under the hood
&lt;/h2&gt;

&lt;p&gt;What really happens when you click a button in a React app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your &lt;code&gt;onClick&lt;/code&gt; handler runs and calls &lt;code&gt;setSomething(newValue)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;React marks that component (and the tree below it) as "needs to re-render".&lt;/li&gt;
&lt;li&gt;React calls your component function again. It gets back a new tree of JSX, in memory only. This in memory tree is sometimes called the &lt;strong&gt;virtual DOM&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;React compares the new tree to the previous one. This is &lt;strong&gt;reconciliation&lt;/strong&gt;. It figures out the smallest set of real DOM changes that would make reality match the new tree.&lt;/li&gt;
&lt;li&gt;It applies only those changes to the real DOM.&lt;/li&gt;
&lt;li&gt;After the DOM is committed, any &lt;code&gt;useEffect&lt;/code&gt; whose dependencies changed runs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is why React feels fast even when you "render the whole thing on every state change". You do not actually rebuild the DOM, you rebuild a cheap description of it, and only the diff hits the real page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tiny tips that will save you later
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start with &lt;code&gt;useState&lt;/code&gt;.&lt;/strong&gt; Add libraries when you actually feel the pain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never store server data in client state.&lt;/strong&gt; Use TanStack Query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat state as immutable.&lt;/strong&gt; No &lt;code&gt;.push&lt;/code&gt;, no &lt;code&gt;obj.x = y&lt;/code&gt;. Always make a new value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the function form of setters&lt;/strong&gt; when the next value depends on the previous: &lt;code&gt;setCount((c) =&amp;gt; c + 1)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick stable keys&lt;/strong&gt; for lists. The array index is a trap as soon as items can move.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Effects sync with the outside world.&lt;/strong&gt; They are not a "do something on click" tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lift state to the lowest common parent that needs it&lt;/strong&gt;, not to the top of the app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reach for Server Components and Server Actions&lt;/strong&gt; when you can. Less JS shipped to the user, and your secrets stay on the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default form library: React Hook Form + Zod.&lt;/strong&gt; Default data fetcher: TanStack Query. Default global UI store: Zustand. You can build 95% of an app with that trio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Vite for SPAs and Next.js (or React Router 7) for full apps.&lt;/strong&gt; Avoid &lt;code&gt;create-react-app&lt;/code&gt;, it has been retired.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;So that is the whole story. We were tired of pushing pixels around by hand. We built a framework where you describe &lt;strong&gt;what&lt;/strong&gt; the UI should be for a given state, and the framework handles &lt;strong&gt;how&lt;/strong&gt; to make the screen match. We broke the UI into small functions called components. We let them take props down and call functions up. We added state to make things change, effects to talk to the outside world, and a tidy little pipeline of modern hooks (&lt;code&gt;useActionState&lt;/code&gt;, &lt;code&gt;useOptimistic&lt;/code&gt;, &lt;code&gt;useTransition&lt;/code&gt;, &lt;code&gt;use&lt;/code&gt;) to handle async work without pain.&lt;/p&gt;

&lt;p&gt;Then we admitted that not every piece of state is the same:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local stuff stays in &lt;code&gt;useState&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Forms go to React Hook Form.&lt;/li&gt;
&lt;li&gt;Anything from a server goes to TanStack Query.&lt;/li&gt;
&lt;li&gt;Shared UI state goes to Zustand.&lt;/li&gt;
&lt;li&gt;Server work goes to Server Components and Server Actions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once that map is in your head, every React tutorial, blog post, and codebase starts to feel familiar. You stop fighting the framework and start moving at speed.&lt;/p&gt;

&lt;p&gt;Happy rendering, and give your virtual cat a virtual scratch from me.&lt;/p&gt;

</description>
      <category>react</category>
      <category>learning</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Learning TailwindCSS As If You Built It Yourself</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Wed, 13 May 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/edriso/learning-tailwindcss-as-if-you-built-it-yourself-1jj0</link>
      <guid>https://dev.to/edriso/learning-tailwindcss-as-if-you-built-it-yourself-1jj0</guid>
      <description>&lt;p&gt;If you have ever opened a year old CSS file and tried to figure out which class does what, you remember the feeling. &lt;code&gt;card-2&lt;/code&gt;, &lt;code&gt;card-2-blue&lt;/code&gt;, &lt;code&gt;card-2-blue-mobile&lt;/code&gt;, &lt;code&gt;card-2-blue-mobile-final&lt;/code&gt;, all in different files, all targeting the same component, all "almost" doing the same thing. Renaming any of them is risky. Deleting any of them is scarier. So nobody does, and the file grows forever.&lt;/p&gt;

&lt;p&gt;That graveyard of dead classes is what Tailwind was invented to prevent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is TailwindCSS, really
&lt;/h2&gt;

&lt;p&gt;Think of Tailwind as &lt;strong&gt;a tool belt of pre cut Lego bricks for styling&lt;/strong&gt;. Instead of inventing class names like &lt;code&gt;card-header-title-medium-blue&lt;/code&gt; and writing CSS for them, you snap together a handful of small, single purpose classes right in your HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"rounded-lg bg-white p-6 shadow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-xl font-semibold text-slate-900"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Mochi&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-2 text-slate-600"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;A small white cat with big plans.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each class does exactly one thing. &lt;code&gt;rounded-lg&lt;/code&gt; is just &lt;code&gt;border-radius: 0.5rem&lt;/code&gt;. &lt;code&gt;p-6&lt;/code&gt; is just &lt;code&gt;padding: 1.5rem&lt;/code&gt;. There is no naming meeting. There is no dead CSS. The styles live next to the markup that uses them. When you delete the markup, the styles go with it.&lt;/p&gt;

&lt;p&gt;Two ideas drive the whole thing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Utility first.&lt;/strong&gt; You compose your design from many tiny utilities, not from a few "components" that grow attribute soup over time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constrained, not free.&lt;/strong&gt; The utilities only exist for the values you defined as design tokens (spacing scale, color palette, font sizes). You cannot accidentally write &lt;code&gt;padding: 13.7px&lt;/code&gt;. You can only pick from &lt;code&gt;p-1&lt;/code&gt;, &lt;code&gt;p-2&lt;/code&gt;, &lt;code&gt;p-3&lt;/code&gt;, etc. The constraint is the point.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the whole vibe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's pretend we are building one
&lt;/h2&gt;

&lt;p&gt;We want a way to write CSS that scales to a team and a year, without inventing names, without dead code, without a fight in code review every Monday. We will call it &lt;strong&gt;Tailwind&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For the running example, we are styling a small &lt;strong&gt;profile card&lt;/strong&gt; with a name, an avatar, a bio, and two buttons. We will grow it piece by piece.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: Ship a sensible default scale
&lt;/h2&gt;

&lt;p&gt;We need a small, consistent set of values for spacing, type, and color. If everything is on the scale, components will feel like they were designed by the same person, even if 12 different people wrote them.&lt;/p&gt;

&lt;p&gt;The defaults that ship with Tailwind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spacing&lt;/strong&gt;: a &lt;code&gt;0.25rem&lt;/code&gt; (4px) base. &lt;code&gt;p-1&lt;/code&gt; = 4px, &lt;code&gt;p-2&lt;/code&gt; = 8px, &lt;code&gt;p-4&lt;/code&gt; = 16px, &lt;code&gt;p-8&lt;/code&gt; = 32px. Same scale used by margin, gap, width, height.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type&lt;/strong&gt;: &lt;code&gt;text-xs&lt;/code&gt;, &lt;code&gt;text-sm&lt;/code&gt;, &lt;code&gt;text-base&lt;/code&gt;, &lt;code&gt;text-lg&lt;/code&gt;, &lt;code&gt;text-xl&lt;/code&gt;, &lt;code&gt;text-2xl&lt;/code&gt;, all the way up to &lt;code&gt;text-9xl&lt;/code&gt;. Each comes with a sensible line height.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Colors&lt;/strong&gt;: 22 named color families (&lt;code&gt;slate&lt;/code&gt;, &lt;code&gt;gray&lt;/code&gt;, &lt;code&gt;red&lt;/code&gt;, &lt;code&gt;green&lt;/code&gt;, &lt;code&gt;blue&lt;/code&gt;, &lt;code&gt;indigo&lt;/code&gt;, ...) with 11 shades each (&lt;code&gt;50&lt;/code&gt; to &lt;code&gt;950&lt;/code&gt;). Plus full opacity control (&lt;code&gt;text-blue-500/50&lt;/code&gt;, half opacity).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Radii, shadows, borders&lt;/strong&gt;: also on a scale. &lt;code&gt;rounded&lt;/code&gt;, &lt;code&gt;rounded-md&lt;/code&gt;, &lt;code&gt;rounded-lg&lt;/code&gt;, &lt;code&gt;rounded-2xl&lt;/code&gt;. &lt;code&gt;shadow-sm&lt;/code&gt;, &lt;code&gt;shadow&lt;/code&gt;, &lt;code&gt;shadow-md&lt;/code&gt;, &lt;code&gt;shadow-lg&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full classes look like this in a real component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"
  inline-flex items-center gap-2
  rounded-md
  bg-blue-600 px-4 py-2
  text-sm font-medium text-white
  hover:bg-blue-500
  focus-visible:outline-2 focus-visible:outline-blue-600 focus-visible:outline-offset-2
  disabled:opacity-50 disabled:cursor-not-allowed
"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Adopt
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it like a sentence, left to right. Layout, then box, then color, then text, then states. Once your eye adjusts (give it a week), it is faster to scan than CSS files split across the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: Variants for state and screen size
&lt;/h2&gt;

&lt;p&gt;Plain CSS uses &lt;code&gt;:hover&lt;/code&gt; and &lt;code&gt;@media&lt;/code&gt;. Tailwind gives you the same power as &lt;strong&gt;prefixes&lt;/strong&gt; on the class name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"
  text-slate-700
  hover:text-slate-900
  focus-visible:underline
  active:text-slate-950
"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Read more&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"
  grid
  grid-cols-1
  md:grid-cols-2
  lg:grid-cols-3
  gap-4
"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The state prefixes you will reach for daily:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;hover:&lt;/code&gt;, &lt;code&gt;focus:&lt;/code&gt;, &lt;code&gt;focus-visible:&lt;/code&gt;, &lt;code&gt;active:&lt;/code&gt;, &lt;code&gt;disabled:&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;group-hover:&lt;/code&gt; and &lt;code&gt;peer-hover:&lt;/code&gt; for "when my parent or sibling is hovered" (super useful)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aria-expanded:&lt;/code&gt;, &lt;code&gt;aria-checked:&lt;/code&gt;, &lt;code&gt;data-[state=open]:&lt;/code&gt; for components driven by ARIA or data attributes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;has-[:invalid]:&lt;/code&gt; for the new CSS &lt;code&gt;:has()&lt;/code&gt; selector&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;not-first:&lt;/code&gt;, &lt;code&gt;first:&lt;/code&gt;, &lt;code&gt;last:&lt;/code&gt;, &lt;code&gt;odd:&lt;/code&gt;, &lt;code&gt;even:&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The screen prefixes (mobile first):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sm:&lt;/code&gt; 640px and up&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;md:&lt;/code&gt; 768px and up&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lg:&lt;/code&gt; 1024px and up&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;xl:&lt;/code&gt; 1280px and up&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;2xl:&lt;/code&gt; 1536px and up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read &lt;code&gt;md:grid-cols-2&lt;/code&gt; as "from 768px and up, use two columns". The bare class is the mobile baseline.&lt;/p&gt;

&lt;p&gt;You can stack prefixes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"
  bg-blue-600 hover:bg-blue-500
  md:bg-emerald-600 md:hover:bg-emerald-500
"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Multi state&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Variants are how Tailwind avoids the "one giant CSS file with sixteen media queries" problem. The state lives next to the value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 3: Dark mode is just another variant
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  ...
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Tailwind v4, &lt;code&gt;dark:&lt;/code&gt; defaults to the user's &lt;code&gt;prefers-color-scheme&lt;/code&gt;. If you want a manual toggle (a moon icon in your nav), you opt into the class strategy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@custom-variant&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dark&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;&amp;lt;html class="dark"&amp;gt;&lt;/code&gt; flips your whole site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 4: Configuration, the v4 way
&lt;/h2&gt;

&lt;p&gt;Tailwind used to live in a &lt;code&gt;tailwind.config.js&lt;/code&gt;. Tailwind v4 (the current major in 2026) moved configuration into CSS itself. You define design tokens with &lt;code&gt;@theme&lt;/code&gt;, and Tailwind generates utilities for you automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* app.css */&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;63%&lt;/span&gt; &lt;span class="m"&gt;0.16&lt;/span&gt; &lt;span class="m"&gt;256&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-brand-50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;96%&lt;/span&gt; &lt;span class="m"&gt;0.02&lt;/span&gt; &lt;span class="m"&gt;256&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="py"&gt;--font-display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"Inter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system-ui&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="py"&gt;--spacing-128&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;32rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="py"&gt;--radius-card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="py"&gt;--breakpoint-3xl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1920px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Defining &lt;code&gt;--color-brand&lt;/code&gt; automatically gives you &lt;code&gt;bg-brand&lt;/code&gt;, &lt;code&gt;text-brand&lt;/code&gt;, &lt;code&gt;border-brand&lt;/code&gt;, &lt;code&gt;ring-brand&lt;/code&gt;. Defining &lt;code&gt;--spacing-128&lt;/code&gt; gives you &lt;code&gt;p-128&lt;/code&gt;, &lt;code&gt;mx-128&lt;/code&gt;, &lt;code&gt;w-128&lt;/code&gt;. Defining &lt;code&gt;--breakpoint-3xl&lt;/code&gt; gives you &lt;code&gt;3xl:&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is the whole config story now. No JS file, no &lt;code&gt;theme.extend&lt;/code&gt;, no rebuilding the world.&lt;/p&gt;

&lt;p&gt;If you need full power, the JS config still works for backwards compatibility. New projects should start in CSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 5: Arbitrary values for the rare one off
&lt;/h2&gt;

&lt;p&gt;Sometimes you need a specific pixel value that is not on the scale. The escape hatch is square brackets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-[317px] mt-[7px] grid-cols-[200px_1fr_60px]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-[#bada55]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;design demanded a specific hex&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-[url('/hero.jpg')]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful, but be honest with yourself. If the same arbitrary value appears more than twice, it should be a token in &lt;code&gt;@theme&lt;/code&gt;, not an inline literal.&lt;/p&gt;

&lt;p&gt;You can also use modifiers for opacity, font weight, etc:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-blue-500/30 text-slate-900/80"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  30% blue background, 80% opaque text
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Decision 6: Composition without losing your mind
&lt;/h2&gt;

&lt;p&gt;Once a button shows up in twenty places with the same long class string, you want to deduplicate. Two senior level moves:&lt;/p&gt;

&lt;h3&gt;
  
  
  Component in your framework, not in CSS
&lt;/h3&gt;

&lt;p&gt;The Tailwind opinion: a component is a React/Vue/Svelte component, not a &lt;code&gt;.btn&lt;/code&gt; CSS class.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Button.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cva&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;VariantProps&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;class-variance-authority&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;clsx&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clsx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cva&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inline-flex items-center gap-2 rounded-md font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 disabled:opacity-50&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;secondary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bg-slate-200 text-slate-900 hover:bg-slate-300&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;ghost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-slate-700 hover:bg-slate-100&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;px-3 py-1.5 text-sm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;px-4 py-2  text-sm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;lg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;px-5 py-2.5 text-base&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;defaultVariants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;md&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;VariantProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ButtonHTMLAttributes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLButtonElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;clsx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pair you will see most: &lt;strong&gt;&lt;code&gt;clsx&lt;/code&gt;&lt;/strong&gt; (or &lt;code&gt;classnames&lt;/code&gt;) for conditional class composition, &lt;strong&gt;&lt;code&gt;tailwind-merge&lt;/code&gt;&lt;/strong&gt; for safely overriding earlier classes (so &lt;code&gt;&amp;lt;Button className="bg-red-500" /&amp;gt;&lt;/code&gt; actually wins), and &lt;strong&gt;&lt;code&gt;class-variance-authority&lt;/code&gt;&lt;/strong&gt; for typed component variants.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;@apply&lt;/code&gt;, the rare exception
&lt;/h3&gt;

&lt;p&gt;If a component is used in plain HTML (no framework), or if you need to style elements you do not control (like markdown HTML), you can extract a class with &lt;code&gt;@apply&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@layer&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.prose-link&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;@apply&lt;/span&gt; &lt;span class="err"&gt;text-blue-600&lt;/span&gt; &lt;span class="err"&gt;underline&lt;/span&gt; &lt;span class="err"&gt;underline-offset-2&lt;/span&gt; &lt;span class="py"&gt;hover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;text-blue-500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@apply&lt;/code&gt; is a tool, not a strategy. If you find yourself reaching for it everywhere, you are fighting Tailwind's grain. Use components instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 7: Layouts with utilities
&lt;/h2&gt;

&lt;p&gt;A side by side layout in pure utilities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;header&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center justify-between gap-4 p-4 border-b"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-lg font-semibold"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Mia's Blog&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;nav&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center gap-6 text-sm text-slate-600"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hover:text-slate-900"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Home&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hover:text-slate-900"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/about"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;About&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/header&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A responsive grid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"grid gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 p-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;article&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"rounded-2xl bg-white p-6 shadow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;article&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"rounded-2xl bg-white p-6 shadow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;article&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"rounded-2xl bg-white p-6 shadow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A modern landing hero with fluid type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"
  mx-auto max-w-3xl px-6 py-20 text-center
  bg-gradient-to-b from-slate-50 to-white
"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-balance text-4xl sm:text-5xl lg:text-6xl font-bold tracking-tight"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    A tiny library for tiny lives.
  &lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-6 text-lg text-slate-600"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Carefully selected books for slow afternoons.
  &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-10 flex justify-center gap-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"rounded-md bg-slate-900 px-5 py-2.5 text-white hover:bg-slate-800"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Browse
    &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"rounded-md px-5 py-2.5 text-slate-700 hover:bg-slate-100"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Learn more
    &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;text-balance&lt;/code&gt; is a small modern win. It tells the browser to balance lines so the heading does not orphan a single word on its own line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 8: Container queries and modern CSS, free
&lt;/h2&gt;

&lt;p&gt;Tailwind v4 ships container queries built in, no plugin needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;aside&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"@container p-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;article&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"
    rounded-lg bg-white p-4
    @md:flex @md:items-center @md:gap-4
  "&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"h-20 w-20 rounded-full"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/mochi.jpg"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"@md:flex-1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;h3&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-lg font-semibold"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Mochi&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-sm text-slate-600"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;A small cat with big plans.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/aside&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@md:&lt;/code&gt; triggers when the &lt;strong&gt;container&lt;/strong&gt; crosses the &lt;code&gt;md&lt;/code&gt; size, not the viewport. The same card looks different in a wide and a narrow column without changing its markup.&lt;/p&gt;

&lt;p&gt;Other modern bits Tailwind exposes nicely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;has-[]&lt;/code&gt;&lt;/strong&gt; for parent based styling: &lt;code&gt;has-[:invalid]:border-red-500&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;group-has-[]&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;peer-has-[]&lt;/code&gt;&lt;/strong&gt; for descendant or sibling based logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;text-balance&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;text-pretty&lt;/code&gt;&lt;/strong&gt; for typography&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;scroll-snap-x&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;snap-mandatory&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;snap-start&lt;/code&gt;&lt;/strong&gt; for snap scrolling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;backdrop-blur&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;backdrop-brightness&lt;/code&gt;&lt;/strong&gt; for frosted glass UIs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;mix-blend-multiply&lt;/code&gt;&lt;/strong&gt; and other blend modes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision 9: Theming with CSS variables
&lt;/h2&gt;

&lt;p&gt;Because v4 builds tokens as real CSS variables, runtime theming is free.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;63%&lt;/span&gt; &lt;span class="m"&gt;0.16&lt;/span&gt; &lt;span class="m"&gt;256&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;:root&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"warm"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--color-brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;70%&lt;/span&gt; &lt;span class="m"&gt;0.15&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"cool"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--color-brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;70%&lt;/span&gt; &lt;span class="m"&gt;0.12&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"green"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--color-brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;70%&lt;/span&gt; &lt;span class="m"&gt;0.15&lt;/span&gt; &lt;span class="m"&gt;150&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;bg-brand&lt;/code&gt; follows the &lt;code&gt;data-theme&lt;/code&gt; attribute. Switching themes is one attribute change in JavaScript, no rebuild, no class swap on every component.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 10: Forms and prose
&lt;/h2&gt;

&lt;p&gt;Two official plugins worth installing on any content site:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@tailwindcss/forms&lt;/code&gt;&lt;/strong&gt; gives all form elements consistent base styles you can build on. Without it, browser defaults are inconsistent across systems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@tailwindcss/typography&lt;/code&gt;&lt;/strong&gt; gives you a &lt;code&gt;.prose&lt;/code&gt; class that styles raw HTML (from a CMS or markdown) beautifully. &lt;code&gt;&amp;lt;article class="prose lg:prose-lg dark:prose-invert"&amp;gt;{markdown}&amp;lt;/article&amp;gt;&lt;/code&gt; is the whole setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add them to &lt;code&gt;app.css&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@plugin&lt;/span&gt; &lt;span class="s1"&gt;"@tailwindcss/forms"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@plugin&lt;/span&gt; &lt;span class="s1"&gt;"@tailwindcss/typography"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(In v4, plugins are declared in CSS, not in a JS config.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 11: The headless component story
&lt;/h2&gt;

&lt;p&gt;Tailwind only handles styles. Real apps need accessible behaviors: dropdowns with focus trap, dialogs that handle escape, comboboxes with type ahead. Hand rolling these is a graveyard.&lt;/p&gt;

&lt;p&gt;The two friends to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Headless UI&lt;/strong&gt; (from the Tailwind team): unstyled but accessible primitives. Bring your own utility classes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Radix UI&lt;/strong&gt; (or &lt;strong&gt;React Aria&lt;/strong&gt; on the React side): a more comprehensive set of accessible primitives. Pairs beautifully with Tailwind.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Combine these with &lt;code&gt;cva&lt;/code&gt; for variants and you have a small, fast, type safe component library that looks however you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 12: Anti patterns, the senior level smell test
&lt;/h2&gt;

&lt;p&gt;A handful of things that will rot your Tailwind codebase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One giant &lt;code&gt;@apply&lt;/code&gt; per component.&lt;/strong&gt; You are reinventing CSS classes inside Tailwind. Extract a real component instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Random arbitrary values.&lt;/strong&gt; &lt;code&gt;w-[317px]&lt;/code&gt; once is fine. &lt;code&gt;w-[317px]&lt;/code&gt; in 30 places is a missing token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inline string of 60 classes with no structure.&lt;/strong&gt; Group by purpose: layout, then box, then color, then text, then states. Or use &lt;code&gt;cva&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping &lt;code&gt;tailwind-merge&lt;/code&gt;&lt;/strong&gt; when your component accepts &lt;code&gt;className&lt;/code&gt;. Without it, callers cannot override.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using &lt;code&gt;@apply&lt;/code&gt; for components used inside React/Vue.&lt;/strong&gt; You are halfway between two worlds. Pick one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hand rolling dropdowns and dialogs&lt;/strong&gt; instead of using Headless UI or Radix. Accessibility lives there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No design tokens.&lt;/strong&gt; Without &lt;code&gt;@theme&lt;/code&gt; variables, you lose the "constrained on purpose" benefit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A peek under the hood
&lt;/h2&gt;

&lt;p&gt;What really happens when Tailwind builds your CSS:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tailwind scans your template files (HTML, JSX, Vue, Svelte, etc).&lt;/li&gt;
&lt;li&gt;It finds every class string used.&lt;/li&gt;
&lt;li&gt;For each class it recognizes, it generates the CSS rule that class needs.&lt;/li&gt;
&lt;li&gt;Unused utilities are not generated. The output stylesheet only contains what your code actually uses.&lt;/li&gt;
&lt;li&gt;The JIT (just in time) engine in v4 uses Rust under the hood and a new Lightning CSS based pipeline. Builds that used to take seconds now take milliseconds.&lt;/li&gt;
&lt;li&gt;The output ships as one small CSS file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last point is why a "huge utility framework" still produces tiny stylesheets in production. You only get the classes you used. Delete a component, the styles disappear with it. There is no dead CSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tiny tips that will save you later
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use the official VS Code extension.&lt;/strong&gt; Autocomplete, hover docs, class sorting. Non negotiable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the Prettier plugin&lt;/strong&gt; (&lt;code&gt;prettier-plugin-tailwindcss&lt;/code&gt;) for canonical class order. Code reviews stop arguing about it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up &lt;code&gt;tailwind-merge&lt;/code&gt;&lt;/strong&gt; in any component that accepts &lt;code&gt;className&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Define design tokens in &lt;code&gt;@theme&lt;/code&gt;&lt;/strong&gt; on day one. You will use them on day two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile first, always.&lt;/strong&gt; Bare class is mobile. &lt;code&gt;md:&lt;/code&gt; is tablet up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;cva&lt;/code&gt;&lt;/strong&gt; for typed component variants. Avoid stringly typed &lt;code&gt;className&lt;/code&gt; props with five conditionals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn on &lt;code&gt;text-balance&lt;/code&gt; for headings&lt;/strong&gt; for free typography wins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrap content with &lt;code&gt;max-w-prose&lt;/code&gt;&lt;/strong&gt; for readable line lengths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Headless UI or Radix&lt;/strong&gt; for any interactive component (modal, menu, combobox).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit unused arbitrary values&lt;/strong&gt; every month or so. Promote the repeated ones into the theme.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;So that is the whole story. We were tired of inventing class names that aged into a graveyard. We built a system where styles are tiny single purpose classes, written next to the markup that uses them. Our design tokens are CSS variables defined in &lt;code&gt;@theme&lt;/code&gt;. Our utilities respect a constrained scale, so layouts feel consistent across the team. Variants give us state and screen size right where the value is. Dark mode is just another variant. Container queries, &lt;code&gt;:has()&lt;/code&gt;, OKLCH colors, and modern features all come along for free.&lt;/p&gt;

&lt;p&gt;We learned to compose into framework components (with &lt;code&gt;cva&lt;/code&gt;, &lt;code&gt;clsx&lt;/code&gt;, &lt;code&gt;tailwind-merge&lt;/code&gt;) instead of CSS components, to keep &lt;code&gt;@apply&lt;/code&gt; rare, and to lean on Headless UI or Radix for accessible behavior. We got rid of dead CSS forever, because Tailwind only generates the classes our code actually uses.&lt;/p&gt;

&lt;p&gt;Once that map is in your head, every Tailwind codebase starts to feel familiar, fast, and shockingly maintainable a year later. Tailwind stops feeling like "ugly inline styles" and starts feeling like the calm, constrained design system every team wishes it had.&lt;/p&gt;

&lt;p&gt;Happy styling, and may your stylesheet stay small.&lt;/p&gt;

</description>
      <category>css</category>
      <category>tailwindcss</category>
      <category>learning</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Learning TypeScript As If You Built It Yourself</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Tue, 12 May 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/edriso/learning-typescript-as-if-you-built-it-yourself-2e90</link>
      <guid>https://dev.to/edriso/learning-typescript-as-if-you-built-it-yourself-2e90</guid>
      <description>&lt;p&gt;If you have ever shipped a JavaScript app of any real size, you know the moment. It is 11pm. You renamed a function from &lt;code&gt;getUser&lt;/code&gt; to &lt;code&gt;loadUser&lt;/code&gt;. The app boots. Tests pass. You ship it. Twenty minutes later a coworker pings you: a button somewhere completely unrelated has stopped working, because something deep inside still called &lt;code&gt;getUser&lt;/code&gt; and the editor never warned you.&lt;/p&gt;

&lt;p&gt;JavaScript is full of these little knives. &lt;code&gt;undefined is not a function&lt;/code&gt;. &lt;code&gt;Cannot read property 'name' of null&lt;/code&gt;. A string that quietly was a number. An object missing a field. The compiler did not catch it because there was no compiler.&lt;/p&gt;

&lt;p&gt;That is the gap TypeScript fills.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is TypeScript, really
&lt;/h2&gt;

&lt;p&gt;Think of TypeScript as a &lt;strong&gt;label maker for JavaScript values&lt;/strong&gt;. In plain JS, every box in your warehouse looks the same: an unlabeled cardboard cube. You have to open it to know what is inside, and you might still be wrong. In TS, every box gets a sticker on the front that says exactly what kind of thing it holds. "string". "User". "Array of pizzas". A picky little manager called the &lt;strong&gt;type checker&lt;/strong&gt; walks the aisles and yells at you if you try to put a tomato in the box labeled "books".&lt;/p&gt;

&lt;p&gt;That manager only works at build time. Once you ship, the labels are stripped off and the boxes go to production as plain JS. So you pay zero runtime cost for all that safety.&lt;/p&gt;

&lt;p&gt;That is the whole vibe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's pretend we are building one
&lt;/h2&gt;

&lt;p&gt;We want to add a type system on top of JavaScript without breaking anything that already works. We will call it &lt;strong&gt;TypeScript&lt;/strong&gt;. As we design it, every decision is a thing you will run into in real code, so let's make them on purpose.&lt;/p&gt;

&lt;p&gt;For the running example, we are opening a tiny pizza shop. Pizzas, toppings, sizes, orders. We will type the whole thing as we go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: Types are notes for the compiler, not new runtime stuff
&lt;/h2&gt;

&lt;p&gt;We do not want to invent a new language. We want JavaScript with extra notes on top. So our compiler will read the notes, check them, and then &lt;strong&gt;erase&lt;/strong&gt; them. The JS that ships to production is the same JS you would have written without types.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After compilation, the file becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The labels are gone at runtime. This has two huge consequences that newcomers miss:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You cannot trust types at runtime.&lt;/strong&gt; If data comes from an API, a form, or &lt;code&gt;JSON.parse&lt;/code&gt;, the type system has no idea what is actually in there. You have to validate it (with Zod, Valibot, or a hand written guard).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No new runtime features (mostly).&lt;/strong&gt; Old TypeScript tried with &lt;code&gt;enum&lt;/code&gt;, decorators, and &lt;code&gt;namespace&lt;/code&gt;. The modern advice is to avoid &lt;code&gt;enum&lt;/code&gt; and prefer plain unions of strings.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision 2: Start with the basics, give two escape hatches
&lt;/h2&gt;

&lt;p&gt;Our type system needs the obvious primitives, plus a way to say "I do not know yet" without giving up on safety entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="kr"&gt;string&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Margherita&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="kr"&gt;number&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;9.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;nothing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;everything&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;literally anything&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// do not use, opts out of safety&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;mystery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;could be anything&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// safe version of any&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;explode&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;boom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// function never returns&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// does not return a value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two escape hatches matter a lot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;any&lt;/code&gt;&lt;/strong&gt; turns off the type checker for that value. Treat it like a swear word. The whole point of TS is to know what you have, and &lt;code&gt;any&lt;/code&gt; says "I do not know and I do not want to". Use only as a last resort, and isolate it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;unknown&lt;/code&gt;&lt;/strong&gt; also says "I do not know yet", but the compiler refuses to let you do anything with it until you prove what it is (with a check or a guard). This is the right tool for incoming data.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;readInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// value.toUpperCase()  // error: object is of type 'unknown'&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// ok now, narrowed to string&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;never&lt;/code&gt; is the bottom type, the type with no values. It shows up when something is logically impossible (a function that always throws, or a switch branch that should never be reached). We will use it as a tool later for exhaustiveness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 3: Describe object shapes with &lt;code&gt;type&lt;/code&gt; or &lt;code&gt;interface&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;A pizza is not just a string. It is a shape with a name, a size, toppings, and a price. We need to describe shapes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Pizza&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;small&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;large&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;toppings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;priceCents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two ways to write this exist, and they confuse newcomers. There are &lt;code&gt;type&lt;/code&gt; aliases and &lt;code&gt;interface&lt;/code&gt; declarations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Pizza&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Practical advice in 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;type&lt;/code&gt; by default.&lt;/strong&gt; It can do everything &lt;code&gt;interface&lt;/code&gt; can, plus unions, intersections, mapped types, and conditional types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;interface&lt;/code&gt; when you need declaration merging&lt;/strong&gt; (mostly for adding fields to library types) or when you are publishing a public class API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be consistent inside one project.&lt;/strong&gt; Do not flip flop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A nice fact about TypeScript that surprises people coming from Java or C#: types are &lt;strong&gt;structural&lt;/strong&gt;, not nominal. If two shapes look the same, they are the same. Names do not matter to the checker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Cat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Dog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;meow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Cat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mochi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;woof&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Dog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;meow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// perfectly fine, shapes match&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is amazing for flexibility. Sometimes too flexible (we will fix that with branded types later).&lt;/p&gt;

&lt;p&gt;A few small tricks worth knowing about object types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Pizza&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// cannot be reassigned after creation&lt;/span&gt;
  &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// optional, may be undefined&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// index signature: any extra keys allowed&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Decision 4: Combine, pick, choose
&lt;/h2&gt;

&lt;p&gt;Real types are not always one shape. They are "this &lt;strong&gt;or&lt;/strong&gt; that", or "this &lt;strong&gt;and&lt;/strong&gt; that".&lt;/p&gt;

&lt;h3&gt;
  
  
  Union types: &lt;code&gt;A | B&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;small&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;large&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;priceFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;small&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1400&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those &lt;code&gt;"small" | "medium" | "large"&lt;/code&gt; are &lt;strong&gt;literal types&lt;/strong&gt;. A literal type is a type whose only value is one specific value. This is how we get the "enum like" experience without the actual &lt;code&gt;enum&lt;/code&gt; keyword.&lt;/p&gt;

&lt;h3&gt;
  
  
  Intersection types: &lt;code&gt;A &amp;amp; B&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;WithTimestamps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;StoredPizza&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Pizza&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;WithTimestamps&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;StoredPizza&lt;/code&gt; is everything a &lt;code&gt;Pizza&lt;/code&gt; is, &lt;strong&gt;and&lt;/strong&gt; has timestamps too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Discriminated unions, the secret weapon
&lt;/h3&gt;

&lt;p&gt;This is the single most useful pattern in real TypeScript. When something can be in multiple shapes depending on a state, give every variant a common literal field, and the checker can tell them apart.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="nl"&gt;pizzaId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cooking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="nl"&gt;pizzaId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;ovenId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delivered&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;pizzaId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;deliveredAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cancelled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Pending pizza &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pizzaId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cooking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Cooking in oven &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ovenId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delivered&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Delivered at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deliveredAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cancelled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Cancelled: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside each &lt;code&gt;case&lt;/code&gt;, TypeScript narrows the type and gives you only the fields that exist for that variant. No more "is this property here on this kind of order? let me check the docs".&lt;/p&gt;

&lt;p&gt;Add a final exhaustiveness check using &lt;code&gt;never&lt;/code&gt; and the compiler will literally yell at you when a future you adds a new status and forgets to handle it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cooking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delivered&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cancelled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;
    &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;_exhaustive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;_exhaustive&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a new case to &lt;code&gt;Order&lt;/code&gt; later, forget to add a branch, and that line lights up red. Free safety net.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 5: Make types reusable with generics
&lt;/h2&gt;

&lt;p&gt;We do not want to write a new shape for "an array of pizzas", "an array of orders", "an array of users". The shape is the same: an array, holding things of some type. So we let the user fill in the blank.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;numberBox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pizzaBox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Pizza&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;somePizza&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;T&lt;/code&gt; is a type parameter. Read it as "for any type T, a Box of T is...".&lt;/p&gt;

&lt;p&gt;Generics shine in functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;pizza1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pizza2&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// p is Pizza | undefined&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;        &lt;span class="c1"&gt;// n is number | undefined&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You did not have to tell &lt;code&gt;first&lt;/code&gt; what type was in there. The compiler &lt;strong&gt;inferred&lt;/strong&gt; it from the argument. Inference is one of TypeScript's superpowers. Lean on it. Do not annotate things the compiler can already figure out.&lt;/p&gt;

&lt;p&gt;You can constrain a type parameter, so it has to satisfy some shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;T&lt;/code&gt; can be anything, as long as it has an &lt;code&gt;id: string&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Real world examples you will see daily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ApiResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchPizza&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ApiResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Pizza&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single pattern (a generic discriminated union) replaces about 80% of "what should the return type be" questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 6: Ship a standard library of utility types
&lt;/h2&gt;

&lt;p&gt;Once we have generics, conditionals, and mapped types (more on those soon), we can pre build a bunch of "type level functions" that everyone needs. These come with the language.&lt;/p&gt;

&lt;p&gt;The greatest hits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;P&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Pizza&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// every field optional&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Required&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Pizza&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// every field required (strips ?)&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;O&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Readonly&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Pizza&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// every field readonly&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NonNullable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// string&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Just&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Pick&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Pizza&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// only those keys&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Without&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Omit&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Pizza&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;priceCents&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// every key except those&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Menu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Pizza&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// an object map: keys -&amp;gt; Pizza&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Parameters&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;priceFor&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// tuple of arg types: [Size]&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Ret&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;priceFor&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// number&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Awaited1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Awaited&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Pizza&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// unwraps promises: Pizza&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SizeOnly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Extract&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;small&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// narrowing a union&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;NotSmall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Exclude&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;small&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// removing from a union&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Memorize these. They make 90% of "how do I express this type" problems trivial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 7: Narrow types as you learn more
&lt;/h2&gt;

&lt;p&gt;The compiler tracks what you know about a value as the code runs. Each check makes the type more specific. This is &lt;strong&gt;narrowing&lt;/strong&gt;, and the rules are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;area&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// here x is string&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// here x is number&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Pizza&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;eat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Salad&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;dress&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;food&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Pizza&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;Salad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;food&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;Pizza&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;food&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eat&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;                       &lt;span class="nx"&gt;food&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dress&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hasName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// narrows to { name: unknown } &amp;amp; object&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For checks the compiler cannot figure out on its own, write a &lt;strong&gt;type predicate&lt;/strong&gt;. It teaches the checker your custom logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isPizza&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;Pizza&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;toppings&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stuff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchSomething&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isPizza&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stuff&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;stuff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toppings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// ok, narrowed to Pizza&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type predicates are how you bring messy outside data into your typed world. For real production code, prefer a runtime schema validator like &lt;strong&gt;Zod&lt;/strong&gt; that gives you the predicate and the type at the same time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PizzaSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;small&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;large&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;toppings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
  &lt;span class="na"&gt;priceCents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;nonnegative&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Pizza&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;PizzaSchema&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// type derived from the schema&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pizza&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PizzaSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unknownInput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// throws if invalid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One schema, both runtime validation and the static type. This is the modern standard at the API boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 8: Let people compute new types from old ones
&lt;/h2&gt;

&lt;p&gt;This is the part that makes TypeScript feel like a tiny programming language at the type level. Powerful, sometimes a little spooky.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mapped types
&lt;/h3&gt;

&lt;p&gt;Walk over the keys of one type and produce another.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]?:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ReadonlyDeep&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;DraftPizza&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Pizza&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// every field optional&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Partial&lt;/code&gt;, &lt;code&gt;Required&lt;/code&gt;, &lt;code&gt;Readonly&lt;/code&gt;, and friends are all built with mapped types behind the scenes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conditional types
&lt;/h3&gt;

&lt;p&gt;If A is a subtype of B, do this, else do that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;IsString&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;A&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;IsString&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// "yes"&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;B&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;IsString&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// "no"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combined with &lt;code&gt;infer&lt;/code&gt;, you can pull pieces out of types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ElementOf&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="nf"&gt;extends &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt; &lt;span class="nx"&gt;U&lt;/span&gt;&lt;span class="p"&gt;)[]&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;U&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Topping&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ElementOf&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Pizza&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;toppings&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Template literal types
&lt;/h3&gt;

&lt;p&gt;Strings as types, with interpolation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EventName&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`on&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Capitalize&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;X&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;EventName&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// "onClick"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can build wild route type DSLs with these (think "/users/:id" inferring the parameter names), but for daily work, the value is mostly readable string keys.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;keyof&lt;/code&gt; and indexed access
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PizzaKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;Pizza&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// "id" | "name" | "size" | "toppings" | "priceCents"&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SizeType&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Pizza&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;size&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// "small" | "medium" | "large"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These two operators are how you keep types in sync. If &lt;code&gt;Pizza&lt;/code&gt; changes, anything derived from &lt;code&gt;keyof Pizza&lt;/code&gt; updates automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 9: Three modern features that level you up
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;as const&lt;/code&gt; for "freeze this exactly"
&lt;/h3&gt;

&lt;p&gt;By default, the compiler widens literal values. &lt;code&gt;const x = "small"&lt;/code&gt; becomes type &lt;code&gt;string&lt;/code&gt;. If you want the literal type, freeze it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SIZES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;small&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;large&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;SIZES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// "small" | "medium" | "large"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last line is gold. It builds the union type directly from the array, so you can never get them out of sync.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;satisfies&lt;/code&gt; for "fits this shape, but keep the specific type"
&lt;/h3&gt;

&lt;p&gt;A common annoying choice in old TS: do I annotate this as &lt;code&gt;Record&amp;lt;string, Pizza&amp;gt;&lt;/code&gt; (and lose the specific keys), or leave it untyped (and lose the shape check)? &lt;code&gt;satisfies&lt;/code&gt; solves both at once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;menu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;margherita&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Margherita&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;small&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;toppings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;            &lt;span class="na"&gt;priceCents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;pepperoni&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Pepperoni&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;toppings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pepperoni&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;priceCents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1100&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Pizza&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;margherita&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// ok, autocomplete works&lt;/span&gt;
&lt;span class="nx"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unknownPizza&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// error, key does not exist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiler verified each entry is a valid &lt;code&gt;Pizza&lt;/code&gt;, but &lt;code&gt;menu&lt;/code&gt; keeps its specific keys. Use &lt;code&gt;satisfies&lt;/code&gt; whenever you would have reached for &lt;code&gt;as&lt;/code&gt;. It is safer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Branded types for nominal typing on the cheap
&lt;/h3&gt;

&lt;p&gt;Structural typing means a &lt;code&gt;UserId&lt;/code&gt; and a &lt;code&gt;PizzaId&lt;/code&gt; are the same type if they are both strings. Sometimes you do not want that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Brand&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;B&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;__brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;B&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UserId&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Brand&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UserId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PizzaId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Brand&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PizzaId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;makePizzaId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;PizzaId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;PizzaId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deletePizza&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PizzaId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;u_42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;deletePizza&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// error, not a PizzaId&lt;/span&gt;
&lt;span class="nf"&gt;deletePizza&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;makePizzaId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;p_42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// ok&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The brand only exists at the type level. At runtime it is just a string. But the compiler will not let you mix them up. Use this any time a primitive (a string id, a number cents, a number millis) carries meaning that should not be mixed with another primitive of the same kind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 10: A &lt;code&gt;tsconfig.json&lt;/code&gt; that prevents pain
&lt;/h2&gt;

&lt;p&gt;The compiler is only as strict as you ask it to be. The settings below are what serious TypeScript codebases turn on. They prevent entire classes of bugs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NodeNext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NodeNext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lib"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DOM"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;                      &lt;/span&gt;&lt;span class="c1"&gt;// turns on all the strict flags&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noUncheckedIndexedAccess"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// arr[i] is T | undefined, not T&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"exactOptionalPropertyTypes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// x?: number does not allow x: undefined&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noImplicitOverride"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;// override must say so&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"noUnusedLocals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noUnusedParameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noFallthroughCasesInSwitch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"forceConsistentCasingInFileNames"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"isolatedModules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="c1"&gt;// helps with bundlers and SWC&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"esModuleInterop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"skipLibCheck"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="c1"&gt;// faster builds, fewer 3rd party errors&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"verbatimModuleSyntax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="c1"&gt;// strict import type / export type&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two flags every senior should know about beyond &lt;code&gt;strict&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;noUncheckedIndexedAccess&lt;/code&gt;&lt;/strong&gt; turns &lt;code&gt;array[i]&lt;/code&gt; and &lt;code&gt;record[key]&lt;/code&gt; into &lt;code&gt;T | undefined&lt;/code&gt;. Almost all "cannot read property of undefined" production bugs disappear when this is on, because the compiler now forces you to check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;exactOptionalPropertyTypes&lt;/code&gt;&lt;/strong&gt; stops &lt;code&gt;{ name?: string }&lt;/code&gt; from secretly meaning &lt;code&gt;{ name?: string | undefined }&lt;/code&gt;. Subtle but it bites at API boundaries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Turn these on early in a new project. They are pure gain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 11: Talk to the rest of the world
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Type only imports and exports
&lt;/h3&gt;

&lt;p&gt;When you import only types, mark them. The bundler removes them entirely, no chance of a runtime side effect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Pizza&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./types&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;makePizza&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./factory&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;verbatimModuleSyntax: true&lt;/code&gt;, the compiler enforces this. It also kills accidental cycles caused by importing a value just for its type.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;.d.ts&lt;/code&gt; declaration files
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;.d.ts&lt;/code&gt; file is types only, no implementation. You write them when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You publish a JS library and want to ship types separately.&lt;/li&gt;
&lt;li&gt;You have a JS file in your project that you want to type from outside.&lt;/li&gt;
&lt;li&gt;You want to add ambient types (like declaring &lt;code&gt;process.env&lt;/code&gt;).
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// env.d.ts&lt;/span&gt;
&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nx"&gt;NodeJS&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ProcessEnv&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;development&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;process.env.DATABASE_URL&lt;/code&gt; is typed as &lt;code&gt;string&lt;/code&gt;, and a typo in the variable name is a compile error.&lt;/p&gt;

&lt;h3&gt;
  
  
  Module augmentation and declaration merging
&lt;/h3&gt;

&lt;p&gt;Sometimes a library is missing a type or you want to extend it. Reopen the module and merge in fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// express.d.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this, &lt;code&gt;req.user&lt;/code&gt; is typed everywhere, with no need to fork the package.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;@types/*&lt;/code&gt; and DefinitelyTyped
&lt;/h3&gt;

&lt;p&gt;If a JS only library does not ship types, look for &lt;code&gt;@types/that-library&lt;/code&gt; on npm. It is published by the community DefinitelyTyped repo. If it does not exist, you can write a small &lt;code&gt;.d.ts&lt;/code&gt; yourself with &lt;code&gt;declare module "that-library";&lt;/code&gt; to silence errors and add types as needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Senior level moves and traps
&lt;/h2&gt;

&lt;p&gt;A handful of patterns and pitfalls you will run into at staff and senior level:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prefer string unions over &lt;code&gt;enum&lt;/code&gt;.&lt;/strong&gt; Enums have weird runtime behavior, do not tree shake well, and break with &lt;code&gt;isolatedModules&lt;/code&gt;. &lt;code&gt;type Status = "open" | "closed"&lt;/code&gt; is simpler and erased.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer &lt;code&gt;unknown&lt;/code&gt; to &lt;code&gt;any&lt;/code&gt; everywhere data crosses a boundary.&lt;/strong&gt; Validate, then narrow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid type assertions (&lt;code&gt;x as T&lt;/code&gt;)&lt;/strong&gt; unless you genuinely know more than the compiler. Each &lt;code&gt;as&lt;/code&gt; is a tiny lie. Reach for &lt;code&gt;satisfies&lt;/code&gt;, type guards, or schema validators first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variance matters more than you think.&lt;/strong&gt; Function parameters are contravariant, return types are covariant. If &lt;code&gt;(a: Animal) =&amp;gt; void&lt;/code&gt; is assignable where &lt;code&gt;(a: Cat) =&amp;gt; void&lt;/code&gt; is expected, that is why. Do not fight this, learn the rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distributive conditional types&lt;/strong&gt; kick in over naked unions. &lt;code&gt;T extends X ? A : B&lt;/code&gt; distributes over each member of &lt;code&gt;T&lt;/code&gt; if &lt;code&gt;T&lt;/code&gt; is a union. Wrap in &lt;code&gt;[T]&lt;/code&gt; to disable: &lt;code&gt;[T] extends [X] ? A : B&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not over engineer types.&lt;/strong&gt; A 200 line conditional type that infers a SQL schema looks impressive in a tweet and is a maintenance nightmare in a real project. Go simple first. Reach for fancy types only when they pay off in safety or DX you can feel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Object.keys&lt;/code&gt; returns &lt;code&gt;string[]&lt;/code&gt;, not &lt;code&gt;(keyof T)[]&lt;/code&gt;.&lt;/strong&gt; This trips everyone. JS objects can have extra keys, so the standard library plays it safe. Cast only if you are sure: &lt;code&gt;Object.keys(obj) as (keyof T)[]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;readonly&lt;/code&gt; is shallow.&lt;/strong&gt; &lt;code&gt;readonly Pizza[]&lt;/code&gt; stops you from &lt;code&gt;push&lt;/code&gt;ing, but the items inside are still mutable. Use &lt;code&gt;DeepReadonly&amp;lt;T&amp;gt;&lt;/code&gt; (or the &lt;code&gt;Readonly&amp;lt;T&amp;gt;&lt;/code&gt; mapped type recursively) when you need it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not store types in classes if a &lt;code&gt;type&lt;/code&gt; works.&lt;/strong&gt; Classes carry runtime cost. Types are free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;NonNullable&lt;/code&gt;, &lt;code&gt;Awaited&lt;/code&gt;, &lt;code&gt;ReturnType&lt;/code&gt;, and &lt;code&gt;Parameters&lt;/code&gt; to derive&lt;/strong&gt; instead of duplicate. Single source of truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep types close to the code they describe.&lt;/strong&gt; Co locating &lt;code&gt;pizza.ts&lt;/code&gt; and &lt;code&gt;pizza.types.ts&lt;/code&gt; (or just one file) beats a global &lt;code&gt;types/&lt;/code&gt; folder for everyone's sanity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A peek under the hood
&lt;/h2&gt;

&lt;p&gt;What really happens when you build a TS project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The compiler (&lt;code&gt;tsc&lt;/code&gt;) reads your &lt;code&gt;tsconfig.json&lt;/code&gt; and walks every &lt;code&gt;.ts&lt;/code&gt;/&lt;code&gt;.tsx&lt;/code&gt; file in scope.&lt;/li&gt;
&lt;li&gt;It builds a graph of types, doing inference where you did not annotate, then checks every assignment, call, and return.&lt;/li&gt;
&lt;li&gt;If anything fails, it prints errors. Nothing is emitted by default in that case (unless &lt;code&gt;noEmitOnError&lt;/code&gt; is off).&lt;/li&gt;
&lt;li&gt;If checks pass, it emits plain &lt;code&gt;.js&lt;/code&gt; files (and optionally &lt;code&gt;.d.ts&lt;/code&gt; files for libraries).&lt;/li&gt;
&lt;li&gt;At runtime, your code is just JavaScript. No types, no overhead, no surprises.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In modern projects, the build is often handled by a fast bundler (Vite, esbuild, SWC, Bun) that strips types blindly without type checking, while a separate &lt;code&gt;tsc --noEmit&lt;/code&gt; runs in CI and your editor for the actual checking. Two jobs, two tools, both happy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tiny tips that will save you later
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lean on inference.&lt;/strong&gt; Do not annotate variables the compiler already knows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Annotate function signatures.&lt;/strong&gt; Especially exported ones. They are your contract with the world.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate at the boundary.&lt;/strong&gt; Schema in, typed everywhere after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;as const&lt;/code&gt; for static lookup tables.&lt;/strong&gt; Use &lt;code&gt;satisfies&lt;/code&gt; for typed but specific configs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brand IDs and units.&lt;/strong&gt; A bug between user ids and pizza ids is one keystroke away without it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crank up &lt;code&gt;strict&lt;/code&gt; plus &lt;code&gt;noUncheckedIndexedAccess&lt;/code&gt; plus &lt;code&gt;exactOptionalPropertyTypes&lt;/code&gt;&lt;/strong&gt; on day one. Adding them later is painful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run &lt;code&gt;tsc --noEmit&lt;/code&gt; in CI.&lt;/strong&gt; Editor squiggles are not enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read other people's types.&lt;/strong&gt; The TS standard library, the React types, and Zod are masterclasses. Click through to definitions in your editor whenever you are curious.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fancy types are a tool, not a flex.&lt;/strong&gt; If a simple type works, use it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;So that is the whole story. We were tired of JavaScript's quiet bugs. We invented a label maker that lives at the layer above the code. We wrote shapes with &lt;code&gt;type&lt;/code&gt; and &lt;code&gt;interface&lt;/code&gt;. We combined them with unions and intersections, and used discriminated unions for state. We made them reusable with generics, derived new ones with mapped and conditional types, and shipped a small standard library of helpers like &lt;code&gt;Partial&lt;/code&gt;, &lt;code&gt;Pick&lt;/code&gt;, and &lt;code&gt;Awaited&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We added modern moves like &lt;code&gt;as const&lt;/code&gt;, &lt;code&gt;satisfies&lt;/code&gt;, and branded types to be precise without losing flexibility. We turned on the strictest flags so the compiler catches the most bugs. We validated at the edges with Zod, narrowed with type guards, and trusted the types only past that boundary.&lt;/p&gt;

&lt;p&gt;Once that map is in your head, every TypeScript codebase starts to feel familiar. You stop fighting red squiggles and start using them as a free pair programmer who reads every line you write and quietly catches the future you about to make a mistake.&lt;/p&gt;

&lt;p&gt;Happy typing, and put a slice aside for me.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>learning</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Learning JavaScript As If You Built It Yourself</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Mon, 11 May 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/edriso/learning-javascript-as-if-you-built-it-yourself-26ah</link>
      <guid>https://dev.to/edriso/learning-javascript-as-if-you-built-it-yourself-26ah</guid>
      <description>&lt;p&gt;If you have ever wanted to make a webpage do something (anything) when you click a button, you have already met JavaScript. It is the only language that ships in every browser on the planet. It runs on servers, on robots, in databases, on watches, on toasters with WiFi.&lt;/p&gt;

&lt;p&gt;It is also the language that confuses new developers more than any other, because it grew in layers over thirty years. Some parts feel modern and clean. Some parts feel like a haunted attic. Both parts ship in every browser, and both parts will outlive us.&lt;/p&gt;

&lt;p&gt;That is the gap JavaScript fills, and the maze it creates.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is JavaScript, really
&lt;/h2&gt;

&lt;p&gt;Think of JavaScript as &lt;strong&gt;the electricity of the web&lt;/strong&gt;. HTML gives a page its shape. CSS gives it style. JavaScript makes it move. Plug it in, things happen. It is the only language a browser understands natively, so if you want a website to react to a click, fetch some data, or animate a thing, you are using JavaScript whether you wrote it yourself or it came from a library.&lt;/p&gt;

&lt;p&gt;The whole language follows three loud rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It runs on a single thread.&lt;/strong&gt; One thing at a time. Long work blocks the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is dynamically typed.&lt;/strong&gt; A variable can hold a number now and a string later. The compiler does not stop you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is built around objects and functions.&lt;/strong&gt; Functions are values. Objects are bags of values. That is most of it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the whole vibe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's pretend we are building one
&lt;/h2&gt;

&lt;p&gt;We want a tiny scripting language that runs in the browser, can manipulate the page, and is friendly enough that anyone can pick it up. We will call it &lt;strong&gt;JavaScript&lt;/strong&gt;. Every decision we make will show up in real code, so let's make them on purpose.&lt;/p&gt;

&lt;p&gt;For our running example, we are building a tiny &lt;strong&gt;mood tracker&lt;/strong&gt;. It saves a mood, lists moods, talks to a server. Small, but it lets us touch every important corner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: Variables, three flavors and the right default
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mood&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;happy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;oldStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;do not use&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cheat sheet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;const&lt;/code&gt;&lt;/strong&gt; for values that will not be reassigned. Default to this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;let&lt;/code&gt;&lt;/strong&gt; for values you actually need to reassign.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;var&lt;/code&gt;&lt;/strong&gt; is the old keyword. It is function scoped (not block scoped) and gets &lt;strong&gt;hoisted&lt;/strong&gt; in confusing ways. Do not use &lt;code&gt;var&lt;/code&gt; in new code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;const&lt;/code&gt; does not mean the value is frozen. It only means the &lt;strong&gt;binding&lt;/strong&gt; does not change. &lt;code&gt;const arr = [1, 2, 3]; arr.push(4)&lt;/code&gt; is fine. The variable still points at the same array, the array just got a new element.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: Two kinds of values, and they behave differently
&lt;/h2&gt;

&lt;p&gt;JavaScript has eight types, and they fall into two camps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Primitives&lt;/strong&gt; (passed by value, immutable): &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, &lt;code&gt;boolean&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt;, &lt;code&gt;undefined&lt;/code&gt;, &lt;code&gt;symbol&lt;/code&gt;, &lt;code&gt;bigint&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Objects&lt;/strong&gt; (passed by reference): everything else, including arrays and functions.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// a is still 1&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// x.value is also 2, because x and y point at the same object&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single distinction explains about 30% of beginner bugs. If you mutate an object, every variable pointing at it sees the change.&lt;/p&gt;

&lt;p&gt;A small note on the weird two: &lt;strong&gt;&lt;code&gt;null&lt;/code&gt;&lt;/strong&gt; is "intentionally nothing" and &lt;strong&gt;&lt;code&gt;undefined&lt;/code&gt;&lt;/strong&gt; is "no value yet". They are different types but most code treats them the same way. Use the loose equality just for them: &lt;code&gt;value == null&lt;/code&gt; matches both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 3: Functions are first class
&lt;/h2&gt;

&lt;p&gt;A function is a value. You can store it in a variable, pass it as an argument, return it from another function. This unlocks everything from callbacks to React.&lt;/p&gt;

&lt;p&gt;Three ways to write one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;            &lt;span class="c1"&gt;// declaration, hoisted&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="c1"&gt;// expression&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mul&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                    &lt;span class="c1"&gt;// arrow, the modern default&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two non obvious things matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Arrow functions do not have their own &lt;code&gt;this&lt;/code&gt;.&lt;/strong&gt; They inherit it from where they were written. This is what you want most of the time, especially in callbacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Function declarations are hoisted to the top of their scope.&lt;/strong&gt; You can call them above where they are written. Function expressions and arrows are not hoisted as values.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Closures, the secret superpower
&lt;/h3&gt;

&lt;p&gt;A function "remembers" the variables that were in scope when it was created, even after that scope is gone. That is a &lt;strong&gt;closure&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// 1&lt;/span&gt;
&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// 2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The inner function still has access to &lt;code&gt;count&lt;/code&gt;. Closures are how every JS pattern with private state works under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 4: Objects, arrays, and the friendly modern syntax
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mood&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;😊&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;happy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;calm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sunny&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;nested&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;weather&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clear&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// "happy"&lt;/span&gt;
&lt;span class="nx"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;label&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;      &lt;span class="c1"&gt;// same thing&lt;/span&gt;
&lt;span class="nx"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nested&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;weather&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// "clear", optional chaining&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;neutral&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// destructuring with default&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nested&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;weather&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;              &lt;span class="c1"&gt;// nested destructuring&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;moodCopy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;joyful&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;    &lt;span class="c1"&gt;// shallow copy with override&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;moods&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;okay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;moods&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                    &lt;span class="c1"&gt;// rest in arrays&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;others&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* others is an array */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The modern bits worth memorizing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;?.&lt;/code&gt;&lt;/strong&gt; is optional chaining. &lt;code&gt;a?.b&lt;/code&gt; returns &lt;code&gt;undefined&lt;/code&gt; if &lt;code&gt;a&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt; or &lt;code&gt;undefined&lt;/code&gt; instead of throwing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;??&lt;/code&gt;&lt;/strong&gt; is nullish coalescing. &lt;code&gt;value ?? "default"&lt;/code&gt; falls back only when &lt;code&gt;value&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt; or &lt;code&gt;undefined&lt;/code&gt; (unlike &lt;code&gt;||&lt;/code&gt;, which also falls back on &lt;code&gt;0&lt;/code&gt; and &lt;code&gt;""&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spread &lt;code&gt;...&lt;/code&gt;&lt;/strong&gt; copies things. &lt;strong&gt;Rest &lt;code&gt;...&lt;/code&gt;&lt;/strong&gt; collects things. Same syntax, opposite meaning, context tells you which.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision 5: Equality, and the eternal &lt;code&gt;===&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;There are two equality operators. Use one of them and forget the other exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// true. Loose equality coerces types. Confusing.&lt;/span&gt;
&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// false. Strict equality. The right answer.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always use &lt;code&gt;===&lt;/code&gt; and &lt;code&gt;!==&lt;/code&gt;. The only honourable exception is &lt;code&gt;value == null&lt;/code&gt;, which is a useful shortcut for "is null or undefined".&lt;/p&gt;

&lt;p&gt;Other equality landmines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kc"&gt;NaN&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;NaN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// false (yes, really)&lt;/span&gt;
&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;NaN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// true (the right way to check)&lt;/span&gt;
&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;NaN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;NaN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Decision 6: Prototypes, and the classes built on top
&lt;/h2&gt;

&lt;p&gt;JavaScript objects do not have classes underneath. They have a &lt;strong&gt;prototype chain&lt;/strong&gt;. Every object has a hidden link to another object. When you ask for a property, JS walks the chain until it finds it or hits the end.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;meow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;meow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mochi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;mochi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mochi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;mochi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;meow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// "meow", inherited&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;class&lt;/code&gt; syntax is just sugar over this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Mood&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Feeling &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Mood&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;calm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// "Feeling calm"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modern classes also support &lt;strong&gt;private fields&lt;/strong&gt; with &lt;code&gt;#&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;value&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use classes when you want a clean way to bundle state with methods. Use plain objects when you do not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 7: &lt;code&gt;this&lt;/code&gt;, the most confusing word
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;this&lt;/code&gt; does not mean what it means in most languages. It depends on &lt;strong&gt;how the function is called&lt;/strong&gt;, not where it was defined (except for arrow functions).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;             &lt;span class="c1"&gt;// "hi", this is obj&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;say&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                   &lt;span class="c1"&gt;// undefined or error in strict mode, this is not bound&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three ways to fix it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use an arrow function (inherits &lt;code&gt;this&lt;/code&gt; from outside).&lt;/li&gt;
&lt;li&gt;Bind explicitly: &lt;code&gt;obj.say.bind(obj)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Call with the right form: &lt;code&gt;obj.say()&lt;/code&gt; instead of pulling the function out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The single most useful rule of thumb: &lt;strong&gt;inside a method, only use &lt;code&gt;this&lt;/code&gt; directly. Inside a callback, use an arrow function or a closed over reference.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 8: Async, the three eras
&lt;/h2&gt;

&lt;p&gt;JavaScript runs on a single thread. To stay responsive, anything that takes time (network, disk, timers) is asynchronous. The way we wrote async code evolved through three eras.&lt;/p&gt;

&lt;h3&gt;
  
  
  Era 1: callbacks (legacy)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mood.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stack callbacks deep enough and you get "callback hell".&lt;/p&gt;

&lt;h3&gt;
  
  
  Era 2: Promises
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;Promise&lt;/strong&gt; is an object that represents a future value. It is in one of three states: pending, fulfilled, rejected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/moods&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;moods&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;moods&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;showError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can chain. You can &lt;code&gt;Promise.all([p1, p2])&lt;/code&gt; to run things in parallel. You can &lt;code&gt;Promise.race&lt;/code&gt;, &lt;code&gt;Promise.any&lt;/code&gt;, &lt;code&gt;Promise.allSettled&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Era 3: async / await (the modern default)
&lt;/h3&gt;

&lt;p&gt;Syntactic sugar over Promises, but reads top to bottom like normal code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadMoods&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/moods&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;moods&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;moods&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;code&gt;async&lt;/code&gt; function always returns a Promise, even if you &lt;code&gt;return 5&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;await&lt;/code&gt; pauses inside the async function until the Promise settles. It does not block other code.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;await&lt;/code&gt; two things in sequence runs them in sequence. Use &lt;code&gt;Promise.all&lt;/code&gt; to run them in parallel.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sequential, slow&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchA&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchB&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// parallel, fast&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;fetchA&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;fetchB&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Decision 9: The event loop, the actual brain
&lt;/h2&gt;

&lt;p&gt;Why does any of this work on a single thread? The &lt;strong&gt;event loop&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Roughly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;JS runs the code on the &lt;strong&gt;call stack&lt;/strong&gt; until it is empty.&lt;/li&gt;
&lt;li&gt;Async work (timers, network, I/O) is handed off to the host (browser or Node).&lt;/li&gt;
&lt;li&gt;When the host is done, it puts a callback into a &lt;strong&gt;task queue&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The event loop pulls from the queue when the stack is empty and runs it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microtasks&lt;/strong&gt; (Promise callbacks, &lt;code&gt;queueMicrotask&lt;/code&gt;) run before the next task. They get priority.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three consequences worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A blocking loop (&lt;code&gt;while (true) {}&lt;/code&gt;) freezes everything, including the page.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;setTimeout(fn, 0)&lt;/code&gt; does not run "immediately". It runs after the current task, possibly after microtasks.&lt;/li&gt;
&lt;li&gt;Awaiting a Promise that resolves with a Promise is fine. JS unwraps them for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision 10: Modules, the modern way to organize code
&lt;/h2&gt;

&lt;p&gt;Modern JavaScript uses &lt;strong&gt;ES Modules&lt;/strong&gt; (ESM). Every file is its own scope. You import what you need and export what you offer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// mood.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Feeling &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_MOOD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;neutral&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;MoodCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// app.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;MoodCard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_MOOD&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./mood.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One file, one module.&lt;/strong&gt; Imports are static, the bundler can tree shake unused exports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top level &lt;code&gt;await&lt;/code&gt; is allowed&lt;/strong&gt; in modules. Useful for config loading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic import&lt;/strong&gt; when you only sometimes need a module: &lt;code&gt;const m = await import("./heavy.js")&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The old format you will still see in legacy Node code is &lt;strong&gt;CommonJS&lt;/strong&gt;: &lt;code&gt;const x = require("x"); module.exports = ...&lt;/code&gt;. New code defaults to ESM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 11: Iterators, generators, and the for...of party
&lt;/h2&gt;

&lt;p&gt;Anything that follows the iterator protocol can be looped.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mood&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;moods&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* arrays, sets, maps, strings */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* objects */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* the keys, including prototype keys, prefer Object.keys */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generators (functions with &lt;code&gt;function*&lt;/code&gt;) let you yield values lazily, perfect for streams or infinite sequences.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* 0, 1, 2, 3, 4 */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most of the time you will not write generators. Most of the time you will use the iterator protocol indirectly through &lt;code&gt;for...of&lt;/code&gt;, spread, and array methods.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 12: Useful collections beyond array and object
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Map&lt;/code&gt;&lt;/strong&gt; for any kind of key (objects too), keeps insertion order, has &lt;code&gt;.size&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Set&lt;/code&gt;&lt;/strong&gt; for unique values. &lt;code&gt;[...new Set(arr)]&lt;/code&gt; deduplicates an array.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WeakMap&lt;/code&gt; / &lt;code&gt;WeakSet&lt;/code&gt;&lt;/strong&gt; for memory friendly key references that allow garbage collection.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;moods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the keys are not strings or you need predictable iteration order, &lt;code&gt;Map&lt;/code&gt; beats a plain object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 13: Errors, and how to handle them
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;riskyThing&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ENOENT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// rethrow if you cannot handle&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modern best practices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Throw &lt;code&gt;Error&lt;/code&gt; objects, not strings.&lt;/strong&gt; They carry a stack trace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subclass &lt;code&gt;Error&lt;/code&gt;&lt;/strong&gt; for app specific kinds: &lt;code&gt;class NotFoundError extends Error {}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;Error&lt;/code&gt; cause&lt;/strong&gt;: &lt;code&gt;new Error("could not load", { cause: original })&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In async code, &lt;code&gt;await&lt;/code&gt; inside &lt;code&gt;try&lt;/code&gt;&lt;/strong&gt;, otherwise the catch will not see the rejection.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision 14: A few modern goodies
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Optional chaining&lt;/strong&gt; (&lt;code&gt;a?.b&lt;/code&gt;) and &lt;strong&gt;nullish coalescing&lt;/strong&gt; (&lt;code&gt;a ?? b&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logical assignment&lt;/strong&gt; (&lt;code&gt;a ||= b&lt;/code&gt;, &lt;code&gt;a ??= b&lt;/code&gt;, &lt;code&gt;a &amp;amp;&amp;amp;= b&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;structuredClone(obj)&lt;/code&gt;&lt;/strong&gt; for a deep clone built into the platform. No more &lt;code&gt;JSON.parse(JSON.stringify(...))&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Object.groupBy(arr, fn)&lt;/code&gt;&lt;/strong&gt; for grouping arrays by a key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Array.prototype.toSorted&lt;/code&gt; / &lt;code&gt;toReversed&lt;/code&gt; / &lt;code&gt;toSpliced&lt;/code&gt; / &lt;code&gt;with&lt;/code&gt;&lt;/strong&gt; for non mutating versions of the classics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Promise.withResolvers()&lt;/code&gt;&lt;/strong&gt; for cleaner manual Promise creation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AbortController&lt;/code&gt;&lt;/strong&gt; for cancelling fetches and other async work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top level &lt;code&gt;await&lt;/code&gt;&lt;/strong&gt; in modules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;BigInt&lt;/code&gt;&lt;/strong&gt; for integers larger than &lt;code&gt;Number.MAX_SAFE_INTEGER&lt;/code&gt; (about 9 quadrillion).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision 15: Tooling that you should know exists
&lt;/h2&gt;

&lt;p&gt;In 2026, almost no real JavaScript project ships hand written, untranspiled, no bundler code. The modern stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt; runs JS on the server. Recent versions ship with a fetch, a test runner, and ESM as default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bun&lt;/strong&gt; and &lt;strong&gt;Deno&lt;/strong&gt; are alternative runtimes. Bun is fast and has a built in bundler. Deno has built in TypeScript and security permissions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vite&lt;/strong&gt; is the bundler of choice for frontend apps. &lt;strong&gt;esbuild&lt;/strong&gt; and &lt;strong&gt;SWC&lt;/strong&gt; power most build tools under the hood.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; adds a static type layer over JavaScript. Almost every team uses it for non trivial apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Biome&lt;/strong&gt; and &lt;strong&gt;ESLint&lt;/strong&gt; lint your code. &lt;strong&gt;Prettier&lt;/strong&gt; formats it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vitest&lt;/strong&gt; is the modern test runner. &lt;strong&gt;Playwright&lt;/strong&gt; does end to end tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not need to learn all of them on day one. Learn that they exist so you recognize the names.&lt;/p&gt;

&lt;h2&gt;
  
  
  A peek under the hood
&lt;/h2&gt;

&lt;p&gt;What really happens when a browser runs JavaScript:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The HTML parser hits a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag and hands the source to the JS engine (V8 in Chrome and Node, JavaScriptCore in Safari, SpiderMonkey in Firefox).&lt;/li&gt;
&lt;li&gt;The engine parses the source into bytecode.&lt;/li&gt;
&lt;li&gt;The bytecode runs in an interpreter while a JIT compiler watches for hot functions and optimizes them into machine code.&lt;/li&gt;
&lt;li&gt;The runtime gives the engine access to host APIs (DOM, fetch, timers in the browser; fs, net in Node).&lt;/li&gt;
&lt;li&gt;The event loop coordinates async work.&lt;/li&gt;
&lt;li&gt;Memory is managed by a garbage collector that periodically frees objects nothing references anymore.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two consequences for senior level work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Functions get faster the more they run&lt;/strong&gt; (warm up, then JIT). This is why microbenchmarks lie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Holding references to big objects in closures or long lived structures leaks memory.&lt;/strong&gt; &lt;code&gt;WeakMap&lt;/code&gt; and breaking references explicitly help.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tiny tips that will save you later
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Default to &lt;code&gt;const&lt;/code&gt;. Use &lt;code&gt;let&lt;/code&gt; only when you actually reassign. Never use &lt;code&gt;var&lt;/code&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;===&lt;/code&gt; always.&lt;/strong&gt; Two characters of safety for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat data as immutable&lt;/strong&gt; when you can. Spread, slice, &lt;code&gt;toSorted&lt;/code&gt;. Mutating shared objects is a footgun.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use arrow functions for callbacks&lt;/strong&gt;, regular methods for object methods.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use &lt;code&gt;async/await&lt;/code&gt;. Wrap parallel work in &lt;code&gt;Promise.all&lt;/code&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use ESM (&lt;code&gt;import&lt;/code&gt;/&lt;code&gt;export&lt;/code&gt;).&lt;/strong&gt; Avoid CJS in new code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;structuredClone&lt;/code&gt; for deep copies.&lt;/strong&gt; No more JSON tricks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;Map&lt;/code&gt; and &lt;code&gt;Set&lt;/code&gt;&lt;/strong&gt; when keys are not strings or uniqueness matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throw real &lt;code&gt;Error&lt;/code&gt; objects with &lt;code&gt;cause&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add TypeScript&lt;/strong&gt; as soon as the project has more than two files. Future you will be grateful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run &lt;code&gt;node --inspect&lt;/code&gt; or DevTools&lt;/strong&gt; when debugging. &lt;code&gt;console.log&lt;/code&gt; is not the only tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;So that is the whole story. We needed a small, friendly language for the browser, and we ended up with one that runs everywhere, with three decades of backwards compatible features piled on top of each other. We have variables (&lt;code&gt;const&lt;/code&gt;, &lt;code&gt;let&lt;/code&gt;), values (primitives and objects), functions as first class citizens, prototypes hidden under classes, asynchronous code that evolved from callbacks to async/await, and an event loop quietly orchestrating everything.&lt;/p&gt;

&lt;p&gt;We learned which corners are sharp (&lt;code&gt;==&lt;/code&gt;, &lt;code&gt;var&lt;/code&gt;, &lt;code&gt;this&lt;/code&gt;, NaN, mutation), which features are modern and beautiful (&lt;code&gt;?.&lt;/code&gt;, &lt;code&gt;??&lt;/code&gt;, modules, generators, structuredClone), and which tools sit around the language (Node, Bun, Vite, TypeScript) to make real apps shippable.&lt;/p&gt;

&lt;p&gt;Once that map is in your head, every blog post, every framework, every "weird JS quirk" tweet starts to feel familiar. JavaScript stops being a haunted attic and starts being the most flexible, most ubiquitous tool in your belt.&lt;/p&gt;

&lt;p&gt;Happy scripting, and may your &lt;code&gt;this&lt;/code&gt; always be what you expect.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>learning</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
