Deploy to Vercel with edge functions, ISR, middleware, and proper caching. Use when deploying Next.js apps, configuring preview environments, or optimizing Vercel builds.
Deploy applications to Vercel with proper runtime selection, caching strategies, and environment configuration. Edge Functions run at CDN edge nodes for low latency (under 50ms cold start), Serverless Functions run in regional data centers (up to 10s execution).
BAD: Kitchen sink configuration
{
"functions": {
"api/**/*.js": {
"runtime": "nodejs18.x",
"maxDuration": 10
}
},
"headers": [],
"rewrites": []
}
GOOD: Targeted runtime and caching
{
"functions": {
"api/slow-process.ts": {
"runtime": "nodejs20.x",
"maxDuration": 60,
"memory": 1024
}
},
"headers": [
{
"source": "/api/static-data",
"headers": [
{
"key": "Cache-Control",
"value": "s-maxage=3600, stale-while-revalidate=86400"
}
]
}
],
"crons": [
{
"path": "/api/cron/cleanup",
"schedule": "0 0 * * *"
}
]
}
BAD: Everything as serverless
// app/api/geo/route.ts
export async function GET(request: Request) {
// Runs in us-east-1 every time, slow for global users
const country = request.headers.get('x-forwarded-for')
return Response.json({ country })
}
GOOD: Edge for geo/auth, serverless for heavy compute
// app/api/geo/route.ts
export const runtime = 'edge'
export async function GET(request: Request) {
const country = request.headers.get('x-vercel-ip-country')
const city = request.headers.get('x-vercel-ip-city')
return Response.json({ country, city })
}
// app/api/process-video/route.ts
export const runtime = 'nodejs'
export const maxDuration = 300 // 5 minutes, requires Pro plan
export async function POST(request: Request) {
const formData = await request.formData()
// Heavy video processing requires serverless
const result = await processVideo(formData)
return Response.json(result)
}
BAD: Auth in every route
// app/api/protected/route.ts
import { verify } from 'jsonwebtoken'
export async function GET(request: Request) {
const token = request.headers.get('authorization')
try {
verify(token, process.env.JWT_SECRET)
} catch {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
// Protected logic
}
GOOD: Middleware at edge for auth and geo
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export const config = {
matcher: ['/api/protected/:path*', '/dashboard/:path*']
}
export function middleware(request: NextRequest) {
// Runs at edge, low latency globally
const token = request.cookies.get('session')?.value
const country = request.geo?.country
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Geo-block sanctioned countries
if (['CU', 'IR', 'KP', 'SY'].includes(country || '')) {
return NextResponse.json(
{ error: 'Service unavailable in your region' },
{ status: 451 }
)
}
// Add custom headers for downstream routes
const response = NextResponse.next()
response.headers.set('x-user-country', country || 'unknown')
return response
}
BAD: Hardcoded secrets, no environment separation
// lib/config.ts
export const config = {
apiKey: 'sk_live_abc123', // NEVER hardcode
dbUrl: process.env.DATABASE_URL // Same DB for preview and production
}
GOOD: Environment-specific variables
// lib/config.ts
export const config = {
apiKey: process.env.API_KEY!, // Set in Vercel dashboard
dbUrl: process.env.DATABASE_URL!,
isProduction: process.env.VERCEL_ENV === 'production',
isPreview: process.env.VERCEL_ENV === 'preview',
deploymentUrl: process.env.VERCEL_URL
}
// Vercel dashboard environment settings:
// API_KEY (production): sk_live_abc123
// API_KEY (preview): sk_test_xyz789
// DATABASE_URL (production): postgres://prod.db
// DATABASE_URL (preview): postgres://staging.db
Vercel automatically provides: VERCEL, VERCEL_ENV, VERCEL_URL, VERCEL_GIT_COMMIT_SHA, VERCEL_GIT_COMMIT_REF.
Every git push to non-production branch creates preview deployment:
# Automatic preview on PR
git checkout -b feature/new-ui
git push origin feature/new-ui
# Vercel creates: https://myapp-git-feature-new-ui-user.vercel.app
# Preview-specific env vars for testing
# Set in Vercel dashboard: ENABLE_FEATURE_FLAG=true (preview only)
Workflow for preview comments:
// app/api/comments/route.ts
export async function POST(request: Request) {
const isPreview = process.env.VERCEL_ENV === 'preview'
if (isPreview) {
// Use test Stripe key, mock email service
return Response.json({
success: true,
note: 'Preview mode, no real charges'
})
}
// Production logic with real services
const result = await stripe.charges.create({...})
return Response.json(result)
}
BAD: No caching, every request hits origin
// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }) {
const post = await fetchPost(params.slug)
return <article>{post.content}</article>
}
GOOD: ISR with on-demand revalidation
// app/blog/[slug]/page.tsx
export const revalidate = 3600 // ISR: regenerate after 1 hour
export async function generateStaticParams() {
const posts = await fetchAllPosts()
return posts.map((post) => ({ slug: post.slug }))
}
export default async function BlogPost({ params }) {
const post = await fetchPost(params.slug)
return <article>{post.content}</article>
}
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache'
export async function POST(request: Request) {
const { slug, secret } = await request.json()
if (secret !== process.env.REVALIDATION_SECRET) {
return Response.json({ error: 'Invalid secret' }, { status: 401 })
}
revalidatePath(`/blog/${slug}`)
return Response.json({ revalidated: true })
}
GOOD: API route caching with stale-while-revalidate
// app/api/stats/route.ts
export async function GET() {
const stats = await fetchStats()
return Response.json(stats, {
headers: {
'Cache-Control': 's-maxage=60, stale-while-revalidate=300'
// Cache for 60s, serve stale for 5min while revalidating
}
})
}
BAD: Unoptimized images
<img src="/hero.png" alt="Hero" /> {/* Serves original 5MB PNG */}
GOOD: next/image with formats and sizes
import Image from 'next/image'
// next.config.js
module.exports = {
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200],
imageSizes: [16, 32, 48, 64, 96, 128, 256],
minimumCacheTTL: 31536000,
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
pathname: '/images/**'
}
]
}
}
// Component
<Image
src="/hero.png"
alt="Hero"
width={1200}
height={600}
priority
sizes="(max-width: 768px) 100vw, 50vw"
/>
GOOD: Turborepo with Vercel
// vercel.json (root)
{
"buildCommand": "turbo run build --filter=web",
"outputDirectory": "apps/web/.next",
"installCommand": "pnpm install"
}
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
}
}
}
// apps/web/package.json
{
"name": "web",
"scripts": {
"build": "next build"
}
}
BAD: No output file tracing
// next.config.js
module.exports = {
// Default includes all node_modules in function bundles
}
GOOD: Output file tracing and standalone build
// next.config.js
module.exports = {
output: 'standalone', // Only includes necessary files
experimental: {
outputFileTracingRoot: require('path').join(__dirname, '../../')
},
webpack: (config, { isServer }) => {
if (isServer) {
// Externalize large dependencies
config.externals.push('canvas', 'bufferutil', 'utf-8-validate')
}
return config
}
}
# Add custom domain (Vercel dashboard or CLI)
vercel domains add example.com
vercel domains add www.example.com
# DNS records (automatic with Vercel nameservers):
# A record: @ -> 76.76.21.21
# CNAME: www -> cname.vercel-dns.com
# SSL is automatic, renews via Let's Encrypt
GOOD: Integrate Vercel Analytics
// app/layout.tsx
import { Analytics } from '@vercel/analytics/react'
import { SpeedInsights } from '@vercel/speed-insights/next'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
<SpeedInsights />
</body>
</html>
)
}
// package.json
{
"dependencies": {
"@vercel/analytics": "^1.1.1",
"@vercel/speed-insights": "^1.0.2"
}
}
# Install Vercel CLI
npm i -g vercel
# Initial setup (links project)
vercel
# Deploy to preview
vercel
# Deploy to production
vercel --prod
# Set environment variable
vercel env add API_KEY production
vercel env add API_KEY preview
# Pull environment variables locally
vercel env pull .env.local
# Check deployment logs
vercel logs https://myapp-abc123.vercel.app
# List deployments
vercel ls
# Promote preview to production
vercel promote https://myapp-git-feature.vercel.app
Before deploying:
next build locally to catch build errorsnpx @next/bundle-analyzerfs)Edge runtime restrictions:
fs, child_process, crypto (use crypto.subtle)Error: Function exceeds maximum size
Solution: Use output: 'standalone' and externalize large deps.
Error: Middleware blocking all traffic
Solution: Check matcher config, ensure auth logic has fallback.
Slow cold starts on serverless Solution: Switch to edge runtime or reduce dependencies.
Preview using production database
Solution: Set environment-specific DATABASE_URL for preview environment.