Skip to content

Services routing

Your vercel.json controls all public routing for the deployment. A service receives public traffic only when there is a top-level rewrite that routes requests to it. Each service then handles its own routes, and the routes of one service are never mixed with another, so every public routing rule has a single owner. For how services are defined and built, see Services.

Where usual rewrite rules target URLs, service rewrites target services instead. Set the destination value in a rewrite rule to an object like { "service": "my_backend" }. For example:

vercel.json
{
  "services": {
    "my_frontend": {
      "root": "frontend/"
    },
    "my_backend": {
      "root": "backend/",
      "entrypoint": "main:app"
    }
  },
  "rewrites": [
    {
      "source": "/api/(.*)",
      "destination": { "service": "my_backend" }
    },
    {
      "source": "/(.*)",
      "destination": { "service": "my_frontend" }
    }
  ]
}

The destination object accepts the following fields:

FieldRequiredDescription
serviceYesName of a service in the same deployment, from your services configuration.
pathNoThe path used to select a route inside the service. It changes which route runs, not the path your code sees. See Rules inside a service for an example.

Vercel evaluates your top-level rewrites in order and routes each request to the first matching service. Using the configuration above, where /api/(.*) routes to my_backend and everything else routes to my_frontend:

RequestHandled byService receives
GET /dashboardmy_frontend/dashboard
GET /api/usersmy_backend/api/users
POST /api/ordersmy_backend/api/orders

The service receives the original request path. GET /api/users reaches my_backend as /api/users, not /users.

Once a request is routed to a service, that service handles the rest of routing. If nothing inside the service matches, the service is expected to return its own 404 or 405 response.

Routing into a service is final. If no route matches inside the service, Vercel does not fall back to your other top-level rewrites. It returns the service's not-found response.

A service can define its own headers, redirects, rewrites, and routes, using the same syntax as the top-level configuration. These run only after a top-level rewrite routes a request into the service, and they can match on the state the rewrite passed in.

For example, capture an organization slug at the top level, pass it to the service as query state, and let the service add a response header based on it:

vercel.json
{
  "services": {
    "my_backend": {
      "root": "backend/",
      "entrypoint": "main:app",
      "headers": [
        {
          "source": "/(.*)",
          "has": [{ "type": "query", "key": "org", "value": "(?<org>.*)" }],
          "headers": [{ "key": "x-org-id", "value": ":org" }]
        }
      ]
    }
  },
  "rewrites": [
    {
      "source": "/org/:orgSlug/api/:path*",
      "destination": {
        "service": "my_backend",
        "path": "/:path*?org=:orgSlug"
      }
    }
  ]
}

For example, GET /org/acme/api/users routes to my_backend with the route lookup path /users and the query state org=acme. The service's own headers rule matches org=acme and adds x-org-id: acme to the response. Your service code still observes the original path /org/acme/api/users: the destination path only selects which route runs and the query state these rules match against, not the path your code sees. To change the path your service code observes, add a request.path transform to the service's own routes.

Top-level rewrites are public ingress. To let one service call another over the internal network, server-side and without a public route, use a service binding instead.

Last updated June 30, 2026

Was this helpful?