Client vs server

Status: 🟩 COMPLETE Last updated: 2026-06-20 Plain-English tagline: The single most important divide in software. The client asks. The server answers. Almost every confusion in web development traces back to losing track of which side a piece of code is on.


In plain English

When two computers talk over a network, one of them is asking for something and the other is providing it. The one asking is called the client. The one providing is called the server. That’s the entire concept.

  • Your browser is a client: it asks for web pages.
  • The computer at Vercel that holds your Next.js app is a server: it answers.

That’s it. There’s no deeper meaning. The labels just describe roles in a conversation.

Where it gets interesting (and confusing) is that a single device can be a client in one conversation and a server in another. Your laptop is a client when it visits a website. It can also be a server β€” if you run npm run dev, your laptop becomes a tiny web server that the browser on the same machine is a client of.

Once you lock this in, half of web development clicks into place.


Why it matters

In modern frameworks like Next.js, the lines blur on purpose β€” Server Components and Client Components live in the same file structure, look similar, and the framework hides a lot of plumbing. That’s powerful, but it also means you have to actively track which side you’re on. Code that β€œfeels the same” in your editor behaves completely differently depending on which side of the divide it ends up running on.

Almost every β€œwhy doesn’t this work?” question in web development boils down to one of:

  • β€œI tried to use a browser API on the server” (e.g. window in a Server Component)
  • β€œI tried to use a server secret in the browser” (e.g. a database password in a Client Component)
  • β€œI expected this to run once but it’s running twice” (because it runs on both sides during SSR)

Lock this concept in and those problems become rare and easy to diagnose.


How the split actually divides

WhatClient (browser) can doServer can do
Show UI to the userβœ… Yes β€” it has the screen❌ No screen attached
React to user input (clicks, typing)βœ… Yes β€” the events happen in the browser❌ No (the user isn’t there)
Read browser-only APIs (window, localStorage, document)βœ… Yes❌ No (no browser to read from)
Read environment variables / secrets⚠️ Only ones the developer chose to exposeβœ… All of them β€” secrets are safe here
Read directly from a database❌ No (would expose credentials)βœ… Yes β€” credentials are server-side
Trust user input❌ Never (the user could be malicious)βœ… Validates input before trusting it
Run forever❌ Closes when the user closes the tabβœ… Yes (kind of β€” see β€œserverless” below)
Be inspected by the userβœ… Yes β€” DevTools shows everything❌ No β€” invisible to the user

The rule of thumb that follows: anything sensitive (secrets, credentials, business logic the user shouldn’t see) goes on the server. Anything interactive (clicks, animations, real-time UI) goes on the client.


The classical model

For the first ~25 years of the web, the split was simple:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”                            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Browser β”‚  ────  HTTP request  ─────▢│ Server  β”‚
β”‚ (client)β”‚                            β”‚         β”‚
β”‚         │◀──── HTML response  ───────│         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • The server held all the code, all the data, all the secrets.
  • The browser displayed whatever HTML the server sent back.
  • Interactivity was limited β€” every click typically meant a full page reload (another request, another response, another full render).

This is how PHP sites (Wordpress, classic e-commerce), old Rails apps, and traditional Django apps worked. Still works. Still fine for many use cases.


The single-page app (SPA) model

Around 2010, JavaScript got powerful enough to take over more of the work:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Browser     β”‚  ── GET / ───────────▢   β”‚ Server  β”‚
β”‚ (lots of JS)β”‚ ◀── empty HTML + JS ──── β”‚         β”‚
β”‚             β”‚                          β”‚         β”‚
β”‚ JS runs in  β”‚ ── API call ─────────▢   β”‚         β”‚
β”‚ browser,    β”‚ ◀── JSON data ────────── β”‚         β”‚
β”‚ builds UI   β”‚                          β”‚         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • The server sends an almost-empty HTML page and a big bundle of JavaScript.
  • The JavaScript runs in the browser and builds the actual UI.
  • Subsequent navigation doesn’t reload the page β€” JavaScript updates the visible content and makes background API calls for data.

This made web apps feel like native apps (smooth, no flashing reloads) but introduced new problems: slow initial loads, bad SEO (search engines don’t see anything until JS runs), and a lot of complexity on the client.

This is the β€œReact app” of the mid-2010s. Still common.


The modern hybrid (Next.js App Router)

The current state of the art is β€œdo each piece on whichever side makes more sense”:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Browser     β”‚  ── GET / ──────────▢    β”‚ Server          β”‚
β”‚             β”‚                          β”‚  - reads DB     β”‚
β”‚             β”‚  ◀── pre-rendered ────── β”‚  - renders HTML β”‚
β”‚             β”‚       HTML+ JS for       β”‚  - includes     β”‚
β”‚             β”‚       interactivity      β”‚    Client       β”‚
β”‚             β”‚                          β”‚    Components   β”‚
β”‚  JS hydrates                           β”‚                 β”‚
β”‚  the page,                             β”‚                 β”‚
β”‚  takes over                            β”‚                 β”‚
β”‚  interactivity                         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • The server runs the data-heavy code (read database, do server-side logic, render initial HTML).
  • The client runs the interactive bits (clicks, form state, animations).
  • The framework manages the handoff between them.

This is what Next.js’s Server Components and Client Components are about. Code in your app/ folder defaults to Server Components (runs only on the server, ships zero JS to the browser). Adding "use client" at the top of a file makes it a Client Component (ships to the browser, can use state and events).

Same project, same folder structure, same JSX β€” but each piece of code runs in exactly the right place.


Serverless β€” does the server still exist?

Modern hosting (Vercel, Cloudflare, AWS Lambda) uses serverless functions. Despite the name, the server still exists β€” you just don’t manage it.

What β€œserverless” means in practice:

  • You write a function. The platform spins up a server (instantly), runs your function, sends the response, then spins the server down.
  • You pay only for the time your function runs.
  • The platform scales up automatically: if 10,000 people hit your function at once, the platform spins up 10,000 instances.

For the client/server split, none of this matters. From the client’s perspective, there’s still a server somewhere that’s answering. The β€œserverless” label is about who manages the server, not whether there is one.


A concrete example: a β€œlike” button in Next.js

Look at this contrived example:

// app/post/[id]/page.tsx β€” SERVER Component (no "use client")
import { LikeButton } from "./LikeButton";
 
export default async function PostPage({ params }: { params: { id: string } }) {
  // This runs on the SERVER:
  const post = await db.from("posts").select("*").eq("id", params.id).single();
  const likeCount = await db.from("likes").count().eq("post_id", params.id);
 
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
      <LikeButton postId={post.id} initialCount={likeCount} />
    </article>
  );
}
// app/post/[id]/LikeButton.tsx β€” CLIENT Component
"use client";
import { useState } from "react";
 
export function LikeButton({ postId, initialCount }) {
  // This runs in the BROWSER:
  const [count, setCount] = useState(initialCount);
 
  const onClick = async () => {
    await fetch(`/api/like/${postId}`, { method: "POST" });
    setCount(count + 1);
  };
 
  return <button onClick={onClick}>❀️ {count}</button>;
}

What happens:

  1. The user visits /post/123.
  2. The server (Next.js running on Vercel) reads from the database, gets the post and the like count.
  3. The server renders the HTML, including the LikeButton’s initial markup.
  4. The HTML is sent to the client (the browser).
  5. The browser displays it instantly.
  6. The browser then downloads the JavaScript for LikeButton and β€œhydrates” it β€” attaching the onClick handler.
  7. The user clicks the button. The onClick runs in the browser. It calls /api/like/123. That POST goes to the server, which writes to the database. The browser then updates the displayed count.

The data fetching ran on the server (where it can safely read the database). The interactivity runs on the browser (where the user actually is). Each piece of code is in the place that fits it.


The β€œwhere does this run?” mental check

When reading or writing code in a modern framework, train yourself to ask β€œwhere does this run?” for every function. The answer is one of:

  • Only on the server β€” Server Components, Server Actions, Route Handlers (route.ts), getServerSideProps (Pages Router), Edge Functions, middleware.
  • Only on the client β€” "use client" files, code inside event handlers (onClick, onChange), useEffect callbacks.
  • Both β€” Plain functions, utility files, library imports. If a function is called from both sides, it runs on both sides at different times.

The signal that you’ve lost track is when something that should work just doesn’t, in a confusing way. The first debugging move is to ask β€œwait, is this running on the server or the client right now?”


Common gotchas

  • Server Components can’t use hooks. useState, useEffect, useContext, useRef β€” only Client Components. If you need them, add "use client".

  • Client Components can’t use server-only APIs. Don’t try to read environment-secret variables, fs.readFile, database clients with admin credentials. If you need server-side data, fetch it in a Server Component and pass it as a prop.

  • window is not defined on the server. If you write window.innerWidth in a component that renders on the server, it crashes. Solutions: wrap in useEffect (which only runs on the client), or check typeof window !== 'undefined' first.

  • process.env.SECRET is undefined on the client unless it has NEXT_PUBLIC_. This is by design β€” Next.js refuses to expose server-side env vars to the browser. If you actually need a value on the client, give it NEXT_PUBLIC_ (and confirm it’s safe to expose).

  • Don’t pass functions across the boundary. Server Components can’t send a function to a Client Component as a prop β€” functions can’t be serialized. Pass data only; have the Client Component define its own callbacks.

  • Hydration errors. If the server-rendered HTML doesn’t exactly match what React expects on the client, React throws β€œHydration failed.” Usual culprits: Date.now(), Math.random(), locale-dependent formatting, conditional rendering based on typeof window.

  • localStorage doesn’t exist on the server. Same as window. Wrap any localStorage access in useEffect or a Client Component.

  • The server has different env vars than your laptop. Local .env.local is yours. Vercel’s env vars are the server’s. They can drift. Use vercel env pull .env.local to sync.


See also


Sources