name: ras-setup description: Use when the user asks to create a new RAS project, set up a Rust Agent Stack workspace, configure Cargo.toml for RAS crates, add RAS dependencies, scaffold a service crate, or asks about RAS workspace structure and crate organization. version: 1.0.0
RAS Project Setup — Workspace Scaffolding & Dependencies
RAS projects follow the same workspace-first, crate-split conventions from the rust-project-setup skill. The key addition: an API crate where macro invocations define the service contract, sitting between the domain core and the binary that wires everything together.
Starter template: For a ready-to-compile RAS project with tests, see the scaffold-fullstack skill.
Project Structure
A typical RAS project adds an API crate to the standard layout:
my-project/
├── Cargo.toml # workspace root
├── crates/
│ ├── my-core/ # domain types, traits (ports), pure logic
│ │ ├── Cargo.toml
│ │ └── src/lib.rs
│ ├── my-api/ # RAS macro invocations + request/response types
│ │ ├── Cargo.toml
│ │ └── src/lib.rs
│ ├── my-service/ # binary: wires implementations into generated builders
│ │ ├── Cargo.toml
│ │ └── src/main.rs
│ └── my-testutils/ # shared fakes and fixtures (dev-dependency only)
│ ├── Cargo.toml
│ └── src/lib.rs
my-core— Domain types and port traits. No IO dependencies. No RAS dependency.my-api— Invokesrest_service!,jsonrpc_service!, etc. Defines request/response types. Depends on RAS macro crates +my-core.my-service— Implements the generated service traits, constructs adapters, wires auth viaArc<dyn AuthProvider>, and runs the Axum server.my-testutils— Hand-written fakes includingFakeAuthProvider. Only a[dev-dependencies]entry.
The API crate exists because macro invocations generate both server traits and a native Rust client — keeping them in a separate crate lets other services depend on just the client and shared types (via features = ["client"]) without pulling in the full service implementation.
Where RAS Macros Live
Macros belong in the API crate, not the domain crate. The API crate defines the contract:
#![allow(unused)] fn main() { // crates/my-api/src/lib.rs use ras_rest_macro::rest_service; use serde::{Deserialize, Serialize}; use schemars::JsonSchema; // Request/response types — must derive all three #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct CreateTaskRequest { pub title: String, pub description: String, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct Task { pub id: String, pub title: String, pub completed: bool, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct TasksResponse { pub tasks: Vec<Task>, pub total: usize, } rest_service!({ service_name: TaskService, base_path: "/api/v1", openapi: true, serve_docs: true, endpoints: [ GET UNAUTHORIZED tasks() -> TasksResponse, POST WITH_PERMISSIONS(["user"]) tasks(CreateTaskRequest) -> Task, GET UNAUTHORIZED tasks/{id: String}() -> Task, DELETE WITH_PERMISSIONS(["admin"]) tasks/{id: String}() -> (), ] }); }
The domain crate stays clean — no macro dependencies, no HTTP/RPC concerns.
With serve_docs: true, the router hosts the built-in RAS API explorer at /api/v1/docs and the OpenAPI JSON at /api/v1/docs/openapi.json. The explorer supports bearer-token testing and keeps tokens in browser sessionStorage, not persistent localStorage.
Workspace Cargo.toml
Follow rust-project-setup conventions with RAS crates in [workspace.dependencies]:
[workspace]
members = ["crates/*"]
resolver = "3"
[workspace.package]
edition = "2024"
rust-version = "1.85"
[workspace.dependencies]
# RAS crates (not on crates.io — use git dependency)
ras-rest-macro = { git = "https://github.com/JedimEmO/rust-agent-stack.git" }
ras-rest-core = { git = "https://github.com/JedimEmO/rust-agent-stack.git" }
ras-jsonrpc-macro = { git = "https://github.com/JedimEmO/rust-agent-stack.git" }
ras-jsonrpc-core = { git = "https://github.com/JedimEmO/rust-agent-stack.git" }
ras-file-macro = { git = "https://github.com/JedimEmO/rust-agent-stack.git" }
ras-auth-core = { git = "https://github.com/JedimEmO/rust-agent-stack.git" }
ras-identity-session = { git = "https://github.com/JedimEmO/rust-agent-stack.git" }
ras-identity-local = { git = "https://github.com/JedimEmO/rust-agent-stack.git" }
ras-identity-oauth2 = { git = "https://github.com/JedimEmO/rust-agent-stack.git" }
ras-observability-core = { git = "https://github.com/JedimEmO/rust-agent-stack.git" }
ras-observability-otel = { git = "https://github.com/JedimEmO/rust-agent-stack.git" }
# Standard deps
serde = { version = "1", features = ["derive"] }
schemars = "1.0.0-alpha.20"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
axum = "0.8"
anyhow = "1"
thiserror = "2"
tracing = "0.1"
async-trait = "0.1"
[workspace.lints.clippy]
pedantic = { level = "warn", priority = -1 }
module_name_repetitions = "allow"
must_use_candidate = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
unwrap_used = "warn"
[workspace.lints.rust]
unsafe_code = "deny"
Read references/cargo-toml-templates.md for complete, copy-pasteable member crate templates.
API Crate Cargo.toml
The API crate depends on RAS macro crates and serialization:
[package]
name = "my-api"
version = "0.1.0"
edition.workspace = true
[lints]
workspace = true
[dependencies]
ras-rest-macro.workspace = true
ras-rest-core.workspace = true
ras-auth-core.workspace = true
serde.workspace = true
schemars.workspace = true
async-trait.workspace = true
[features]
default = ["server", "client"]
server = []
client = []
All request/response types must derive Serialize, Deserialize, and JsonSchema. Missing JsonSchema causes a compile error when openapi: true is set.
Binary Crate — Wiring
The service crate implements the generated trait and wires everything together using Arc<dyn AuthProvider>:
// crates/my-service/src/main.rs use my_api::{TaskServiceBuilder, TaskServiceTrait}; use ras_auth_core::AuthenticatedUser; use ras_rest_core::{RestResult, RestResponse, RestError}; use ras_identity_session::{SessionService, SessionConfig, JwtAuthProvider}; use std::sync::Arc; struct TaskServiceImpl { /* domain deps via Arc<dyn Trait> */ } #[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> { // Authenticated endpoints receive &AuthenticatedUser automatically Ok(RestResponse::created(Task { /* ... */ })) } // ... } #[tokio::main] async fn main() -> anyhow::Result<()> { let jwt_secret = std::env::var("JWT_SECRET").context("JWT_SECRET must be set")?; let session_service = Arc::new(SessionService::new(SessionConfig::new(jwt_secret)?)?); let auth: Arc<dyn ras_auth_core::AuthProvider> = Arc::new(JwtAuthProvider::new(session_service)); let service = TaskServiceImpl { /* inject domain deps */ }; let router = TaskServiceBuilder::new(service) .auth_provider(auth) .build(); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; axum::serve(listener, router).await.context("server failed")?; Ok(()) }
Adding a New Service to an Existing Workspace
- Create the API crate:
cargo new crates/my-new-api --lib - Add RAS macro deps to its
Cargo.toml(inherit from workspace) - Define types and invoke the service macro
- Implement the generated trait in the service crate (or a new service crate)
- Wire into the existing Axum router via
.merge()or.nest()
Multiple RAS services compose naturally — each macro generates an independent Router:
#![allow(unused)] fn main() { let app = Router::new() .merge(task_router) .merge(user_router) .merge(otel.metrics_router()); }
Related Skills
For general workspace conventions, crate-split decisions, and feature flag strategy, see the rust-project-setup skill.
For the trait-as-interface DI pattern used for AuthProvider wiring, see the rust-architecture skill.
For macro syntax and endpoint definition, see the ras-api-design skill.