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:
{
"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:
| Field | Required | Description |
|---|---|---|
service | Yes | Name of a service in the same deployment, from your services configuration. |
path | No | The 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:
| Request | Handled by | Service receives |
|---|---|---|
GET /dashboard | my_frontend | /dashboard |
GET /api/users | my_backend | /api/users |
POST /api/orders | my_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.
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:
{
"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.
Was this helpful?