Project layout and rws.app.toml
The conventional layout of an rws application — where routes, models, jobs, migrations, templates and tests go — and the rws.app.toml manifest that names the app and any path that differs.
Since 18.28.0 an rws application has one conventional layout, so a person or
a coding agent can find (and add) a route, a model or a migration without
reading the project first. The starters
use it; the dev MCP endpoint's layout tool reports it,
and rws gen (18.29.0) writes models, routes,
migrations and tests into it:
rws gen model Post title:String # src/models/post.rs + migrations/0001_create_posts.sql
rws gen route posts # src/routes/posts.rs, registered in app()
rws gen test posts # tests/posts.rs
rws db migrate # apply migrations/ (rws db status|rollback|seed)
my-app/
├── rws.app.toml the manifest: the app's name, paths that differ
├── Cargo.toml
├── src/
│ ├── main.rs wiring only: settings, dev_mcp, Server::run_tls
│ ├── lib.rs app(): the routes and middleware, built in a function
│ ├── routes/ typed handlers (#[get] …), one module per resource
│ │ ├── mod.rs
│ │ └── posts.rs
│ ├── models/ #[derive(Model)] types, one module per table
│ └── jobs/ background jobs and scheduled tasks
├── migrations/ 0001_create_posts.sql, 0001_create_posts.down.sql, …
├── templates/ Tera templates
├── tests/ integration tests against app() (TestClient)
└── static/ files served as they are| Role | Path | What goes there |
|---|---|---|
main |
src/main.rs |
Wiring only: Server::setup(), app().dev_mcp_with(…), Server::run_tls. Nothing a test needs. |
lib |
src/lib.rs |
pub fn app() -> … building every route and layer, so tests/ can call it; the state type; dev_tools() |
routes |
src/routes/ |
One module per resource: its typed handlers and a register(app) -> app that adds them (app.route(list).route(show)) |
models |
src/models/ |
#[derive(Model)] types, one module per table |
jobs |
src/jobs/ |
Jobs and scheduled tasks |
migrations |
migrations/ |
NNNN_name.sql, applied in order, each with an optional NNNN_name.down.sql for rollback (migrations) |
templates |
templates/ |
Tera templates |
tests |
tests/ |
Integration tests: TestClient::new(my_app::app()) |
static |
static/ |
Static files |
A binary-only crate can't be tested from tests/, hence src/lib.rs: the
binary is a few lines that serve what the library builds.
// src/main.rs
use rust_web_server::dev_mcp::DevMcpExt;
use rust_web_server::server::Server;
#[tokio::main]
async fn main() {
let (listener, pool) = Server::setup().expect("server setup");
let app = my_app::app().dev_mcp_with(my_app::dev_tools());
Server::run_tls(listener, pool, app).await;
}
// src/lib.rs
pub mod routes;
use rust_web_server::dev_mcp::DevTools;
use rust_web_server::prelude::*;
use rust_web_server::state::AppWithState;
#[derive(Clone, Default)] // the server clones the app per worker
pub struct Db {
pub posts: std::collections::HashMap<u64, String>, // or a DbPool
}
pub fn app() -> AppWithState<Db> {
routes::posts::register(App::with_state(Db::default()))
}
pub fn dev_tools() -> DevTools {
DevTools::new().app_dir(env!("CARGO_MANIFEST_DIR"))
}
// src/routes/posts.rs
use rust_web_server::error::AppError;
use rust_web_server::handler::{Json, Path};
use rust_web_server::state::AppWithState;
use rust_web_server::get;
use crate::Db;
#[get("/posts/{id}")]
pub fn show(Path(id): Path<u64>, db: &Db) -> Result<Json<String>, AppError> {
db.posts.get(&id).cloned().map(Json).ok_or_else(|| AppError::NotFound(format!("no post {id}")))
}
pub fn register(app: AppWithState<Db>) -> AppWithState<Db> {
app.route(show)
}rws.app.toml
The manifest sits at the project root. It names the app and says where the project differs from the layout; every key is optional, so an empty file — or none — means "conventional":
[app]
name = "blog" # default: Cargo.toml's [package] name, else the directory's
[paths]
static = "frontend/dist" # only what differs; the rest keep their defaults| Key | Default |
|---|---|
[app] name |
[package] name from Cargo.toml, else the directory's name |
[paths] main |
src/main.rs |
[paths] lib |
src/lib.rs |
[paths] routes |
src/routes |
[paths] models |
src/models |
[paths] jobs |
src/jobs |
[paths] migrations |
migrations |
[paths] templates |
templates |
[paths] tests |
tests |
[paths] static |
static |
- Values are TOML strings (
"…"or'…');# commentsare allowed;paths.static = "…"at the top level is the same as the key in[paths]. - Paths are relative to the project root and stay inside it: a leading
/, a drive letter or a..is an error, as is a key set twice. Errors name the line:rws.app.toml line 3: `routes` = "/srv/x" is absolute; paths are relative to the project root. - Unknown keys and tables are warnings, not errors, so a manifest written for a newer release still loads.
- The manifest describes the project; rws doesn't move files or change
routing because of it.
rws genwrites where it says. Themcpstarter, whose "routes" are MCP tools, saysroutes = "src/tools".
Read it in code with app_manifest::AppManifest:
use rust_web_server::app_manifest::AppManifest;
let manifest = AppManifest::load(env!("CARGO_MANIFEST_DIR"))?; // conventional when absent
println!("{} keeps its models in {}", manifest.name, manifest.paths.models);
for entry in manifest.layout(env!("CARGO_MANIFEST_DIR")) {
println!("{:<10} {:<14} {}", entry.role, entry.path, if entry.exists { "ok" } else { "missing" });
}AppManifest::parse(text) (or text.parse::<AppManifest>()) reads a
manifest from a string; files over 64 KiB are refused.