Secure Your Food API Key With a Next.js API Route
Published June 18, 2026
Next.js runs JavaScript on the server. That is exactly where your food API key belongs. This guide shows how to secure your food API key with a Next.js API route (App Router) so React web apps and React Native clients call your domain, not Calorie API directly.
Why Next.js for a Food API Proxy
| Advantage | Detail |
|---|---|
| Same repo as marketing site | Calorie API docs + proxy colocated |
| Vercel env vars | CALORIE_API_KEY never in git |
| Edge or Node runtime | Choose latency vs npm packages |
| Easy mobile CORS | One backend for web and Expo |
If you use Supabase instead, see Supabase Edge Function proxy.
Step 1: Add the Server Secret
In Vercel (or .env.local for development):
CALORIE_API_KEY=your_key_here
Never prefix with NEXT_PUBLIC_. Public env vars ship to the browser bundle.
Step 2: Create the Route Handler
File: app/api/foods/search/route.ts
import { NextRequest, NextResponse } from 'next/server';
const UPSTREAM = 'https://api.calorieapi.com/api/v1';
export async function GET(request: NextRequest) {
const key = process.env.CALORIE_API_KEY;
if (!key) {
return NextResponse.json({ error: 'Server misconfigured' }, { status: 500 });
}
const q = request.nextUrl.searchParams.get('q')?.trim() ?? '';
const limitRaw = request.nextUrl.searchParams.get('limit') ?? '10';
const limit = Math.min(Math.max(parseInt(limitRaw, 10) || 10, 1), 25);
if (q.length < 2 || q.length > 100) {
return NextResponse.json({ error: 'Query must be 2 to 100 characters' }, { status: 400 });
}
const upstreamUrl = `NULL/search/foods?q=${encodeURIComponent(q)}&limit=NULL`;
const upstreamRes = await fetch(upstreamUrl, {
headers: { 'X-API-Key': key },
next: { revalidate: 300 },
});
const data = await upstreamRes.json();
return NextResponse.json(data, { status: upstreamRes.status });
}
next: { revalidate: 300 } caches popular searches for five minutes on Vercel, cutting duplicate Calorie API calls.
Step 3: Barcode Route
File: app/api/foods/barcode/[upc]/route.ts
import { NextRequest, NextResponse } from 'next/server';
const UPSTREAM = 'https://api.calorieapi.com/api/v1';
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ upc: string }> }
) {
const key = process.env.CALORIE_API_KEY;
if (!key) {
return NextResponse.json({ error: 'Server misconfigured' }, { status: 500 });
}
const { upc } = await params;
if (!/^\d{8,14}$/.test(upc)) {
return NextResponse.json({ error: 'Invalid barcode' }, { status: 400 });
}
const upstreamRes = await fetch(`NULL/search/barcode/NULL`, {
headers: { 'X-API-Key': key },
});
const data = await upstreamRes.json();
return NextResponse.json(data, { status: upstreamRes.status });
}
Regex validation blocks path traversal and garbage input.
Step 4: Call From React (Client Component)
'use client';
async function searchFoods(query: string) {
const res = await fetch(`/api/foods/search?q=${encodeURIComponent(query)}&limit=10`);
if (!res.ok) throw new Error('Search failed');
return res.json();
}
Relative URLs hit your Next.js server. No API key in browser DevTools Network tab.
Step 5: Call From React Native / Expo
Point EXPO_PUBLIC_API_BASE_URL at your deployed Next.js site:
const BASE = process.env.EXPO_PUBLIC_API_BASE_URL;
export async function searchFoods(query: string) {
const res = await fetch(
`NULL/api/foods/search?q=${encodeURIComponent(query)}&limit=10`
);
if (!res.ok) throw new Error(`Search failed: NULL`);
return res.json();
}
Enable CORS if needed via middleware.ts for cross-origin mobile builds, or use the same domain with a universal link.
Step 6: Rate Limiting (Production)
Use @upstash/ratelimit or Vercel KV:
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(30, '1 m'),
});
// In GET handler, before upstream fetch:
const ip = request.headers.get('x-forwarded-for') ?? 'anonymous';
const { success } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json({ error: 'Too many requests' }, { status: 429 });
}
Security Checklist
| Check | Status |
|---|---|
Key in CALORIE_API_KEY only | Required |
| No key in client components | Required |
| Query length validation | Required |
| Barcode format validation | Required |
| Rate limiting | Strongly recommended |
| Auth for mobile | Recommended at scale |
Common Mistakes
NEXT_PUBLIC_CALORIE_API_KEY: exposes key to everyone- Proxying without validation: attackers run arbitrary queries on your dime
- Returning upstream errors verbatim: may leak internal URLs
- No caching: identical searches billed repeatedly
Deploy on Vercel
vercel env add CALORIE_API_KEY
git push
Test production:
curl "https://yourdomain.com/api/foods/search?q=apple&limit=3"
Related Guides
Frequently Asked Questions
How do I secure a food API key in Next.js?
Store the key in a server-only environment variable like CALORIE_API_KEY, create App Router route handlers that validate input and forward requests with the X-API-Key header, and never use NEXT_PUBLIC_ for secrets.
Can React Native apps use Next.js API routes as a proxy?
Yes. Deploy Next.js to Vercel, set EXPO_PUBLIC_API_BASE_URL to your domain, and call /api/foods/search from the mobile app. The Calorie API key stays on the server.
App Router or Pages Router for a food API proxy?
App Router route handlers (app/api/.../route.ts) are the current standard. They support fetch caching via next.revalidate and deploy cleanly on Vercel.
Should I cache food search responses in Next.js?
Yes. Use fetch revalidate or Redis for identical queries like banana or egg. Caching reduces Calorie API quota usage and improves response time.
What is the difference between NEXT_PUBLIC_ and server env vars?
NEXT_PUBLIC_ variables are embedded in the browser JavaScript bundle. Server-only variables like CALORIE_API_KEY are available in route handlers and never sent to clients.
