RizTech Academy logo
RizTech Academy
How a Full-Stack Application Fits TogetherLesson 2 of 425 min

What runs where, and why it matters so much

The last lesson said two computers. This one is about the consequences, because "which machine is this line on?" is the question that resolves most full-stack confusion — and getting it wrong produces bugs that range from puzzling to expensive.

The browser is a hostile environment

Not because your users are attackers. Because anyone can be a user, and the browser gives all of them the same tools.

Everything the browser receives can be read. Open DevTools on any site and you can see the JavaScript, the HTML, every network request and response, and every value in local storage.

Everything the browser sends can be forged. A request does not have to come from your form — curl works fine, and so does editing the request in DevTools and replaying it.

So:

// In the browser. All of it visible.
const API_KEY = "sk_live_abc123";

That key is now public. Not "could be leaked" — published, to everyone who visits.

// In the browser
if (user.isAdmin) {
  showDeleteButton();
}

Hiding a button is a convenience, not a control. The endpoint behind it must check permissions itself, because nothing stops someone calling it directly.

The browser decides what to show. The server decides what is allowed.

The classic price bug

<input type="hidden" name="price" value="250">

The server reads price from the form and charges it. A user changes 250 to 1 in DevTools and buys a ₹15,000 item for one rupee.

This is not theoretical; it has happened to real shops. The fix is structural:

// The client sends what it wants, not what it costs.
{ productId: "atta-5kg", quantity: 2 }

The server looks up the price. Send identifiers, not values the server should already know.

The same reasoning applies to a user id in a request ("update user 42's address"), a discount percentage, an order status, and stock levels. If the answer matters, the server works it out.

Validating twice, on purpose

Beginners ask why validate in both places when the server check is the one that counts.

They are for different things.

Client-side validation is for the user. It tells them the email is malformed before they wait for a round trip. It is a feature, and it can be bypassed entirely without consequence.

Server-side validation is for the system. It is the only one that protects your data, and it must assume the client check did not happen.

Write both. Never rely on the first.

What belongs where

Server:

  • Authentication and authorisation
  • Anything touching the database
  • Prices, totals, stock, discounts
  • Anything using a secret
  • Business rules of any kind
  • Sending email, taking payment

Browser:

  • Rendering and layout
  • Animations and transitions
  • Showing and hiding parts of the interface
  • Client-side validation for feedback
  • Ephemeral state — an open dropdown, a focused field
  • Formatting values that have already been decided

Either, deliberately:

  • Formatting currency and dates — pick one and be consistent, or the two will disagree
  • Sorting and filtering — small lists in the browser, large ones on the server
  • Search — trivial in the browser, real search on the server

Environment variables

The most common way to leak a secret in a Next.js project, and worth being precise about:

DATABASE_URL=postgres://...          server only
API_SECRET=abc123                    server only
NEXT_PUBLIC_SITE_URL=https://...     sent to the browser

The NEXT_PUBLIC_ prefix means "put this in the JavaScript bundle". It is not a naming convention you can ignore; it changes behaviour. Anything with that prefix is public, permanently, to everyone.

Without the prefix, the variable exists only in server code. Read it in a client component and you get undefined — which is the framework protecting you, not a bug to work around by adding the prefix.

The rule: if you are tempted to add NEXT_PUBLIC_ to make something work, stop. That code probably belongs on the server.

Two failure modes

Too much on the client. Fetching every product and filtering in the browser works with fifty items and falls over with five thousand — slow on a cheap phone, expensive on mobile data, and it exposes data the user should not see. Filtering belongs where the data is.

Too much on the server. Round-tripping for something the browser already knows — validating an obviously empty field, re-rendering a page to open a dropdown — makes the interface feel sluggish for no benefit.

The judgement: does this need data or authority the browser does not have? Yes means server.

Latency is the reason this is hard

A server request takes 50–500ms, more on a poor mobile connection in a basement. That is the tax on every interaction that crosses the gap.

Which is why cart quantity updates feel instant in good shops: the browser updates immediately and tells the server afterwards. If the server disagrees — stock ran out — the interface corrects itself. That is an optimistic update, and it arrives in module 9.

The technique matters because the alternative is a spinner on every tap, and users abandon carts over exactly that.

Check your work

Why the browser is hostile: everything sent to it can be read, and everything it sends can be forged. Not because users are attackers, but because anyone can be a user.

Price, or id: a well-built shop sends { productId, quantity }. The server looks up the price. Sending a price would let the client set it.

Server, browser or either: checking a coupon is valid is the server. Showing the coupon field is the browser. Calculating the discount is the server. The discount percentage itself lives on the server. "3 left in stock" is the server's number, displayed by the browser. Deciding whether an order can complete is the server, always.

The attack on a trusted hidden price, and the fix: the user edits the value and buys a ₹15,000 item for ₹1. The fix is to send an identifier and look the price up on the server.

The .env lines: DATABASE_URL=... and PAYMENT_SECRET=... with no prefix; NEXT_PUBLIC_SITE_URL=... with it. Prefixing the payment secret puts it in the JavaScript bundle, published to every visitor permanently.

What each choice costs: a blank page without JavaScript means no crawler, no social preview, and nothing for a user on a failed script load. Rendering on the server costs server time and some complexity.

Practice

  1. Open an e-commerce site's DevTools. Find a network request made when you change a quantity. What did the browser send — a price, or an id?
  2. Look at the JavaScript bundle of any site (Sources tab) and search for key or secret. Note what is and is not there.
  3. Sort these into server, browser or either: checking a coupon is valid · showing a coupon field · calculating a discount · the discount percentage itself · showing "3 left in stock" · deciding whether an order can complete.
  4. Describe how you would attack a checkout that trusts a hidden price field. Then describe the fix in one sentence.
  5. Write out three .env lines — a database URL, a payment secret and a public site URL — with correct prefixes. Say what happens if you prefix the second with NEXT_PUBLIC_.
  6. Find a site where disabling JavaScript leaves a blank page, and one where the content still appears. What does each choice cost?

Next: HTTP itself, since every one of these conversations is a request and a response.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship