Services
Cargo packages Encore builds, runs and provisions for
A service in an Encore.rs app is a directory with an encore.service.json file and a Cargo package that builds the service's binary. The service's code is ordinary Rust: it serves HTTP with your own web framework, and declares and uses infrastructure through the SDK.
Defining a service
Put an encore.service.json beside the service's Cargo.toml:
users/encore.service.json{
"name": "users",
"lang": "rust",
"description": "Accounts and sign-in.",
"api": [
{ "type": "http", "prefix": "/users", "expose": true }
]
}
Every file in the directory, and in the directories below it, belongs to the service. Services can't be nested: a directory belongs to exactly one service.
On disk, an app with two Rust services might look like this:
/my-app
├── encore.app
├── users // users service
│ ├── encore.service.json
│ ├── Cargo.toml
│ ├── migrations
│ └── src
│ └── main.rs
└── billing // billing service
├── encore.service.json
├── Cargo.toml
└── src
└── main.rs
See App Structure for how services fit together in an app.
The service file
Serving HTTP
An http entry in api says the service serves everything at or below a path prefix itself. Encore doesn't need to know the routes under it, so the routing stays in your web framework.
A service with no api entries serves no HTTP from the gateway. A service that runs only workers sets its run to "none", and has no main process at all.
api also takes openapi, graphql and grpc entries, which describe what the service serves with a spec document.
How Encore runs a Rust service
With the default rust strategies, Encore runs Cargo in the service's directory:
Encore enables the features through the name your Cargo.toml gives the encore-dev dependency, so the package must depend on it directly (or through workspace = true). See Cargo features.
With the rust strategies, a service with a main process must build exactly one binary: that binary is the service. A service with workers and a main process also has one [[bin]] per worker that uses entry; the service's own binary is the one no worker names as its entry. A service with run: "none" has no service binary and needs only the binaries or commands used by its workers.
Under encore run, the service gets its address in two environment variables:
ENCORE_LISTEN_ADDR, thehost:portto listen on.PORT, the same port, for libraries that read only that.
fn listen_addr() -> String {
std::env::var("ENCORE_LISTEN_ADDR").unwrap_or_else(|_| {
let port = std::env::var("PORT").unwrap_or_else(|_| "8080".to_string());
format!("0.0.0.0:{port}")
})
}
When a service's code changes, encore run rebuilds and restarts it.
Custom commands
A custom strategy runs a command of your choosing instead:
{
"name": "worker",
"lang": "rust",
"run": {
"type": "custom",
"cmd": ["cargo", "run", "--release", "--features", "encore-dev/local"]
}
}
Encore adds no features to a custom command. Pass --features encore-dev/local (with the name you depend on the SDK by) in run and test commands, or buckets and Pub/Sub won't work locally. A service with a custom build must also set docker.entrypoint to be packaged as an image.
Cargo workspaces
Each service is its own Cargo package, but the packages can share a Cargo workspace, for example with a Cargo.toml at the app root:
Cargo.toml[workspace]
members = ["users", "billing", "libs/*"]
resolver = "2"
[workspace.dependencies]
encore-dev = { path = "/path/to/encore-dev" }
users/Cargo.toml[package]
name = "users"
version = "0.1.0"
edition = "2024"
[dependencies]
encore-dev = { workspace = true }
shared = { path = "../libs/shared" }
A library crate a service depends on by path, directly or through the workspace, is part of that service's build: changing it rebuilds and restarts the services that use it.
Which service uses what
Encore attributes everything to the service whose directory contains the code:
- Usage. Each call on a resource, such as
DB.query(…)orUPLOADS.upload(…), is recorded for the service the calling file belongs to, and that service is granted access to the resource. A call outside every service directory is an error, because there is no service to grant it to. - Handles. Creating a handle, such as
DB.read()orUPLOADS.reference::<perms::Uploader>(), is the usage. Library code outside the services takes the handle as an argument and records nothing itself. See Databases. - Ownership. A few kinds of resource belong to the service that declares them: a Pub/Sub subscription is the declaring service's, a cron job requests a path on the declaring service, and a
SecretorConfigholds a value for the declaring service only. Databases, buckets, topics and Redis databases belong to the app, and any service may use them.
To use a resource that another service declares, declare a second static for it by name, such as Database::named("users"). This works across languages too.
Calling other services
encore::services::base_url returns the base URL the calling service reaches another service at, without a trailing slash. Use it with any HTTP client:
async fn fetch_user(id: i64) -> Result<String, reqwest::Error> {
let users = encore::services::base_url("users");
reqwest::get(format!("{users}/users/{id}")).await?.text().await
}
Looking up a service's URL makes the calling service depend on it, and Encore reads that dependency from the code:
- The service name must be a string literal.
- The function must be called directly, not stored or passed around.
- The call must be inside a service.
base_url panics if the runtime knows no address for the service. That happens for a name that isn't a service of the app, and under encore test, which starts no other services.
Static rules
Encore reads services and their resources from the source, so the declarations have a fixed shape:
- A resource is declared as a module-level
static, with aconstconstructor such asDatabase::new("users"). A declaration inside a function, behind a condition, or wrapped in another call is an error. - Resource names are string literals. Options are chained onto the constructor, each set once from a literal.
- A resource is used by calling its methods on the
static. Assigning it to a variable or passing it to a function of your own is an error: create a handle instead. The SDK functions that take a resource, such asSubscription::new(&TOPIC, …)and the testing helpers, are the exception. - Every use of a resource is inside a service.
Encore reports these errors when it parses the app, on encore run, encore test or encore build.