Vary lets you write a service as a plain interface and implementation, then tell the compiler how to expose it. You do not write routes, annotations, or framework boilerplate. The expose statement handles that.
expose ItemService via http
The via http part is a plugin. Today the only backend is HTTP (powered by Quarkus), but the design is intentional: expose separates what your service does from how it gets exposed. Future plugins could target gRPC, WebSockets, message queues, or CLI wrappers without changing the service code. You write the interface once, and the exposure mechanism is a one-line swap.
This is still early. Only the HTTP plugin exists today. But the architecture is there for others.
expose statementDefine a service as an interface, implement it, and the compiler generates REST and RPC endpoints:
interface ItemService {
def get_item(self, id: Int) -> Str {
}
def list_items(self) -> Str {
}
def create_item(self, name: Str, price: Int) -> Str {
}
def delete_item(self, id: Int) -> Str {
}
}
class ItemServiceImpl() implements ItemService {
def get_item(self, id: Int) -> Str {
return json_dumps({"id": id, "name": "Widget"})
}
def list_items(self) -> Str {
return json_dumps([{"id": 1, "name": "Widget"}])
}
def create_item(self, name: Str, price: Int) -> Str {
return json_dumps({"created": True, "name": name})
}
def delete_item(self, id: Int) -> Str {
return json_dumps({"deleted": id})
}
}
expose ItemService via http
Running with vary run starts a Quarkus server. The compiler generates JAX-RS resource classes.
The method name prefix determines the HTTP verb:
| Prefix | HTTP verb | Example |
|---|---|---|
get_ | GET | get_item |
list_ | GET | list_items |
find_ | GET | find_orders |
create_ | POST | create_item |
update_ | PUT | update_item |
patch_ | PATCH | patch_item |
delete_ | DELETE | delete_item |
The base path is the interface name in kebab-case: ItemService becomes /item-service.
The method path is the name with the verb prefix stripped: get_item becomes /item.
Parameters are classified based on their name and the HTTP verb:
| Verb | id / *_id (Int or Str) | Other parameters |
|---|---|---|
| GET | Path parameter (/item/{id}) | Query parameter (?name=...) |
| POST / PUT / PATCH | Path parameter | JSON body |
| DELETE | Path parameter | Not allowed (compile error) |
Any parameter named id or ending in _id (of type Int or Str) becomes a path parameter:
def get_order(self, user_id: Int, order_id: Int) -> Str {
# GET /order/{user_id}/{order_id}
}
For GET requests, non-id parameters become query parameters:
def list_notes(self, limit: Int, offset: Int) -> Str {
# GET /notes?limit=10&offset=0
}
For POST, PUT, and PATCH requests, non-id parameters are sent as a JSON body:
def create_note(self, title: Str, body: Str) -> Str {
# POST /note
# Body: {"title": "...", "body": "..."}
}
If the method takes a single data class parameter, that data class is used directly as the request body. Otherwise, a synthetic request DTO is generated automatically.
DELETE methods only accept id parameters. This prevents ambiguous endpoints:
# OK
def delete_note(self, id: Int) {
}
# Compile error: non-id parameter 'reason'
def delete_note(self, id: Int, reason: Str) {
}
Run vary routes <file> to see your exact endpoints:
$ vary routes main.vary
NoteService (/note-service):
GET /note-service/health get_health() -> Str
GET /note-service/notes list_notes() -> Str
GET /note-service/note/{id} get_note(id: Int) -> Str
POST /note-service/note create_note(title: Str, body: Str) -> Str
Body: { title: Str, body: Str }
DELETE /note-service/note/{id} delete_note(id: Int) -> Str
GET /q/openapi OpenAPI spec
GET /q/swagger-ui Swagger UI
| Flag | Effect |
|---|---|
--json | Machine-readable JSON output |
--curl | Copy-paste curl examples |
When you run vary run, a route summary is printed in the server startup banner.
Every method also gets an RPC endpoint at POST /<service-path>/rpc/<method_name> that accepts a JSON body with named parameters.
Every exposed interface gets OpenAPI documentation automatically:
| Endpoint | Purpose |
|---|---|
GET /q/openapi | OpenAPI spec |
GET /q/swagger-ui | Swagger UI |
Synthetic request DTOs appear as named schemas in the OpenAPI spec, making the API self-documenting.
While expose defines the server side, service declarations define typed client stubs that consume HTTP APIs:
service UserApi at "https://api.example.com" {
endpoint get_user(id: Int) -> User via GET "/users/{id}"
endpoint create_user(body: CreateUserRequest) -> User via POST "/users"
endpoint delete_user(id: Int) -> Str via DELETE "/users/{id}"
}
Service declarations specify:
| Element | Description |
|---|---|
| Base URL | Where the API lives |
| Endpoints | Name, parameters, return type, method, path |
| Body parameter | For POST/PUT/PATCH, body is the request body |
Use vary client to generate service declarations from existing expose statements:
vary client main.vary # print to stdout
vary client main.vary -o client.vary # write to file
This inspects the module's exposed interfaces and produces matching service declarations.
Two stdlib modules support typed service communication:
| Module | Functions |
|---|---|
service_client | new(base_url), new_with_headers(base_url, headers) |
service_response | decode(response), from_error(error), from_result(result) |
The VCA005 check rule compares service declarations against expose statements and flags mismatches in endpoint names, HTTP methods, paths, parameter types, and return types. See Check rules for details.
Configure allowed and denied hosts in vary.toml under [services]. The VCS008 rule enforces these restrictions. See Configuration for details.
Set QUARKUS_HTTP_PORT to change the listening port (default: 8080).