Cadmus
v0.1.2

Guides

Your own code

Adding your own routes next to the generated ones, using the database in them, and returning errors the Cadmus way.

The admin and the API cover a lot, but most apps need some pages or endpoints of their own. They go in src/routes.rs, as ordinary axum routes, served next to the generated ones.

#A route

use axum::Router;
use axum::routing::get;

pub fn router() -> Router {
    Router::new().route("/health", get(|| async { "ok" }))
}

#Using the database

Pass the connection to your router. In src/main.rs:

let app = App::from_settings(settings::settings()?).await?.migrations(migrations::all());
let db = app.db().clone();
models::register(app).routes(routes::router(db)).run().await

and in src/routes.rs:

use axum::extract::{Path, State};
use axum::routing::get;
use axum::{Json, Router};
use cadmus::{AdminError, ApiResult};
use sea_orm::{DatabaseConnection, EntityTrait};

use crate::models::post;

pub fn router(db: DatabaseConnection) -> Router {
    Router::new()
        .route("/posts/{id}/title", get(title))
        .with_state(db)
}

async fn title(State(db): State<DatabaseConnection>, Path(id): Path<i32>) -> ApiResult<Json<String>> {
    let post = post::Entity::find_by_id(id)
        .one(&db)
        .await?
        .ok_or_else(|| AdminError::NotFound("post".into()))?;
    Ok(Json(post.title))
}

#Errors

Every failure in Cadmus is an AdminError, shown in the right form for where the request went:

Request toAn error looks like
/api/...JSON: {"error": {"code", "message", "field"}}
/admin/...a page in the admin's style (error.html, which you can replace)
anything elseplain text, or whatever your handler returns

Unknown addresses, wrong methods (405) and crashes in a handler become proper error responses too. Internal details (SQL, file paths, crash messages) are logged, never sent to the visitor.

In your own handlers, return ApiResult<T> to get JSON errors. Database, storage and I/O errors convert with ?, and AdminError::custom makes an error with your own status and code:

if item.left == 0 {
    return Err(AdminError::custom(StatusCode::CONFLICT, "out_of_stock", "sold out").into());
}

#Logging

Messages go to the terminal (standard error). log_level in the settings, or the LOG_LEVEL variable, sets how much: error, warn, info (default), debug (also every SQL statement) or trace. Each request is logged on one line with method, address, status and duration. If your main installs its own tracing subscriber before run(), that one is used instead.

#In the API documentation

Your own routes are not discovered automatically. Add them to the documentation with DocsConfig::extend (see API documentation).