This post is gonna be super brief—just enough to demonstrate how exposed your API design can be when you’re calling it directly from a client-side component.
I also want you to understand why, at some point in your web dev journey, a senior developer might tell you:
“Move that API call to the server.”
And ideally, you’ll get why that advice matters.
The problem
Let me spin up a barebones Next.js app to demo this. I’ll use reqres.in since it’s a public API—perfect for testing stuff.
As usual, we start by adding a .env
file:
NEXT_PUBLIC_API_URL=https://reqres.in/api
Then, in our homepage component src/app/page.tsx
, we mark it as a client component with 'use client'
and write a simple API call inside it:
// src/app/page.tsx
const fetchData = async () => {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/users?page=2`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error("Network response was not ok");
}
const data = await response.json();
console.log(data);
};
Now, check this out:
It’s kind of hilarious (and terrifying?)—your supposedly “hidden” .env
value shows right up in the browser’s DevTools network tab.
Why?
Because anything prefixed with NEXT_PUBLIC_
is meant to be exposed to the client. It’s no longer a secret—it’s just config.
That means you’re exposing not just the endpoint, but potentially the structure of your system.
So while it’s not necessarily dangerous on its own, it definitely increases the surface area for abuse.
The fix: Hide that endpoint
So how do we protect it?
One of the simplest solutions is to move the API call to the server. Next.js makes this super easy. There are multiple ways to do it (read the official docs for more), but let’s just focus on one for this demo.
First, create a new file:
src/app/api/users/route.ts
The /users
part reflects the data we’re fetching. Here’s the code inside:
// src/app/api/users/route.ts
import { NextRequest } from "next/server";
export async function GET(request: NextRequest,) {
const page = request.nextUrl.searchParams.get("page") || 1;
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/users?page=${page}`, {
headers: {
"Content-Type": "application/json",
},
});
const data = await res.json();
return Response.json(data);
}
Then, update your frontend fetchData
function to call your internal API route instead—and since the fetching now happens on the server, you can remove that 'use client'
directive at the top.
// src/app/page.tsx
const fetchData = async () => {
const pageNumber = 2;
const res = await fetch(`/api/users?page=${pageNumber}`);
if (!res.ok) throw new Error('Failed to fetch users');
const data = await res.json();
console.log(data);
};
And now? Take a look at the DevTools again:
Boom—just a clean call to /api/users
, with the actual external endpoint nicely hidden away on the server.
That’s it. Just wanted to put this out there.
See ya!