Implement Vercel reference architecture with best-practice project layout. Use when designing new Vercel integrations, reviewing project structure, or establishing architecture standards for Vercel...
Implement a production-ready Vercel project architecture with clear separation across edge, server, and client layers. Covers directory structure, middleware patterns, API route organization, shared utilities, and configuration management.
my-vercel-app/
āāā public/ # Static assets (served from CDN)
ā āāā favicon.ico
ā āāā images/
āāā src/
ā āāā app/ # Next.js App Router pages
ā ā āāā layout.tsx # Root layout
ā ā āāā page.tsx # Home page
ā ā āāā api/ # API routes (serverless functions)
ā ā ā āāā health/route.ts
ā ā ā āāā users/route.ts
ā ā ā āāā webhooks/
ā ā ā āāā vercel/route.ts
ā ā āāā dashboard/ # Protected pages
ā ā ā āāā layout.tsx
ā ā ā āāā page.tsx
ā ā āāā (marketing)/ # Public pages (route group)
ā ā āāā pricing/page.tsx
ā ā āāā about/page.tsx
ā āāā lib/ # Shared utilities (server + client)
ā ā āāā api-client.ts # External API wrapper
ā ā āāā db.ts # Database client (lazy singleton)
ā ā āāā env.ts # Typed environment variables
ā ā āāā errors.ts # Error classes
ā āāā components/ # React components
ā ā āāā ui/ # Design system primitives
ā ā āāā features/ # Feature-specific components
ā āāā middleware.ts # Edge Middleware (auth, redirects)
āāā vercel.json # Vercel configuration
āāā next.config.js # Next.js configuration
āāā tsconfig.json
āāā package.json
āāā .env.example # Required env vars (no values)
// src/lib/env.ts ā validate env vars at import time
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
API_SECRET: z.string().min(16),
NEXT_PUBLIC_API_URL: z.string().url(),
VERCEL_ENV: z.enum(['production', 'preview', 'development']).default('development'),
VERCEL_URL: z.string().optional(),
});
// Fails fast at startup if env vars are missing
export const env = envSchema.parse(process.env);
// Type-safe access throughout the app
// Usage: import { env } from '@/lib/env'; env.DATABASE_URL
// src/lib/db.ts ā lazy init to minimize cold starts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined };
export const db = globalForPrisma.prisma ?? new PrismaClient({
log: process.env.VERCEL_ENV === 'development' ? ['query'] : ['error'],
});
// Prevent multiple instances in development (hot reload)
if (process.env.VERCEL_ENV !== 'production') {
globalForPrisma.prisma = db;
}
// src/app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { env } from '@/lib/env';
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const limit = Number(searchParams.get('limit') ?? 20);
const users = await db.user.findMany({ take: limit });
return NextResponse.json({ users }, {
headers: { 'Cache-Control': 's-maxage=60, stale-while-revalidate=300' },
});
} catch (error) {
console.error('GET /api/users failed:', error);
return NextResponse.json(
{ error: 'Internal server error', requestId: crypto.randomUUID() },
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const user = await db.user.create({ data: body });
return NextResponse.json({ user }, { status: 201 });
} catch (error) {
console.error('POST /api/users failed:', error);
return NextResponse.json(
{ error: 'Failed to create user' },
{ status: 400 }
);
}
}
// src/middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Skip auth for public routes
if (pathname.startsWith('/api/health') || pathname.startsWith('/api/webhooks')) {
return NextResponse.next();
}
// Check auth for dashboard routes
if (pathname.startsWith('/dashboard') || pathname.startsWith('/api/')) {
const token = request.cookies.get('session')?.value;
if (!token) {
if (pathname.startsWith('/api/')) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
return NextResponse.redirect(new URL('/login', request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
// src/app/api/health/route.ts
import { db } from '@/lib/db';
export const dynamic = 'force-dynamic'; // Never cache health checks
export async function GET() {
const checks: Record<string, 'ok' | 'error'> = {};
// Database connectivity
try {
await db.$queryRaw`SELECT 1`;
checks.database = 'ok';
} catch {
checks.database = 'error';
}
const allHealthy = Object.values(checks).every(v => v === 'ok');
return Response.json({
status: allHealthy ? 'healthy' : 'degraded',
checks,
version: process.env.VERCEL_GIT_COMMIT_SHA?.slice(0, 7) ?? 'local',
region: process.env.VERCEL_REGION ?? 'local',
timestamp: new Date().toISOString(),
}, {
status: allHealthy ? 200 : 503,
});
}
// vercel.json
{
"regions": ["iad1"],
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }
]
}
],
"rewrites": [
{ "source": "/docs/:path*", "destination": "https://docs.example.com/:path*" }
],
"redirects": [
{ "source": "/old-page", "destination": "/new-page", "permanent": true }
]
}
| Layer | Runtime | Responsibilities |
|---|---|---|
| Edge (middleware.ts) | V8 isolates | Auth, redirects, A/B testing, headers |
| Server (api routes) | Node.js | Database queries, business logic, webhooks |
| Static (pages) | CDN | Pre-rendered pages, ISR, images |
| Client (components) | Browser | Interactivity, client state |
| Error | Cause | Solution |
|---|---|---|
| Env validation fails on deploy | Missing required variable | Add to Vercel dashboard for target environment |
| Middleware runs on static assets | Matcher too broad | Add exclusions for _next/static, _next/image |
| Database connection pool exhausted | Too many concurrent functions | Use connection pooler (PgBouncer, Prisma Accelerate) |
| API route not found | Wrong directory structure | Must be in src/app/api/ with route.ts filename |
Implement edge middleware only for routing and authorization decisions, keep database access inside server routes, and expose a non-sensitive health endpoint that verifies dependency reachability without returning credentials. Deploy the architecture to preview, test static, edge, and server paths with synthetic identities, and measure middleware overhead. Preserve the prior deployment as rollback until observability confirms the new path is stable under normal traffic.
For multi-environment setup, see vercel-multi-env-setup.