How to Secure API Keys in a Next.js Application
- Author
- Vishal Maurya
- Published on
- Reading time
- 5 min read
Overview
A Next.js application may connect to an email provider, an AI API, a payment service, or an internal backend. Those integrations often require credentials. If a secret is included in JavaScript delivered to the browser, users can inspect the downloaded code or network requests and recover it.
Obfuscation does not protect a credential that the browser must use. The fix is to keep the secret on the server and make the browser call your own endpoint, which validates the request and then calls the provider.
1. Understand public environment variables
Next.js exposes environment variables prefixed with NEXT_PUBLIC_ to browser code. For example:
# Server-only credentials
EMAIL_PROVIDER_API_KEY=replace-with-a-secret
AI_PROVIDER_API_KEY=replace-with-a-secret
# Fine to expose only if this URL is intended to be public
NEXT_PUBLIC_API_BASE_URL=https://api.example.com
Do not prefix a secret with NEXT_PUBLIC_. Also do not assume a variable is safe just because it is stored in .env.local; how the value is imported and used determines whether it can reach the client bundle. Keep secret-bearing files out of version control and use your hosting platform's secret configuration in production.
2. Move provider calls into a Route Handler
A browser component should not call a secret-bearing provider directly. Create a server endpoint such as app/api/send-email/route.ts and call the provider from that file using a server-only environment variable.
// app/api/send-email/route.ts
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const apiKey = process.env.EMAIL_PROVIDER_API_KEY;
if (!apiKey) {
return NextResponse.json(
{ error: "Email service is not configured" },
{ status: 500 },
);
}
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}
if (
typeof body !== "object" ||
body === null ||
!("email" in body) ||
typeof body.email !== "string"
) {
return NextResponse.json(
{ error: "A valid email is required" },
{ status: 400 },
);
}
const email = body.email.trim();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || email.length > 254) {
return NextResponse.json({ error: "Invalid email" }, { status: 400 });
}
// Authenticate/authorize the caller and apply rate limits before sending.
// Call the provider here using apiKey; never return the key to the client.
return NextResponse.json({ message: "Request validated" });
}
This snippet demonstrates the server boundary and validation; it intentionally does not send an email yet. Replace the final section with the provider's documented SDK or API call, handle provider errors, and return only a safe result. The unknown request body is narrowed through explicit checks rather than assumed to have the expected shape.
3. Call your own endpoint from the client
"use client";
export async function sendEmail(email: string) {
const response = await fetch("/api/send-email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
if (!response.ok) {
throw new Error("Unable to submit the request");
}
return response.json();
}
The browser can see /api/send-email; that is expected. The endpoint is not a secret. Its job is to enforce the application's policy while the provider credential remains server-side.
4. Protect the endpoint itself
Moving the credential to the server does not automatically make the operation safe. If anyone can call the endpoint repeatedly, they may still trigger unwanted email or create provider costs.
- Authenticate callers when the operation is for signed-in users.
- Authorize the specific action and resource, not just the session.
- Validate input size, type, and allowed values.
- Add rate limits and abuse controls.
- Return safe error messages rather than raw provider responses or stack traces.
- Log the operation without recording credentials or sensitive payloads.
- Use credentials with the smallest practical permissions and quotas.
For endpoints that mutate data, also consider replay protection, idempotency, and CSRF protections appropriate to your authentication model.
5. Store production secrets appropriately
Use .env.local for local development and exclude it from Git. In production, store credentials in your deployment platform's environment or secret-management facility. Keep separate credentials for development, staging, and production where possible. Avoid making a developer's personal API key the permanent credential for a deployed service.
For cloud workloads, grant secret access to the runtime identity that needs it. Do not distribute a broad cloud credential to every part of the application if a narrower identity or provider key will do.
6. Respond correctly if a key was exposed
If a secret has appeared in browser assets, a public repository, or an unintended log, treat it as compromised.
- Revoke or rotate the credential with the provider.
- Check usage, billing, and audit logs for suspicious activity.
- Remove the browser-side use and move the integration to server-side code.
- Update deployment secrets and redeploy.
- Remove the secret from repository history and artifacts where possible, while recognizing that copies may remain in forks, caches, or downloaded bundles.
- Search for other credentials exposed through the same path.
Removing the string from the latest commit is not enough. Rotation is the immediate protective step.
7. Verify the change
Inspect the production build and browser network activity. Confirm that private credentials do not appear in downloaded JavaScript, the browser calls your application endpoint, and the server makes the provider request. Test unauthenticated requests, invalid input, and excessive request rates. Check that errors do not reveal secrets or internal details.
Secret scanning in CI and code review can catch accidental commits earlier, but it should complement—not replace—good runtime boundaries.
Conclusion
A secure Next.js integration keeps private credentials on the server and treats browser-accessible endpoints as public entry points that need appropriate validation and access controls. If you need to move a provider integration out of the client, investigate an exposed key, or review a Next.js backend boundary, I can help implement the change and verify it in production.
Contact me with a short description of the integration and the environment where it runs.