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.
windowin 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
| What | Client (browser) can do | Server 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:
- The user visits
/post/123. - The server (Next.js running on Vercel) reads from the database, gets the post and the like count.
- The server renders the HTML, including the
LikeButtonβs initial markup. - The HTML is sent to the client (the browser).
- The browser displays it instantly.
- The browser then downloads the JavaScript for
LikeButtonand βhydratesβ it β attaching theonClickhandler. - The user clicks the button. The
onClickruns 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),useEffectcallbacks. - 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. -
windowis not defined on the server. If you writewindow.innerWidthin a component that renders on the server, it crashes. Solutions: wrap inuseEffect(which only runs on the client), or checktypeof window !== 'undefined'first. -
process.env.SECRETisundefinedon the client unless it hasNEXT_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 itNEXT_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 ontypeof window. -
localStoragedoesnβt exist on the server. Same aswindow. Wrap any localStorage access inuseEffector a Client Component. -
The server has different env vars than your laptop. Local
.env.localis yours. Vercelβs env vars are the serverβs. They can drift. Usevercel env pull .env.localto sync.
See also
- How the web works π© β the conversation in detail
- What is a backend? π© β the server side
- Frontend (section) β the client side
- Next.js β Server vs Client Components π© π¦
- SPA vs MPA vs SSR vs SSG π©
- Serverless functions π©
- Environment variables π©
- Secrets management π©
- Glossary: Client, Server