Nextjs
April 15, 2025 • 1 min read

Stop exposing your API endpoints in Next.js

A quick look at why calling APIs from the client in Next.js can expose sensitive info—and how moving the call to the server keeps your endpoints safer.

Stop exposing your API endpoints in Next.js
Table of contents
Table of contents

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:

API call through client side in nextjs

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:

API call through server side in nextjs

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!