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. |
Edgenivo 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.
The examples above route by path prefix on a shared domain. You can also route by hostname, so each service is reached on its own subdomain. Routing by host keeps existing production hostnames unchanged when you move several services into one deployment.
A service rewrite accepts the same has and missing conditions as an ordinary rewrite. Add a has condition with type: "host" to require a specific request host before the rule matches. For the full condition syntax, see conditional matching with has and missing.
The following configuration routes service-one.example.com to service_one and service-two.example.com to service_two, with a final rule for requests that match neither host:
{
"services": {
"service_one": {
"root": "service-one/"
},
"service_two": {
"root": "service-two/"
},
"site": {
"root": "site/"
}
},
"rewrites": [
{
"source": "/(.*)",
"has": [{ "type": "host", "value": { "pre": "service-one." } }],
"destination": { "service": "service_one" }
},
{
"source": "/(.*)",
"has": [{ "type": "host", "value": { "pre": "service-two." } }],
"destination": { "service": "service_two" }
},
{
"source": "/(.*)",
"destination": { "service": "site" }
}
]
}With this configuration:
| Request | Handled by | Service receives |
|---|---|---|
GET https://service-one.example.com/users | service_one | /users |
GET https://service-two.example.com/health | service_two | /health |
GET https://example.com/ | site | / |
GET https://<deployment-url>.vercel.app/ | site | / |
The service still receives the original request path, exactly as with path-based routing. Host and path conditions combine: source matches the path and has adds the host requirement, so a rule can require both. For everything else rewrites support, including all condition types and matching options, see Rewrites and the full routing documentation.
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?