Embedded DSLs

HTTP services

How it works

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.

The expose statement

Define 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.

HTTP verb inference

The method name prefix determines the HTTP verb:

PrefixHTTP verbExample
get_GETget_item
list_GETlist_items
find_GETfind_orders
create_POSTcreate_item
update_PUTupdate_item
patch_PATCHpatch_item
delete_DELETEdelete_item

Path generation

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.

Parameter rules

Parameters are classified based on their name and the HTTP verb:

Verbid / *_id (Int or Str)Other parameters
GETPath parameter (/item/{id})Query parameter (?name=...)
POST / PUT / PATCHPath parameterJSON body
DELETEPath parameterNot allowed (compile error)

Path parameters

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}
}

Query parameters (GET)

For GET requests, non-id parameters become query parameters:

def list_notes(self, limit: Int, offset: Int) -> Str {
    # GET /notes?limit=10&offset=0
}

JSON body (POST/PUT/PATCH)

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 strictness

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) {
}

Inspecting routes

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
FlagEffect
--jsonMachine-readable JSON output
--curlCopy-paste curl examples

When you run vary run, a route summary is printed in the server startup banner.

RPC fallback

Every method also gets an RPC endpoint at POST /<service-path>/rpc/<method_name> that accepts a JSON body with named parameters.

OpenAPI / Swagger

Every exposed interface gets OpenAPI documentation automatically:

EndpointPurpose
GET /q/openapiOpenAPI spec
GET /q/swagger-uiSwagger UI

Synthetic request DTOs appear as named schemas in the OpenAPI spec, making the API self-documenting.

Typed service clients

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:

ElementDescription
Base URLWhere the API lives
EndpointsName, parameters, return type, method, path
Body parameterFor POST/PUT/PATCH, body is the request body

Generating client stubs

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.

Stdlib modules

Two stdlib modules support typed service communication:

ModuleFunctions
service_clientnew(base_url), new_with_headers(base_url, headers)
service_responsedecode(response), from_error(error), from_result(result)

Drift detection

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.

Host restrictions

Configure allowed and denied hosts in vary.toml under [services]. The VCS008 rule enforces these restrictions. See Configuration for details.

Port configuration

Set QUARKUS_HTTP_PORT to change the listening port (default: 8080).

← Embedded DSLs
SQLite databases →