
If you host your frontend on Vercel and your backend on a custom VPS, your frontend usually makes API requests directly to the VPS's public IP address or domain. While this works, it exposes your backend server's true IP address to the world, making it vulnerable to direct DDoS attacks, port scanners, and malicious bots.
By using Vercel Rewrites, you can use Vercel's edge network as a shield. To the outside world, your API requests appear to be going to Vercel, while Vercel secretly fetches the data from your VPS behind the scenes.
Here is how you can set up a secure API proxy in Next.js (App Router) and lock down your backend to reject any request that tries to bypass Vercel.
The Architecture
sequenceDiagram
actor User
participant Vercel as Vercel (Next.js)
participant VPS as VPS (NestJS Backend)
User->>Vercel: Fetch /api-proxy/data
Note over Vercel: Middleware injects secret header<br/>next.config.ts rewrites path
Vercel->>VPS: Fetch /data (with Secret Header)
alt Valid Secret
VPS-->>Vercel: 200 OK (Data)
Vercel-->>User: 200 OK (Data)
else Missing/Invalid Secret
VPS-->>Vercel: 403 Forbidden
end
User-xVPS: Direct Request (Blocked 403)
Step 1: Configure Next.js Rewrites
First, we need to tell Next.js to intercept requests made to a specific proxy path (e.g., /api-proxy/*) and forward them to your VPS. We achieve this by adding a rewrite rule to next.config.ts.
To ensure we don't accidentally create proxy loops, we will use a specific environment variable BACKEND_URL solely for the rewrite destination.
// next.config.ts
export default {
async rewrites() {
// The true IP of your VPS, safely hidden in Vercel environment variables
const apiUrl = process.env.BACKEND_URL || 'http://localhost:4000';
return [
{
source: '/api-proxy/:path*',
destination: `${apiUrl}/:path*`
},
];
},
};
Step 2: Inject a Secret Header at the Edge
Now that the request is proxying through Vercel, we need a way for our backend to know that the request actually came from Vercel. We do this by injecting a secret header.
In Next.js, we can use Edge Middleware to intercept the request and inject the header before the next.config.ts rewrite takes effect.
// middleware.ts (or proxy.ts if you use a custom setup)
import { NextResponse, type NextRequest } from 'next/server';
export function middleware(req: NextRequest) {
const requestHeaders = new Headers(req.headers);
const proxySecret = process.env.VERCEL_PROXY_SECRET;
// Inject the secret key into the headers
if (proxySecret) {
requestHeaders.set('x-vercel-proxy-secret', proxySecret);
}
// Pass the modified headers along to the Next.js rewrite engine
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}
export const config = {
matcher: ['/api-proxy/:path*'],
};
Step 3: Lock Down the Backend
If an attacker guesses your VPS IP, they could still bypass Vercel entirely. We need to configure the backend to reject any request that lacks the secret header.
Here is an example of how to enforce this using a global hook in a NestJS + Fastify backend:
// backend/src/main.ts
const fastifyInstance = app.getHttpAdapter().getInstance();
fastifyInstance.addHook('onRequest', (request, reply, done) => {
const proxySecret = process.env.VERCEL_PROXY_SECRET;
// Exclude localhost (127.0.0.1) so internal Docker health checks still work!
const isLocal = request.ip === '127.0.0.1' || request.ip === '::1';
if (proxySecret && !isLocal) {
const incomingSecret = request.headers['x-vercel-proxy-secret'];
if (incomingSecret !== proxySecret) {
reply.status(403).send({ message: 'Forbidden: Invalid proxy secret' });
return;
}
}
done();
});
[!TIP] Docker Health Checks: Notice the
isLocalcheck. If you use Docker, your container's internal health check pinginglocalhostwon't have the secret header. Bypassing the check for local IPs prevents your deployments from rolling back due to failed health checks!
Step 4: Fixing Server-Side Rendering (SSR/SSG)
There's one hidden "gotcha" with this setup. During the Next.js build process (next build), Next.js generates static pages and makes fetch requests directly to your backend API.
Because these requests originate from the build server and bypass your Next.js middleware, they will fail with a 403 Forbidden error because they lack the x-vercel-proxy-secret header.
To fix this, you must manually inject the secret into your internal API client or Apollo Client configuration:
// lib/apollo-server.ts
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
export const apolloServerClient = new ApolloClient({
ssrMode: true,
link: new HttpLink({
uri: `${API_URL}/graphql`,
fetch,
headers: process.env.VERCEL_PROXY_SECRET
? { 'x-vercel-proxy-secret': process.env.VERCEL_PROXY_SECRET }
: {}
}),
cache: new InMemoryCache(),
});
Summary
By combining Next.js rewrites, Edge middleware, and a simple backend validation check, you can successfully shield your custom VPS behind Vercel's robust infrastructure. Your true IP address remains a secret, and your API is locked down against direct unauthorized access!
Comments (0)
Login to post a comment.