DEV Community

Cover image for I Thought This E-commerce Site Was Making Zero API Requests. I Was Wrong.
Muhammad Rabbi
Muhammad Rabbi

Posted on

I Thought This E-commerce Site Was Making Zero API Requests. I Was Wrong.

I Thought This E-commerce Site Was Making Zero API Requests. I Was Wrong.

A few days ago, I was inspecting an e-commerce website because I wanted to understand one thing:

"How the hell is this page getting its data without making an obvious API request?"

The site was Ghorer Bazar, and I was looking at a collection page:

/collections/honey-2

I opened Chrome DevTools, went straight to the Network tab, filtered the requests, and started looking for the usual suspects:

/api/products
/api/products?category=honey
/graphql
/products
Enter fullscreen mode Exit fullscreen mode

Nothing obvious.

And that tiny observation sent me down a much deeper rabbit hole.

I started asking questions that turned into a complete architecture investigation:

  • Is the data being fetched from an API?
  • Is it rendered on the server?
  • Is the HTML cached?
  • Is Cloudflare involved?
  • Why does navigation sometimes feel instant?
  • Is the browser prefetching pages?
  • What exactly are Priority Hints?
  • Could I build my own Next.js e-commerce site like this?
  • Most importantly, how do all these pieces fit together?

This article is the story of what I learned.


The First Clue: "Where Is the API?"

I started with the most basic assumption.

An e-commerce page has products.

Products live in a database.

Therefore:

Browser
   ↓
API
   ↓
Database
Enter fullscreen mode Exit fullscreen mode

Right?

That's one common architecture.

For example:

Browser
   ↓
GET /api/products?category=honey
   ↓
Backend
   ↓
PostgreSQL
Enter fullscreen mode Exit fullscreen mode

The browser receives JSON:

{
  "products": [
    {
      "id": 1,
      "name": "Premium Honey",
      "price": 850
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Then JavaScript renders the products.

So naturally I expected to see something similar in DevTools.

But I didn't.

Instead, the important request looked like this:

GET https://ghorerbazar.com/collections/honey-2

Status: 200
Type: document
Content-Type: text/html
Enter fullscreen mode Exit fullscreen mode

And that changed the direction of the investigation.


The Browser Was Already Receiving HTML

The request wasn't:

/api/products
Enter fullscreen mode Exit fullscreen mode

It was:

/collections/honey-2
Enter fullscreen mode Exit fullscreen mode

And the response was:

text/html
Enter fullscreen mode Exit fullscreen mode

That means the browser wasn't necessarily asking:

"Give me the product data."

It could simply be asking:

"Give me the page."

And somewhere on the server, the application can do the database work before sending the result back.

Conceptually:

Browser
   │
   │ GET /collections/honey-2
   ▼
Server
   │
   │ Query database
   ▼
Database
   │
   │ Products
   ▼
Server
   │
   │ Render HTML
   ▼
Browser
Enter fullscreen mode Exit fullscreen mode

So there doesn't have to be a visible browser request such as:

/api/products
Enter fullscreen mode Exit fullscreen mode

The database query can happen entirely on the server.

That distinction was probably the biggest lesson of this whole investigation.


"No API Request" Does Not Mean "No Data Request"

This is something I think many frontend developers misunderstand at first.

There are two very different things.

Browser → API

Browser → /api/products → Server → DB
Enter fullscreen mode Exit fullscreen mode

And:

Browser → Page → Server → DB

Browser → /collections/honey-2 → Server → DB
Enter fullscreen mode Exit fullscreen mode

In the second architecture, the database request exists.

You just don't see it from the browser.

It's server-side work.

This is one of the reasons frameworks such as Next.js are interesting.

With the App Router, Next.js supports Server Components and server-side data fetching, so application code can retrieve data on the server rather than forcing every database interaction through a browser-visible REST endpoint.

For example, conceptually:

export default async function CollectionPage() {
  const products = await prisma.product.findMany({
    where: {
      category: "honey",
    },
  });

  return (
    <div>
      {products.map((product) => (
        <ProductCard
          key={product.id}
          product={product}
        />
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The browser doesn't need to know that Prisma exists.

It simply receives the rendered result.


Then I Looked at the Network Headers

Now things became even more interesting.

The collection page response showed headers like:

Server: cloudflare
Content-Encoding: br
Cache-Control: max-age=600, public
CF-Cache-Status: DYNAMIC
Enter fullscreen mode Exit fullscreen mode

There were also cookies being set.

The response was going through Cloudflare.

This immediately gave me another piece of the architecture.

Instead of:

Browser
   ↓
Next.js
   ↓
Database
Enter fullscreen mode Exit fullscreen mode

the architecture can look more like:

Browser
   ↓
Cloudflare
   ↓
Application Server
   ↓
Database
Enter fullscreen mode Exit fullscreen mode

Cloudflare can sit in front of the application as a CDN, reverse proxy, security layer, and cache.

But this is where I made another mistake.


Cloudflare ≠ Everything Is Cached

Seeing:

Server: cloudflare
Enter fullscreen mode Exit fullscreen mode

does not automatically mean:

"This page came from Cloudflare's cache."

The response I inspected had:

CF-Cache-Status: DYNAMIC
Enter fullscreen mode Exit fullscreen mode

That means the particular response was classified as dynamic rather than being returned as a cache hit.

This is an important distinction.

Cloudflare can be sitting in front of your application even when a request still reaches your origin server.

So the architecture could still be:

Browser
   ↓
Cloudflare
   ↓
Origin server
   ↓
Database
Enter fullscreen mode Exit fullscreen mode

Cloudflare is still useful even if that HTML response isn't cached.


Then Why Was There max-age=600?

This was another nice DevTools lesson.

The page had:

Cache-Control: max-age=600, public
Enter fullscreen mode Exit fullscreen mode

At first glance, that looks like:

"Great, the page is cached for 10 minutes."

But caching is more complicated than reading one header.

There are multiple layers involved:

Browser cache
       +
CDN cache
       +
Application/framework cache
       +
Database
Enter fullscreen mode Exit fullscreen mode

A response can have cache-related headers without necessarily behaving exactly how you expect at every caching layer.

Things like cookies, cache rules, request headers, and CDN configuration can affect the result.

That's why:

Cache-Control: public
Enter fullscreen mode Exit fullscreen mode

and:

CF-Cache-Status: DYNAMIC
Enter fullscreen mode Exit fullscreen mode

can appear together.

The lesson wasn't:

"Cloudflare caching is broken."

The lesson was:

Never assume the caching architecture from a single header.

Inspect the entire request/response behavior.


The Page Was Loading A Lot More Than Just HTML

At this point I started looking at the complete Network waterfall instead of one request.

On my captured run, the page had roughly:

148 requests
4.6 MB transferred
14.5 MB resources
DOMContentLoaded ≈ 2.62s
Load ≈ 3.74s
Enter fullscreen mode Exit fullscreen mode

There were:

  • JavaScript bundles
  • CSS
  • fonts
  • images
  • analytics
  • third-party scripts
  • security scripts
  • chat-related resources
  • application code

One JavaScript bundle alone was around hundreds of kilobytes.

So the page wasn't magically fast because it loaded "nothing."

It was loading plenty.

What mattered was how the work was organized.


Then I Discovered the Difference Between HTML and Navigation

This was the next puzzle.

I noticed that navigating around modern websites can feel almost instant.

You click:

Honey

Then:

Dates

Then:

Oil

And it feels like the browser never fully reloads.

But does that mean there was no network request?

Not necessarily.

Modern frameworks can change the meaning of navigation.

With Next.js App Router, client-side navigation, prefetching, Server Components, streaming, and caching can all work together.

So there are several possibilities.


Possibility One: Full Page Request

The simplest case:

Click link
   ↓
Browser requests page
   ↓
Server responds with HTML
   ↓
Browser loads it
Enter fullscreen mode Exit fullscreen mode

This is a traditional navigation.


Possibility Two: Prefetching

Here's where things get interesting.

Imagine you are on:

/
Enter fullscreen mode Exit fullscreen mode

And there is a link:

/collections/honey-2
Enter fullscreen mode Exit fullscreen mode

The framework may decide to fetch information for that route before you click it.

So the timeline can become:

Page loaded
    ↓
Browser sees link
    ↓
Prefetch happens
    ↓
User clicks
    ↓
Already-prepared data is used
    ↓
Navigation feels instant
Enter fullscreen mode Exit fullscreen mode

Now imagine opening DevTools after you've clicked.

You might think:

"There was no request!"

But the request may have happened before you clicked.

That completely changes how you debug performance.

You shouldn't only ask:

"What happened after the click?"

You should ask:

"What happened before the click?"


That Was My "Aha!" Moment

I had been treating the browser like a simple messenger:

User click
   ↓
Request
   ↓
Response
Enter fullscreen mode Exit fullscreen mode

Modern applications are more like:

Initial load
   ↓
Prefetch
   ↓
Caching
   ↓
Server rendering
   ↓
Client-side navigation
   ↓
Streaming
   ↓
Image optimization
Enter fullscreen mode Exit fullscreen mode

Several of those can happen before the user consciously notices anything.

That's why performance debugging requires watching the entire lifecycle rather than one request.


Then I Found Priority Hints

Another thing that caught my attention was Priority Hints.

This is one of those features that sounds complicated until you reduce it to one question:

"Which resource should the browser care about first?"

Imagine a product page contains:

Hero image
Product image
20 review avatars
Footer logo
Chat icon
Analytics script
Enter fullscreen mode Exit fullscreen mode

The browser has limited bandwidth and connection resources.

Should all of them have the same urgency?

Obviously not.

Your hero image might be extremely important because it is part of the first visible screen.

The footer logo?

Probably not.

Priority Hints allow developers to provide the browser with a hint about resource importance.

Conceptually:

<img
  src="/hero.webp"
  fetchpriority="high"
/>
Enter fullscreen mode Exit fullscreen mode

versus:

<img
  src="/background-decoration.webp"
  fetchpriority="low"
/>
Enter fullscreen mode Exit fullscreen mode

The important word is hint.

It's not a command saying:

"Browser, you MUST download this first."

It's a signal about relative priority.

And that distinction matters.


Priority and Lazy Loading Are Not the Same Thing

This was another useful distinction.

Developers often mix these together:

Lazy loading

and:

Fetch priority

They solve different problems.

Lazy loading asks:

"Should this resource wait until it is near the viewport?"

Priority asks:

"Once this resource is eligible to load, how urgent is it compared with other resources?"

For an e-commerce page, that can become a nice combination:

Hero image
→ load early
→ high importance

Below-the-fold images
→ lazy load
→ lower urgency

Decorative assets
→ low priority
Enter fullscreen mode Exit fullscreen mode

Next.js's image tooling is designed to help with responsive image delivery, loading behavior, and image optimization.

But there is another important lesson:

Don't mark everything as high priority.

If everything is "high priority," then the concept loses much of its value.


This Made Me Think About My Own E-commerce Project

At this point, the investigation stopped being about another website.

I started thinking:

"What would I build if I wanted this same kind of architecture?"

My stack is already something like:

Next.js
Prisma
Neon PostgreSQL
Enter fullscreen mode Exit fullscreen mode

So I started designing the architecture around the principles I had learned.

And this is what I came up with.


The Architecture I Want

Instead of building:

Browser
   ↓
REST API
   ↓
Database
Enter fullscreen mode Exit fullscreen mode

for every single page, I can use:

                    ┌──────────────────────┐
                    │      Cloudflare      │
                    │ CDN / Security/Cache │
                    └──────────┬───────────┘
                               │
                               ▼
                    ┌──────────────────────┐
                    │       Next.js        │
                    │     App Router       │
                    └──────────┬───────────┘
                               │
             ┌─────────────────┼─────────────────┐
             │                 │                 │
             ▼                 ▼                 ▼
      Server Components   Client Components   Server Actions/
             │                 │               Route Handlers
             │                 │                 │
             ▼                 ▼                 ▼
          Prisma          Cart / UI          Orders / mutations
             │
             ▼
      Neon PostgreSQL
Enter fullscreen mode Exit fullscreen mode

That architecture is much closer to how I now think about a modern e-commerce application.


Product Pages Don't Need to Be Fully Dynamic

This is another big lesson.

Consider:

/products/honey
Enter fullscreen mode Exit fullscreen mode

Does every visitor need a completely different product page?

Usually, no.

The product title is shared.

The description is shared.

The images are shared.

The category is shared.

The public price may be shared.

So a product page can often take advantage of server rendering and caching strategies.

Next.js supports static and dynamic rendering patterns, allowing developers to choose where content needs to be generated dynamically and where it can be reused.

That means I can think about my website in categories:

Public catalog
    ↓
Cache-friendly

Search
    ↓
More dynamic

Cart
    ↓
Client-side state

Checkout
    ↓
Server-controlled

Admin
    ↓
Private / dynamic
Enter fullscreen mode Exit fullscreen mode

This is much more useful than saying:

"Everything should be SSR."

or:

"Everything should be a client component."

Architecture is about choosing the right behavior for each type of data.


The Cart Is Different

A shopping cart is highly interactive.

The user can do:

+
-
remove
update quantity
apply coupon
open cart
close cart
Enter fullscreen mode Exit fullscreen mode

That is a great place for client-side interaction.

So I don't necessarily want every tiny cart interaction to involve a full server round trip.

Something like:

Product page
    ↓
"Add to cart"
    ↓
Client-side cart state
    ↓
Cart UI updates instantly
Enter fullscreen mode Exit fullscreen mode

But there is a very important security boundary.

At checkout:

Client says:

"I want 3 products."

        ↓

Server checks:

- current product price
- stock
- discounts
- user/session
- totals

        ↓

Create order
Enter fullscreen mode Exit fullscreen mode

The client should never be trusted with final pricing logic.


The Most Dangerous Cloudflare Rule

After learning all this, I can imagine the next beginner mistake.

You add Cloudflare.

Everything looks fast.

Then you think:

"Let's cache everything!"

Bad idea.

Imagine:

/cart
/checkout
/account
/admin
Enter fullscreen mode Exit fullscreen mode

These pages can contain personalized or sensitive information.

Caching them carelessly can create serious correctness and security problems.

So a much safer mental model is:

PUBLIC

/products/*
/collections/*
/
/blog/*

↓

Potentially cacheable
Enter fullscreen mode Exit fullscreen mode

while:

PRIVATE

/cart
/checkout
/account/*
/admin/*
Enter fullscreen mode Exit fullscreen mode

should generally be treated very differently.

Cloudflare provides Cache Rules specifically for configuring caching behavior rather than blindly caching everything.


My Final Mental Model

Before this investigation, I thought performance looked like:

Frontend
   ↓
API
   ↓
Database
Enter fullscreen mode Exit fullscreen mode

Now I see a much larger picture:

                         USER
                           │
                           ▼
                    ┌─────────────┐
                    │  Browser    │
                    └──────┬──────┘
                           │
             prefetch / navigation / requests
                           │
                           ▼
                  ┌─────────────────┐
                  │   Cloudflare    │
                  │ CDN / Security  │
                  │     / Cache     │
                  └────────┬────────┘
                           │
                           ▼
                  ┌─────────────────┐
                  │     Next.js     │
                  │   App Router    │
                  └────────┬────────┘
                           │
             ┌─────────────┼─────────────┐
             │             │             │
             ▼             ▼             ▼
        Server          Client       Mutations
       Components     Components      / APIs
             │             │             │
             ▼             │             ▼
          Prisma           │          Server
             │             │          validation
             ▼             │             │
      Neon PostgreSQL      │             ▼
                           │           Order
                           ▼
                         Cart
Enter fullscreen mode Exit fullscreen mode

And around all of that:

Images
   ↓
Next.js Image Optimization

Resources
   ↓
Priority hints / lazy loading

Navigation
   ↓
Prefetching

Public pages
   ↓
Caching / revalidation

Dynamic pages
   ↓
Server rendering

Static assets
   ↓
CDN
Enter fullscreen mode Exit fullscreen mode

What I Actually Learned

The most valuable thing I learned wasn't one specific technology.

It was how to read a website from the outside.

When I see:

200 document
Enter fullscreen mode Exit fullscreen mode

I don't immediately assume the site is static.

When I see:

Server: cloudflare
Enter fullscreen mode Exit fullscreen mode

I don't assume everything is cached.

When I don't see:

/api/products
Enter fullscreen mode Exit fullscreen mode

I don't assume there is no backend data fetching.

When navigation feels instant, I don't assume there is no network activity.

And when I see a fast page, I don't just ask:

"What framework are they using?"

I ask:

"Where is the work happening, when is it happening, and which layer is responsible for making it feel fast?"

That question is much more powerful.


The Bigger Lesson

Modern web performance isn't one trick.

It's a chain of decisions.

Maybe the database query happens on the server.

Maybe the page is statically generated.

Maybe the browser prefetched the next route.

Maybe Cloudflare delivered an asset from the edge.

Maybe Brotli reduced transfer size.

Maybe the hero image was prioritized.

Maybe below-the-fold images were lazy-loaded.

Maybe the browser reused previously fetched resources.

Maybe several of those things happened together.

That's why a site can look like it is doing "almost nothing" in the browser while actually doing a lot of work behind the scenes.

And honestly, that's what made this investigation fun.

I started by asking:

"Where is the API request?"

I ended by asking:

"What is the entire delivery architecture?"

That is a much better question to ask when building fast web applications.


What I'm Taking Into My Next.js Projects

My current checklist is now roughly:

✓ Server-render public catalog pages where appropriate
✓ Keep database access on the server
✓ Use client components only where interaction needs them
✓ Prefetch useful navigation where appropriate
✓ Optimize images
✓ Lazy-load content below the fold
✓ Prioritize critical resources selectively
✓ Use Cloudflare for the right caching layer
✓ Never blindly cache personalized pages
✓ Validate prices/stock on the server
✓ Measure with DevTools instead of guessing
Enter fullscreen mode Exit fullscreen mode

The biggest change isn't in my code.

It's in how I think about performance.

Don't just look for requests. Look for the system behind the requests.


Final Thoughts

This whole thing started with a very simple question:

"Why can't I find the API request?"

But that question led me into:

  • Server-side rendering
  • Server Components
  • Next.js App Router
  • Prefetching
  • Client-side navigation
  • Cloudflare
  • CDN caching
  • Cache-Control
  • Brotli compression
  • Priority Hints
  • Lazy loading
  • Image optimization
  • Static vs dynamic rendering
  • Client-side cart architecture
  • Server-side checkout validation

And that's exactly what I love about web development.

Sometimes you open DevTools looking for one request...

…and end up discovering an entire architecture.


Further Reading


Don't just look for the API request. Look for where the work is happening.

Top comments (0)