| title | API Key Proxy (BFF Pattern) | ||||||
|---|---|---|---|---|---|---|---|
| impact | HIGH | ||||||
| impactDescription | Placing X-Api-Key in client-side code exposes the key to all users via browser DevTools and network requests | ||||||
| type | capability | ||||||
| tags |
|
Proxy all NextDNS API calls through Next.js Route Handlers to keep X-Api-Key server-side only
The NextDNS X-Api-Key grants full account access. It must never appear in browser-visible
code. Next.js Route Handlers (app/api/) run exclusively on the server, making them the correct
place to attach the key before forwarding requests to api.nextdns.io.
Browser → /api/* (Next.js Route Handler) → api.nextdns.io (X-Api-Key added here)
# .env.local (gitignored by default)
NEXTDNS_API_KEY=YOUR_API_KEY
NEXTDNS_PROFILE_ID=abc123// ✅ lib/nextdns.ts — reusable server-side fetcher
export async function nextdnsFetch<T>(path: string, options?: RequestInit): Promise<T> {
const apiKey = process.env.NEXTDNS_API_KEY;
if (!apiKey) throw new Error('NEXTDNS_API_KEY is not set');
const res = await fetch(`https://api.nextdns.io${path}`, {
...options,
headers: {
'X-Api-Key': apiKey,
'Content-Type': 'application/json',
...(options?.headers ?? {}),
},
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body?.errors?.[0]?.detail ?? `NextDNS API error: ${res.status}`);
}
return res.json() as Promise<T>;
}// ✅ app/api/profiles/route.ts
import { nextdnsFetch } from '@/lib/nextdns';
import { NextResponse } from 'next/server';
export async function GET() {
const data = await nextdnsFetch('/profiles');
return NextResponse.json(data);
}// ✅ app/api/profiles/[id]/route.ts
import { nextdnsFetch } from '@/lib/nextdns';
import { NextResponse } from 'next/server';
export async function GET(_req: Request, context: RouteContext<{ id: string }>) {
const { id } = await context.params;
const data = await nextdnsFetch(`/profiles/${id}`);
return NextResponse.json(data);
}
export async function PATCH(req: Request, context: RouteContext<{ id: string }>) {
const { id } = await context.params;
const body = await req.json();
const data = await nextdnsFetch(`/profiles/${id}`, {
method: 'PATCH',
body: JSON.stringify(body),
});
return NextResponse.json(data);
}// ❌ Never prefix the API key with NEXT_PUBLIC_ — it gets bundled into the client JS
// .env.local
NEXT_PUBLIC_NEXTDNS_API_KEY = YOUR_API_KEY; // ❌ Visible in browser source
// ❌ Never call api.nextdns.io directly from a Client Component
('use client');
const res = await fetch('https://api.nextdns.io/profiles', {
headers: { 'X-Api-Key': process.env.NEXT_PUBLIC_NEXTDNS_API_KEY }, // ❌ Key exposed
});- One utility, one key location: Use
lib/nextdns.tsas the single place where the key is attached — never repeatprocess.env.NEXTDNS_API_KEYacross Route Handler files. - No
NEXT_PUBLIC_prefix: Non-prefixed env vars in.env.localare server-only and never bundled into client JavaScript. UseNEXTDNS_API_KEY, notNEXT_PUBLIC_NEXTDNS_API_KEY. - Rotate keys without redeploying: Keep the key in
.env.localor a hosting secret store.
Symptoms: The utility throws NEXTDNS_API_KEY is not set.
Solution: Ensure the variable is defined in .env.local (development) or in your hosting
platform's environment secrets (production). Next.js does NOT auto-load .env files at runtime in
production — secrets must be set as OS-level env vars.
# Verify the env var is set
echo $NEXTDNS_API_KEYSymptoms: The key appears in browser DevTools → Application → JavaScript.
Solution: Remove the NEXT_PUBLIC_ prefix and move all fetching logic to Route Handlers or
Server Components.