Hedronite · Backend Dev · Phase B · Mon 2026-10-12 · BACKFILL

Loco skeleton loco new, the route table, and the controller generator

A generator writes files you now own. Read the route table before you trust a URL.

Lesson Class: Duha (T3 · phase B Product)
Cursor: B row 1: Loco skeleton
Lab: loco 1.2.0 · cargo test 2 passed
Next B-day: SeaORM
Choose the Starter
--db, --bg, --assets as flags.
Route Table Is the Truth
cargo loco routes, then curl.
Generator Writes Code You Own
Review the diff.
The prefix said api/health/; the slash returned 404.

A generator writes files you now own. Read the route table before you trust a URL.

§I. Frame

Sunday built a health endpoint by hand on Tokio. Loco is the product path: an opinionated Rust framework on Axum and Tokio, with generators, config per environment, workers, and test helpers. This is the first B-day row, so the job is the skeleton only. SeaORM is the next B-day.

Done bar: generate an app with no database, add one controller with the generator, read the route table, pass the generated tests, and hit the endpoint with curl.

§II. Three techniques

Choose the Starter on Purpose (named technique). loco new --help (loco 1.2.0) lists the choices that shape the whole app:

FlagValuesToday
--dbsqlite, postgres, nonenone (SeaORM is next B-day)
--bgasync, queue-redis, queue-postgres, queue-sqlite, blockingasync
--assetsserverside, clientside, nonenone (Asr owns the frontend)

Pass them as flags so the run is repeatable; the interactive prompt hides what you chose.

The Route Table Is the Truth (named technique). cargo loco routes prints every route the app will serve. Read it before writing a client.

A Generator Writes Code You Own (named technique). cargo loco generate controller health printed what it touched:

added: "src/controllers/health.rs"
injected: "src/controllers/mod.rs"
injected: "src/app.rs"
added: "tests/requests/health.rs"
injected: "tests/requests/mod.rs"

Two new files, three edits. Review the diff like a teammate's PR. It also printed that --api is no longer needed in 1.0 because generators adapt to the app.

§III. Run it

cargo install loco --locked
loco new --name duha_loco --db none --bg async --assets none --os macos
cd duha_loco
cargo loco generate controller health

The generated handler returned format::empty(). One edit makes it match Sunday's binary:

#[debug_handler]
pub async fn index(State(_ctx): State<AppContext>) -> Result<Response> {
    format::json(serde_json::json!({ "status": "ok" }))
}

pub fn routes() -> Routes {
    Routes::new().prefix("api/health/").add("/", get(index))
}

cargo loco routes (real):

/_health GET                                       /_health
/_ping GET                                         /_ping
/_readiness GET                                    /_readiness
/api
   ├─ GET                                          /api
   └─ /health GET                                  /api/health

cargo test: requests::home::can_get_home ... ok, requests::health::can_get_health ... ok, 2 passed.

PORT=5151 cargo loco start, then curl (real):

/api/health  -> 200 {"status":"ok"}
/api/health/ -> 404 (HTML error page)
/_health     -> 200 {"ok":true}

The prefix in code is api/health/, yet the table lists /api/health and the trailing-slash URL returns 404. Trust the table and the curl, not the string in the source. The config comment also warns that every Loco app defaults to port 5150, hence PORT=5151.

§IV. What the skeleton gives you

§V. Drills

  1. Your client calls /api/health/ and gets 404. First command? (cargo loco routes.)
  2. Which flag would you change on the next B-day to add SeaORM? (--db sqlite or --db postgres, on a fresh app.)
  3. Where do /_ping and /_health come from? (AppRoutes::with_default_routes().)
  4. Two Loco apps on one machine fail to start. Why? (Both default to port 5150; set PORT.)

Close

Choose the Starter on Purpose, The Route Table Is the Truth, A Generator Writes Code You Own. Generate, review the diff, read the table, curl it. Then add the database.

Related