Calling Private APIs Safely
Many web apps need to call services that require private credentials. An email provider may require an API key. A payment provider may require a secret key. A CRM, database, AI service, shipping provider, analytics export, or internal company API may require a token that must never appear in browser JavaScript. Vercel can help by giving you server-side Functions and Environment Variables, but you still need to design the boundary correctly.
The core rule is simple: if a credential grants private or privileged access, keep it server-side. The browser is not a secret place. Users can inspect JavaScript bundles, network requests, source maps, local storage, session storage, and DevTools output. If the frontend has the secret, the user can usually extract it.
Public API vs Private API
A public API is designed to be called from a browser or public client. It may use public keys, domain restrictions, scoped tokens, rate limits, or unauthenticated read-only endpoints. Public does not mean careless, but it means the API provider expects client-side access.
A private API expects trusted server-side callers. It might allow sending email, issuing refunds, reading customer records, creating admin users, modifying inventory, or accessing a database. A private API key is not safe in React, Vite, plain JavaScript, or a Client Component.
Before connecting any API, classify it:
Can this key be visible to users?
Can this key write data?
Can this key charge money or send emails?
Can this key read private customer data?
Can this key bypass normal user authorization?
Can this key be restricted to a narrow domain or action?
If exposure would create real damage, treat it as private.
The Safe Middle-Layer Pattern
The common safe pattern is a server-side middle layer:
Browser -> Vercel Function -> private API
The browser sends a limited request to your Function. The Function validates the request, reads the private API key from Vercel Environment Variables, calls the private API, filters the response, and returns only safe data to the browser.
This design protects the secret and gives your app control over behavior. You can reject invalid input before it reaches the provider. You can map provider errors into user-friendly messages. You can rate limit, log, retry, cache, or block abusive requests. You can change providers later without rewriting every frontend component.
A contact form is the simplest example:
Browser sends name, email, and message.
Function validates the fields.
Function calls the email provider with a server-side token.
Function returns ok or a small error.
The frontend never needs the email provider token.
What Not To Do
Do not put private keys in frontend environment variables. In Vite, variables prefixed with VITE_ are intended for browser exposure. In Next.js, variables prefixed with NEXT_PUBLIC_ are intended for browser exposure. Those prefixes are useful for public configuration, not for secrets.
This is unsafe:
VITE_EMAIL_API_KEY=secret
NEXT_PUBLIC_STRIPE_SECRET_KEY=secret
NEXT_PUBLIC_DATABASE_URL=secret
The names are already a warning. A value that is bundled into browser JavaScript is not private, even if the variable was stored in the Vercel dashboard first.
Do not hide secrets in source files, comments, JSON fixtures, generated static files, public assets, .env files committed to Git, or frontend build output. Do not rely on .vercelignore as secret management. The secret should not be in the repository in the first place.
A Small Function Shape
A private API wrapper should be narrow. It should accept only the inputs the frontend is allowed to control. It should decide sensitive values on the server.
A conceptual endpoint might look like this:
export async function POST(request: Request) {
const body = await request.json();
if (typeof body.message !== 'string' || body.message.length > 2000) {
return Response.json({ error: 'Message is invalid.' }, { status: 400 });
}
const token = process.env.EMAIL_API_TOKEN;
if (!token) {
console.error('EMAIL_API_TOKEN is missing');
return Response.json({ error: 'Service unavailable.' }, { status: 500 });
}
return Response.json({ ok: true });
}
This is not a complete email integration. It shows the shape: parse, validate, read the secret on the server, handle missing configuration, and return a small response. Real provider calls should also handle provider errors, timeouts, retries where appropriate, and abuse controls.
Validate Before Calling The Provider
Validation protects your app and reduces unnecessary provider usage. If a request is obviously invalid, reject it before calling the private API. This matters for cost, rate limits, abuse, and clear user feedback.
For a contact form, validate field presence, length, type, and basic format. For a checkout endpoint, validate product identifiers against server-side data instead of trusting a browser-provided price. For a database lookup, validate that the current user is allowed to access the requested record. For an AI API, validate prompt length and allowed use before spending tokens.
Frontend validation is still useful for a better user experience, but it is not a security boundary. Anyone can bypass it and call the Function directly.
Filter Responses
Private APIs often return more data than the browser needs. Do not pass provider responses through blindly. Map the response into your own contract.
A payment provider might return internal ids, risk information, customer metadata, or debugging details. An email provider might return account information. A CRM might return private notes. A database might return columns that were not meant for this page.
A safe wrapper returns only what the frontend needs:
{
"ok": true,
"message": "Your request was sent."
}
That small response is easier to test and safer to change. It also prevents your frontend from depending on a third-party provider's full response format.
Keep Authorization On The Server
A private API wrapper is not safe just because the key is hidden. The Function must still decide whether the caller is allowed to perform the action. If an endpoint updates a user profile, the server should know which user is authenticated and which profile can be changed. If an endpoint reads an invoice, the server should verify ownership. If an endpoint creates a paid action, the server should decide the price and product.
Do not let the browser send fields like this and trust them blindly:
{
"userId": "someone-else",
"isAdmin": true,
"price": 1
}
The browser can suggest intent, but the server must enforce authority.
Environment Scopes
Use separate Vercel Environment Variable values for Local, Preview, and Production when the provider supports separate environments. A Preview Deployment should usually talk to a sandbox, test account, staging database, or non-production provider. Production should use production credentials only after the deployment has been reviewed.
A useful pattern is:
Local: test provider key
Preview: staging provider key
Production: production provider key
Keep variable names stable across environments. Change the value, not the application code. This makes deployments easier to reason about and reduces conditional branches in the frontend.
When rotating a secret, update the provider and Vercel environment values deliberately. Redeploy anything that needs build-time access. Test both success and failure paths after rotation.
Logging And Errors
Logs help you debug private API calls, but logs can also leak private data. Log the provider name, status code, request id, or failure category. Do not log API keys, authorization headers, cookies, full customer records, full webhook payloads, or payment details.
Return user-safe errors to the browser. A provider stack trace or raw JSON error may reveal implementation details. The frontend usually needs a short message such as Service unavailable or Please try again later, while the Runtime Logs keep the internal status for developers.
Design A Narrow Contract
A safe private API wrapper should feel smaller than the provider it calls. The frontend should not know every provider option, internal account id, retry flag, or administrative parameter. Give the browser a contract that matches the user action. For a contact form, that contract might be name, email, and message. For a checkout flow, it might be a product id and quantity. For a support lookup, it might be the current user's own ticket id.
This narrow contract reduces both security risk and future maintenance. If the provider changes its field names, authentication scheme, or response format, the frontend can keep calling your stable endpoint. If an attacker modifies a request, there are fewer dangerous fields to tamper with. If a teammate reviews the endpoint, the intent is easier to understand because each accepted field has a clear reason to exist.
When you design the contract, also design the failure behavior. Decide which errors the user can fix, which errors should be retried, and which errors should only be logged for developers. A private API wrapper is part of your product experience, not just a hidden tunnel to another service.
For client and business projects, review the contract with the same care as a public API. Confirm who can call it, what each field means, which provider account it uses, what happens during provider downtime, and who owns secret rotation. That review catches many mistakes before the endpoint becomes a production dependency.
Common Mistakes
The first mistake is assuming an environment variable is secret after it has been bundled into frontend JavaScript.
The second mistake is using a Vercel Function only as a thin proxy with no validation, filtering, authorization, or error mapping.
The third mistake is trusting browser-provided prices, roles, user ids, or provider parameters.
The fourth mistake is logging the exact secret you were trying to protect.
The fifth mistake is using production provider credentials in Preview Deployments without a reason.
The sixth mistake is returning the entire provider response to the frontend.
Review Questions
- How can you tell whether an API key must stay server-side?
- Why is the browser not a safe place for private credentials?
- What does the server-side middle-layer pattern protect?
- Why should a Function filter provider responses?
- What belongs in Runtime Logs, and what should never be logged?
- Why should Preview and Production use different provider credentials when possible?