If you have been copy-pasting regex into vercel.json for years, 2026 brings a small but welcome upgrade: vercel.ts is now the recommended way to configure a Vercel project. You get real TypeScript types, helper functions for common patterns, and the ability to branch on environment variables — all of which JSON could only approximate.
This post walks through migrating a real vercel.json to vercel.ts, covers the patterns that get noticeably better, and flags a few things that do not belong in this file at all.
Install and Setup
Terminal
pnpm add -D @vercel/config
rm vercel.json
touch vercel.ts
vercel.ts
import { routes, deploymentEnv, type VercelConfig } from '@vercel/config/v1';
export const config: VercelConfig = {
framework: 'nextjs',
regions: ['iad1', 'sfo1'],
};
The CLI picks up vercel.ts automatically. If both files exist, vercel.ts wins, so you can migrate one section at a time.
Rewrites, Redirects, and Headers
The routes helper exposes typed builders for the common cases. Each one returns the same shape the JSON config expected, so the generated output is stable.
vercel.ts — routes
import { routes, type VercelConfig } from '@vercel/config/v1';
export const config: VercelConfig = {
framework: 'nextjs',
rewrites: [
routes.rewrite('/api/(.*)', 'https://api.example.com/$1'),
routes.rewrite('/blog/:slug', '/articles/:slug'),
],
redirects: [
routes.redirect('/old-docs', '/docs', { permanent: true }),
routes.redirect('/twitter', 'https://x.com/acme'),
],
headers: [
routes.cacheControl('/static/(.*)', {
public: true,
maxAge: '1 week',
immutable: true,
}),
routes.securityHeaders('/(.*)'),
],
};
Notice routes.securityHeaders(): a batteries-included helper that emits a sensible Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options, and friends. The previous approach was to copy headers from a blog post and get them slightly wrong.
Branching on Environment
The deploymentEnv helper is the reason most teams actually migrate. You can branch the config on whether the deployment is production, preview, or development.
vercel.ts — env-aware config
import { routes, deploymentEnv, type VercelConfig } from '@vercel/config/v1';
const isProd = deploymentEnv === 'production';
export const config: VercelConfig = {
framework: 'nextjs',
headers: [
routes.header('/(.*)', 'X-Robots-Tag', isProd ? 'all' : 'noindex'),
],
crons: isProd
? [{ path: '/api/cleanup', schedule: '0 3 * * *' }]
: [],
};
The practical wins: preview deployments no longer get indexed by Google, and you do not accidentally run cron jobs on every preview branch. Both of these were possible with vercel.json only by using env vars and conditionals in the hosted dashboard — which nothing on disk reflected.
Crons and Queues in One Place
Crons have moved from the dashboard into code for years; Queues are the newer addition. Co-locating both with the rest of the config keeps infra declarative and reviewable.
vercel.ts — crons and queues
export const config: VercelConfig = {
framework: 'nextjs',
crons: [
{ path: '/api/cron/digest', schedule: '0 8 * * MON' },
{ path: '/api/cron/cleanup', schedule: '0 3 * * *' },
],
queues: [
{
name: 'emails',
consumer: '/api/queues/emails',
maxRetries: 3,
maxConcurrency: 20,
},
],
};
Pro Tip: Wrap complex rewrite sets in plain functions and
as constarrays. TypeScript will catch typos in route patterns at compile time instead of at 3am when a redirect loop takes down production.
What Does Not Go In vercel.ts
- Environment variables — use
vercel envor the dashboard. Secrets should never live in a committed file. - Per-function runtime config (memory, maxDuration, runtime) — keep that in the route file's
export constdeclarations so it lives next to the code it affects. - Feature flags — use a flag service or the Vercel Flags SDK, not a
deploymentEnvbranch in config.
Running the Migration
- Install
@vercel/configas a dev dependency - Copy
vercel.jsoncontents intovercel.tsusing therouteshelpers — TypeScript will flag shape mismatches - Run
vercel buildlocally; inspect.vercel/output/config.jsonto confirm the generated output matches the old JSON - Delete
vercel.json - Commit and deploy a preview to verify routing and headers behave identically
