Migrating vercel.json to vercel.ts: Field Notes on Typed Vercel Config
Headline:
vercel.tsis Vercel's new TypeScript-based project configuration file, and it replacesvercel.jsonwith full type checking, dynamic logic, and direct environment variable access at build time. Migrating three of my own projects to it caught two typos a JSON linter would never have flagged.
Key takeaways
-
vercel.tsis the current recommended way to configure a Vercel project, replacing the staticvercel.jsonfile. - Importing
VercelConfigfrom@vercel/config/v1gives a typed config object, so a misspelled key likerewrtesfails the build instead of silently doing nothing. - Helper builders —
routes.rewrite(),routes.redirect(),routes.cacheControl()— replacevercel.json's raw array-of-objects shape. -
vercel.tsis executable TypeScript evaluated at build/deploy time, so it can read env vars and branch per environment — a pure-JSON file structurally cannot do that. -
vercel.tsdoes not run per request. Request-time branching still belongs in Next.js Middleware or Routing Middleware.
I treat Vercel project config the way I treat everything else that used to be a JSON file with opinions: as soon as it can be TypeScript, it should be. vercel.json never complained about a typo'd key, a malformed cron string, or a rewrite pattern that didn't match anything — it just silently did nothing, and I found out in production.
What is vercel.ts, and why does it replace vercel.json?
vercel.ts is a TypeScript file at the project root that exports a typed config object describing build settings, routing, headers, and crons for a Vercel project. Vercel added it as the recommended configuration format, built on the @vercel/config package, to replace the schema-less JSON file every Vercel project has used since the platform's early versions.
The core problem it solves: vercel.json is data, not code. A data file can't validate itself, reference an environment variable, or express "do X in preview, do Y in production" without a hand-rolled script generating JSON before build. vercel.ts is just a Node module, so all three become ordinary TypeScript.
What does a working vercel.ts look like?
A minimal but realistic one for a Next.js app proxying an API and running a nightly cleanup job:
// vercel.ts import { routes, type VercelConfig } from '@vercel/config/v1'; export const config: VercelConfig = { buildCommand: 'npm run build', framework: 'nextjs', rewrites: [ routes.rewrite('/api/(.*)', 'https://backend.example.com/$1'), ], redirects: [ routes.redirect('/old-docs', '/docs', { permanent: true }), ], headers: [ routes.cacheControl('/static/(.*)', { public: true, maxAge: '1 week', immutable: true }), ], crons: [{ path: '/api/cleanup', schedule: '0 0 * * *' }], }; Every field is checked against the VercelConfig type at build time. Get the cron schedule string wrong, or pass a rewrite destination that isn't a string, and the build fails locally before a single byte reaches Vercel's build servers.
How do I migrate an existing vercel.json without breaking a working deploy?
- Install
@vercel/configas a dev dependency. - Create
vercel.tsand portbuildCommandandframeworkfirst. - Port
rewritestoroutes.rewrite(),redirectstoroutes.redirect(),headerstoroutes.cacheControl(). - Port
cronslast — a typo'd cron expression is the easiest mistake to miss in a diff review. - Run a full local build, ship to a preview deployment, and diff response headers and redirect behavior against the previous production deploy.
- Delete
vercel.jsononly after the preview checks out. Two sources of truth for the same settings is a drift bug waiting to happen.
What actually breaks when config becomes executable TypeScript?
| Failure mode | Why it happens | Fix |
|---|---|---|
| Config drifts between preview and production | vercel.ts runs as real code, so Date.now() or randomness baked into the config differs run to run | Treat vercel.ts as pure and deterministic — no clocks, no randomness, no network calls |
Env var is undefined inside the config | The file evaluates at build time, so it only sees build-exposed variables, not runtime-only secrets | Confirm the variable is set for the Build environment, not only Production/Preview runtime |
| Old CLI ignores the file entirely | A CLI version predating vercel.ts support doesn't know to evaluate it | Pin the CLI version in CI and upgrade deliberately |
Does vercel.ts replace next.config.ts too?
No. next.config.ts configures the Next.js framework itself — image domains, experimental flags, the Next-specific build pipeline. vercel.ts configures the Vercel platform layer underneath any framework — routing, crons, build command, headers that apply regardless of what's generating the response. They overlap at the edges (both can define headers and redirects). Pick exactly one file to own a given rule; defining the same redirect in both is how you end up debugging a redirect loop at 2 a.m.
FAQ
Q: Does vercel.ts replace vercel.json entirely, or can I keep both?
A: Vercel still reads vercel.json if present, but running both invites the two files to disagree. Pick one and delete the other once migration is verified.
Q: Does vercel.ts run on every incoming request?
A: No. It's evaluated at build and deploy time only. Per-request branching belongs in Next.js Middleware or Routing Middleware.
Q: Do I need to install anything extra to use vercel.ts?
A: Yes — the @vercel/config package, importing VercelConfig and the routes helpers from @vercel/config/v1.
Q: Will an older Vercel CLI read my new vercel.ts?
A: An older CLI predating vercel.ts support won't evaluate it, so pin a current CLI version in CI before adopting the file.
Q: Does vercel.ts only work with Next.js?
A: No. It's framework-agnostic platform configuration — it works the same for Nuxt, SvelteKit, Astro, or a plain Node backend on Vercel.
Originally published on devya.dev. Also on eng-ahmed.com. Built by Devya Solutions.