name: ras-api-design description: Use when the user asks about defining REST endpoints, JSON-RPC methods, file service routes, or WebSocket services with Rust API Stack/RAS macros, designing request/response types, path parameters, query parameters, macro syntax for rest_service!, jsonrpc_service!, file_service!, or jsonrpc_bidirectional_service!, or asks about OpenAPI/OpenRPC generation.
RAS API Design - Macro Syntax & Endpoint Definition
Rust API Stack (RAS) macros generate typed service traits, builders, Axum routers, native Rust clients, and OpenAPI/OpenRPC specs from declarative API contracts. Keep the macro invocation in a shared API crate so server crates can enable server and callers can enable client without pulling in the service implementation.
All service macros share the same auth syntax:
#![allow(unused)] fn main() { UNAUTHORIZED WITH_PERMISSIONS(["user"]) WITH_PERMISSIONS(["admin"] | ["support", "users:write"]) WITH_PERMISSIONS([]) }
UNAUTHORIZEDis public and does not pass a user to the handler.WITH_PERMISSIONS(["a", "b"])requires authentication plus both permissions.WITH_PERMISSIONS(["a"] | ["b", "c"])accepts either group:a, or bothbandc.WITH_PERMISSIONS([])means authenticated-only with no permission strings.
rest_service! - REST APIs
#![allow(unused)] fn main() { use ras_rest_macro::rest_service; rest_service!({ service_name: TaskService, base_path: "/api/v1", openapi: true, serve_docs: true, docs_path: "/docs", endpoints: [ GET UNAUTHORIZED tasks() -> TasksResponse, GET UNAUTHORIZED tasks/{id: String}() -> Task, GET UNAUTHORIZED search/tasks ? q: String & limit: Option<u32> () -> TasksResponse, POST WITH_PERMISSIONS(["tasks:write"]) tasks(CreateTaskRequest) -> Task, PUT WITH_PERMISSIONS(["tasks:write"]) users/{user_id: String}/tasks/{task_id: String}(UpdateTaskRequest) -> Task, DELETE WITH_PERMISSIONS(["admin"] | ["tasks:delete"]) tasks/{id: String}() -> (), ] }); }
Endpoint syntax:
METHOD AUTH_REQUIREMENT path/{param: Type}/segments ? query: Type & opt: Option<Type> (RequestBody) -> ResponseType
| Component | Options |
|---|---|
| Method | GET, POST, PUT, DELETE, PATCH |
| Auth requirement | UNAUTHORIZED, WITH_PERMISSIONS([...]), WITH_PERMISSIONS([...]\|[...]), WITH_PERMISSIONS([]) |
| Path params | {name: Type} inline in the path |
| Query params | ? param: Type & param2: Option<Type> after the path |
| Request body | (RequestType); use () when there is no body |
| Response | -> ResponseType; use () for empty responses |
With serve_docs: true, the generated router serves the built-in API explorer at base_path + docs_path and the OpenAPI document at base_path + docs_path + "/openapi.json". For example, base_path: "/api/v1" and docs_path: "/docs" serve /api/v1/docs and /api/v1/docs/openapi.json.
Request & Response Types
Types used in macro invocations should derive serialization and schema traits:
#![allow(unused)] fn main() { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct CreateTaskRequest { pub title: String, pub description: String, } }
SerializeandDeserializeare required for JSON encoding.JsonSchemais required when generating OpenAPI/OpenRPC specs.
REST Handlers
Protected endpoints receive &AuthenticatedUser before path, query, and body arguments:
#![allow(unused)] fn main() { use ras_auth_core::AuthenticatedUser; use ras_rest_core::{RestError, RestResponse, RestResult}; #[async_trait::async_trait] impl TaskServiceTrait for TaskServiceImpl { async fn get_tasks(&self) -> RestResult<TasksResponse> { Ok(RestResponse::ok(TasksResponse { tasks: vec![], total: 0 })) } async fn post_tasks( &self, user: &AuthenticatedUser, request: CreateTaskRequest, ) -> RestResult<Task> { Ok(RestResponse::created(self.create_for_user(user, request).await?)) } } }
Use RestResponse helpers for success responses and RestError for failures:
#![allow(unused)] fn main() { Ok(RestResponse::ok(task)) Ok(RestResponse::created(task)) Ok(RestResponse::with_status(202, task)) Err(RestError::bad_request("invalid task id")) Err(RestError::unauthorized("invalid token")) Err(RestError::forbidden("insufficient permissions")) Err(RestError::not_found("task not found")) Err(RestError::with_internal(500, "internal error", db_error)) }
Generated REST Client
The API crate must forward generated-client features. For the default reqwest-backed client:
[features]
default = []
server = [
"ras-rest-macro/server",
"dep:ras-rest-core",
"dep:ras-auth-core",
"dep:async-trait",
"dep:axum",
"dep:axum-extra",
"dep:tokio",
]
client = ["ras-rest-macro/reqwest", "ras-transport-core/reqwest"]
The macro crate's client feature emits client types and build_with_transport(...). Its reqwest feature also emits the default build(). If you inject a custom transport, forward ras-rest-macro/client plus dep:ras-transport-core instead of ras-rest-macro/reqwest.
Pass the server origin to the generated REST client; the macro joins base_path automatically:
#![allow(unused)] fn main() { let mut client = TaskServiceClient::builder("http://localhost:3000") .with_timeout(std::time::Duration::from_secs(10)) .build()?; client.set_bearer_token(Some(jwt_token)); let tasks = client.get_tasks().await?; let task = client.get_tasks_by_id("task-123".to_string()).await?; let created = client.post_tasks(CreateTaskRequest { title: "New task".into(), description: "Details".into(), }).await?; }
jsonrpc_service! - JSON-RPC
#![allow(unused)] fn main() { use ras_jsonrpc_macro::jsonrpc_service; jsonrpc_service!({ service_name: UserService, openrpc: true, methods: [ UNAUTHORIZED sign_in(SignInRequest) -> SignInResponse, WITH_PERMISSIONS(["user"]) get_profile(()) -> UserProfile, WITH_PERMISSIONS(["admin"] | ["support", "users:write"]) disable_user(String) -> (), ] }); }
JSON-RPC methods map to JSON-RPC 2.0 method strings. Protected methods receive &AuthenticatedUser before the request payload. Enable the API crate client with:
client = ["ras-jsonrpc-macro/reqwest", "ras-transport-core/reqwest"]
Use the generated builder with the JSON-RPC endpoint URL:
#![allow(unused)] fn main() { let mut client = UserServiceClientBuilder::new("http://localhost:3000/rpc") .with_timeout(std::time::Duration::from_secs(10)) .build()?; let signed_in = client.sign_in(SignInRequest { email: "alice@example.com".to_string(), password: "correct horse battery staple".to_string(), }).await?; client.set_bearer_token(Some(signed_in.token)); let profile = client.get_profile(()).await?; }
With openrpc: true, the macro emits generate_userservice_openrpc() and generate_userservice_openrpc_to_file(). The generated document includes auth metadata, flattened x-permissions, grouped x-permission-groups, and version metadata when present.
file_service! - File Upload/Download
Use file_service! for APIs that stream uploads and downloads. The generated server authenticates and checks permissions before consuming upload bodies, validates multipart fields, and streams bytes instead of buffering whole files.
#![allow(unused)] fn main() { use ras_file_macro::file_service; file_service!({ service_name: DocumentService, base_path: "/api/documents", openapi: true, endpoints: [ UPLOAD WITH_PERMISSIONS(["files:write"]) upload multipart { max_total_bytes: 52428800, reject_unknown_fields: true, parts: [ file file { required: true, max_count: 1, max_bytes: 52428800, content_types: ["application/pdf", "text/plain"], filename: optional, }, json metadata: UploadMetadata { required: false, max_bytes: 4096, content_types: ["application/json"], }, ], } -> UploadResponse, DOWNLOAD WITH_PERMISSIONS(["files:read"]) download/{file_id: String} { content_types: ["application/octet-stream"], ranges: true, }, ] }); }
File API crates usually expose:
server = [
"ras-file-macro/server",
"dep:ras-file-core",
"dep:ras-auth-core",
"dep:async-trait",
"dep:axum",
"dep:schemars",
"dep:serde_json",
]
client = ["ras-file-macro/reqwest", "ras-transport-core/reqwest"]
fs = ["ras-file-macro/fs", "ras-transport-core/fs"]
Enable fs for native helpers that stream file parts from disk.
jsonrpc_bidirectional_service! - WebSocket JSON-RPC
#![allow(unused)] fn main() { jsonrpc_bidirectional_service!({ service_name: ChatService, client_to_server: [ UNAUTHORIZED connect(ConnectRequest) -> ConnectResponse, WITH_PERMISSIONS(["user"]) send_message(SendMessageRequest) -> SendMessageResponse, ], server_to_client: [ message_received(MessageNotification), user_joined(UserJoinedNotification), ] }); }
Use bidirectional JSON-RPC for WebSocket-style workflows. It shares the same WITH_PERMISSIONS(["a"] | ["b", "c"]) auth syntax as HTTP JSON-RPC.
Permission Metadata
OpenAPI and OpenRPC output include both:
x-permissions: flattened compatibility list.x-permission-groups: the original OR/AND groups used by runtime checks.
Enable the macro crate's permissions feature plus ras-permission-manifest when admin tooling, tests, or token issuing code should import compile-checked permission constants:
[features]
permissions = ["ras-rest-macro/permissions"]
For a service named UserService, the macro emits generate_userservice_permission_manifest() and a userservice_permissions module with permission constants and operation requirements.