window is not defined. Module not found: Can't resolve 'fs'. Two of the most common errors in a Next.js project, and neither one is really a bug in your code. Both mean the same thing: a piece of code ran somewhere it was never going to work.

The idea

A web app is split across two computers. The runs on your visitor's machine, inside their browser, where they can read it, edit it and lie to it. The runs on a machine you control, where your secrets are safe. The sits behind the backend and remembers everything after the visitor closes the tab. A lot of the errors that make no sense early on are code running on the wrong side of that line.

code
        USER'S MACHINE                 |            YOUR SERVER
                                       |
   +------------------------+          |     +------------------------+
   |  browser               |  request |     |  backend               |
   |  HTML, CSS, JavaScript | -------> |     |  route handlers        |
   |  = the frontend        | <------- |     |  API keys, passwords   |
   +------------------------+ response |     +-----------+------------+
                                       |                 |
   anything here is visible            |                 | queries
   to the user and editable            |                 v
   by the user                         |     +------------------------+
                                       |     |  database (Postgres)   |
                                       |     +------------------------+

How it works

One page view is a short conversation:

  1. The browser asks your server for a URL.
  2. Your server runs backend code. It may read the database, call another company's service, check who is logged in.
  3. Your server sends back HTML plus a bundle of JavaScript.
  4. The browser draws the page, then runs that JavaScript for clicks, typing and animation.
  5. Every later action (submit a form, load more rows) is another request over the same line.

The line in the middle is a trust boundary. Everything on the left arrives on a stranger's computer. Browser code can call window and document because a browser has them, and it cannot call fs because it has no filesystem. Server code is the reverse. A Stripe secret key in a frontend file is published, to every visitor, in a file they can read.

What to do

  1. For any file you are about to write, answer out loud: does this run in the browser or on the server? In Next.js the App Router treats components as server code by default, and "use client" marks where a component starts running in the browser too. Note "too". A "use client" file is still rendered on the server first, which is exactly why one that reads window while it renders fails with window is not defined. Browser-only work goes inside useEffect, which never runs on the server.
  2. Put anything with a password, an API key or a database connection under a server-only folder such as src/server/, and never import it from a component that carries "use client". The folder name is a habit you keep, not a guard the framework enforces. What Next.js does enforce is the opposite direction: an environment variable named with the NEXT_PUBLIC_ prefix is compiled into the browser bundle on purpose, so a secret handed that prefix ships to every visitor. Section 5 is where you go looking for one.
  3. When an error names a browser thing (window, localStorage) or a server thing (fs, process), read it as a location complaint before you read it as a logic bug. Tell your AI assistant which side the file is supposed to run on. It will usually fix it in one move.

Where it breaks

Modern frameworks blur the line on purpose, which is why it confuses people. A Next.js server component and a client component look like the same React code in the same folder. Some apps have no backend at all: a static site is frontend files sitting on a file host with nothing running behind them, and that works fine until you need to keep a secret. Others push backend work to someone else's servers, so Supabase or Firebase is your backend and you never write one. The split still exists in all of those. It just moved.

That last case changes where the checks go. Later pages tell you to verify things on the server. If you never wrote a server, those checks do not disappear. They become rules attached to the database tables themselves, and the browser talks to the database directly.