Vercel deployments are often treated as immutable, but vercel.json is your secret weapon for shaping their behavior after they’ve been built.

Let’s see vercel.json in action. Imagine a static Next.js site deployed to Vercel. By default, Vercel handles routing and asset serving perfectly.

// vercel.json
{
  "version": 2,
  "builds": [
    {
      "src": "package.json",
      "use": "@vercel/next"
    }
  ],
  "routes": [
    {
      "src": "/api/(.*)",
      "dest": "/api/$1"
    },
    {
      "src": "/(.*)",
      "dest": "/index.html"
    }
  ]
}

This basic configuration tells Vercel to use @vercel/next for building from package.json. The routes section is where the magic happens for serving. The first route matches any request starting with /api/ and forwards it to the corresponding file in the api/ directory. The second route, (.*), is a catch-all that sends all other requests to index.html, which is standard for Single Page Applications (SPAs) like those built with React, Vue, or Svelte. Vercel’s build process for Next.js automatically handles this, so you often don’t need to explicitly define these for a standard Next.js app.

The real power of vercel.json comes when you need to deviate from the defaults, perhaps to handle redirects, rewrite URLs, or serve static assets with specific caching rules.

Consider a scenario where you’re migrating an existing application and need to redirect old URLs to new ones.

// vercel.json
{
  "version": 2,
  "builds": [
    {
      "src": "package.json",
      "use": "@vercel/next"
    }
  ],
  "routes": [
    {
      "src": "/old-path",
      "dest": "/new-path",
      "status": 301
    },
    {
      "src": "/another-old/(.*)",
      "dest": "/new-directory/$1",
      "status": 302
    },
    {
      "src": "/api/(.*)",
      "dest": "/api/$1"
    },
    {
      "src": "/(.*)",
      "dest": "/index.html"
    }
  ]
}

Here, we’ve added two redirect rules. The first redirects a specific path /old-path to /new-path with a permanent 301 status code. The second redirects any path starting with /another-old/ to a corresponding path under /new-directory/, using a temporary 302 status code. The order of these routes matters; Vercel processes them from top to bottom, applying the first match it finds. This allows you to define more specific rules before broader ones.

You can also leverage vercel.json to configure middleware, headers, and even environment variables for specific functions.

// vercel.json
{
  "version": 2,
  "builds": [
    {
      "src": "package.json",
      "use": "@vercel/next"
    }
  ],
  "functions": {
    "api/**/*.js": {
      "runtime": "nodejs18.x",
      "memory": 1024,
      "maxDuration": 10
    },
    "api/auth/login.js": {
      "runtime": "nodejs18.x",
      "env": {
        "SECRET_KEY": "@my-secret-key-from-env-vars"
      }
    }
  },
  "routes": [
    {
      "src": "/api/(.*)",
      "dest": "/api/$1"
    },
    {
      "src": "/(.*)",
      "dest": "/index.html"
    }
  ]
}

In this example, we’re specifying configuration for our serverless functions. The api/**/*.js glob pattern applies to all JavaScript files within the api directory. We’re setting their runtime to nodejs18.x, allocating 1024 MB of memory, and setting a maximum duration of 10 seconds. For a specific function, api/auth/login.js, we’re also injecting an environment variable SECRET_KEY, pulling its value from a Vercel Environment Variable named @my-secret-key-from-env-vars. This is a powerful way to manage sensitive credentials without hardcoding them.

The headers property within routes is another crucial element for fine-tuning how your assets are served. You can set custom cache control directives, CORS headers, or security headers.

// vercel.json
{
  "version": 2,
  "builds": [
    {
      "src": "package.json",
      "use": "@vercel/next"
    }
  ],
  "routes": [
    {
      "src": "/(.*)",
      "dest": "/index.html",
      "headers": {
        "Cache-Control": "public, max-age=3600, stale-while-revalidate=86400"
      }
    },
    {
      "src": "/assets/images/(.*)",
      "dest": "/assets/images/$1",
      "headers": {
        "Cache-Control": "public, max-age=31536000, immutable"
      }
    }
  ]
}

Here, all requests handled by index.html get a Cache-Control header allowing public caching for an hour and serving stale content for up to a day. Static assets in /assets/images/ are configured for a full year of caching and marked as immutable, signaling to the browser and any intermediate caches that these files will never change, allowing for aggressive caching and significant performance gains.

A lesser-known but incredibly useful feature is the ability to define custom domain configurations directly within vercel.json, though this is more commonly managed through the Vercel dashboard. When you do use it, it’s typically for automated setup during CI/CD pipelines or for very specific advanced routing scenarios not easily achievable via the UI.

The framework property is also essential for Vercel to correctly identify and configure your project, often automatically detecting it but explicit definition can prevent misconfigurations.

// vercel.json
{
  "version": 2,
  "framework": "nextjs",
  "builds": [
    {
      "src": "package.json",
      "use": "@vercel/next"
    }
  ],
  "routes": [
    {
      "src": "/api/(.*)",
      "dest": "/api/$1"
    },
    {
      "src": "/(.*)",
      "dest": "/index.html"
    }
  ]
}

By explicitly setting "framework": "nextjs", you ensure Vercel applies Next.js-specific optimizations and build steps, even if your project structure might otherwise confuse its auto-detection.

Understanding vercel.json allows you to move beyond simple deployments and sculpt the exact behavior of your application on Vercel, from complex routing and redirects to fine-grained performance optimizations and function configurations.

Once you’ve mastered custom routes and headers, your next exploration will likely be into advanced build configurations and custom build commands.

Want structured learning?

Take the full Vercel course →