Fixing Next.js URL Rewriting: Use a Proxy to Hide Your Backend API
I’m working on a Next.js application, and today I discovered some interesting points regarding URL rewriting. If you’re not familiar with URL rewriting, it’s a feature that allows you to rewrite any URL in different ways. Essentially, it can forward requests to another server/url, functioning similarly to a proxy server. This way, the original backend API or any target server where you forward requests is not directly visible to the frontend, which I find pretty cool.
Most people might simply forward any request directly to the backend, assuming it’s the backend developer’s responsibility to handle it. While most people might not check where a request is being forwarded, I always do! I like to inspect where each request is headed, what headers are added, and the responses from APIs. It feels amazing to dive deep into the system. That’s why I test my servers to ensure nothing is exposed to the public that could potentially harm the business.
Long story short, I’m working on a Next.js application with a Laravel backend. Since the project began, I set up separate routes in Next.js’s API folder to forward requests to the server. This approach worked well initially, but as the project grew, it became less efficient. Now, I’m searching for a better alternative.
I’m looking for a more optimal solution to keep things secure and manageable as the application scales.
After researching, I discovered Next.js’s URL rewrite feature in the next.config.js
file. With this feature, I can add a rewrite
function to define the source
and destination
URLs, which works perfectly. It’s incredibly helpful for redirecting URLs without returning a 301 response code. This feature has a lot of flexibility, allowing you to fully customize redirection behavior, from simple URL redirects to fully dynamic URL patterns.
You can read more about it here.
You can simply define like this. Here you can see we telling that redirect the /about
to the homepage or /
module.exports = {
async rewrites() {
return [
{
source: '/about',
destination: '/',
},
]
},
}
But my problem is that not that simple i cannot define a static url like so i have modify this little bit.
module.exports = {
async rewrites() {
return [
{
source: "/api/proxy/:endpoint",
destination: `${process.env.REACT_APP_BASE_URL}/:endpoint`,
},
]
},
}
I’ve set up a forwarding mechanism for any requests coming to /api/proxy/*
so they’re redirected directly to the backend server. Here’s an example of how it works:
- Source:
https://www.example.com/api/proxy/product
- Destination:
https://admin.example.com/product
This setup can handle multiple requests dynamically. So far, it’s been working well, but one day, while working on a different module, I discovered an issue. POST requests to the endpoint were returning a 404 error, while GET requests were working fine. This caught me by surprise 🙂, and after some debugging, I couldn’t find a fix.
Then, an idea struck: what if I created a catch-all route in Next.js? This would be similar to the pattern used by NextAuth (aka Auth.js), where a catch-all route is set up to handle all incoming requests. For instance, their catch-all route follows this structure:
api/auth/[...nextauth]/route.ts
Using a similar approach, I could route all requests to the backend server. However, I’d need a robust solution that can handle various request types with proper error handling. My goal is to create a long-term solution that won’t require further changes down the line.
So I have created this route folder
api/admin/[...path]/route.ts
In this code I’m handling all types of requests I have created and handleRequest()
function which is used for all requests here we have also included the required headres the best thing all things will remain in the server side this is how Next.js works.
We have also handled the error as will which make code pretty solid.
import { auth } from "@/auth";
import { NextRequest, NextResponse } from "next/server";
const BACKEND_URL = process.env.REACT_APP_BASE_URL;
export async function GET(request: NextRequest) {
return handleRequest(request);
}
export async function POST(request: NextRequest) {
return handleRequest(request);
}
export async function PUT(request: NextRequest) {
return handleRequest(request);
}
export async function DELETE(request: NextRequest) {
return handleRequest(request);
}
async function handleRequest(request: NextRequest) {
const session = await auth();
try {
const path = request.url.split("/api/admin/")[1];
const url = `${BACKEND_URL}/${path}`;
// Forward the request
const response = await fetch(url, {
method: request.method,
headers: {
"Content-Type": "application/json",
"device-type": process.env.DEVICE_TYPE!,
"app-version": process.env.API_VERSION!,
Authorization: `Bearer ${session?.user?.token}`,
},
body: request.method !== "GET" ? await request.text() : undefined,
});
if (!response.ok) {
throw response;
}
const data = await response.json();
return NextResponse.json(data);
} catch (error) {
// Handle different types of errors
if (error instanceof Response) {
// This is a response error from the backend
try {
const errorData = await error.json();
return NextResponse.json(
{
success: false,
...errorData,
},
{ status: error.status }
);
} catch {
return NextResponse.json(
{ success: false, message: error.statusText || "Backend error" },
{ status: error.status }
);
}
}
// For fetch errors (network issues, etc)
if (error instanceof TypeError) {
return NextResponse.json(
{ error: "Network error", details: error.message },
{ status: 503 }
);
}
// For any other errors
console.error("Proxy error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
This proxy approach has some serious benefits:
- Better UX: Proper error handling means your users get meaningful feedback when something goes wrong.
- Security: Your backend URLs stay completely hidden from users, reducing the risk of unauthorized access.
- Flexibility: You can update or even change your backend without affecting the frontend code, as long as the API contract stays the same.
- Reusability: The proxy can be reused across your whole app, simplifying API consumption and ensuring consistent error handling.
- Type Safety: By using TypeScript, we can catch potential issues during development, not at runtime.
Wrapping Up
When Next.js URL rewriting just isn’t cutting it, a dedicated API proxy can be the solution you need to keep your backend API endpoints secure and hidden from your users. By forwarding requests through a proxy layer, you can maintain full control over your API access while also improving the overall flexibility and maintainability of your application.
Give this proxy approach a try in your Next.js project, and let me know if you have any questions!