<?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: Janarthanan Soundararajan (Jana)</title>
    <description>The latest articles on DEV Community by Janarthanan Soundararajan (Jana) (@janarthanan_soundararajan).</description>
    <link>https://dev.to/janarthanan_soundararajan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4064706%2Fa88f1e6d-2e06-462a-ae72-7d011147ba2e.png</url>
      <title>DEV Community: Janarthanan Soundararajan (Jana)</title>
      <link>https://dev.to/janarthanan_soundararajan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/janarthanan_soundararajan"/>
    <language>en</language>
    <item>
      <title>Monorepo, Turborepo, Modular Monolith, and Microservices: What Actually Changes?</title>
      <dc:creator>Janarthanan Soundararajan (Jana)</dc:creator>
      <pubDate>Tue, 25 Aug 2026 18:08:36 +0000</pubDate>
      <link>https://dev.to/janarthanan_soundararajan/monorepo-turborepo-modular-monolith-and-microservices-what-actually-changes-29kl</link>
      <guid>https://dev.to/janarthanan_soundararajan/monorepo-turborepo-modular-monolith-and-microservices-what-actually-changes-29kl</guid>
      <description>&lt;p&gt;A Turborepo can contain multiple applications under &lt;code&gt;apps/&lt;/code&gt;. If those applications can be built and deployed separately, does that mean the system uses microservices?&lt;/p&gt;

&lt;p&gt;Not necessarily. Multiple applications in one repository tell us how the source code is organized. Microservices are defined by runtime and operational boundaries.&lt;/p&gt;

&lt;p&gt;Consider this small structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;platform/
├── apps/
│   ├── marketing/
│   └── app/
└── packages/
    └── db/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is this a monorepo, a modular monolith, or a microservices architecture?&lt;/p&gt;

&lt;p&gt;It is clearly a monorepo. It might contain a modular-monolith application. The tree alone, however, cannot prove that any microservices exist.&lt;/p&gt;

&lt;p&gt;While formalizing the architecture of SlotSyncro, a portfolio application, I wanted to sharpen how I distinguished these familiar concepts. They are often discussed together, even though each describes a different layer of a system. Placing them at their correct layers turns a broad understanding into a more precise architectural model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four concepts at four different layers
&lt;/h2&gt;

&lt;p&gt;Here is the central distinction:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;What it describes&lt;/th&gt;
&lt;th&gt;Primary question&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monorepo&lt;/td&gt;
&lt;td&gt;Repository organization&lt;/td&gt;
&lt;td&gt;Where does the code live?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turborepo&lt;/td&gt;
&lt;td&gt;Task orchestration&lt;/td&gt;
&lt;td&gt;How are builds, tests, and other tasks coordinated?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modular monolith&lt;/td&gt;
&lt;td&gt;Internal application design&lt;/td&gt;
&lt;td&gt;How is one deployable application divided?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microservices&lt;/td&gt;
&lt;td&gt;Runtime architecture&lt;/td&gt;
&lt;td&gt;How are services deployed, scaled, and connected?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Turbopack is related by name, but it solves another problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Repository organization → Monorepo
Task orchestration      → Turborepo
Application structure   → Modular monolith
Runtime architecture    → Microservices
Compilation             → Turbopack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turborepo coordinates tasks across workspaces. Turbopack bundles a Next.js application. These concepts do not occupy the same architectural layer, so they are not direct alternatives.&lt;/p&gt;

&lt;p&gt;That is why questions such as “Should I use a monorepo or microservices?” are difficult to answer as stated. A team can use both, either, or neither.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes SlotSyncro a monorepo?
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;monorepo&lt;/strong&gt; is one repository that contains multiple related projects. Those projects might be applications, shared libraries, configuration packages, or services. They share Git history and can be changed together, but they can still have separate dependencies, scripts, and release processes.&lt;/p&gt;

&lt;p&gt;SlotSyncro currently has this workspace structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;slotsyncro/
├── apps/
│   ├── marketing/    # Public marketing website
│   └── app/          # Scheduling product
├── packages/
│   └── db/           # Prisma and database infrastructure
├── package.json
├── pnpm-workspace.yaml
└── turbo.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The examples in this article are based on SlotSyncro, a scheduling product I am creating as a portfolio application. The repository is publicly viewable as a practical implementation reference: &lt;a href="https://github.com/TechAaroorian/slotsyncro" rel="noopener noreferrer"&gt;SlotSyncro&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is a monorepo because multiple identifiable projects live in one Git repository. The applications and database package have their own &lt;code&gt;package.json&lt;/code&gt; files, while pnpm workspaces connect them. A change to the product and its database package can be reviewed and committed together.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;workspace&lt;/strong&gt; is one project recognized and managed by the repository's package manager. In this example, &lt;code&gt;apps/app&lt;/code&gt;, &lt;code&gt;apps/marketing&lt;/code&gt;, and &lt;code&gt;packages/db&lt;/code&gt; are separate pnpm workspaces. Each can define its own package name, dependencies, and commands, while using another local workspace as a dependency without publishing it to a package registry.&lt;/p&gt;

&lt;p&gt;The monorepo label does not tell us how many production processes exist. It does not mean every folder is independently deployed, every application is a microservice, or every shared file needs its own package.&lt;/p&gt;

&lt;p&gt;Turborepo is not required either. A repository can be a monorepo using only pnpm, npm, or Yarn workspaces. Turborepo is an additional task runner, not the feature that makes it a monorepo.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Turborepo actually does
&lt;/h2&gt;

&lt;p&gt;In this repository, the tools sit roughly in this order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Git repository
      ↓
pnpm workspaces
      ↓
Turborepo
      ↓
Next.js, Prisma, Vitest, and other workspace tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The root commands delegate work across the repository. Typical Turborepo commands look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;turbo run build
turbo run &lt;span class="nb"&gt;test
&lt;/span&gt;turbo run lint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turborepo understands workspace dependencies, schedules tasks in the right order, caches successful outputs, avoids repeated work, and can filter execution to selected workspaces. For example, a task graph can ensure that database-client generation runs before dependent application builds.&lt;/p&gt;

&lt;p&gt;None of this provides service-to-service communication. Turborepo does not create API gateways, message queues, independent databases, distributed transactions, container orchestration, or production scaling rules.&lt;/p&gt;

&lt;p&gt;Turborepo knows that one workspace depends on another. It does not decide whether those workspaces become separate production services.&lt;/p&gt;

&lt;p&gt;In current Next.js versions, &lt;a href="https://nextjs.org/docs/app/getting-started/installation" rel="noopener noreferrer"&gt;Turbopack is the default bundler&lt;/a&gt; for development and production builds unless Webpack is explicitly selected. The similar names can make the tools sound interchangeable, but their scopes are different: Turborepo operates on the repository's task graph, while Turbopack operates on an application's module graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  A modular monolith inside a monorepo
&lt;/h2&gt;

&lt;p&gt;Now move one level inward, from the repository to &lt;code&gt;apps/app&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;modular monolith&lt;/strong&gt; is one deployable application organized into distinct business modules. The modules have defined responsibilities and controlled ways to interact, but they remain part of the same application boundary instead of becoming independently operated network services. It keeps the operational simplicity of a monolith while making internal ownership and dependencies clearer.&lt;/p&gt;

&lt;p&gt;SlotSyncro’s current product code is still mainly grouped by technical concerns such as routes, actions, components, and utilities. The accepted direction is to evolve it incrementally into business modules. A possible target shape is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/app/
├── app/                       # Next.js routes
└── modules/                   # Proposed structure
    ├── identity/
    ├── availability/
    ├── booking/
    ├── polls/
    ├── meetings/
    └── notifications/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These would be business modules, not microservices. They would have clear responsibilities and controlled interfaces, but remain part of the same Next.js application, deploy together, and use one PostgreSQL database.&lt;/p&gt;

&lt;p&gt;The runtime remains simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Monorepo
├── Marketing application
└── Scheduling application ───────── one deployment boundary
    ├── Identity module
    ├── Availability module
    ├── Booking module
    ├── Polls module
    └── Notifications module
                 ↓
             PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The boundaries are architectural, but they are not network boundaries. A booking operation can call availability logic as a typed function and update related state in one local database transaction.&lt;/p&gt;

&lt;p&gt;That is the useful middle ground of a modular monolith: explicit ownership without immediately accepting the operational cost of a distributed system. A monolith is not synonymous with unstructured legacy code. It can have deliberate, well-tested internal boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Folder names are not module boundaries
&lt;/h2&gt;

&lt;p&gt;A booking module might eventually use a small internal structure like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;modules/booking/
├── domain/
│   ├── booking.ts
│   └── booking-rules.ts
├── application/
│   ├── create-booking.ts
│   └── cancel-booking.ts
├── infrastructure/
│   └── booking-repository.ts
├── ui/
│   └── booking-form.tsx
└── index.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact folders matter less than the ownership they express. The module’s &lt;code&gt;index.ts&lt;/code&gt; can act as its public API:&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;createBooking&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;@/modules/booking&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;Code outside the module should avoid reaching through that API into implementation details:&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;// Avoid reaching into another module's internals.&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;bookingRepository&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;@/modules/booking/infrastructure/booking-repository&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;Folders help people see the intended boundary, but they do not enforce it. Public APIs, code review, dependency rules, and tests make the boundary real. Modular-monolith architecture depends more on enforced boundaries than on folder names.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why multiple applications still do not mean microservices
&lt;/h2&gt;

&lt;p&gt;Return to the original two applications:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/
├── marketing/
└── app/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;marketing&lt;/code&gt; presents public product information. &lt;code&gt;app&lt;/code&gt; owns authentication, availability, bookings, polls, and other scheduling workflows. They are different user-facing surfaces, and the repository can build them independently.&lt;/p&gt;

&lt;p&gt;That still says nothing about the product’s internal business capabilities being independently operated services. Its booking, polls, and notification concerns currently belong to the same product application. No internal network call is required merely because the repository contains two Next.js apps.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Multiple applications” is a repository observation. “Microservices” is a runtime conclusion.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Even independent deployment of the marketing and product sites would not make either one a microservice architecture. They may simply be two deployable web applications with different purposes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What would actually create microservices?
&lt;/h2&gt;

&lt;p&gt;Imagine the repository later changed to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;platform/
├── apps/
│   ├── marketing/
│   ├── web/
│   ├── scheduling-service/
│   ├── billing-service/
│   ├── notification-service/
│   └── background-worker/
└── packages/
    ├── contracts/
    ├── observability/
    └── config/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The names suggest microservices, but the folders still do not prove it. These components become services through meaningful operational independence: they run as separate processes, deploy and scale independently, communicate through APIs or messages, isolate at least some failures, and have intentionally defined business and data ownership.&lt;/p&gt;

&lt;p&gt;A simplified runtime might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────┐
│   Web application   │  Deployment A
└──────────┬──────────┘
           │ HTTPS
┌──────────▼──────────┐
│ Scheduling service │  Deployment B
└──────────┬──────────┘
           │ MeetingScheduled event
┌──────────▼──────────┐
│Notification service│  Deployment C
└─────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the boundaries are also network and deployment boundaries. That creates capabilities such as independent scaling and release ownership, but it also introduces timeouts, retries, message delivery, distributed tracing, versioned contracts, and consistency decisions.&lt;/p&gt;

&lt;p&gt;The trade-off is easier to see side by side:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Modular monolith&lt;/th&gt;
&lt;th&gt;Microservices&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Modules call one another in-process&lt;/td&gt;
&lt;td&gt;Services communicate over a network or queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usually deployed together&lt;/td&gt;
&lt;td&gt;Independently deployable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Easier cross-module transactions&lt;/td&gt;
&lt;td&gt;Distributed consistency must be handled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One primary runtime unit&lt;/td&gt;
&lt;td&gt;Multiple runtime units&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lower operational complexity&lt;/td&gt;
&lt;td&gt;Higher operational complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boundaries enforced in code&lt;/td&gt;
&lt;td&gt;Boundaries also enforced by the network&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Independent deployment is important, but it is not the whole definition. If several processes must always release together, share unclear data ownership, and cannot fail independently, the system may be a distributed monolith instead.&lt;/p&gt;

&lt;p&gt;Microservices are not the inevitable final stage of a successful modular monolith. For SlotSyncro’s current scale and requirements, local calls and transactions are valuable, while independent service operation has not earned its cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turborepo supports either runtime architecture
&lt;/h2&gt;

&lt;p&gt;Turborepo can coordinate a modular-monolith repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/marketing
apps/app
packages/db
packages/ui       # possible future extraction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It can also coordinate a repository containing services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/web
apps/scheduling-service
apps/notification-service
apps/worker
packages/contracts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In both cases, it can run the complete build or target a workspace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;turbo run build
turbo run build &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;scheduling-service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second repository may use microservices because of how those applications run, communicate, and deploy—not because Turborepo launched their build tasks. Turborepo can manage a repository containing microservices, but it does not turn applications into microservices.&lt;/p&gt;

&lt;p&gt;A monorepo can therefore contain a marketing application, a modular-monolith product, and several genuine services at the same time. Repository boundaries and runtime boundaries do not need to match one-to-one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep dependency direction clear
&lt;/h2&gt;

&lt;p&gt;One general monorepo rule prevents a great deal of confusion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Applications may import packages.
Packages must not import applications.
Good: apps/app → packages/db
Bad:  packages/db → apps/app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;packages/db&lt;/code&gt; imports application authentication code, the dependency becomes circular:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/app → packages/db → apps/app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package is no longer reusable infrastructure; it secretly depends on one consumer. Ownership becomes unclear, and isolated testing becomes harder.&lt;/p&gt;

&lt;p&gt;For example, database infrastructure should not retrieve application-specific session context:&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;// Bad: packages/db knows about an application's authentication.&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;getCurrentUser&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;../../apps/app/auth&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;Instead, the database package can expose infrastructure without importing from an application:&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;// packages/db&lt;/span&gt;
&lt;span class="k"&gt;export&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;./client&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 booking module can own its product-specific query while depending on that infrastructure:&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;// apps/app/modules/booking/infrastructure/booking-repository.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;@slotsyncro/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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;findBookings&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="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="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;booking&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;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;userId&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 application layer supplies the authentication context it owns:&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;// apps/app/modules/booking/application/list-my-bookings.ts&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="nf"&gt;getCurrentUser&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;bookings&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;findBookings&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This principle applies to monorepos generally. Turborepo can understand a dependency graph, but it cannot decide whether that graph represents sensible ownership.&lt;/p&gt;

&lt;h2&gt;
  
  
  When should a module become a package?
&lt;/h2&gt;

&lt;p&gt;Once developers see a &lt;code&gt;packages/&lt;/code&gt; directory, it is tempting to move every cleanly named capability into it. That usually creates boundaries before there is evidence for them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Application module&lt;/th&gt;
&lt;th&gt;Shared package&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Belongs to one application&lt;/td&gt;
&lt;td&gt;Lives outside a specific application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Represents a product capability&lt;/td&gt;
&lt;td&gt;Provides a reusable capability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May depend on application conventions&lt;/td&gt;
&lt;td&gt;Should expose a stable interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usually has one consumer&lt;/td&gt;
&lt;td&gt;Often has multiple consumers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploys with its application&lt;/td&gt;
&lt;td&gt;May be consumed by several applications&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For example, SlotSyncro should begin with booking behavior close to its only consumer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/app/modules/booking/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It should not create &lt;code&gt;packages/booking&lt;/code&gt; merely to make the repository tree look more sophisticated. Keeping code inside its only consumer is not poor architecture. It is often the clearest expression of ownership.&lt;/p&gt;

&lt;p&gt;Possible future extractions include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;packages/
├── domain/    # Framework-independent scheduling rules
├── email/     # Shared templates or rendering infrastructure
└── ui/        # Components genuinely shared by both applications
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are proposals, not current SlotSyncro workspaces. &lt;code&gt;packages/domain&lt;/code&gt; becomes useful if scheduling calculations need to run in the product, a worker, and isolated tests. &lt;code&gt;packages/email&lt;/code&gt; becomes useful after multiple notification workflows need common rendering. &lt;code&gt;packages/ui&lt;/code&gt; becomes especially useful when the marketing and product applications share a maintained design system, or when the component system has meaningful independent development and testing value.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical extraction test
&lt;/h2&gt;

&lt;p&gt;A package should have at least one strong justification, and preferably more than one.&lt;/p&gt;

&lt;h3&gt;
  
  
  More than one consumer
&lt;/h3&gt;

&lt;p&gt;Two applications or runtimes need the same capability. Reuse is real, not predicted from one similar-looking component.&lt;/p&gt;

&lt;h3&gt;
  
  
  Framework-independent domain value
&lt;/h3&gt;

&lt;p&gt;Scheduling calculations can run without React, Next.js, Prisma, or a provider SDK. That isolation makes the code easier to reuse and reason about.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stable inputs and outputs
&lt;/h3&gt;

&lt;p&gt;The capability exposes a small, intentional API instead of importing application internals. If its interface changes with every feature, extraction may be premature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Independent testing value
&lt;/h3&gt;

&lt;p&gt;Its behavior can be tested meaningfully without starting the complete application. This is especially useful for scheduling, timezone, or consensus rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Different build or deployment concerns
&lt;/h3&gt;

&lt;p&gt;Database generation, email rendering, shared contracts, or worker code may need distinct tasks. A workspace boundary can make those workflows explicit.&lt;/p&gt;

&lt;p&gt;Do not extract code merely because &lt;code&gt;packages/&lt;/code&gt; makes the repository look more sophisticated. Extraction should clarify ownership or enable a real workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  A decision framework
&lt;/h2&gt;

&lt;p&gt;When the terminology starts to blur, I return to these questions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do multiple related projects need to live together?
→ Consider a monorepo.
Do you need caching and coordinated workspace tasks?
→ Consider Turborepo.
Does one deployable application need stronger internal boundaries?
→ Use a modular monolith.
Does one capability require independent deployment, scaling,
failure isolation, data ownership, or team autonomy?
→ Consider extracting a service.
Is some code used by only one application?
→ Keep it close to that application for now.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sequence avoids using a runtime solution for a repository problem—or creating a network boundary when a code boundary is enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository note:&lt;/strong&gt; SlotSyncro is a public portfolio and reference repository, not an open-source project or starter template. Some modules and packages discussed in this article are architectural proposals and do not exist in the current implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The complete mental model
&lt;/h2&gt;

&lt;p&gt;A monorepo organizes where code lives.&lt;/p&gt;

&lt;p&gt;A modular monolith organizes boundaries inside one application.&lt;/p&gt;

&lt;p&gt;Microservices define independently operated runtime boundaries.&lt;/p&gt;

&lt;p&gt;Turborepo coordinates tasks across the repository.&lt;/p&gt;

&lt;p&gt;Turbopack compiles a Next.js application.&lt;/p&gt;

&lt;p&gt;The number of folders under &lt;code&gt;apps/&lt;/code&gt; does not determine the runtime architecture. Deployment, communication, scaling, and ownership boundaries do.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>architecture</category>
      <category>monorepo</category>
      <category>turborepo</category>
    </item>
    <item>
      <title>I thought Vite broke my 3D physics. It actually taught me how tree-shaking really works.</title>
      <dc:creator>Janarthanan Soundararajan (Jana)</dc:creator>
      <pubDate>Sun, 16 Aug 2026 09:05:54 +0000</pubDate>
      <link>https://dev.to/janarthanan_soundararajan/i-thought-vite-broke-my-3d-physics-it-actually-taught-me-how-tree-shaking-really-works-30oj</link>
      <guid>https://dev.to/janarthanan_soundararajan/i-thought-vite-broke-my-3d-physics-it-actually-taught-me-how-tree-shaking-really-works-30oj</guid>
      <description>&lt;p&gt;In modern browser-based game development, every kilobyte matters. Larger JavaScript payloads increase download, parse, compile, and execution times, which can delay interactivity—especially on slower devices or networks.&lt;/p&gt;

&lt;p&gt;To enforce a strict, lightweight architecture for our latest project at &lt;strong&gt;techaaroorian-games&lt;/strong&gt;, we utilized Babylon's ES module packages (&lt;code&gt;@babylonjs/core&lt;/code&gt;). For a modular application, this structure gives the bundler as much opportunity as possible to remove code the application doesn't actively use.&lt;/p&gt;

&lt;p&gt;We set up Vite, TypeScript, Babylon.js, and the Havok WebAssembly physics engine.&lt;/p&gt;

&lt;p&gt;Then, the engine crashed.&lt;br&gt;
&lt;em&gt;Error: `No Physics Engine available.&lt;/em&gt;`&lt;/p&gt;

&lt;p&gt;Here is the debugging story of why our physics vanished, and what it teaches us about modern web bundlers.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. The Simple Tree-Shaking Model
&lt;/h3&gt;

&lt;p&gt;To understand why my game crashed, I had to look at how modern bundlers view our code. Vite uses &lt;strong&gt;Rolldown&lt;/strong&gt; for its production builds—it doesn't read our minds; it reads the module graph.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Is the code reachable and needed?&lt;/strong&gt; → Keep it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can the bundler safely determine it's unnecessary?&lt;/strong&gt; → Remove it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But what happens when a module doesn't export an API you need to call, but instead contains &lt;em&gt;module-level registration code&lt;/em&gt; you need to run?&lt;/p&gt;
&lt;h3&gt;
  
  
  2. The Wasm Trap
&lt;/h3&gt;

&lt;p&gt;My first hurdle wasn't even tree-shaking; it was dependency optimization. My terminal completely froze when I tried to start the development server.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@babylonjs/havok&lt;/code&gt; ships JavaScript alongside WebAssembly assets, and in my setup, Vite's dependency optimization stalled while processing the package.&lt;/p&gt;

&lt;p&gt;I fixed it by telling Vite not to pre-bundle that dependency:&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;// vite.config.ts&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;optimizeDeps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="na"&gt;exclude&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;@babylonjs/havok&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;This lets Vite handle the package outside the dependency pre-bundling step.&lt;/p&gt;

&lt;p&gt;With the server finally running, I expected to see my 3D sphere bouncing with gravity. Instead, I got a blank canvas and the fatal error: &lt;strong&gt;&lt;code&gt;No Physics Engine available.&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The "Aha!" Moment: Vite Didn't Break My Physics
&lt;/h3&gt;

&lt;p&gt;Vite didn't break my physics. &lt;strong&gt;It exposed an architectural design I didn't know existed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To keep base payloads lightweight, highly optimized frameworks separate their pure implementation logic from their &lt;em&gt;runtime registration&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Because I only imported the core &lt;code&gt;Scene&lt;/code&gt; and &lt;code&gt;Mesh&lt;/code&gt; builders, I never directly referenced the physics module's exported API. My import graph didn't establish a need for that registration module to execute. Because its runtime registration wasn't reachable through the imports I had written, the production build could treat it as unnecessary.&lt;/p&gt;

&lt;p&gt;The physics code wasn't missing because it was broken. It was missing because its only purpose was to execute registration code, and I didn't tell my bundler that mattered!&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The Fix: Side-Effect Imports
&lt;/h3&gt;

&lt;p&gt;To fix this, I had to learn the difference between asking a bundler for an &lt;em&gt;API&lt;/em&gt; versus asking it for an &lt;em&gt;action&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Normal Import:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Mesh&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;@babylonjs/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Translation: "I need to use the Mesh API."&lt;/span&gt;
&lt;span class="c1"&gt;// Bundler trace: API is used, preserve the module.&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Side-Effect Import:&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="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@babylonjs/core/Physics/physicsEngineComponent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Translation: "I don't need an exported API from this module, but I do need its module-level registration code to execute."&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By adding that single side-effect import, the module executes immediately upon loading. It performs the registration necessary for Babylon's physics functionality to be available at runtime, while still allowing the bundler to aggressively tree-shake everything else.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Result
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F85gfxz8zz52x5uml8tqu.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F85gfxz8zz52x5uml8tqu.gif" alt="Marble in Canvas" width="799" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Big Lesson
&lt;/h3&gt;

&lt;p&gt;As I continue building out techaaroorian-games, this completely changed how I look at build tools.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static dependency&lt;/strong&gt; → Bundler can trace it → &lt;strong&gt;Keep what's reachable&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Module-level side effect&lt;/strong&gt; → Execution itself matters → &lt;strong&gt;Make that dependency explicit&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tree-shaking isn't magic. It's static analysis. And once I understood that, "unused code" became a much more interesting concept. The bundler isn't trying to understand what my game means; it is analyzing what my module graph tells it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(I am documenting my whole journey learning WebGPU and game dev. You can clone the full working engine setup we built today in the &lt;a href="https://github.com/techaaroorian-games/techaaroorian-games" rel="noopener noreferrer"&gt;techaaroorian-games GitHub repository&lt;/a&gt;!)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>vite</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>The DOM Is Lying to Your Types: 3 Subtle React + Zod Validation Traps</title>
      <dc:creator>Janarthanan Soundararajan (Jana)</dc:creator>
      <pubDate>Thu, 13 Aug 2026 06:38:47 +0000</pubDate>
      <link>https://dev.to/janarthanan_soundararajan/the-dom-is-lying-to-your-types-3-subtle-react-zod-validation-traps-2a0p</link>
      <guid>https://dev.to/janarthanan_soundararajan/the-dom-is-lying-to-your-types-3-subtle-react-zod-validation-traps-2a0p</guid>
      <description>&lt;p&gt;A common production stack for React forms is &lt;strong&gt;React Hook Form&lt;/strong&gt; for client-side state, &lt;strong&gt;Zod&lt;/strong&gt; for schema validation, and &lt;strong&gt;Next.js Server Actions&lt;/strong&gt; for the backend mutation.&lt;/p&gt;

&lt;p&gt;It feels incredibly robust. But beneath these excellent tools, a subtle friction point is waiting to corrupt your data: &lt;strong&gt;Native form values are transport-oriented, not domain-oriented.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Prefer a visual explanation? &lt;a href="https://youtu.be/eyE8JeGp2CI?si=s9s4yoNKM4BeuvC1" rel="noopener noreferrer"&gt;https://youtu.be/eyE8JeGp2CI?si=s9s4yoNKM4BeuvC1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Even when the browser exposes typed helpers like &lt;code&gt;input.valueAsNumber&lt;/code&gt; or &lt;code&gt;input.checked&lt;/code&gt;, the raw values crossing a form boundary rarely match your application's domain model directly. React Hook Form can help normalize some primitives at registration (e.g., &lt;code&gt;{ valueAsNumber: true }&lt;/code&gt;), but this doesn't eliminate the need for boundary validation: &lt;code&gt;NaN&lt;/code&gt;, empty values, optional semantics, and unexpected server-side inputs still require explicit handling.&lt;/p&gt;

&lt;p&gt;When engineers rush to bridge the gap between transport data and strict TypeScript domain logic, it’s incredibly easy to implement "quick fixes" (like Zod's &lt;code&gt;z.coerce&lt;/code&gt;) that make the TypeScript compiler happy but introduce silent data corruption.&lt;/p&gt;

&lt;p&gt;To build robust systems, we need to internalize a core architectural principle:&lt;br&gt;
&lt;strong&gt;Validation ≠ Type Conversion ≠ Normalization.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Look at the modern form pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DOM
 ↓
React Hook Form / FormData (Transport Representation)
 ↓
Normalization (Where "30" becomes 30)
 ↓
Validation (Does 30 meet our business rules?)
 ↓
Domain Value (Strictly Typed)
 ↓
Persistence (Database)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whether you pull data from React Hook Form state or native &lt;code&gt;FormData&lt;/code&gt;, your transport representation is limited. It contains strings and &lt;code&gt;File&lt;/code&gt; objects, while missing fields are represented by the absolute absence of an entry.&lt;/p&gt;

&lt;p&gt;As engineers, our golden rule at the normalization boundary should be: &lt;strong&gt;Don't ask "How do I make Zod accept this?" Ask "What does this external value actually mean in my domain?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here are three subtle data-boundary traps in the React/Zod ecosystem that happen when those responsibilities blur, and how to design reliable pipelines to prevent them.&lt;/p&gt;




&lt;h3&gt;
  
  
  Trap 1: The Number Coercion "Zero-Bypass" (Semantic Loss)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Scenario:&lt;/strong&gt; You are building a scheduling app and have an optional "Buffer Time" input (&lt;code&gt;&amp;lt;input type="number" name="buffer" /&amp;gt;&lt;/code&gt;). Because the DOM sends a string, &lt;code&gt;z.number().optional()&lt;/code&gt; fails. To fix it, you reach for Zod's coercion API.&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;// ❌ The Trap&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&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;bufferTime&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="nx"&gt;coerce&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;optional&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;strong&gt;The Actual Problem:&lt;/strong&gt; This isn't just a type error; it's semantic data loss.&lt;br&gt;
In JavaScript, &lt;code&gt;Number("")&lt;/code&gt; evaluates to &lt;code&gt;0&lt;/code&gt;. If a user leaves the input blank because they &lt;em&gt;don't want a buffer&lt;/em&gt;, Zod intercepts the empty string, coerces it to &lt;code&gt;0&lt;/code&gt;, and validates it. Because &lt;code&gt;0&lt;/code&gt; is a valid number, the &lt;code&gt;.optional()&lt;/code&gt; check is bypassed entirely.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;User Intent&lt;/th&gt;
&lt;th&gt;DOM Input&lt;/th&gt;
&lt;th&gt;Naive Coercion&lt;/th&gt;
&lt;th&gt;Real Problem&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No buffer configured&lt;/td&gt;
&lt;td&gt;&lt;code&gt;""&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Semantic Loss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit 0 minutes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"0"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You just erased the semantic distinction between "not provided" (&lt;code&gt;undefined&lt;/code&gt;) and "explicitly provided as zero minutes" (&lt;code&gt;0&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt;&lt;br&gt;
Define explicit normalization semantics using &lt;code&gt;z.preprocess()&lt;/code&gt; before Zod attempts to validate. Crucially, if the user submits garbage data (like &lt;code&gt;"banana"&lt;/code&gt;), we must not silently swallow it—we pass it through so &lt;code&gt;z.number()&lt;/code&gt; can properly reject 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="c1"&gt;// ✅ Explicit Normalization&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;optionalNumberSchema&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;preprocess&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;val&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;// 1. Preserve the semantic meaning of "empty"&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;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;val&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="nx"&gt;val&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Safely attempt conversion&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;val&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&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;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&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;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="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Pass bad data through&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;val&lt;/span&gt;&lt;span class="p"&gt;;&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;min&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;optional&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; 

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(Note: Normalization answers "What value is this?" by intentionally accepting JavaScript's flexible numeric string syntax, like &lt;code&gt;"+30"&lt;/code&gt;. If your domain requires stricter constraints—for example, whole integers only—you enforce that as a **Validation&lt;/em&gt;* rule: &lt;code&gt;z.number().int().min(0)&lt;/code&gt;).*&lt;/p&gt;




&lt;h3&gt;
  
  
  Trap 2: The Boolean Checkbox Nightmare
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Scenario:&lt;/strong&gt; You have a checkbox for an event setting (&lt;code&gt;&amp;lt;input type="checkbox" name="isPrivate" /&amp;gt;&lt;/code&gt;). You grab the value from React Hook Form or &lt;code&gt;FormData&lt;/code&gt; and pass it to Zod.&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;// ❌ The Trap&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&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;isPrivate&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="nx"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&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;strong&gt;The Actual Problem:&lt;/strong&gt; Native HTML checkboxes do not submit &lt;code&gt;false&lt;/code&gt; when unchecked; the field is omitted entirely. Calling &lt;code&gt;formData.get("isPrivate")&lt;/code&gt; therefore returns &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Furthermore, if you are passing JSON payloads from a client UI, passing the string &lt;code&gt;"false"&lt;/code&gt; through &lt;code&gt;Boolean("false")&lt;/code&gt; actually evaluates to &lt;code&gt;true&lt;/code&gt; (because it's a non-empty string). Zod's &lt;code&gt;z.coerce.boolean()&lt;/code&gt; is blind to your domain intent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt;&lt;br&gt;
Don't use arbitrary form strings for boolean coercion. Normalize the transport representation explicitly, and—just like with numbers—don't silently swallow invalid data.&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;// ✅ Explicit Normalization&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkboxSchema&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;isPrivate&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;preprocess&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;val&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;// Recognize explicit truthy transport values&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;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;on&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;val&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="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;// Recognize explicit falsy transport values&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;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;false&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;val&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="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="c1"&gt;// Let unexpected values pass through so z.boolean() can catch and reject them!&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;;&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;boolean&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;This guarantees that expected inputs map cleanly to &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;, but if a bad actor or a bug sends &lt;code&gt;"banana"&lt;/code&gt;, it reaches &lt;code&gt;z.boolean()&lt;/code&gt; and gets properly rejected. &lt;strong&gt;Unknown ≠ false.&lt;/strong&gt; Normalization should not silently destroy information.&lt;/p&gt;




&lt;h3&gt;
  
  
  Trap 3: The Timezone Date Parse (Ambiguous Instants)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Scenario:&lt;/strong&gt; A user selects a date and time for an event using &lt;code&gt;&amp;lt;input type="datetime-local" /&amp;gt;&lt;/code&gt;. It outputs a string like &lt;code&gt;"2026-08-15T09:00"&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;// ❌ The Trap&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&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;startTime&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="nx"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&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;strong&gt;The Actual Problem:&lt;/strong&gt; A JavaScript &lt;code&gt;Date&lt;/code&gt; object represents an &lt;em&gt;absolute instant&lt;/em&gt; in time (milliseconds since the epoch). But a user's selection of "August 15, 09:00" is a &lt;em&gt;local wall-clock time&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If a user in New York submits &lt;code&gt;"2026-08-15T09:00"&lt;/code&gt; and the server interprets the value using its own runtime timezone, the application has created an absolute instant without knowing which timezone the user intended. You've created an ambiguous point in time.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Note: This is one of the exact problems the JavaScript &lt;code&gt;Temporal&lt;/code&gt; proposal is designed to model explicitly: separating a &lt;code&gt;PlainDateTime&lt;/code&gt; from a &lt;code&gt;ZonedDateTime&lt;/code&gt;.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt;&lt;br&gt;
Never parse localized date strings natively in your schema without explicitly capturing and applying the timezone context.&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;// ✅ The Fix&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&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="c1"&gt;// Accept the raw wall-clock time string (e.g., "2026-08-15T09:00")&lt;/span&gt;
  &lt;span class="na"&gt;localTime&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="nf"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\d{4}&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;T&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="c1"&gt;// Accept the user's explicit timezone (e.g., "America/New_York")&lt;/span&gt;
  &lt;span class="na"&gt;timeZone&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="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;transform&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="c1"&gt;// Explicitly combine the wall-clock time and timezone &lt;/span&gt;
  &lt;span class="c1"&gt;// into an absolute UTC instant before saving to the database&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;absoluteUtcInstant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;convertToUtcInstant&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="nx"&gt;localTime&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="nx"&gt;timeZone&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;
  
  
  The Takeaway
&lt;/h3&gt;

&lt;p&gt;Validation isn't just about rejecting invalid input. At application boundaries, it's also where we define how external, untyped representations officially become typed domain values.&lt;/p&gt;

&lt;p&gt;By explicitly separating normalization from validation—and paying attention to semantic zero-bypasses, truthy strings, and wall-clock ambiguity—you can build resilient systems that prevent data corruption before it ever reaches your database.&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why Vite Asked Me to Choose Between Oxlint and ESLint</title>
      <dc:creator>Janarthanan Soundararajan (Jana)</dc:creator>
      <pubDate>Sat, 08 Aug 2026 10:39:44 +0000</pubDate>
      <link>https://dev.to/janarthanan_soundararajan/why-vite-asked-me-to-choose-between-oxlint-and-eslint-3ihl</link>
      <guid>https://dev.to/janarthanan_soundararajan/why-vite-asked-me-to-choose-between-oxlint-and-eslint-3ihl</guid>
      <description>&lt;p&gt;While initializing a new Vite project (&lt;code&gt;npx create-vite&lt;/code&gt;), I hit an interactive CLI prompt that paused my standard workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;◆ Which linter to use?
  ● Oxlint
  ○ ESLint

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Having relied on ESLint as my default linter for years, this piqued my curiosity. The filled bullet (&lt;code&gt;●&lt;/code&gt;) wasn't a formal endorsement from Vite—it was simply where the CLI cursor landed—but it made me pause. &lt;strong&gt;Why is Vite offering Oxlint alongside ESLint, and when does it make sense to use it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here are my first impressions after looking into what Oxlint is, how it works under the hood, and how I plan to test it out.&lt;/p&gt;




&lt;h2&gt;
  
  
  ❓ Why Is Vite Offering Oxlint?
&lt;/h2&gt;

&lt;p&gt;Vite doesn't recommend Oxlint over ESLint—it simply presents both as first-class options during project creation. Given the growing adoption of native tooling such as SWC, Biome, and Rolldown, adding Oxlint feels consistent with the broader direction of the frontend ecosystem, while still leaving the choice to developers.&lt;/p&gt;

&lt;p&gt;Rather than forcing one choice, Vite simply lets developers decide which workflow best fits their needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 What is Oxlint?
&lt;/h2&gt;

&lt;p&gt;From my preliminary research, &lt;strong&gt;Oxlint&lt;/strong&gt; is a linter developed as part of the &lt;strong&gt;Oxc&lt;/strong&gt; (Oxidized Compiler) project.&lt;/p&gt;

&lt;p&gt;The core difference seems to be the engine: while ESLint runs on Node.js/JavaScript, Oxlint is written in &lt;strong&gt;Rust&lt;/strong&gt;. Because Oxlint is a native executable rather than a JavaScript package running inside Node.js, it can reduce startup overhead and take advantage of parallel execution on modern CPUs.&lt;/p&gt;

&lt;p&gt;Rather than trying to replace every custom rule or plugin in the JavaScript ecosystem on day one, Oxlint's current focus appears to be catching &lt;strong&gt;common correctness bugs, React hook pitfalls, and type safety issues&lt;/strong&gt; with minimal setup overhead.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚔️ How They Seem to Compare
&lt;/h2&gt;

&lt;p&gt;Here is a high-level snapshot based on the documentation and early community feedback I've gathered so far:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric / Feature&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;ESLint&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Oxlint&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runtime Engine&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Node.js / JavaScript&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Rust (Oxidized)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Execution Speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Varies by project size, plugins, and custom rules&lt;/td&gt;
&lt;td&gt;Official Oxlint benchmarks report significantly faster execution than ESLint, and early community feedback has generally reflected similar experiences, though results depend on the project.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup &amp;amp; Config&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires &lt;code&gt;eslint.config.js&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Designed around sensible zero-config defaults&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin Ecosystem&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Very mature and extensive&lt;/td&gt;
&lt;td&gt;Smaller plugin ecosystem, with built-in support for many common React, TypeScript, JSX, and Jest rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Primary Value&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Deep customization &amp;amp; strict team policies&lt;/td&gt;
&lt;td&gt;Fast local feedback and low initial setup friction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  ⚡ Why the Speed Pitch Caught My Eye
&lt;/h2&gt;

&lt;p&gt;What made me want to investigate Oxlint further is the promise of shorter developer feedback loops.&lt;/p&gt;

&lt;p&gt;In past projects, running pre-commit hooks (&lt;code&gt;husky&lt;/code&gt; / &lt;code&gt;lint-staged&lt;/code&gt;) or saving files during local hot reloading (HMR) sometimes created a small delay while validating code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Typical local feedback loop:&lt;/span&gt;
&lt;span class="c"&gt;# 1. Save or stage a file.&lt;/span&gt;
&lt;span class="c"&gt;# 2. Run the linting process.&lt;/span&gt;
&lt;span class="c"&gt;# 3. Wait for rule validation before moving on.&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Rust-based linters can make that validation step feel nearly instant during active coding, that could be a subtle but nice improvement in daily flow. I'm curious to see if that holds true as my project grows from a fresh scaffold into a complete application.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ The Hybrid Idea I'm Curious to Try
&lt;/h2&gt;

&lt;p&gt;As I read through discussions on how developers are approaching this, one approach I came across repeatedly was &lt;strong&gt;a hybrid setup&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For local dev &amp;amp; pre-commit hooks:&lt;/strong&gt; Use &lt;strong&gt;Oxlint&lt;/strong&gt; as a lightweight frontline defender to catch common mistakes instantly without slowing down commits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For CI/CD pipelines:&lt;/strong&gt; Keep &lt;strong&gt;ESLint&lt;/strong&gt; running in GitHub Actions if complex, custom ESLint plugins or team-wide policies are required later on.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;package.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Conceptual&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;hybrid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;setup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;I&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;may&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;experiment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&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;"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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Frontline&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;check&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;HMR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;pre-commit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;hooks&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint:fast"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"oxlint --deny-warnings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Deep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;structural&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;audit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;CI/CD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;pipelines&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint:ci"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eslint ."&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;h2&gt;
  
  
  🧪 Next Steps &amp;amp; What I'm Testing Next
&lt;/h2&gt;

&lt;p&gt;I decided to select &lt;strong&gt;Oxlint&lt;/strong&gt; for this new Vite build as an initial experiment. Since this is a brand-new project, it felt like a good opportunity to evaluate a newer tool without worrying about migrating an existing lint configuration.&lt;/p&gt;

&lt;p&gt;Right now, the codebase is in its early stages, so getting started required virtually no configuration. As I build out components, integrate state management, and add visualizations (like D3 charts), I'm planning to observe:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How well Oxlint catches real React/TypeScript errors during active development.&lt;/li&gt;
&lt;li&gt;Whether I run into any rule limitations that force me to bring ESLint back into the loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I'll be keeping notes as the project develops and might share a follow-up post with actual hands-on impressions once the site is fully built!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; If my experience changes after building a larger React application, I'll publish a follow-up comparing Oxlint and ESLint with real-world usage rather than first impressions.&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>oxlint</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Fixing Vitest CI Worker Crashes by Switching from jsdom to happy-dom</title>
      <dc:creator>Janarthanan Soundararajan (Jana)</dc:creator>
      <pubDate>Fri, 07 Aug 2026 10:43:45 +0000</pubDate>
      <link>https://dev.to/janarthanan_soundararajan/fixing-vitest-ci-worker-crashes-by-switching-from-jsdom-to-happy-dom-1h95</link>
      <guid>https://dev.to/janarthanan_soundararajan/fixing-vitest-ci-worker-crashes-by-switching-from-jsdom-to-happy-dom-1h95</guid>
      <description>&lt;p&gt;Our Vitest suite passed every time locally, but GitHub Actions failed before executing a single test. Instead of failed assertions, every test worker crashed during startup, leaving us with 0% coverage and a broken CI pipeline.&lt;/p&gt;

&lt;p&gt;After narrowing the issue down to our test environment, we replaced &lt;code&gt;jsdom&lt;/code&gt; with &lt;code&gt;happy-dom&lt;/code&gt;. That change resolved the worker crashes in our environment and restored a stable GitHub Actions pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Environment Context
&lt;/h2&gt;

&lt;p&gt;To help determine if this applies to your setup, here is the stack where we encountered this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Framework:&lt;/strong&gt; Next.js (App Router)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing:&lt;/strong&gt; Vitest, &lt;code&gt;@testing-library/react&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Package Manager:&lt;/strong&gt; &lt;code&gt;pnpm&lt;/code&gt; (Workspace / Monorepo)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI Environment:&lt;/strong&gt; GitHub Actions (&lt;code&gt;ubuntu-latest&lt;/code&gt;, Node.js 20)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. The Error: Unhandled Worker Failures in CI
&lt;/h2&gt;

&lt;p&gt;During the &lt;code&gt;pnpm --filter app test:coverage&lt;/code&gt; step in GitHub Actions, Vitest failed immediately upon initializing the worker pool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Vitest caught 7 unhandled errors during the test run.

⎯⎯⎯⎯⎯⎯ Unhandled Error ⎯⎯⎯⎯⎯⎯⎯
Error: [vitest-pool]: Failed to start forks worker for test files /apps/app/__tests__/poll.test.ts.

Caused by: TypeError: webidl.util.markAsUncloneable is not a function
 ❯ new CacheStorage ../node_modules/jsdom/node_modules/undici/lib/web/cache/cachestorage.js:20:17
 ❯ Object.&amp;lt;anonymous&amp;gt; ../node_modules/jsdom/node_modules/undici/index.js:179:25
 ❯ Object.&amp;lt;anonymous&amp;gt; ../node_modules/jsdom/lib/api.js:12:33

 Test Files  no tests
      Tests  no tests
     Errors  7 errors

ERROR: Coverage for lines (0%) does not meet global threshold (80%)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you encounter errors such as &lt;code&gt;webidl.util.markAsUncloneable is not a function&lt;/code&gt; or &lt;code&gt;Failed to start forks worker&lt;/code&gt; in a &lt;code&gt;vitest-pool&lt;/code&gt; setup, the problem may lie in your test environment's dependency tree rather than in your test code.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Investigation
&lt;/h2&gt;

&lt;p&gt;At first, we assumed one of the test files was failing or throwing an uncaught exception.&lt;/p&gt;

&lt;p&gt;But the stack trace told a different story. None of the 7 test files had actually started executing. Every failure occurred while Vitest was initializing worker processes, which meant the problem was happening before our application code even loaded.&lt;/p&gt;

&lt;p&gt;The stack trace pointed directly into &lt;code&gt;jsdom&lt;/code&gt;'s internal &lt;code&gt;undici&lt;/code&gt; dependency during worker initialization (&lt;code&gt;jsdom/node_modules/undici/lib/web/cache/cachestorage.js&lt;/code&gt;). Rather than a bug in our application code, this suggested an environment-level compatibility issue involving the test environment rather than our application code.&lt;/p&gt;

&lt;p&gt;Beyond initial loading issues, &lt;code&gt;jsdom&lt;/code&gt; emulates a large portion of standard browser behavior and maintains a detailed DOM object model. In resource-constrained CI environments (like standard Linux runners in GitHub Actions), this extra memory footprint can increase resource usage and startup time, especially during coverage runs.&lt;/p&gt;

&lt;p&gt;We could have spent more time investigating the exact dependency mismatch, but since our tests only required standard DOM APIs, switching to a lighter implementation was the simpler and lower-risk solution.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The Switch: Moving to &lt;code&gt;happy-dom&lt;/code&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why &lt;code&gt;happy-dom&lt;/code&gt; Worked
&lt;/h3&gt;

&lt;p&gt;Unlike &lt;code&gt;jsdom&lt;/code&gt;, &lt;code&gt;happy-dom&lt;/code&gt; focuses specifically on implementing the browser APIs commonly needed by modern frontend unit tests (such as React synthetic events and DOM queries) rather than reproducing every full browser behavior. Because of its smaller scope, &lt;code&gt;happy-dom&lt;/code&gt; generally starts faster and has a lower resource footprint than &lt;code&gt;jsdom&lt;/code&gt; for many component-testing scenarios, making it a good fit for React component testing in Vitest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Replace Dependencies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nt"&gt;--filter&lt;/span&gt; app remove jsdom
pnpm &lt;span class="nt"&gt;--filter&lt;/span&gt; app add &lt;span class="nt"&gt;-D&lt;/span&gt; happy-dom @vitest/coverage-v8

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Update Vitest Configuration
&lt;/h3&gt;

&lt;p&gt;Update &lt;code&gt;vitest.config.ts&lt;/code&gt; to use &lt;code&gt;happy-dom&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;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="s1"&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="s1"&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;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&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;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;alias&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="s1"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src&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;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="s1"&gt;happy-dom&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="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="s1"&gt;./vitest.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;coverage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reporter&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="s1"&gt;text&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="s1"&gt;json&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="s1"&gt;html&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="s1"&gt;json-summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;exclude&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="s1"&gt;node_modules/&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="s1"&gt;.next/&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="s1"&gt;vitest.config.ts&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="s1"&gt;vitest.setup.ts&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="s1"&gt;**/*.d.ts&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Recommended Test Setup
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;vitest.setup.ts&lt;/code&gt;, ensure component trees are explicitly unmounted after each test block:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cleanup&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="s1"&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;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="s1"&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;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="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;&lt;em&gt;Note: This cleanup setup was not required to fix the worker initialization issue, but we keep it to ensure each test runs with a clean DOM and to avoid state leaking between tests.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Results
&lt;/h2&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;Before (&lt;code&gt;jsdom&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;After (&lt;code&gt;happy-dom&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Worker Initialization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Failed across all 7 test files&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;All workers started successfully&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code Coverage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 0% (Threshold failure)&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Coverage generated successfully&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pipeline Status&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ CI failed consistently&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;CI passed consistently&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Execution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Failed during worker initialization&lt;/td&gt;
&lt;td&gt;⚡ &lt;strong&gt;Entire suite finished in ~15 seconds&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;While we didn't identify the exact package combination that triggered the incompatibility, replacing &lt;code&gt;jsdom&lt;/code&gt; removed the issue entirely for our CI environment, making further investigation unnecessary for our use case.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. When Should You Keep &lt;code&gt;jsdom&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;Every tool comes with trade-offs, and &lt;code&gt;happy-dom&lt;/code&gt; isn't a 1:1 replacement for every project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep &lt;code&gt;jsdom&lt;/code&gt; if:&lt;/strong&gt; Your tests depend on advanced browser APIs that &lt;code&gt;happy-dom&lt;/code&gt; doesn't fully implement, or you are testing behavior that closely mirrors browser layout and parsing internals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;happy-dom&lt;/code&gt; if:&lt;/strong&gt; You are primarily running React, Vue, or Svelte component tests with &lt;code&gt;@testing-library&lt;/code&gt;, where rendering components and asserting on DOM output is all you need.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6. Lessons Learned &amp;amp; Takeaways
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Takeaway:&lt;/strong&gt; Before changing test code, check whether the failure occurs before the first test executes. If worker initialization fails, the root cause is often the test environment, dependency graph, or runtime—not your application logic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The lesson wasn't that &lt;code&gt;happy-dom&lt;/code&gt; is universally better than &lt;code&gt;jsdom&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It was that your test environment should match your testing needs. For our React component tests, a lightweight DOM implementation was enough—and choosing the simplest test environment that satisfied our requirements made the CI pipeline both faster and more reliable.&lt;/p&gt;

&lt;p&gt;Sometimes the quickest path to a stable CI pipeline isn't finding the perfect root cause—it's choosing the simplest tool that satisfies your testing requirements.&lt;/p&gt;

</description>
      <category>react</category>
      <category>nextjs</category>
      <category>cicd</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Should You Unit Test Glue Files? A Practical Decision Matrix</title>
      <dc:creator>Janarthanan Soundararajan (Jana)</dc:creator>
      <pubDate>Thu, 06 Aug 2026 05:57:22 +0000</pubDate>
      <link>https://dev.to/janarthanan_soundararajan/should-you-unit-test-glue-files-a-practical-decision-matrix-1k0b</link>
      <guid>https://dev.to/janarthanan_soundararajan/should-you-unit-test-glue-files-a-practical-decision-matrix-1k0b</guid>
      <description>&lt;p&gt;Have you ever spent an entire afternoon writing a unit test for a five-line setup component?&lt;/p&gt;

&lt;p&gt;You mock three context providers, an &lt;code&gt;Auth.js&lt;/code&gt; session hook, and a router adapter—only to assert that a button renders in the DOM. At that point, it’s worth asking:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are we testing our application, or just testing our mocks?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In React applications, not every file benefits equally from isolated unit tests—but figuring out which ones those are isn't always obvious. Over time, I’ve moved away from chasing high coverage numbers for its own sake toward a more pragmatic framework for balancing unit tests (with &lt;strong&gt;Vitest&lt;/strong&gt; or &lt;strong&gt;Jest&lt;/strong&gt;) and End-to-End integration tests (with &lt;strong&gt;Playwright&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;Here is how I think about testing wiring code without grinding developer velocity to a halt.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Are "Glue Files"?
&lt;/h2&gt;

&lt;p&gt;"Glue files" isn't an official software engineering term. It's simply the name I use for files whose primary responsibility is connecting parts of an application rather than implementing business logic.&lt;/p&gt;

&lt;p&gt;Most React developers will recognize these files instantly in their codebases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Entry &amp;amp; Wiring Files:&lt;/strong&gt; &lt;code&gt;main.tsx&lt;/code&gt;, &lt;code&gt;App.tsx&lt;/code&gt;, &lt;code&gt;providers.tsx&lt;/code&gt;, &lt;code&gt;layout.tsx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routing &amp;amp; Client Config:&lt;/strong&gt; &lt;code&gt;router.tsx&lt;/code&gt;, &lt;code&gt;queryClient.ts&lt;/code&gt;, &lt;code&gt;middleware.ts&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store Initialization:&lt;/strong&gt; &lt;code&gt;store.ts&lt;/code&gt; (when primarily configuring Redux/Zustand rather than implementing custom middleware or complex state logic)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth &amp;amp; Localization Wrappers:&lt;/strong&gt; &lt;code&gt;&amp;lt;SignInButton/&amp;gt;&lt;/code&gt; wrappers around &lt;code&gt;Auth.js&lt;/code&gt; or Firebase, and language switchers wrapping &lt;code&gt;next-intl&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI Primitives:&lt;/strong&gt; Base design system elements (&lt;code&gt;components/ui/*&lt;/code&gt; built over Radix UI or Tailwind primitives)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These files act as the "mortar" holding your external dependencies, global state trees, and visual layout together.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hidden Cost of Unit Testing Pure Glue Files
&lt;/h2&gt;

&lt;p&gt;Writing unit tests for pure domain functions is fast and rewarding. Writing unit tests for glue files often leads to three common headaches:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. 90% Mock Setup, 10% Actual Assertion
&lt;/h3&gt;

&lt;p&gt;To unit test a file that simply instantiates a client or wires up a third-party hook, you often end up writing dozens of lines of mock setup code just to test a few lines of JSX:&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;// tests/AuthButton.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="s1"&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;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="s1"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Mocking external library hooks and routers&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="s1"&gt;next-auth/react&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;useSession&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;data&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="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="s1"&gt;unauthenticated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;signIn&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="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="s1"&gt;next/navigation&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;useRouter&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;push&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="p"&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="s1"&gt;renders sign-in 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="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;SignInButton&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;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sign In&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="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When 90% of a test file consists of &lt;code&gt;vi.mock()&lt;/code&gt;, you aren't really testing application behavior—you are mostly testing your ability to write mocks.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. High Fragility with Low Return
&lt;/h3&gt;

&lt;p&gt;Because these tests are tightly bound to the internal signatures of third-party libraries, updating a dependency often breaks your unit tests even if the actual user-facing feature works perfectly fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Testing Framework Behavior Instead of Your Logic
&lt;/h3&gt;

&lt;p&gt;Maintainers of packages like TanStack Query, Radix UI, or React Router already maintain extensive unit test suites. Writing unit tests to verify that &lt;code&gt;&amp;lt;QueryClientProvider client="{queryClient}"&amp;gt;&lt;/code&gt; passes down context means you're often verifying framework behavior rather than your own application's logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Decision Matrix: &lt;strong&gt;Does This File Make Decisions?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;In my experience, asking this single question eliminates most debates about whether a file deserves unit tests: &lt;strong&gt;Does this file make decisions?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────┐
│    Does this file make decisions?   │
└─────────────────────────────────────┘
                 │
        ┌────────┴────────┐
        │                 │
       Yes               No
        │                 │
        ▼                 ▼
┌───────────────┐   ┌────────────────────┐
│   Unit Test   │   │ Is it wiring only? │
│ Vitest / Jest │   └─────────┬──────────┘
└───────────────┘             │
                       ┌───────┴────────┐
                       │                │
                      Yes              No
                       │                │
                       ▼                ▼
             Verify Through       Re-evaluate File
             Integration / E2E    Responsibilities
              (Playwright)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Seeing the Matrix in Practice
&lt;/h2&gt;

&lt;p&gt;Let’s look at two concrete examples to see how this decision rule works in real code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 1: &lt;code&gt;queryClient.ts&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Pure Wiring (Skip Unit Tests):&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="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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;const&lt;/span&gt; &lt;span class="nx"&gt;queryClient&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decision Check:&lt;/strong&gt; Does this file make decisions? &lt;strong&gt;No.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strategy:&lt;/strong&gt; Skip unit testing. Verify data fetching behavior through Playwright integration or end-to-end smoke tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Contains Decision Logic (Unit Test It!):&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="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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;const&lt;/span&gt; &lt;span class="nx"&gt;queryClient&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="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;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&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="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="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decision Check:&lt;/strong&gt; Does this file make decisions? &lt;strong&gt;Yes.&lt;/strong&gt; It evaluates the current environment to choose retry behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strategy:&lt;/strong&gt; Write a Vitest unit test to verify that retries evaluate correctly across environments.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Example 2: &lt;code&gt;router.tsx&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Static Routes (Skip Unit Tests):&lt;/strong&gt;&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createBrowserRouter&lt;/span&gt;&lt;span class="p"&gt;([&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="s1"&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;element&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;Home&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="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="s1"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;element&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;Dashboard&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;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decision Check:&lt;/strong&gt; Does this file make decisions? &lt;strong&gt;No.&lt;/strong&gt; It's purely declarative static wiring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Conditional Routing (Unit Test It!):&lt;/strong&gt;&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAppRoutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userRole&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;createBrowserRouter&lt;/span&gt;&lt;span class="p"&gt;([&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="s1"&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;element&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;Home&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="nx"&gt;userRole&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&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;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;element&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;AdminPanel&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="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;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decision Check:&lt;/strong&gt; Does this file make decisions? &lt;strong&gt;Yes.&lt;/strong&gt; It computes routes based on role permissions.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Quick Checklist: When Should You Unit Test a Glue File?
&lt;/h2&gt;

&lt;p&gt;My default rule is: &lt;strong&gt;In most cases, I don't unit test pure glue files because they don't contain business logic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;However, a glue file becomes worth unit testing as soon as it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Contains branching or conditional logic (&lt;code&gt;if / else&lt;/code&gt;, ternary checks)&lt;/li&gt;
&lt;li&gt;✅ Transforms configuration dynamically at runtime&lt;/li&gt;
&lt;li&gt;✅ Chooses behavior based on environment variables&lt;/li&gt;
&lt;li&gt;✅ Applies feature flags or user permissions&lt;/li&gt;
&lt;li&gt;✅ Has custom calculation logic that can fail independently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Otherwise, skip the unit test and verify it at the integration or E2E level.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This isn't a rigid rule—it's a heuristic. Every codebase is different, and your testing strategy should reflect your team's priorities, risk tolerance, and architecture.&lt;/p&gt;

&lt;p&gt;Coverage tells you how much code executed. Good tests tell you how confident you should be when shipping.&lt;/p&gt;

&lt;p&gt;If there's one takeaway to keep in mind when designing your test suite, it's this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Code deserves tests in proportion to the decisions it makes.&lt;br&gt;
Business logic makes decisions.&lt;br&gt;
Glue code makes connections.&lt;br&gt;
Test them accordingly.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>nextjs</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
