Vercel’s vercel.json is how you tell the platform how to serve your frontend application, and it’s way more powerful than just setting environment variables.
Here’s a basic vercel.json for a Next.js app:
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/next"
}
],
"rewrites": [
{
"source": "/api/:match*",
"destination": "/api/:match*"
}
],
"headers": [
{
"source": "/(.*).png",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
]
}
This configuration does a few things:
version: Specifies the schema version forvercel.json. Always use2.builds: Tells Vercel how to build your project. Here, it uses@vercel/nextfor a Next.js app based onpackage.json.rewrites: This is where you can internally map one URL path to another without changing the URL in the browser. Thesourceis the path Vercel receives, anddestinationis the path Vercel internally serves. This is great for routing API calls to a backend or handling specific path structures.headers: Allows you to set custom HTTP headers for specific routes. This is crucial for performance optimization (like settingCache-Control) or security.
Let’s dive into the core features:
Rewrites
Rewrites are like internal proxies. When a request comes in for /api/users, if you have a rewrite like this:
{
"source": "/api/:path*",
"destination": "/api/:path*"
}
Vercel will internally serve the content from /api/users on your serverless functions or static files, but the browser still sees /api/users. This is fundamental for building applications where your frontend and backend logic are co-located or for creating cleaner URL structures.
A common use case is routing external API calls:
{
"source": "/api/external/:path*",
"destination": "https://your-external-api.com/:path*"
}
Here, requests to /api/external/data will be proxied to https://your-external-api.com/data. The browser only ever sees /api/external/data.
Redirects
Redirects, on the other hand, do change the URL in the browser. They tell the user’s browser (or search engine bots) to go to a different URL. You define them similarly to rewrites but with an additional statusCode.
{
"source": "/old-page",
"destination": "/new-page",
"statusCode": 301
}
statusCode: 301: Permanent redirect. The browser and search engines will cache this and always go to/new-pagefor/old-page.statusCode: 302: Temporary redirect. Less aggressive caching, useful for A/B testing or temporary changes.
You can also redirect to external domains:
{
"source": "/docs",
"destination": "https://docs.your-company.com",
"statusCode": 302
}
Headers
Headers are about controlling how requests and responses are handled at the HTTP level.
{
"source": "/(.*)",
"headers": [
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "X-Content-Type-Options",
"value": "nosniff"
}
]
}
This applies X-Frame-Options: DENY and X-Content-Type-Options: nosniff to all requests, which is a good security practice.
The most common use for custom headers is performance optimization. For static assets like images, fonts, or CSS files, you want to tell browsers to cache them aggressively.
{
"source": "/static/css/styles.css",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
public: Allows caching by any cache.max-age=31536000: Cache for one year (365 days * 24 hours * 60 minutes * 60 seconds).immutable: Tells the browser that the content of this file will never change. This allows browsers to skip revalidation checks entirely for that asset. Use this with caution and only for files whose names are versioned (e.g.,styles.a1b2c3d4.css).
Combining Them
You can combine all these directives in a single vercel.json file. The order matters for rewrites and redirects: Vercel processes them in the order they appear in the array.
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/next"
}
],
"redirects": [
{
"source": "/about-us",
"destination": "/about",
"statusCode": 301
}
],
"rewrites": [
{
"source": "/api/users/:id",
"destination": "/api/getUser?id=:id"
},
{
"source": "/api/:path*",
"destination": "/api/:path*"
}
],
"headers": [
{
"source": "/images/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=86400"
}
]
},
{
"source": "/(.*)",
"headers": [
{
"key": "X-Robots-Tag",
"value": "noindex"
}
]
}
]
}
In this example, /about-us will redirect to /about. Then, /api/users/123 will be internally rewritten to /api/getUser?id=123. Any other /api/* requests will be handled by your serverless function. Images in /images will be cached for a day. All other pages will have X-Robots-Tag: noindex applied.
The most surprising thing is how Vercel’s routing engine resolves conflicts between rewrites and redirects. If a source path matches both a redirect and a rewrite, the redirect takes precedence. This means if you have a redirect for /old-path and a rewrite for /old-path, the user will be redirected, not internally rewritten.
Understanding vercel.json is key to optimizing your Vercel deployments for performance, SEO, and a seamless user experience. The next step is often exploring how these configurations interact with specific frameworks like Next.js’s routing capabilities.