Vercel en Producción: La Guía Definitiva de Deployment que Nadie Escribe
Vercel deployment best practices for production: vercel.json config, preview deployments as QA, strategic caching with revalidateTag, monorepo setup, and structured logging.
La Mayoría de Developers Despliegan en Vercel Como Si Fuera Shared Hosting
Subes el código. Haces push. Funciona.
Y en producción con tráfico real, todo colapsa.
*El problema no es Vercel. Es que usas una plataforma de deployment profesional como si fuera un hosting estático.*
Esta guía no repite lo básico. Asume que ya sabes hacer `vercel deploy`. Lo que vas a ver aquí es la arquitectura de decisiones que la documentación oficial nunca prioriza.
La Arquitectura Real de un Deploy en Vercel
Vercel no es solo "hosting para Next.js". Es una plataforma con tres capas que la mayoría confunde:
→ Edge Network — CDN global, caché de assets estáticos, middleware
→ Serverless Functions — Node.js runtime, APIs, SSR
→ Edge Functions — JavaScript V8 isolates, sin Node.js APIs, latencia mínima
*El error crítico es mezclar estas capas sin entender sus trade-offs.*
Cuando despliegas sin estrategia, Vercel ejecuta todo en Serverless por defecto. Eso funciona. Pero no es óptimo ni para rendimiento ni para coste.
1. Estructura tu `vercel.json` desde el día uno
La mayoría crea el archivo `vercel.json` cuando algo falla. Ya es tarde.
Este es el esqueleto que uso en todos mis proyectos de producción:
```json
{
"version": 2,
"regions": ["cdg1", "mad1"],
"functions": {
"app/api/webhooks/**": {
"maxDuration": 60,
"memory": 1024
},
"app/api/ai/**": {
"maxDuration": 300,
"memory": 3009
},
"app/api/**": {
"maxDuration": 10,
"memory": 512
}
},
"headers": [
{
"source": "/api/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "no-store" },
{ "key": "X-Content-Type-Options", "value": "nosniff" }
]
}
]
}
```
Tres puntos críticos aquí:
Primero, `regions` define dónde se despliegan tus Serverless Functions. `cdg1` es París, `mad1` es Madrid. Si tus usuarios son europeos, esto reduce la latencia de forma significativa.
Segundo, los `maxDuration` por ruta. Las funciones de webhook necesitan más tiempo. Las funciones de AI aún más. Las APIs estándar deben ser rápidas o hay un problema de arquitectura.
Tercero, headers de seguridad en el nivel de infraestructura, no en el código de la aplicación.
Preview Deployments: La Herramienta de QA que Infrautilizas
La mayoría usa los preview deployments solo para "ver cómo queda". Error.
*El preview deployment es tu entorno de staging gratuito con URL pública.*
Cada push a cualquier rama genera una URL única e inmutable. Eso significa:
→ Puedes compartirla con el cliente antes de mergear
→ Puedes ejecutar tests E2E contra una URL real
→ Puedes hacer QA en mobile sin configurar nada
Este es el workflow que uso con GitHub Actions:
```yaml
name: E2E Tests on Preview
on:
deployment_status:
types: [created]
jobs:
test-preview:
if: github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
steps:
uses: actions/checkout@v4
name: Get Preview URL
id: preview
run: |
echo "url=${{ github.event.deployment_status.target_url }}" >> $GITHUB_OUTPUT
name: Run Playwright Tests
uses: microsoft/playwright-github-action@v1
with:
args: test --project=chromium
env:
PLAYWRIGHT_TEST_BASE_URL: ${{ steps.preview.outputs.url }}
```
Cada preview deployment dispara tests automáticos. Si fallan, el PR no se mergea.
Este pipeline solo funciona correctamente si tus environment variables están configuradas por entorno — no globalmente. Vercel permite variables específicas para production, preview, y development. Úsalas.
Caching Estratégico: La Diferencia entre 50ms y 2 segundos
❌ Lo que hace la mayoría:
```javascript
// app/api/products/route.ts
export async function GET() {
const products = await db.query('SELECT * FROM products');
return Response.json(products);
}
```
Cero caching. Cada request hits the database. Con tráfico real, esto es un cuello de botella garantizado.
✅ Lo que debes hacer:
```javascript
// app/api/products/route.ts
import { unstable_cache } from 'next/cache';
const getCachedProducts = unstable_cache(
async () => {
return db.query('SELECT * FROM products WHERE active = true');
},
['products-list'],
{
revalidate: 300, // 5 minutos
tags: ['products']
}
);
export async function GET() {
const products = await getCachedProducts();
return Response.json(products);
}
// En tu action de actualización:
export async function updateProduct(id: string, data: ProductData) {
await db.update('products', id, data);
revalidateTag('products'); // Invalida el cache selectivamente
}
```
*La clave no es cachear todo. Es invalidar selectivamente.*
`revalidateTag` es la función que separa las arquitecturas de producción del prototipo. Cuando actualizas un producto, invalidas exactamente ese cache. No recargas la página. No haces un deploy.
2. Route Segment Config para Control Granular
```typescript
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic'; // Nunca cachear
export const revalidate = 0;
// app/blog/[slug]/page.tsx
export const revalidate = 3600; // 1 hora
export const dynamicParams = true;
// app/pricing/page.tsx
export const revalidate = false; // Static, build time only
```
Tres páginas. Tres estrategias diferentes. Cada una optimizada para su patrón de datos.
El dashboard necesita datos en tiempo real. El blog puede ser relativamente estático. Pricing casi nunca cambia.
Sin esta granularidad, Vercel no puede optimizar nada. Tú tomas las decisiones. Vercel las ejecuta.
Monorepos en Vercel: La Configuración que el 90% Ignora
Si tienes un monorepo con Turborepo y múltiples apps, la configuración por defecto de Vercel es un desastre.
Vercel rebuildeará todas las apps en cada push si no configuras los `ignoreBuildStep`.
```json
// vercel.json en /apps/web
{
"ignoreCommand": "npx turbo-ignore"
}
```
```json
// vercel.json en /apps/dashboard
{
"ignoreCommand": "npx turbo-ignore"
}
```
`turbo-ignore` detecta automáticamente si los cambios afectan a esa app específica. Si no hay cambios relevantes, cancela el build.
*En un monorepo activo, esto reduce los builds innecesarios drásticamente.*
Para proyectos con Turborepo, también activa Remote Caching de Vercel. Es el mismo mecanismo que `turbo-ignore` pero para los artefactos de build. Tu CI descarga el cache de builds previos en vez de recompilar desde cero.
```bash
Conectar Turborepo Remote Cache con Vercel
npx turbo login
npx turbo link
```
Observabilidad: Logs que Realmente Te Dicen Algo
❌ El approach típico:
Revisar logs en el dashboard de Vercel cuando algo falla. Reactive, no proactive.
✅ El approach de producción:
Structured logging desde el primer día + alertas automáticas.
```typescript
// lib/logger.ts
type LogLevel = 'info' | 'warn' | 'error';
interface LogEntry {
level: LogLevel;
message: string;
requestId?: string;
duration?: number;
metadata?: Record<string, unknown>;
}
export function log(entry: LogEntry) {
// Vercel captura stdout como logs estructurados
console.log(JSON.stringify({
...entry,
timestamp: new Date().toISOString(),
environment: process.env.VERCEL_ENV,
region: process.env.VERCEL_REGION,
}));
}
// Uso en un API route:
export async function POST(request: Request) {
const start = Date.now();
const requestId = crypto.randomUUID();
try {
const data = await processWebhook(request);
log({
level: 'info',
message: 'webhook_processed',
requestId,
duration: Date.now() - start,
metadata: { eventType: data.type }
});
return Response.json({ ok: true });
} catch (error) {
log({
level: 'error',
message: 'webhook_failed',
requestId,
duration: Date.now() - start,
metadata: { error: String(error) }
});
return Response.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
```
Vercel captura `console.log` como logs estructurados cuando emites JSON. Puedes filtrar por `level`, `requestId`, o cualquier campo del metadata directamente en el dashboard.
Integra Vercel Log Drains con Datadog, Axiom, o Logtail para persistencia y alertas. Los logs nativos de Vercel tienen retención limitada.
Variables de Entorno: El Error de Seguridad más Común
```bash
❌ Nunca pongas secrets en el código
const apiKey = "sk-1234567890abcdef";
✅ Usa el sistema de env vars de Vercel con prefijos correctos
Para el cliente (expuesta al browser):
NEXT_PUBLIC_ANALYTICS_ID=xxx
Para el servidor únicamente:
DATABASE_URL=postgresql://...
OPENAI_API_KEY=sk-...
STRIPE_SECRET_KEY=sk_live_...
```
Vercel tiene tres scopes para variables: Production, Preview, Development.
Usa distintos valores por entorno. Tu `OPENAI_API_KEY` en preview puede apuntar a un proyecto con límites más bajos. Tu `DATABASE_URL` en preview debe apuntar a una base de datos de staging, nunca a producción.
*Una sola variable mal configurada puede significar datos reales en un entorno de test.*
Takeaways: Vercel Deployment Best Practices que Importan
→ `vercel.json` desde el día uno — define regiones, memory limits y maxDuration por ruta
→ Preview deployments como staging — conecta Playwright o Cypress a cada preview URL
→ Cache con invalidación por tags — usa `unstable_cache` + `revalidateTag`, nunca cacheo estático indiscriminado
→ Route Segment Config granular — cada página con su estrategia: `force-dynamic`, `revalidate: 3600`, o `false`
→ Monorepos con `turbo-ignore` — elimina builds innecesarios y activa Remote Caching
→ Structured logging en JSON — emite objetos con `level`, `requestId` y `duration` desde el primer commit
→ Environment variables por scope — Production, Preview y Development con valores distintos
Vercel deployment best practices no son un checklist que completas una vez. Son decisiones de arquitectura que defines antes de escribir la primera línea de lógica de negocio.
Los proyectos que escalan en Vercel no tienen mejor código. Tienen mejor configuración de infraestructura.
La plataforma es sólida. El trabajo es tuyo.
Lee el artículo completo en brianmenagomez.com


