Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
File diff suppressed because one or more lines are too long
+8
View File
@@ -0,0 +1,8 @@
[alias]
xtask = "run --package xtask --"
# https://github.com/rust-lang/rust/issues/141626
# (can be removed once link.exe is fixed)
[target.x86_64-pc-windows-msvc]
linker = "rust-lld"
+6
View File
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "fc45f210f9c72a29562120d378151c895eddd8ab"
},
"path_in_vcs": ""
}
+1
View File
@@ -0,0 +1 @@
cognitive-complexity-threshold = 40
+1
View File
@@ -0,0 +1 @@
github: loco-rs
@@ -0,0 +1,25 @@
---
name: Bug Report
about: Report behavior that deviates from specification or expectation
title: ""
labels: assessment
assignees: ""
---
**Description**
Provide a clear and concise explanation of the bug, including references to any conflicting documentation or an elucidation of why the current functionality doesn't align with your expectations.
**To Reproduce**
Please outline the steps to replicate the bug, and if possible, provide a comprehensive code example consisting of both `main.rs` and `Cargo.toml`` files. A complete and functional code snippet would be highly appreciated.
**Expected Behavior**
A clear and concise description of what you expected to happen.
**Environment:**
**Additional Context**
Include additional context about the issue in this section. For instance, share insights into the bug's discovery process or any hypotInclude additional context about the issue in this section. For instance, share insights into the bug's discovery process or any hypotheses you may have regarding the root cause or specific aspects where Loco may be malfunctioning.
@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Question
url: https://github.com/loco-rs/loco/discussions
about: Please ask questions or raise indefinite concerns on Discussions
@@ -0,0 +1,21 @@
---
name: Feature Request
about: Suggest a new feature
title: ""
labels: enhancement
assignees: ""
---
## Feature Request
**Is your feature request related to a problem? Please describe.**
Provide a succinct and clear description of the problem. For example, I encounter an issue when...
**Describe the solution you'd like**
Articulate a clear and concise depiction of your desired outcome.
**Describe alternatives you've considered**
Offer a clear and concise description of any alternative solutions or features you have contemplated.
@@ -0,0 +1,26 @@
---
name: Suggestion
about: Suggest a change or improvement to existing functionality
title: ""
labels: assessment
assignees: ""
---
**Description**
Please provide a well-defined and succinct explanation of the issue. Include references to any conflicting documentation or clarify why the current functionality deviates from your expectations.
**To Reproduce**
Please outline the steps to replicate the bug, and if possible, provide a comprehensive code example consisting of both `main.rs` and `Cargo.toml`` files. A complete and functional code snippet would be highly appreciated.
**Expected Behavior**
Provide a straightforward and brief explanation of your anticipated outcome.
**Environment:**
**Additional Context**
Include additional details about the issue in this section. For instance, share insights into how you discovered the bug or any hypotheses you may have regarding what might be causing the problem with Loco.
+12
View File
@@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 0
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
+78
View File
@@ -0,0 +1,78 @@
name: "[docs]"
on:
push:
branches:
- master
pull_request:
env:
RUST_TOOLCHAIN: stable
TOOLCHAIN_PROFILE: minimal
jobs:
check:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout the code
uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Verify curated LLM docs (llms.txt / llms-full.txt) against docs tree
run: cargo run -p xtask -- llms-check
- name: Compile the docs example app (verified-docs spine)
run: cargo build --manifest-path examples/demo/Cargo.toml
- name: Install zola
uses: taiki-e/install-action@v2
with:
tool: zola@0.21.0
- name: Build the docs site (fails on broken internal links)
run: zola build
working-directory: docs-site
- run: cargo install snipdoc --features exec
- run: snipdoc check
continue-on-error: true
env:
SNIPDOC_SKIP_EXEC_COMMANDS: true
# Builds the deployed site (Astro/Starlight in website/). The `check` job
# above still verifies the docs *source* (docs-site/, snipdoc, llms-check);
# this job verifies the artifact that actually ships to loco.rs.
website:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout the code
uses: actions/checkout@v7
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
# The site lives in website/; read its packageManager pin, since
# there is no root package.json.
package_json_file: website/package.json
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: website/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
working-directory: website
- name: Unit tests (migration / link-rewrite scripts)
run: pnpm test
working-directory: website
- name: Build the Astro site (fails on broken links)
run: pnpm build
working-directory: website
- name: URL parity (old loco.rs URLs still resolve)
run: |
pnpm url-parity
pnpm url-parity-blog
working-directory: website
@@ -0,0 +1,83 @@
name: "[loco-gen:ci]"
on:
push:
branches:
- master
paths:
- "loco-gen/**"
pull_request:
paths:
- "loco-gen/**"
env:
RUST_TOOLCHAIN: stable
TOOLCHAIN_PROFILE: minimal
defaults:
run:
working-directory: ./loco-gen
jobs:
style:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout the code
uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
components: rustfmt
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
- name: Run cargo fmt
run: cargo fmt --all -- --check
- name: Run cargo clippy
run: cargo clippy --all-features -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms
test:
needs: [style]
runs-on: ubuntu-latest
permissions:
contents: read
services:
postgres:
image: postgres
env:
POSTGRES_DB: postgres_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
# Set health checks to wait until postgres has started
options: --health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout the code
uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
- name: Install seaorm cli
run: cargo install sea-orm-cli
- run: |
cargo install --path ../loco-new
- name: Run cargo test
run: cargo test --all-features
env:
LOCO_DEV_MODE_PATH: ${{ github.workspace }}
DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres_test
@@ -0,0 +1,46 @@
name: "[loco-gen-deploy]"
on:
schedule:
- cron: "0 0 * * *"
env:
RUST_TOOLCHAIN: stable
TOOLCHAIN_PROFILE: minimal
jobs:
g-deploy-docker:
# This workflow creates a new Loco application and builds a Docker image
# We only want this to run on the main repository (loco-rs/loco) and not on forks because:
# 1. It consumes GitHub Actions minutes unnecessarily on forks
# 2. The Docker build and deployment is specific to the main repository
# 3. Forks typically don't need to run this automated deployment process
if: github.repository == 'loco-rs/loco'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout the code
uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
- name: Install seaorm cli
run: cargo install sea-orm-cli
- name: install 'loco new'
run: |
cargo install loco
- name: create myapp
run: |
loco new -n myapp --db sqlite --bg async --assets serverside -a
- name:
run: cargo loco generate deployment docker && docker build -t demo .
working-directory: ./myapp
@@ -0,0 +1,85 @@
name: "[loco-new:ci]"
on:
push:
branches:
- master
paths:
- "loco-new/**"
- "loco-gen/**"
pull_request:
paths:
- "loco-new/**"
- "loco-gen/**"
env:
RUST_TOOLCHAIN: stable
TOOLCHAIN_PROFILE: minimal
jobs:
style:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout the code
uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
components: rustfmt
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
- run: cargo fmt --all -- --check
working-directory: ./loco-new
- name: Run cargo clippy
run: cargo clippy --all-features -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms
working-directory: ./loco-new
test:
# needs: [style]
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
permissions:
contents: read
steps:
- name: Checkout the code
uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
- name: Configure sccache
run: |
echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV
echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.10
- name: Install seaorm cli
run: cargo install sea-orm-cli
- name: Free Disk Space
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
- name: Run cargo test
run: cargo test --all-features -- --test-threads 1
working-directory: ./loco-new
env:
LOCO_DEV_MODE_PATH: ${{ github.workspace }}
# NOTE NOTE NOTE: this is for optimizing build and may result in strange behavior
CARGO_TARGET_DIR: /tmp/shared-target
@@ -0,0 +1,66 @@
# To optimize CI runtime:
# A simpler "sanity check" workflow is introduced.
# This workflow only runs if changes in the PR do NOT include
# the `loco-gen` or `loco-new` paths.
# (When changes are made to `loco-gen` or `loco-new`,
# we run comprehensive tests to validate every generator command
# and template option.)
# Purpose of the sanity check:
# It performs basic validation by comparing the local changes
# against the templates.
# If any breaking changes are detected in the templates,
# the sanity check will fail, signaling an issue.
name: "[loco_rs:sanity]"
on:
push:
branches:
- master
pull_request:
env:
RUST_TOOLCHAIN: stable
TOOLCHAIN_PROFILE: minimal
jobs:
sanity:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout the code
uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
- name: Install seaorm cli
run: cargo install sea-orm-cli
- run: cargo install --path loco-new
- run: |
loco new -n myappdb --db sqlite --bg async --assets serverside -a
cd myappdb
cargo check
cargo build --release
cargo test --all-features
env:
LOCO_DEV_MODE_PATH: ${{ github.workspace }}
- run: |
loco new -n myapp --db none --bg async --assets none -a
cd myapp
cargo check
cargo build --release
cargo test --all-features
env:
LOCO_DEV_MODE_PATH: ${{ github.workspace }}
@@ -0,0 +1,89 @@
name: "[loco_rs:ci]"
on:
push:
branches:
- master
pull_request:
env:
RUST_TOOLCHAIN: stable
TOOLCHAIN_PROFILE: minimal
jobs:
style:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout the code
uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
components: rustfmt
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
- name: Run cargo fmt
run: cargo fmt --all -- --check
- name: Run cargo clippy
run: cargo clippy --all-features -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms
check:
needs: [style]
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout the code
uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@v2
with:
tool: cargo-hack
- run: cargo hack check --each-feature
build:
needs: [check, style]
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout the code
uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
- name: Run cargo build
run: cargo build --release
test:
needs: [check, style]
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout the code
uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
- name: Run cargo test
run: cargo test --all-features --workspace --exclude loco-gen --exclude loco
+3
View File
@@ -0,0 +1,3 @@
max_width = 100
use_small_heuristics = "Default"
style_edition = "2021"
+3
View File
@@ -0,0 +1,3 @@
{
"rust-analyzer.showUnlinkedFileNotification": false
}
+247
View File
@@ -0,0 +1,247 @@
# Building Loco apps — agent guide
This file teaches an AI agent how to build **Loco** (loco.rs) applications
correctly. Loco is an all-in-one, batteries-included Rust web framework (think
"Rails for Rust"): one binary and one set of conventions give you routing,
an ORM, background jobs, a scheduler, mailers, tasks, storage, caching, and
testing. Because it is batteries-included, the single most common failure mode
for LLMs is **reaching for external crates and hand-wiring infrastructure that
Loco already provides**. Prefer Loco's built-ins and generators.
This guide targets **Loco 1.0** (Sea-ORM 2.0, sqlx 0.9, edition 2024 for the
framework itself — generated apps are still edition 2021). For prose docs see
https://loco.rs/docs, and for a single-file reference see
https://loco.rs/llms-full.txt.
## Golden rules
1. **Use the generators.** `cargo loco generate model|scaffold|controller|
worker|task|scheduler|mailer|migration|deployment|override ...` writes
correct, convention-following code. Generate, then edit — don't hand-write
boilerplate. `scaffold`/`controller` require exactly one of `--api`/
`--html`/`--htmx` (no default; omitting all is a hard error).
2. **Everything hangs off `AppContext`.** Handlers, workers, tasks, and
initializers receive `&AppContext` (`ctx`), with 8 fields: `db` (`with-db`
only), `config`, `mailer`, `storage`, `cache`, `queue_provider`,
`shared_store` (a type-keyed DI container), `environment`. Do not create
your own DB pool, HTTP server, or job queue.
3. **`use loco_rs::prelude::*;`** at the top of controllers/models/workers/tasks
brings in the common types (`AppContext`, `Result`, `Routes`, `Json`,
`State`, the Sea-ORM traits, JWT auth extractors under `auth`, etc.).
If a common type is "missing", it is almost always in the prelude.
4. **`Result<T>` is `loco_rs::Result<T>`** and `Error` is `loco_rs::Error`
(`#[non_exhaustive]` — match with a `_ =>` arm; `EnvVar`/`Hash`/`SemVer`/
`TaskJoinError` were removed). Use `?`; don't invent your own error enum
for app code.
5. **Config is YAML per-environment** in `config/*.yaml`, read through
`ctx.config`. Don't read env vars ad hoc; use the config + `get_env` Tera
helper inside the YAML.
## Project layout
```
src/
app.rs # Hooks impl: registers routes, workers, tasks, etc.
lib.rs / main.rs / bin/
controllers/ # HTTP handlers, grouped into Routes
models/
_entities/ # Sea-ORM entities (generated; don't hand-edit)
*.rs # your model logic (ActiveModel hooks, finders)
views/ # response shaping (JSON/HTML)
workers/ # background jobs
tasks/ # one-off / CLI tasks
mailers/ # email
initializers/ # startup hooks
migration/ # Sea-ORM migrations (separate crate)
config/ # development.yaml, production.yaml, test.yaml
tests/ # request/model/task tests
assets/ frontend/ # static assets / SPA (optional)
```
The `App` type implements the `Hooks` trait in `src/app.rs`. That is where you
**register** routes, workers, tasks, and initializers — a newly generated
controller/worker/task is not active until it is wired in there (the generators
do this for you). `Hooks::boot`'s second parameter is `environment:
&Environment` (the enum), not `&str` — copy from generated code, not memory.
## Models & migrations (Sea-ORM 2.0)
- Generate: `cargo loco generate model posts title:string! content:text
user:references`. This writes a migration and regenerates the entity.
- Apply: `cargo loco db migrate`; regenerate entities: `cargo loco db entities`.
- **Primary and foreign keys are 64-bit (`i64` / BIGINT) in 1.0.** The `int`
field type is also `i64`/BIGINT now (it was `i32` pre-1.0) — match key
types when relating tables. `small_int` still maps to `i16` if you need it.
- Entities live in `src/models/_entities/` and are **generated** — put custom
logic in `src/models/<name>.rs` (e.g. `ActiveModelBehavior`, finders).
- Query with Sea-ORM: `Entity::find_by_id(id).one(&ctx.db).await?`,
`Entity::find().filter(Column::Field.eq(x)).all(&ctx.db).await?`. Create/update
via `ActiveModel` + `.insert`/`.update`/`.save`. For ad-hoc filters, prefer
Loco's `query::condition()...build()` DSL (`eq`, `like`, `contains`,
`is_in`, `date_range`, ~18 ops) over hand-rolling `Condition`s.
- Pagination: `query::paginate(&ctx.db, Entity::find(), Some(condition),
&pagination_query).await?` → `PageResponse { page, meta: PagerMeta { page,
page_size, total_pages, total_items } }`.
- Sea-ORM 2.0 note: raw-`Statement` execution methods carry a `_raw` suffix
(`execute_raw`, `query_one_raw`, `query_all_raw`); most apps never touch these.
## Controllers & routing
```rust
use loco_rs::prelude::*;
pub async fn list(State(ctx): State<AppContext>) -> Result<Response> {
format::json(Entity::find().all(&ctx.db).await?)
}
pub fn routes() -> Routes {
Routes::new()
.prefix("api/posts/")
.add("/", get(list))
.add("{id}", get(get_one))
.add("/", post(add))
}
```
- Handlers are `async fn(State(ctx): State<AppContext>, ...) -> Result<Response>`.
Extract a body with `Json(params): Json<Params>`, a path with `Path(id):
Path<i64>`, query with `Query(...)`.
- Build a `Routes` group with `.prefix(...)` + `.add(path, method(handler))` and
return it from `routes()`; register it in `app.rs` `Hooks::routes`.
- Shape responses with `format::json(...)`, `format::html(...)`, or the view
layer. Validate request bodies with the `JsonValidate` extractor + `validator`
derive. Errors map to HTTP: `NotFound`→404, `Unauthorized`→401,
`BadRequest`/`Validation`→400, everything else (DB, IO, etc.)→500.
## Authentication (`auth`, default feature)
- Feature is named **`auth`**. Default signing algorithm is
**HS512**; `auth.jwt.secret` **must be valid base64** — plain strings fail
at token-generate/validate time, not config-load time.
- Extractors: `auth::JWT` (claims only, no DB needed), `auth::JWTWithUser<T>`
(claims + loaded user, needs `with-db`), `auth::ApiToken<T>` (bearer API
key → user, needs `with-db`; always reads the `Authorization: Bearer`
header regardless of `auth.jwt.location`).
- `JWTWithUser`/`ApiToken` require your user model to implement
`loco_rs::model::Authenticable` (`find_by_api_key`, `find_by_claims_key`).
- Password hashing: `loco_rs::hash::{hash_password, verify_password,
random_string}` (Argon2id, always compiled).
## Background workers (with priority)
```rust
use loco_rs::prelude::*;
pub struct DownloadWorker;
#[async_trait]
impl BackgroundWorker<DownloadWorkerArgs> for DownloadWorker {
fn build(ctx: &AppContext) -> Self { Self }
async fn perform(&self, args: DownloadWorkerArgs) -> Result<()> { Ok(()) }
}
// enqueue (returns the job id):
let job_id = DownloadWorker::perform_later(&ctx, args).await?;
// enqueue at a priority (higher runs first), on ANY backend:
DownloadWorker::perform_later_with_priority(&ctx, args, Some(100)).await?;
```
- Backends: Postgres and SQLite ship by default (feature `worker`); Redis needs
the `worker_redis` feature. The backend is chosen at runtime (config
`workers.mode` + `queue.kind`). Register workers in `app.rs`
`Hooks::connect_workers`.
- `perform_later`/`perform_later_with_priority` return the job id
(`Result<String>`). Priority (full `i32` range) works on all three
backends. Redis fully supports job admin now (cancel/clear/requeue/dump/
import) — it is not Postgres/SQLite-only.
- Manage from the CLI: `cargo loco jobs cancel|tidy|purge|dump|import|requeue`.
## Scheduler, mailers, tasks
- **Scheduler:** cron-like jobs in `config/*.yaml` under `scheduler:`; run with
`cargo loco scheduler` or `cargo loco start --scheduler`. Jobs run shell
commands or registered tasks.
- **Mailers:** generate with `cargo loco generate mailer`; send with
`Mailer::mail`/`mail_template`. Templates live under `src/mailers/<name>/`.
Configure SMTP TLS explicitly — `mailer.smtp.tls: starttls|implicit|none`
**overrides** the legacy `secure` bool; implicit TLS / port 465 (SMTPS)
needs `tls: implicit`, since `secure: true` alone only ever means STARTTLS.
- **Tasks:** implement the `Task` trait; run with `cargo loco task <name>`.
Great for admin/data operations that need `AppContext`.
## Configuration
`config/development.yaml`, `production.yaml`, `test.yaml`. Selected by
`LOCO_ENV` → `RAILS_ENV` → `NODE_ENV` → `development`. `{env}.local.yaml`
overrides `{env}.yaml` when both exist. Access through `ctx.config`. Secrets
come from the environment via the `get_env` Tera helper *inside* the YAML,
e.g. `password: "{{ get_env(name='SMTP_PASSWORD') }}"`. Don't scatter
`std::env::var` calls through app code.
## Testing
```rust
use loco_rs::testing::prelude::*;
#[tokio::test]
#[serial]
async fn can_list() {
request::<App>(|request, _ctx| async move { // NOTE: ::<App>, not ::<App, _, _>
let res = request.get("/api/posts/").await;
assert_eq!(res.status_code(), 200);
})
.await;
}
```
- Requires the `testing` feature (off by default in a plain lib dep, on for
the generated app's dev-dependencies).
- Request helpers take a callback `|request, ctx| async move { ... }` — call
`request::<App, _, _>(...)`. Boot helper `boot_test::<H>()` is
**single-generic** (not `boot_test::<App, Migrator>()`).
- Use `request_with_create_db::<App, _, _>(...)` for DB-backed tests (fresh DB,
auto-cleaned), seed with fixtures, and snapshot with `insta` (use
`testing::redaction::cleanup_user_model()`/`cleanup_email()` filters).
`#[serial]` DB tests that share state.
## Common LLM pitfalls (avoid these)
- ❌ Adding `axum`, `sqlx`, `tokio`, `lettre`, a job runner, etc. directly and
wiring a server by hand. ✅ They're already integrated behind Loco — use
`ctx` and the generators.
- ❌ Hand-writing entities in `_entities/`. ✅ Generate via migrations.
- ❌ `i32` primary keys / `Path<i32>`, or assuming `int` fields are 32-bit.
✅ `i64` everywhere in 1.0 (keys, FKs, and the `int` field type).
- ❌ `boot_test::<App, Migrator>()` (the old two-generic boot helper). ✅
`boot_test::<H>()` in 1.0.
- ❌ Building routes without registering them in `app.rs`. ✅ Return `Routes`
from `routes()` and register in `Hooks::routes`.
- ❌ Custom error types for handlers. ✅ Return `loco_rs::Result<Response>` and
use `?`; match `Error` with a `_ =>` arm (it's `#[non_exhaustive]`).
- ❌ Reading env vars directly. ✅ YAML config + `ctx.config` + `get_env`.
- ❌ Assuming `secure: true` covers implicit TLS. ✅ Use `tls: implicit` for
port 465.
- ❌ `scaffold`/`controller` generation without a kind flag. ✅ pass one of
`--api`/`--html`/`--htmx` — there's no default.
## The CLI you will use most
```
cargo loco start [--server-and-worker | --worker | --scheduler | --all]
cargo loco generate model|scaffold|controller|worker|task|scheduler|mailer|
migration|deployment|override [--api|--html|--htmx]
cargo loco db migrate|entities|reset|seed
cargo loco task <name>
cargo loco jobs cancel|tidy|purge|dump|import|requeue
cargo loco routes # list all routes
cargo loco doctor # check environment / versions
```
`loco new` (the separate app-generator binary, `cargo install loco`) flags:
`--name --db <sqlite|postgres|none> --bg <async|queue|blocking> --assets
<serverside|clientside|none> --os <linux|windows|macos> --allow-in-git-repo`.
There is **no `--template`/`--verbose`** flag — template choice is
interactive-only.
When unsure, run `cargo loco generate <thing> --help` and read the produced code
— it is the canonical, up-to-date pattern.
File diff suppressed because it is too large Load Diff
+128
View File
@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement (open an issue to reach out).
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+116
View File
@@ -0,0 +1,116 @@
# Contributing to Loco
Thank you for taking the time to read this.
The first way to show support is to star our repos :).
Loco is a community driven project. We welcome you to participate, contribute and together build a productivity-first web and api framework in Rust.
## Code of Conduct
This project is follows [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code.
## I have a question
If you have a question to ask, feel free to open an new [discussion](https://github.com/loco-rs/loco/discussions). There are no dumb questions.
## I need a feature
Feature requests from anyone is definitely welcomed! You can open an [issue](https://github.com/loco-rs/loco/issues/new/choose). When you can, illustrate a feature with code, simulated console output, and "make believe" console interactions, so we know what you want and what you expect.
## I want to support
Awesome! The best way to support us is to recommend it to your classmates/colleagues/friends, write blog posts and tutorials on our projects and help out other users in the community.
## I want to join
We are always looking for long-term contributors. If you want to commit longer-term to Loco's open source effort, definitely talk with us!
* From time to time we will make issues clear for newcomers with `mentoring` and `good-first-issue`
* If no issue exist, just open an issue and ask how to help
### Using an example app to test
Our testing grounds is [examples/demo](examples/demo/) which is pointing to the latest local `loco` framework. You can use it to test out an actual app, using a locally modified `loco`.
## Code style
We use `rustfmt`/`cargo fmt`. A few code style options are set in the [.rustfmt.toml](.rustfmt.toml) file, and some of them are not stable yet and require a nightly version of rustfmt.
If you're using rustup, the nightly version of rustfmt can be installed by doing the following:
```sh
rustup component add rustfmt --toolchain nightly
```
And then format your code by running:
```sh
rustup default nightly
cargo fmt --all
cargo fmt --all --manifest-path loco-new/Cargo.toml
cargo clippy --fix --allow-dirty --workspace --all-features -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms
cargo clippy --fix --allow-dirty --workspace --all-features --manifest-path loco-new/Cargo.toml -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms
rustup default stable
```
## Testing
Just clone the project and run `cargo test`.
You can see how we test in [.github/workflows](.github/workflows/)
#### Snapshots
We use [insta](https://github.com/mitsuhiko/insta) for snapshot testing, which helps us detect changes in output formats and behavior. To work with snapshots:
1. Install the insta CLI tool:
```sh
cargo install cargo-insta
```
2. Run tests and review/update snapshots:
```sh
cargo insta test --review
```
For CLI-related changes, we maintain separate snapshots of binary command outputs. To update these CLI snapshots:
```sh
LOCO_CI_MODE=true TRYCMD=overwrite cargo test
```
## Docs
The documentation consists of two main components:
+ The [loco.rs website](https://loco.rs) with its source code available [here](./docs-site/).
+ RustDocs.
To reduce duplication in documentation and examples, we use [snipdoc](https://github.com/kaplanelad/snipdoc). As part of our CI process, we ensure that the documentation remains consistent.
Updating the Documentation
+ Download [snipdoc](https://github.com/kaplanelad/snipdoc).
+ Create the snippet in the [yaml file](./snipdoc.yml) or inline the code.
+ Run `snipdoc run`.
To run the documentation site locally, we use [zola](https://www.getzola.org/) so you'll need to [install](https://www.getzola.org/documentation/getting-started/installation/) it. The documentation site works with zola version `0.19.2` and since zola still has breaking changes, we make no guarantees about other versions.
Running the local preview
+ `cd docs-site`
+ `npm run serve` or `zola serve`
## Open A Pull Request
The most recommended and straightforward method to contribute changes to the project involves forking it on GitHub and subsequently initiating a pull request to propose the integration of your modifications into our repository.
Changes a starters project are not recommended. read more [here](./starters/README.md)
### In Your Pull Request Description, Include:
- References to any bugs fixed by the change
- Informative notes for the reviewer, aiding their comprehension of the necessity for the change or providing insights on how to conduct a more effective review.
- A clear explanation of how you tested your changes.
### Your PR must also:
- be based on the master branch
- adhere to the code [style](#code-style)
- Successfully passes the [test suite](#testing)
+7580
View File
File diff suppressed because it is too large Load Diff
+393
View File
@@ -0,0 +1,393 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2024"
rust-version = "1.94"
name = "loco-rs"
version = "1.0.1"
build = "build.rs"
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "The one-person framework for Rust"
homepage = "https://loco.rs/"
documentation = "https://docs.rs/loco-rs"
readme = "README.md"
license = "Apache-2.0"
repository = "https://github.com/loco-rs/loco"
[package.metadata.docs.rs]
features = ["testing"]
[features]
all_storage = [
"storage_aws_s3",
"storage_azure",
"storage_gcp",
]
auth = [
"dep:jsonwebtoken",
"jsonwebtoken/rust_crypto",
]
cache_inmem = ["dep:moka"]
cache_redis = [
"dep:bb8-redis",
"dep:bb8",
]
cli = ["dep:clap"]
default = [
"auth",
"cli",
"with-db",
"cache_inmem",
"worker",
]
embedded_assets = []
redis_tls = [
"redis/tokio-rustls-comp",
"redis/tls-rustls-webpki-roots",
"dep:rustls",
]
storage_aws_s3 = ["opendal/services-s3"]
storage_azure = ["opendal/services-azblob"]
storage_gcp = ["opendal/services-gcs"]
testing = [
"dep:axum-test",
"dep:scraper",
"dep:tree-fs",
]
with-db = [
"dep:sea-orm",
"dep:sea-orm-migration",
"dep:sqlx",
"loco-gen/with-db",
]
worker = [
"dep:sqlx",
"dep:ulid",
]
worker_redis = [
"worker",
"dep:redis",
]
[lib]
name = "loco_rs"
path = "src/lib.rs"
[[test]]
name = "mod"
path = "tests/mod.rs"
[dependencies.argon2]
version = "0.5"
features = ["std"]
[dependencies.async-trait]
version = "0.1.74"
[dependencies.axum]
version = "0.8.1"
features = [
"macros",
"multipart",
]
[dependencies.axum-client-ip]
version = "1.3"
features = ["forwarded-header"]
[dependencies.axum-extra]
version = "0.10"
features = ["cookie"]
[dependencies.axum-test]
version = "17.0.1"
optional = true
[dependencies.backtrace_printer]
version = "1.3.0"
[dependencies.bb8]
version = "0.9"
optional = true
[dependencies.bb8-redis]
version = "0.26"
optional = true
[dependencies.byte-unit]
version = "5.1"
[dependencies.bytes]
version = "1.11"
[dependencies.cargo-lock]
version = "11"
default-features = false
[dependencies.chrono]
version = "0.4"
features = ["serde"]
[dependencies.clap]
version = "4.4.7"
features = ["derive"]
optional = true
[dependencies.colored]
version = "3.0"
[dependencies.cruet]
version = "1.0"
[dependencies.dashmap]
version = "6"
[dependencies.duct]
version = "1.0.0"
[dependencies.english-to-cron]
version = "0.1.2"
[dependencies.futures-util]
version = "0.3"
[dependencies.heck]
version = "0.5.0"
[dependencies.include_dir]
version = "0.7.3"
[dependencies.jsonwebtoken]
version = "10.3.0"
optional = true
default-features = false
[dependencies.lettre]
version = "0.11.4"
features = [
"builder",
"hostname",
"smtp-transport",
"tokio1-rustls-tls",
]
default-features = false
[dependencies.loco-gen]
version = "1.0.0"
[dependencies.moka]
version = "0.12.7"
features = ["future"]
optional = true
[dependencies.notify]
version = "8.1.0"
[dependencies.opendal]
version = "0.57"
features = [
"services-memory",
"services-fs",
"layers-retry",
]
default-features = false
[dependencies.rand]
version = "0.9"
features = ["std"]
[dependencies.redis]
version = "1"
features = [
"aio",
"tokio-comp",
]
optional = true
[dependencies.regex]
version = "1"
[dependencies.rustls]
version = "0.23"
features = ["ring"]
optional = true
default-features = false
[dependencies.scraper]
version = "0.25.0"
features = ["deterministic"]
optional = true
[dependencies.sea-orm]
version = "2.0"
features = [
"sqlx-postgres",
"sqlx-sqlite",
"runtime-tokio-rustls",
"macros",
]
optional = true
[dependencies.sea-orm-migration]
version = "2.0"
features = [
"runtime-tokio-rustls",
"sqlx-postgres",
"sqlx-sqlite",
]
optional = true
[dependencies.semver]
version = "1"
[dependencies.serde]
version = "1"
[dependencies.serde_json]
version = "1"
[dependencies.serde_variant]
version = "0.1.2"
[dependencies.serde_yaml]
version = "0.10"
package = "serde_yaml_ng"
[dependencies.sqlx]
version = "0.9"
features = [
"json",
"postgres",
"chrono",
"sqlite",
"tls-rustls-ring-webpki",
]
optional = true
default-features = false
[dependencies.tera]
version = "1.19.1"
[dependencies.thiserror]
version = "2"
[dependencies.tokio]
version = "1.45"
default-features = false
[dependencies.tokio-cron-scheduler]
version = "0.15"
features = ["signal"]
[dependencies.tokio-util]
version = "0.7"
[dependencies.toml]
version = "0.8"
[dependencies.tower]
version = "0.5"
[dependencies.tower-http]
version = "0.6.8"
features = [
"trace",
"catch-panic",
"timeout",
"add-extension",
"cors",
"fs",
"set-header",
"compression-full",
]
[dependencies.tracing]
version = "0.1.40"
[dependencies.tracing-appender]
version = "0.2.3"
default-features = false
[dependencies.tracing-subscriber]
version = "0.3.16"
features = [
"env-filter",
"json",
"ansi",
]
default-features = false
[dependencies.tree-fs]
version = "0.3"
optional = true
[dependencies.ulid]
version = "1"
optional = true
[dependencies.url]
version = "2"
[dependencies.uuid]
version = "1.10.0"
features = [
"v4",
"fast-rng",
]
[dependencies.validator]
version = "0.20.0"
features = ["derive"]
[dev-dependencies.insta]
version = "1.34.0"
features = [
"redactions",
"yaml",
"filters",
]
[dev-dependencies.reqwest]
version = "0.12.7"
features = ["json"]
[dev-dependencies.rstest]
version = "0.26.1"
[dev-dependencies.serial_test]
version = "3.5.0"
[dev-dependencies.sqlx]
version = "0.9"
features = [
"macros",
"json",
"postgres",
"chrono",
"sqlite",
"migrate",
]
default-features = false
[dev-dependencies.testcontainers]
version = "0.27"
[dev-dependencies.tower]
version = "0.5"
features = ["util"]
[dev-dependencies.tree-fs]
version = "0.3"
+247
View File
@@ -0,0 +1,247 @@
[workspace]
members = ["xtask", "loco-gen"]
exclude = ["starters", "examples"]
[workspace.package]
edition = "2024"
# MSRV floor raised for Sea-ORM 2.0 + sqlx 0.9: sea-orm 2.0.0 declares rustc
# 1.94 as its own MSRV, so we match it.
# Edition 2024 needs rustc >= 1.85, satisfied by the 1.94 floor above.
rust-version = "1.94"
license = "Apache-2.0"
[package]
name = "loco-rs"
version = "1.0.1"
description = "The one-person framework for Rust"
homepage = "https://loco.rs/"
documentation = "https://docs.rs/loco-rs"
repository = "https://github.com/loco-rs/loco"
license.workspace = true
edition.workspace = true
rust-version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = [
"auth",
"cli",
"with-db",
"cache_inmem",
"worker",
]
# jsonwebtoken 10 no longer bundles a crypto backend, so `auth` selects the
# pure-Rust `rust_crypto` one. This keeps `auth` self-contained (enabling it
# alone still builds, even with default-features = false) and needs no C
# toolchain — matching Loco's zero-friction onboarding.
auth = ["dep:jsonwebtoken", "jsonwebtoken/rust_crypto"]
cli = ["dep:clap"]
testing = ["dep:axum-test", "dep:scraper", "dep:tree-fs"]
with-db = [
"dep:sea-orm",
"dep:sea-orm-migration",
"dep:sqlx",
"loco-gen/with-db",
]
# Storage features
all_storage = ["storage_aws_s3", "storage_azure", "storage_gcp"]
storage_aws_s3 = ["opendal/services-s3"]
storage_azure = ["opendal/services-azblob"]
storage_gcp = ["opendal/services-gcs"]
# Cache feature
cache_inmem = ["dep:moka"]
cache_redis = ["dep:bb8-redis", "dep:bb8"]
worker = ["dep:sqlx", "dep:ulid"]
worker_redis = ["worker", "dep:redis"]
# Redis over TLS (`rediss://`) for managed providers (ElastiCache, Upstash,
# Azure Cache, ...). Arms both the worker and cache redis paths — the same
# `redis` crate, unified by Cargo — with webpki-bundled roots (portable to
# slim/distroless images). Enable alongside `worker_redis`/`cache_redis` and
# use a `rediss://` URL in config; no code changes required. See `dep:rustls`
# above for why the provider is pinned to ring.
redis_tls = ["redis/tokio-rustls-comp", "redis/tls-rustls-webpki-roots", "dep:rustls"]
# Embed assets into binary
embedded_assets = []
[dependencies]
loco-gen = { version = "1.0.0", path = "./loco-gen" }
backtrace_printer = { version = "1.3.0" }
# cli
clap = { version = "4.4.7", features = ["derive"], optional = true }
colored = { workspace = true }
sea-orm = { version = "2.0", features = [
"sqlx-postgres", # `DATABASE_DRIVER` feature
"sqlx-sqlite",
"runtime-tokio-rustls",
"macros",
], optional = true }
tokio = { version = "1.45", default-features = false }
tokio-util = "0.7"
# the rest
serde = { workspace = true }
serde_json = { workspace = true }
# serde_yaml (dtolnay) is archived; serde_yaml_ng is the maintained, drop-in
# continuation. Kept under the `serde_yaml` name so source paths are unchanged.
serde_yaml = { package = "serde_yaml_ng", version = "0.10" }
serde_variant = "0.1.2"
toml = "0.8"
async-trait = { workspace = true }
axum = { workspace = true }
axum-extra = { version = "0.10", features = ["cookie"] }
regex = { workspace = true }
# mailer
tera = { workspace = true }
heck = { workspace = true }
cruet = "1.0"
lettre = { version = "0.11.4", default-features = false, features = [
"builder",
"hostname",
"smtp-transport",
"tokio1-rustls-tls",
] }
include_dir = "0.7.3"
thiserror = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { version = "0.3.16", default-features = false, features = [
"env-filter",
"json",
"ansi",
] }
tracing-appender = { version = "0.2.3", default-features = false }
duct = { workspace = true }
tower-http = { workspace = true }
byte-unit = "5.1"
argon2 = { version = "0.5", features = ["std"] }
rand = { version = "0.9", features = ["std"] }
jsonwebtoken = { version = "10.3.0", optional = true, default-features = false }
validator = { version = "0.20.0", features = ["derive"] }
futures-util = "0.3"
tower = { workspace = true }
bytes = "1.11"
axum-client-ip = { version = "1.3", features = ["forwarded-header"] }
semver = "1"
url = "2"
cargo-lock = { version = "11", default-features = false }
axum-test = { version = "17.0.1", optional = true }
tree-fs = { version = "0.3", optional = true }
chrono = { workspace = true }
uuid = { version = "1.10.0", features = ["v4", "fast-rng"] }
# File Upload
opendal = { version = "0.57", default-features = false, features = [
"services-memory",
"services-fs",
"layers-retry",
] }
# cache
moka = { version = "0.12.7", features = ["future"], optional = true }
bb8-redis = { version = "0.26", optional = true }
bb8 = { version = "0.9", optional = true }
# Scheduler
tokio-cron-scheduler = { version = "0.15", features = ["signal"] }
english-to-cron = { version = "0.1.2" }
# worker: pg + sqlite backed queue workers.
# `tls-rustls-ring-webpki` gives the standalone pg/sqlite worker pool a TLS
# backend on its own (matching sea-orm's `runtime-tokio-rustls`), so a
# worker-only build — a pg queue without `with-db` — can still reach a
# TLS-only managed Postgres. In `with-db` builds this unifies with sea-orm's
# ring-based rustls. Managed Postgres TLS is then opt-in purely via the
# connection URL (`sslmode=require`, `sslrootcert=...`); see the how-to docs.
sqlx = { version = "0.9", default-features = false, features = [
"json",
"postgres",
"chrono",
"sqlite",
"tls-rustls-ring-webpki",
], optional = true }
ulid = { version = "1", optional = true }
# worker_redis: redis backed queue workers
redis = { version = "1", features = ["aio", "tokio-comp"], optional = true }
# `redis_tls` only: redis' TLS path calls `rustls::ClientConfig::builder()`,
# which resolves rustls' *process-default* crypto provider at build time. redis
# pulls rustls with no provider of its own, so we bring rustls with the
# pure-Rust `ring` provider (no C toolchain — matching Loco's zero-friction
# onboarding) to guarantee a provider is compiled in even for redis-only builds.
# Unifies with sea-orm/sqlx, which also select ring.
rustls = { version = "0.23", default-features = false, features = [
"ring",
], optional = true }
scraper = { version = "0.25.0", features = ["deterministic"], optional = true }
dashmap = "6"
notify = "8.1.0"
[workspace.dependencies]
tera = { version = "1.19.1" }
colored = { version = "3.0" }
chrono = { version = "0.4", features = ["serde"] }
tracing = "0.1.40"
regex = "1"
thiserror = "2"
serde = "1"
serde_json = "1"
async-trait = { version = "0.1.74" }
axum = { version = "0.8.1", features = ["macros", "multipart"] }
tower = "0.5"
tower-http = { version = "0.6.8", features = [
"trace",
"catch-panic",
"timeout",
"add-extension",
"cors",
"fs",
"set-header",
"compression-full",
] }
heck = "0.5.0"
duct = { version = "1.0.0" }
[dependencies.sea-orm-migration]
optional = true
version = "2.0"
features = [
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
# e.g.
"runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
"sqlx-postgres", # `DATABASE_DRIVER` feature
"sqlx-sqlite",
]
[package.metadata.docs.rs]
features = ["testing"]
[dev-dependencies]
loco-rs = { path = ".", features = ["testing"] }
rstest = "0.26.1"
insta = { version = "1.34.0", features = ["redactions", "yaml", "filters"] }
tree-fs = { version = "0.3" }
reqwest = { version = "0.12.7", features = ["json"] }
tower = { workspace = true, features = ["util"] }
sqlx = { version = "0.9", default-features = false, features = [
"macros",
"json",
"postgres",
"chrono",
"sqlite",
"migrate",
] }
testcontainers = { version = "0.27" }
serial_test = { version = "3.5.0" }
+141
View File
@@ -0,0 +1,141 @@
## Blessed dependencies maintenance and `loco doctor`
Loco contain a few major and "blessed" dependencies, these appear **both** in an app that was generated at the surface level in their `Cargo.toml` and in the core Loco framework.
If stale, may require an upgrade as a must.
Example for such dependencies:
* The `sea-orm-cli` - while Loco uses `SeaORM`, it uses the `SeaORM` CLI to generate entities, and so there may be an incompatibility if `SeaORM` has a too large breaking change between their CLI (which ships separately) and their framework.
* `axum`
* etc.
This is why we are checking these automatically as part of `loco doctor`.
We keep minimal version requirements for these. As a maintainer, you can update these **minimal** versions, only if required in [`doctor.rs`](src/doctor.rs).
## Running Tests
Before running tests make sure that:
[ ] redis is running
[ ] starters/saas frontend package is built:
```
$ cd starters/saas/frontend
$ npm i -g pnpm
$ pnpm i && pnpm build
```
Running all tests should be done with:
```
$ cargo xtask test
```
### Docker / testcontainers
Database and Redis tests use [testcontainers](https://crates.io/crates/testcontainers),
which spin up their own Postgres/Redis containers on demand — so you need a
running Docker daemon, but you do **not** need to start Postgres/Redis yourself.
If you use a non–Docker Desktop runtime (Colima, OrbStack, Podman, ...), the
Docker socket is not at the default `/var/run/docker.sock` and testcontainers
will fail with `SocketNotFoundError("/var/run/docker.sock")`. Point it at your
socket, e.g. for Colima:
```
$ export DOCKER_HOST="unix://$HOME/.colima/default/docker.sock"
```
## Rebuilding your database and local generated entities
This should write out a fresh DB structure (drops and migrates):
```
$ cargo loco db reset
```
And then, the entities generators connect to that newly minted DB, to generate a corresponding entities code:
```
$ cargo loco db entities
```
## Publishing a new version
**Test your changes**
* [ ] Ensure you have the necessary local resources, such as `DB`/`Redis`, by executing the command `cargo loco doctor --environment test`. In case you don't have them, refer to the relevant documentation section for guidance.
* [ ] run `cargo test` on the root to test Loco itself
* [ ] cd `examples/demo` and run `cargo test` to test our "driver app" which exercises the framework in various ways
* [ ] push your changes to Github to get the CI running and testing in various additional configurations that you don't have
* [ ] CI should pass. Take note that all `starters-*` CI are using a **fixed version** of Loco and are not seeing your changes yet
**Actually bump version + test and align starters**
* [ ] in project root, run `cargo xtask bump-version` and give it the next version. Versions are without `v` prefix. Example: `0.1.3`.
* [ ] Did the xtask testing workflow fail?
* [ ] YES: fix errors, and re-run `cargo xtask bump-version` **with the same version as before**.
* [ ] NO: great, move to publishing
* [ ] Your repo may be dirty with fixes. Now that tests are passing locally commit the changes. Then run `cargo publish` to publish the next Loco version (remember: the starters at this point are pointing to the **next version already**, so we don't want to push until publish finished)
* [ ] When publish finished successfully, push your changes to github
* [ ] Wait for CI to finish. You want to be focusing more at the starters CI, because they will now pull the new version.
* [ ] Did CI fail?
* [ ] YES: This means you had a circumstance that's not predictable (e.g. some operating system issue). Fix the issue and **repeat the bumping process, advance a new version**.
* [ ] NO: all good! you're done.
**Book keeping**
* [ ] Update changelog: (1) move vnext to be that new version of yours, (2) create a blank vnext
* [ ] Think about if any of the items in the new version needs new documentation or update to the documentation -- and do it
## Errors
Errors are done with `thiserror`. We adopt a minimalistic approach to errors.
* We try to have _one error kind_ for the entirety of Loco.
* Errors that cannot be handled, are _informative_ and so can be opaque (we don't offer deep matching on those)
* Errors that can be handled and reasoned upon should be able to be matched and extract good knowledge from
* To users, error should _not be cryptic_, and should indicate how to fix issues as much as possible, or point to the issue precisely
### Auto conversions
When possible use `from` conversions.
```rust
#[error(transparent)]
JSON(#[from] serde_json::Error),
```
When complicated, implement a `From` trait yourself. This is done to _centralize_ errors into one place and not litter needless `map_err` code which holds error conversion logic (an exception is Context, see below).
### Context
When you know a user might need context, resort to manually shaping the error with extra information. First, define the error:
```rust
#[error("cannot parse `{1}`: {0}")]
YAMLFile(#[source] serde_yaml::Error, String),
```
Then, shape it:
```rust
serde_yaml::from_str(&rendered)
.map_err(|err| Error::YAMLFile(err, selected_path.to_string_lossy().to_string()))
```
In this example, the information about where `rendered` came from was long lost at the `serde_yaml::from_str` callsite. Which is why errors were cryptic indicating bad YAML format, but not where it comes from (which file).
In this case, we duplicate the YAML error type, leave one of those for auto conversions with `from`, where we don't have a file, and create a new specialized error type with the file information: `YAMLFile`.
## The `CONTRIBUTORS` comment
Some files contain a special `CONTRIBUTORS` comment. This comment should
contain context, special notes for that module, and a checklist if needed, so please make sure to follow it.
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2022] Dotan Nahum, Elad Kaplan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+118
View File
@@ -0,0 +1,118 @@
<div align="center">
<img src="https://github.com/loco-rs/loco/assets/83390/992d215a-3cd3-42ee-a1c7-de9fd25a5bac"/>
<h1>Bem-vindo ao Loco</h1>
<h3>
<!-- <snip id="description" inject_from="yaml"> -->
🚂 Loco is Rust on Rails.
<!--</snip> -->
</h3>
[![crate](https://img.shields.io/crates/v/loco-rs.svg)](https://crates.io/crates/loco-rs)
[![docs](https://docs.rs/loco-rs/badge.svg)](https://docs.rs/loco-rs)
[![Discord channel](https://img.shields.io/badge/discord-Join-us)](https://discord.gg/fTvyBzwKS8)
</div>
[English](./README.md) · [中文](./README-zh_CN.md) · [Français](./README.fr.md) · Portuguese (Brazil) ・ [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Русский](./README.ru.md) · [Español](./README.es.md)
## O que é o Loco?
`Loco` é fortemente inspirado no Rails. Se você conhece Rails e Rust, se sentirá em casa. Se você só conhece Rails e é novo em Rust, achará o Loco refrescante. Não presumimos que você conheça o Rails.
Para uma imersão mais profunda em como o Loco funciona, incluindo guias detalhados, exemplos e referências da API, confira nosso [site de documentação](https://loco.rs).
## Recursos do Loco:
* `Convenção sobre Configuração:` Semelhante ao Ruby on Rails, o Loco enfatiza simplicidade e produtividade ao reduzir a necessidade de código boilerplate. Ele utiliza padrões sensatos, permitindo que os desenvolvedores se concentrem em escrever a lógica de negócios em vez de perder tempo com configuração.
* `Desenvolvimento Rápido:` Com o objetivo de alta produtividade para o desenvolvedor, o design do Loco se concentra em reduzir código boilerplate e fornecer APIs intuitivas, permitindo que os desenvolvedores iteren rapidamente e construam protótipos com esforço mínimo.
* `Integração ORM:` Modele seu negócio com entidades robustas, eliminando a necessidade de escrever SQL. Defina relacionamentos, validações e lógica personalizada diretamente em suas entidades para melhorar a manutenção e escalabilidade.
* `Controladores:` Manipule os parâmetros de solicitações web, corpo, validação e renderize uma resposta que é consciente do conteúdo. Usamos Axum para o melhor desempenho, simplicidade e extensibilidade. Os controladores também permitem que você construa facilmente middlewares, que podem ser usados para adicionar lógica como autenticação, registro ou tratamento de erros antes de passar as solicitações para as ações principais do controlador.
* `Views:` O Loco pode se integrar com mecanismos de template para gerar conteúdo HTML dinâmico a partir de templates.
* `Trabalhos em segundo plano:` Realize trabalhos intensivos de computação ou I/O em segundo plano com uma fila baseada em Redis ou com threads. Implementar um trabalhador é tão simples quanto implementar uma função de execução para o trait Worker.
* `Scheduler:` Simplifica o tradicional e frequentemente complicado sistema crontab, tornando mais fácil e elegante agendar tarefas ou scripts shell.
* `Mailers:` Um mailer entregará e-mails em segundo plano usando a infraestrutura de trabalhador existente do loco. Tudo será transparente para você.
* `Armazenamento:` No Armazenamento do Loco, facilitamos o trabalho com arquivos por meio de várias operações. O armazenamento pode ser em memória, no disco ou utilizar serviços em nuvem, como AWS S3, GCP e Azure.
* `Cache:` O Loco fornece uma camada de cache para melhorar o desempenho da aplicação armazenando dados acessados frequentemente.
Para ver mais recursos do Loco, confira nosso [site de documentação](https://loco.rs/docs/getting-started/tour/).
## Começando
<!-- <snip id="quick-installation-command" inject_from="yaml" template="sh"> -->
```sh
cargo install loco
cargo install sea-orm-cli # Only when DB is needed
```
<!-- </snip> -->
Agora você pode criar seu novo aplicativo (escolha "`SaaS` app").
<!-- <snip id="loco-cli-new-from-template" inject_from="yaml" template="sh"> -->
```sh
❯ loco new
✔ ❯ App name? · myapp
✔ ❯ What would you like to build? · Saas App with client side rendering
✔ ❯ Select a DB Provider · Sqlite
✔ ❯ Select your background worker type · Async (in-process tokio async tasks)
🚂 Loco app generated successfully in:
myapp/
- assets: You've selected `clientside` for your asset serving configuration.
Next step, build your frontend:
$ cd frontend/
$ npm install && npm run build
```
<!-- </snip> -->
Agora execute `cd` no seu `myapp` e inicie seu aplicativo:
<!-- <snip id="starting-the-server-command-with-output" inject_from="yaml" template="sh"> -->
```sh
$ cargo loco start
▄ ▀
▀ ▄
▄ ▀ ▄ ▄ ▄▀
▄ ▀▄▄
▄ ▀ ▀ ▀▄▀█▄
▀█▄
▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█
██████ █████ ███ █████ ███ █████ ███ ▀█
██████ █████ ███ █████ ▀▀▀ █████ ███ ▄█▄
██████ █████ ███ █████ █████ ███ ████▄
██████ █████ ███ █████ ▄▄▄ █████ ███ █████
██████ █████ ███ ████ ███ █████ ███ ████▀
▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ██▀
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
https://loco.rs
listening on port 5150
```
<!-- </snip> -->
## Impulsionado pelo Loco
+ [SpectralOps](https://spectralops.io) - vários serviços impulsionados pelo framework Loco
+ [Nativish](https://nativi.sh) - backend do aplicativo impulsionado pelo framework Loco
## Contribuidores ✨
Agradecimentos a essas pessoas maravilhosas:
<a href="https://github.com/loco-rs/loco/graphs/contributors">
<img src="https://contrib.rocks/image?repo=loco-rs/loco" />
</a>
+58
View File
@@ -0,0 +1,58 @@
<div align="center">
<img src="https://github.com/loco-rs/loco/assets/83390/992d215a-3cd3-42ee-a1c7-de9fd25a5bac"/>
<h1>Loco</h1>
[![crate](https://img.shields.io/crates/v/loco-rs.svg)](https://crates.io/crates/loco-rs)
[![docs](https://docs.rs/loco-rs/badge.svg)](https://docs.rs/loco-rs)
[![Discord channel](https://img.shields.io/badge/discord-Join-us)](https://discord.gg/fTvyBzwKS8)
</div>
[English](./README.md) · 中文 · [Français](./README.fr.md) · [Portuguese (Brazil)](./README-pt_BR.md) ・ [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Русский](./README.ru.md) · [Español](./README.es.md)
Loco 是一个用 Rust 编写的 Web 框架,类似于 Rails。Loco 提供快速构建 Web 应用的功能,并且允许创建自定义任务,可以通过 CLI 运行。
## 特性
- **简单的 API**: 使用 Rust 的强类型系统确保安全性和可靠性。
- **快速开发**: 提供快速构建 Web 应用的工具和模板。
- **CLI 支持**: 可以创建和运行自定义 CLI 任务。
- **灵活性**: 支持自定义配置和扩展。
## 安装
通过 Cargo 安装 Loco:
```sh
cargo install loco
```
## 快速开始
创建一个新的 Loco 项目:
```sh
loco new my_project
cd my_project
```
启动开发服务器:
```sh
cargo loco start
```
## 贡献
欢迎对 Loco 的贡献!请阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 了解更多信息。
## 许可证
Loco 在 MIT 许可证下发布。详情请参阅 [LICENSE](LICENSE)。
---
For more details, you can visit the [original README file](https://github.com/loco-rs/loco/blob/master/README.md).
+117
View File
@@ -0,0 +1,117 @@
<div align="center">
<img src="https://github.com/loco-rs/loco/assets/83390/992d215a-3cd3-42ee-a1c7-de9fd25a5bac"/>
<h1>Bienvenido a Loco</h1>
<h3>
<!-- <snip id="description" inject_from="yaml"> -->
🚂 Loco es Rust on Rails.
<!--</snip> -->
</h3>
[![crate](https://img.shields.io/crates/v/loco-rs.svg)](https://crates.io/crates/loco-rs)
[![docs](https://docs.rs/loco-rs/badge.svg)](https://docs.rs/loco-rs)
[![Discord channel](https://img.shields.io/badge/discord-Join-us)](https://discord.gg/fTvyBzwKS8)
</div>
Español · [English](./README.md) · [中文](./README-zh_CN.md) · [Français](./README.fr.md) · [Português (Brasil)](./README-pt_BR.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Русский](./README.ru.md) · Español
## ¿Qué es Loco?
`Loco` está fuertemente inspirado en Rails. Si conoces Rails y Rust, te sentirás como en casa. Si solo conoces Rails y eres nuevo en Rust, encontrarás Loco refrescante. No asumimos que conozcas Rails.
Para una explicación más profunda de cómo funciona Loco, incluyendo guías detalladas, ejemplos y referencias de la API, consulta nuestro [sitio de documentación](https://loco.rs).
## Características de Loco
* `Convención sobre configuración:` Al igual que Ruby on Rails, Loco enfatiza la simplicidad y la productividad al reducir la necesidad de código repetitivo. Utiliza valores predeterminados sensatos, permitiendo a los desarrolladores centrarse en la lógica de negocio en lugar de perder tiempo en la configuración.
* `Desarrollo rápido:` Loco está diseñado para una alta productividad del desarrollador, reduciendo el código repetitivo y proporcionando APIs intuitivas, permitiendo iterar rápidamente y construir prototipos con un esfuerzo mínimo.
* `Integración ORM:` Modela tu negocio con entidades robustas, eliminando la necesidad de escribir SQL. Define relaciones, validaciones y lógica personalizada directamente en tus entidades para una mayor mantenibilidad y escalabilidad.
* `Controladores:` Maneja parámetros de solicitudes web, cuerpo, validación y renderiza una respuesta consciente del contenido. Usamos Axum para el mejor rendimiento, simplicidad y extensibilidad. Los controladores también permiten construir middlewares fácilmente, que pueden usarse para agregar lógica como autenticación, registro o manejo de errores antes de pasar las solicitudes a las acciones principales del controlador.
* `Vistas:` Loco puede integrarse con motores de plantillas para generar contenido HTML dinámico a partir de plantillas.
* `Trabajos en segundo plano:` Realiza trabajos intensivos en computación o I/O en segundo plano con una cola respaldada por Redis o con hilos. Implementar un worker es tan simple como implementar una función perform para el trait Worker.
* `Planificador:` Simplifica el tradicional y a menudo engorroso sistema crontab, facilitando y haciendo más elegante la programación de tareas o scripts de shell.
* `Mailers:` Un mailer enviará correos electrónicos en segundo plano usando la infraestructura de background worker de Loco. Todo será transparente para ti.
* `Almacenamiento:` En Loco Storage, facilitamos el trabajo con archivos a través de múltiples operaciones. El almacenamiento puede ser en memoria, en disco o usar servicios en la nube como AWS S3, GCP y Azure.
* `Caché:` Loco proporciona una capa de caché para mejorar el rendimiento de la aplicación almacenando datos de acceso frecuente.
Para ver más características de Loco, consulta nuestro [sitio de documentación](https://loco.rs/docs/getting-started/tour/).
## Primeros pasos
<!-- <snip id="quick-installation-command" inject_from="yaml" template="sh"> -->
```sh
cargo install loco
cargo install sea-orm-cli # Solo si necesitas base de datos
```
<!-- </snip> -->
Ahora puedes crear tu nueva app (elige "`SaaS` app").
<!-- <snip id="loco-cli-new-from-template" inject_from="yaml" template="sh"> -->
```sh
❯ loco new
✔ ❯ ¿Nombre de la app? · miapp
✔ ❯ ¿Qué te gustaría construir? · App SaaS con renderizado del lado del cliente
✔ ❯ Selecciona un proveedor de BD · Sqlite
✔ ❯ Selecciona el tipo de worker en segundo plano · Async (tareas async in-process con tokio)
🚂 App Loco generada exitosamente en:
miapp/
- assets: Has seleccionado `clientside` para la configuración de tu servidor de assets.
Siguiente paso, construye tu frontend:
$ cd frontend/
$ npm install && npm run build
```
<!-- </snip> -->
Ahora entra en tu `miapp` y arranca tu app:
<!-- <snip id="starting-the-server-command-with-output" inject_from="yaml" template="sh"> -->
```sh
$ cargo loco start
▄ ▀
▀ ▄
▄ ▀ ▄ ▄ ▄▀
▄ ▀▄▄
▄ ▀ ▀ ▀▄▀█▄
▀█▄
▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█
██████ █████ ███ █████ ███ █████ ███ ▀█
██████ █████ ███ █████ ▀▀▀ █████ ███ ▄█▄
██████ █████ ███ █████ █████ ███ ████▄
██████ █████ ███ █████ ▄▄▄ █████ ███ █████
██████ █████ ███ ████ ███ █████ ███ ████▀
▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ██▀
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
https://loco.rs
listening on port 5150
```
<!-- </snip> -->
## Proyectos impulsados por Loco
* [SpectralOps](https://spectralops.io) - varios servicios impulsados por el framework Loco
* [Nativish](https://nativi.sh) - backend de la app impulsado por el framework Loco
## Contribuidores ✨
Gracias a estas personas maravillosas:
<a href="https://github.com/loco-rs/loco/graphs/contributors">
<img src="https://contrib.rocks/image?repo=loco-rs/loco" />
</a>
+114
View File
@@ -0,0 +1,114 @@
<div align="center">
<img src="https://github.com/loco-rs/loco/assets/83390/992d215a-3cd3-42ee-a1c7-de9fd25a5bac"/>
<h1>Loco vous souhaite la bienvenue</h1>
<h3>
🚂 Loco c'est Rust on Rails.
</h3>
[![crate](https://img.shields.io/crates/v/loco-rs.svg)](https://crates.io/crates/loco-rs)
[![docs](https://docs.rs/loco-rs/badge.svg)](https://docs.rs/loco-rs)
[![Discord channel](https://img.shields.io/badge/discord-Join-us)](https://discord.gg/fTvyBzwKS8)
</div>
[English](./README.md) · [中文](./README-zh_CN.md) · Français · [Portuguese (Brazil)](./README-pt_BR.md) ・ [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Русский](./README.ru.md) · [Español](./README.es.md)
## À propos de Loco
`Loco` est fortement inspiré de Rails. Si vous connaissez Rails et Rust, vous vous sentirez chez vous. Si vous ne connaissez que Rails et que vous êtes nouveau sur Rust, vous trouverez Loco rafraîchissant. Nous ne supposons pas que vous connaissez Rails.
Pour un aperçu plus approfondie du fonctionnement de Loco, y compris des guides détaillés, des exemples et des références API, consultez notre [site Web de documentation](https://loco.rs).
## Caractéristiques de Loco:
* `Convention plutôt que configuration`: Semblable à Ruby on Rails, Loco met l'accent sur la simplicité et la productivité en réduisant le besoin de code passe-partout. Il utilise des valeurs par défaut raisonnables, permettant aux développeurs de se concentrer sur l'écriture de la logique métier plutôt que de consacrer du temps à la configuration.
* `Développement rapide`: Visant une productivité élevée des développeurs, la conception de Loco se concentre sur la réduction du code passe-partout et la fourniture d'API intuitives, permettant aux développeurs d'intégrer rapidement et de créer des prototypes avec un minimum d'effort.
* `Intégration ORM`: Modélisez avec des entités robustes, éliminant le besoin d'écrire du SQL. Définissez les relations, la validation et la logique sur mesure directement sur vos entités pour une maintenabilité et une évolutivité améliorées.
* `Contrôleurs`: Gérez les paramètres et le contenu des requêtes Web, la validation des requêtes et affichez une réponse tenant compte du contenu. Nous utilisons Axum pour une meilleure performance, simplicité et extensibilité. Les contrôleurs vous permettent également de créer facilement des middlewares, qui peuvent être utilisés pour ajouter une logique telle que l'authentification, la journalisation (logging) ou la gestion des erreurs avant de transmettre les requêtes aux actions du contrôleur principal.
* `Vues`: Loco peut s'intégrer aux moteurs de _templates_ pour générer du contenu HTML dynamique à partir de modèles template.
* `Tâches en arrière-plan`: Effectuer des calculs informatiques ou d'I/O (Entrée/Sortie) intensives en arrière-plan avec une file d'attente sauvegardée Redis ou avec des threads. Implémenter un travailleur (worker) est aussi simple que d'implémenter une fonction d'exécution pour le trait Worker.
* `Scheduler`: Simplifie le système crontab traditionnel, souvent encombrant, en rendant plus facile et plus élégante la planification de tâches ou de scripts shell.
* `Mailers`: Un logiciel de messagerie enverra des e-mails en arrière-plan en utilisant l'infrastructure de travail d'arrière-plan de Loco existante. Tout se passera sans problème pour vous.
* `Stockage`: Loco Storage facilite le travail avec des fichiers via plusieurs opérations. Le stockage peut être en mémoire, sur disque ou utiliser des services cloud tels qu'AWS S3, GCP et Azure.
* `Cache :` Loco fournit une strate cache pour améliorer les performances des applications en stockant les données fréquemment consultées.
Pour en savoir plus sur les fonctionnalités de Loco, consultez notre [site Web de documentation](https://loco.rs/docs/getting-started/tour/).
## Commencez rapidement
<!-- <snip id="quick-installation-command" inject_from="yaml" template="sh"> -->
```sh
cargo install loco
cargo install sea-orm-cli # Only when DB is needed
```
<!-- </snip> -->
Vous pouvez maintenant créer votre nouvelle application (choisissez "`SaaS` app").
<!-- <snip id="loco-cli-new-from-template" inject_from="yaml" template="sh"> -->
```sh
❯ loco new
✔ ❯ App name? · myapp
✔ ❯ What would you like to build? · Saas App with client side rendering
✔ ❯ Select a DB Provider · Sqlite
✔ ❯ Select your background worker type · Async (in-process tokio async tasks)
🚂 Loco app generated successfully in:
myapp/
- assets: You've selected `clientside` for your asset serving configuration.
Next step, build your frontend:
$ cd frontend/
$ npm install && npm run build
```
<!-- </snip> -->
Maintenant, faite `cd` dans votre `myapp` et démarrez votre application:
<!-- <snip id="starting-the-server-command-with-output" inject_from="yaml" template="sh"> -->
```sh
$ cargo loco start
▄ ▀
▀ ▄
▄ ▀ ▄ ▄ ▄▀
▄ ▀▄▄
▄ ▀ ▀ ▀▄▀█▄
▀█▄
▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█
██████ █████ ███ █████ ███ █████ ███ ▀█
██████ █████ ███ █████ ▀▀▀ █████ ███ ▄█▄
██████ █████ ███ █████ █████ ███ ████▄
██████ █████ ███ █████ ▄▄▄ █████ ███ █████
██████ █████ ███ ████ ███ █████ ███ ████▀
▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ██▀
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
https://loco.rs
listening on port 5150
```
<!-- </snip> -->
## Servi par Loco
+ [SpectralOps](https://spectralops.io) - divers services servi par le framework Loco
+ [Nativish](https://nativi.sh) - app backend servi par le framework Loco
## Contributeurs ✨
Merci à ces personnes formidables :
<a href="https://github.com/loco-rs/loco/graphs/contributors">
<img src="https://contrib.rocks/image?repo=loco-rs/loco" />
</a>
+100
View File
@@ -0,0 +1,100 @@
<div align="center">
<img src="https://github.com/loco-rs/loco/assets/83390/992d215a-3cd3-42ee-a1c7-de9fd25a5bac"/>
<h1>Locoへようこそ</h1>
<h3>
🚂 LocoはRust on Railsです。
</h3>
[![crate](https://img.shields.io/crates/v/loco-rs.svg)](https://crates.io/crates/loco-rs)
[![docs](https://docs.rs/loco-rs/badge.svg)](https://docs.rs/loco-rs)
[![Discord channel](https://img.shields.io/badge/discord-Join-us)](https://discord.gg/fTvyBzwKS8)
</div>
English · [中文](./README-zh_CN.md) · [Français](./README.fr.md) · [Portuguese (Brazil)](./README-pt_BR.md) ・ 日本語 · [한국어](./README.ko.md) · [Русский](./README.ru.md)
## Locoとは?
`Loco`はRailsに強くインスパイアされています。RailsとRustの両方を知っているなら、すぐに馴染むでしょう。Railsしか知らなく、Rustに新しい方でも、Locoは新鮮に感じるでしょう。Railsを知っているとは仮定していません。
Locoの動作についての詳細なガイド、例、APIリファレンスは、[ドキュメント](https://loco.rs)をチェックしてください。
## Locoの特徴:
* `設定より規約:` Ruby on Railsに似て、Locoはボイラープレートコードを減らすことでシンプルさと生産性を発揮します。合理的なデフォルトを使用し、開発者が設定に時間を費やすのではなく、ビジネスロジックの記述に集中できるようにします。
* `迅速な開発:` 高い開発者生産性を目指し、Locoの設計はボイラープレートコードを減らし、直感的なAPIを提供することに焦点を当てています。これにより、開発者は迅速に反復し、最小限の努力でプロトタイプを構築できます。
* `ORM統合:` ビジネスモデルを堅牢なエンティティで表現し、SQLを書く必要をなくします。エンティティに直接関係、検証、およびカスタムロジックを定義でき、メンテナンス性とスケーラビリティが向上します。
* `コントローラー:` ウェブリクエストのパラメータ、ボディ、検証を処理し、コンテンツに応じたレスポンスをレンダリングします。最高のパフォーマンス、シンプルさ、拡張性のためにAxumを使用しています。コントローラーは、認証、ロギング、エラーハンドリングなどのロジックを追加するためのミドルウェアを簡単に構築できます。
* `ビュー:` Locoはテンプレートエンジンと統合し、テンプレートから動的なHTMLコンテンツを生成できます。
* `バックグラウンドジョブ:` Redisバックエンドキューやスレッドを使用して、計算またはI/O集約型のジョブをバックグラウンドで実行します。ワーカーを実装するのは、Workerトレイトのperform関数を実装するだけです。
* `スケジューラー:` 従来の、しばしば面倒なcrontabシステムを簡素化し、タスクやシェルスクリプトをスケジュールするのをより簡単かつエレガントにします。
* `メール送信:` メール送信者は、既存のLocoバックグラウンドワーカーインフラストラクチャを使用して、バックグラウンドでメールを配信します。すべてがシームレスに行われます。
* `ストレージ:` Locoのストレージでは、ファイル操作を簡素化します。ストレージはメモリ内、ディスク上、またはAWS S3、GCP、Azureなどのクラウドサービスを使用できます。
* `キャッシュ:` Locoは、頻繁にアクセスされるデータを保存することでアプリケーションのパフォーマンスを向上させるためのキャッシュレイヤーを提供します。
Locoの詳細な機能については、[ドキュメントウェブサイト](https://loco.rs/docs/getting-started/tour/)を確認してください。
## 始め方
```sh
cargo install loco
cargo install sea-orm-cli # データベースが必要な場合のみ
```
以下で新しいアプリを作成できます(「`SaaS`アプリ」を選択)。
```sh
❯ loco new
✔ ❯ App name? · myapp
✔ ❯ What would you like to build? · SaaS app (with DB and user auth)
✔ ❯ Select a DB Provider · Sqlite
✔ ❯ Select your background worker type · Async (in-process tokio async tasks)
✔ ❯ Select an asset serving configuration · Client (configures assets for frontend serving)
🚂 Loco app generated successfully in:
myapp/
```
次に`myapp`に移動し、アプリを起動します:
```sh
$ cargo loco start
▄ ▀
▀ ▄
▄ ▀ ▄ ▄ ▄▀
▄ ▀▄▄
▄ ▀ ▀ ▀▄▀█▄
▀█▄
▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█
██████ █████ ███ █████ ███ █████ ███ ▀█
██████ █████ ███ █████ ▀▀▀ █████ ███ ▄█▄
██████ █████ ███ █████ █████ ███ ████▄
██████ █████ ███ █████ ▄▄▄ █████ ███ █████
██████ █████ ███ ████ ███ █████ ███ ████▀
▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ██▀
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
https://loco.rs
listening on port 5150
```
## Locoによって開発されています
+ [SpectralOps](https://spectralops.io) - Locoフレームワークによる各種サービス
+ [Nativish](https://nativi.sh) - Locoフレームワークによるアプリバックエンド
## 貢献者 ✨
これらの素晴らしい人々に感謝します:
<a href="https://github.com/loco-rs/loco/graphs/contributors">
<img src="https://contrib.rocks/image?repo=loco-rs/loco" />
</a>
+115
View File
@@ -0,0 +1,115 @@
<div align="center">
<img src="https://github.com/loco-rs/loco/assets/83390/992d215a-3cd3-42ee-a1c7-de9fd25a5bac"/>
<h1>Loco에 오신 것을 환영합니다</h1>
<h3>
🚂 Loco는 Rust on Rails입니다.
</h3>
[![crate](https://img.shields.io/crates/v/loco-rs.svg)](https://crates.io/crates/loco-rs)
[![docs](https://docs.rs/loco-rs/badge.svg)](https://docs.rs/loco-rs)
[![Discord channel](https://img.shields.io/badge/discord-Join-us)](https://discord.gg/fTvyBzwKS8)
</div>
[English](./README.md) · [中文](./README-zh_CN.md) · [Français](./README.fr.md) · [Portuguese (Brazil)](./README-pt_BR.md) ・ [日本語](./README.ja.md) · 한국어 · [Русский](./README.ru.md) · [Español](./README.es.md)
## Loco란?
`Loco`는 Rails에서 강한 영감을 받았습니다. Rails와 Rust를 모두 알고 계신다면 친숙하게 느껴지실 것이며, Rails만 알고 Rust를 처음 접하시는 분들에게도 Loco는 새롭게 다가올 것입니다. 참고로, Rails에 대한 사전 지식은 필수가 아닙니다.
Loco의 작동 방식에 대해 더 자세히 알아보려면 가이드, 예제, API 참조를 포함한 [문서 웹사이트](https://loco.rs)를 확인해보세요.
## Loco의 주요 기능:
* `설정보다 관습`: Ruby on Rails와 유사하게, Loco는 상용구 코드의 필요성을 줄임으로써 단순성과 생산성을 강조합니다. 합리적인 기본값을 사용하여 개발자가 설정보다는 비즈니스 로직 작성에 집중할 수 있게 합니다.
* `빠른 개발`: 높은 개발자 생산성을 목표로 하며, Loco의 설계는 상용구 코드를 줄이고 직관적인 API를 제공하여 개발자가 최소한의 노력으로 빠르게 반복하고 프로토타입을 구축할 수 있도록 합니다.
* `ORM 통합`: SQL 작성 없이 비즈니스를 강력한 엔티티로 모델링합니다. 관계, 유효성 검사, 사용자 정의 로직을 엔티티에 직접 정의하여 유지보수성과 확장성을 향상시킵니다.
* `컨트롤러`: 웹 요청 매개변수, 본문, 유효성 검사를 처리하고 컨텐츠를 인식하는 응답을 렌더링합니다. 최고의 성능, 단순성, 확장성을 위해 Axum을 사용합니다. 또한 컨트롤러를 통해 인증, 로깅, 오류 처리와 같은 로직을 추가할 수 있는 미들웨어를 쉽게 구축할 수 있습니다.
* `뷰`: Loco는 템플릿에서 동적 HTML 콘텐츠를 생성하기 위해 템플릿 엔진과 통합할 수 있습니다.
* `백그라운드 작업`: Redis 기반 큐 또는 스레드를 사용하여 계산이나 I/O 집약적인 작업을 백그라운드에서 수행합니다. Worker 트레이트에 대한 perform 함수를 구현하는 것만으로도 워커를 구현할 수 있습니다.
* `스케줄러`: 전통적이고 번거로운 crontab 시스템을 단순화하여 작업이나 셸 스크립트를 더 쉽고 우아하게 예약할 수 있습니다.
* `메일러`: 메일러는 기존 loco 백그라운드 워커 인프라를 사용하여 이메일을 백그라운드에서 전달합니다. 모든 과정이 매끄럽게 처리됩니다.
* `스토리지`: Loco 스토리지는 여러 작업을 통해 파일 작업을 용이하게 합니다. 메모리 내, 디스크, AWS S3, GCP, Azure와 같은 클라우드 서비스를 사용할 수 있습니다.
* `캐시`: Loco는 자주 접근하는 데이터를 저장하여 애플리케이션 성능을 향상시키는 캐시 레이어를 제공합니다.
더 많은 Loco 기능을 보려면 [문서 웹사이트](https://loco.rs/docs/getting-started/tour/)를 확인하세요.
## 시작하기
<!-- <snip id="quick-installation-command" inject_from="yaml" template="sh"> -->
```sh
cargo install loco
cargo install sea-orm-cli # Only when DB is needed
```
<!-- </snip> -->
이제 새로운 앱을 만들 수 있습니다 ("`SaaS 앱`" 선택).
<!-- <snip id="loco-cli-new-from-template" inject_from="yaml" template="sh"> -->
```sh
❯ loco new
✔ ❯ App name? · myapp
✔ ❯ What would you like to build? · Saas App with client side rendering
✔ ❯ Select a DB Provider · Sqlite
✔ ❯ Select your background worker type · Async (in-process tokio async tasks)
🚂 Loco app generated successfully in:
myapp/
- assets: You've selected `clientside` for your asset serving configuration.
Next step, build your frontend:
$ cd frontend/
$ npm install && npm run build
```
<!-- </snip> -->
이제 `myapp` 디렉토리로 이동하여 앱을 시작하세요:
<!-- <snip id="starting-the-server-command-with-output" inject_from="yaml" template="sh"> -->
```sh
$ cargo loco start
▄ ▀
▀ ▄
▄ ▀ ▄ ▄ ▄▀
▄ ▀▄▄
▄ ▀ ▀ ▀▄▀█▄
▀█▄
▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█
██████ █████ ███ █████ ███ █████ ███ ▀█
██████ █████ ███ █████ ▀▀▀ █████ ███ ▄█▄
██████ █████ ███ █████ █████ ███ ████▄
██████ █████ ███ █████ ▄▄▄ █████ ███ █████
██████ █████ ███ ████ ███ █████ ███ ████▀
▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ██▀
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
https://loco.rs
listening on port 5150
```
<!-- </snip> -->
## Loco 사용 사례
+ [SpectralOps](https://spectralops.io) - Loco 프레임워크로 구동되는 다양한 서비스
+ [Nativish](https://nativi.sh) - Loco 프레임워크로 구동되는 앱 백엔드
## 기여자 ✨
이 멋진 분들께 감사드립니다:
<a href="https://github.com/loco-rs/loco/graphs/contributors">
<img src="https://contrib.rocks/image?repo=loco-rs/loco" />
</a>
+120
View File
@@ -0,0 +1,120 @@
<div align="center">
<img src="https://github.com/loco-rs/loco/assets/83390/992d215a-3cd3-42ee-a1c7-de9fd25a5bac"/>
<h1>Welcome to Loco</h1>
<h3>
<!-- <snip id="description" inject_from="yaml"> -->
🚂 Loco is Rust on Rails.
<!--</snip> -->
</h3>
[![crate](https://img.shields.io/crates/v/loco-rs.svg)](https://crates.io/crates/loco-rs)
[![docs](https://docs.rs/loco-rs/badge.svg)](https://docs.rs/loco-rs)
[![Discord channel](https://img.shields.io/badge/discord-Join-us)](https://discord.gg/fTvyBzwKS8)
</div>
English · [中文](./README-zh_CN.md) · [Français](./README.fr.md) · [Portuguese (Brazil)](./README-pt_BR.md) ・ [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Русский](./README.ru.md) · [Español](./README.es.md)
## What's Loco?
`Loco` is strongly inspired by Rails. If you know Rails and Rust, you'll feel at home. If you only know Rails and new to Rust, you'll find Loco refreshing. We do not assume you know Rails.
For a deeper dive into how Loco works, including detailed guides, examples, and API references, check out our [documentation website](https://loco.rs).
## Features of Loco:
* `Convention Over Configuration:` Similar to Ruby on Rails, Loco emphasizes simplicity and productivity by reducing the need for boilerplate code. It uses sensible defaults, allowing developers to focus on writing business logic rather than spending time on configuration.
* `Rapid Development:` Aim for high developer productivity, Loco’s design focuses on reducing boilerplate code and providing intuitive APIs, allowing developers to iterate quickly and build prototypes with minimal effort.
* `ORM Integration:` Model your business with robust entities, eliminating the need to write SQL. Define relationships, validation, and custom logic directly on your entities for enhanced maintainability and scalability.
* `Controllers`: Handle web requests parameters, body, validation, and render a response that is content-aware. We use Axum for the best performance, simplicity, and extensibility. Controllers also allow you to easily build middlewares, which can be used to add logic such as authentication, logging, or error handling before passing requests to the main controller actions.
* `Views:` Loco can integrate with templating engines to generate dynamic HTML content from templates.
* `Background Jobs:` Perform compute or I/O intensive jobs in the background with a Redis backed queue, or with threads. Implementing a worker is as simple as implementing a perform function for the Worker trait.
* `Scheduler:` Simplifies the traditional, often cumbersome crontab system, making it easier and more elegant to schedule tasks or shell scripts.
* `Mailers:` A mailer will deliver emails in the background using the existing loco background worker infrastructure. It will all be seamless for you.
* `Storage:` In Loco Storage, we facilitate working with files through multiple operations. Storage can be in-memory, on disk, or use cloud services such as AWS S3, GCP, and Azure.
* `Cache:` Loco provides an cache layer to improve application performance by storing frequently accessed data.
So see more Loco features, check out our [documentation website](https://loco.rs/docs/getting-started/tour/).
## Getting Started
<!-- <snip id="quick-installation-command" inject_from="yaml" template="sh"> -->
```sh
cargo install loco
cargo install sea-orm-cli # Only when DB is needed
```
<!-- </snip> -->
Now you can create your new app (choose "`SaaS` app").
<!-- <snip id="loco-cli-new-from-template" inject_from="yaml" template="sh"> -->
```sh
❯ loco new
✔ ❯ App name? · myapp
✔ ❯ What would you like to build? · Saas App with client side rendering
✔ ❯ Select a DB Provider · Sqlite
✔ ❯ Select your background worker type · Async (in-process tokio async tasks)
🚂 Loco app generated successfully in:
myapp/
- assets: You've selected `clientside` for your asset serving configuration.
Next step, build your frontend:
$ cd frontend/
$ npm install && npm run build
```
<!-- </snip> -->
Now `cd` into your `myapp` and start your app:
<!-- <snip id="starting-the-server-command-with-output" inject_from="yaml" template="sh"> -->
```sh
$ cargo loco start
▄ ▀
▀ ▄
▄ ▀ ▄ ▄ ▄▀
▄ ▀▄▄
▄ ▀ ▀ ▀▄▀█▄
▀█▄
▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█
██████ █████ ███ █████ ███ █████ ███ ▀█
██████ █████ ███ █████ ▀▀▀ █████ ███ ▄█▄
██████ █████ ███ █████ █████ ███ ████▄
██████ █████ ███ █████ ▄▄▄ █████ ███ █████
██████ █████ ███ ████ ███ █████ ███ ████▀
▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ██▀
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
https://loco.rs
listening on port 5150
```
<!-- </snip> -->
## Powered by Loco
+ [SpectralOps](https://spectralops.io) - various services powered by Loco
framework
+ [Nativish](https://nativi.sh) - app backend powered by Loco framework
## Contributors ✨
Thanks goes to these wonderful people:
<a href="https://github.com/loco-rs/loco/graphs/contributors">
<img src="https://contrib.rocks/image?repo=loco-rs/loco" />
</a>
+109
View File
@@ -0,0 +1,109 @@
<div align="center">
<img src="https://github.com/loco-rs/loco/assets/83390/992d215a-3cd3-42ee-a1c7-de9fd25a5bac"/>
<h1>Добро пожаловать в *Loco*</h1>
<h3>
<!-- <snip id="description" inject_from="yaml"> -->
🚂 Loco is Rust on Rails.
<!--</snip> -->
</h3>
[![crate](https://img.shields.io/crates/v/loco-rs.svg)](https://crates.io/crates/loco-rs)
[![docs](https://docs.rs/loco-rs/badge.svg)](https://docs.rs/loco-rs)
[![Discord channel](https://img.shields.io/badge/discord-Join-us)](https://discord.gg/fTvyBzwKS8)
</div>
[English](./README.md) · [中文](./README-zh_CN.md) · [Français](./README.fr.md) · [Portuguese (Brazil)](./README-pt_BR.md) ・ [日本語](./README.ja.md) · Русский · [Español](./README.es.md)
## Что такое Loco?
*Loco* сильно вдохновлён проектом *Ruby on Rails*. Если вы знакомы и с *Rails*, и с *Rust*, вы будете чувствовать себя как дома. Если вы знаете только *Rails*, и не знакомы с *Rust*, *Loco* будет для вас чем-то освежающим.
Если вам интересно узнать внутрение устройство *Loco*, включая детальные гайды, примеры, и устройство API, почитайте нашу [документацию](https://loco.rs).
## Фишки Loco:
- **Простота превыше конфигурации**: Подобно *Ruby on Rails*, *Loco* делает упор на простоту и продуктивность, снижая потребность в лишнем коде. *Loco* использует оптимальные настройки по-умолчанию, давая разработчикам возможность сфокусироваться на написании бизнес логики, а не конфигурации.
- **Быстрая разработка**: Ставя акцент на высокой производительности разработчика, Дизайн *Loco* фокусируется на сокращении ненужного кода и предоставления интуитивного API. Это позволяет быстро создавать прототипы без лишних усилий.
- **ORM интеграция**: Стройте свой бизнес с крепкими составляющими, убирая необходимость писать SQL. Определяйте взаимосвязи, проверку, и кастомную логику прямо в составляющих, упрощая поддержку и рост кодовой базы.
- **Контролеры**: Обрабатывайте параметры и данные web-запросов, проверяйте их содержимое, отображайте ответ с учетом запроса. Мы используем *Axum* для достижения наилучшей производительности, простоты, и возможности расширения. Также, контролеры облегчают внедрение middleware. Это может быть использовано для добавления всевозможной логики: аутентификации, логгинга, или обработки ошибок перед отправкой на сервер.
- **Виды**: *Loco* может интегрироваться с template-движками для генерации динамического HTML из шаблонов.
- **Фоновые задачи**: Исполняйте I/O и другие тяжелые операции в фоновом режиме с помощью *Redis*, или потоков. Для написания функционала фоновой задачи нужно всего лишь написать функцию `perform` из `trait Worker`.
- **Планировщик**: Облегчает традиционную, часто громоздкую систему, упрощая планировку задач и исполнение shell-скриптов.
- **Отправка электронной почты**: Отправка электронной почты в фоновом режиме, без необходимости создавать новую фоновую задачу.
- **Хранилище**: Мы способствуем работе с файлами несколькими путями: хранение в памяти, на диске, или использование облачных сервисов как *AWS*, *S3*, *GCP*, и *Azure*.
- **Кэширование**: *Loco* кэширует частые запросы для улучшения производительности приложения.
У *Loco* есть ещё множество фишек, котрые вы можете посмотреть на [сайте документации](https://loco.rs/docs/getting-started/tour/).
## Установка
<!-- <snip id="quick-installation-command" inject_from="yaml" template="sh"> -->
```sh
cargo install loco
cargo install sea-orm-cli # Only when DB is needed
```
<!-- </snip> -->
Теперь вы можете создать свое новое приложение (выберете "`SaaS` app").
<!-- <snip id="loco-cli-new-from-template" inject_from="yaml" template="sh"> -->
```sh
❯ loco new
✔ ❯ App name? · myapp
✔ ❯ What would you like to build? · Saas App with client side rendering
✔ ❯ Select a DB Provider · Sqlite
✔ ❯ Select your background worker type · Async (in-process tokio async tasks)
🚂 Loco app generated successfully in:
myapp/
- assets: You've selected `clientside` for your asset serving configuration.
Next step, build your frontend:
$ cd frontend/
$ npm install && npm run build
```
<!-- </snip> -->
Теперь выполните `cd` в папку `myapp` и запускайте приложение:
<!-- <snip id="starting-the-server-command-with-output" inject_from="yaml" template="sh"> -->
```sh
$ cargo loco start
▄ ▀
▀ ▄
▄ ▀ ▄ ▄ ▄▀
▄ ▀▄▄
▄ ▀ ▀ ▀▄▀█▄
▀█▄
▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█
██████ █████ ███ █████ ███ █████ ███ ▀█
██████ █████ ███ █████ ▀▀▀ █████ ███ ▄█▄
██████ █████ ███ █████ █████ ███ ████▄
██████ █████ ███ █████ ▄▄▄ █████ ███ █████
██████ █████ ███ ████ ███ █████ ███ ████▀
▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ██▀
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
https://loco.rs
listening on port 5150
```
<!-- </snip> -->
## Проекты, использующие *Loco*
+ [SpectralOps](https://spectralops.io) - различные сервисы, использующие *Loco*
framework
+ [Nativish](https://nativi.sh) - backend приложения, использующий *Loco*
## Контрибьютеры ✨
Спасибо всем этим прекрасным людям:
<a href="https://github.com/loco-rs/loco/graphs/contributors">
<img src="https://contrib.rocks/image?repo=loco-rs/loco" />
</a>
+10
View File
@@ -0,0 +1,10 @@
# Security Policy
By researching and submitting a vulnerability, you'll be helping open source and this project's goal to provide a fast to build fast to run Web framework based on Rust.
## Reporting a Vulnerability
Please report directly to [dotan@rng0.io](mailto:dotan@rng0.io).
We will credit you as a committer with every vulnerability you find that we can validate.
+32
View File
@@ -0,0 +1,32 @@
#[cfg(feature = "embedded_assets")]
use std::{env, path::Path};
fn main() {
#[cfg(feature = "embedded_assets")]
embedded_assets_main();
#[cfg(not(feature = "embedded_assets"))]
{
// No-op when feature is disabled
}
}
#[cfg(feature = "embedded_assets")]
fn embedded_assets_main() {
// Import the embedded_assets module from the build directory
#[path = "build/embedded_assets.rs"]
mod embedded_assets;
use embedded_assets::build_static_assets;
// Get OUT_DIR environment variable - this is required for build scripts
let out_dir = env::var("OUT_DIR").unwrap_or_else(|e| {
// This should trigger a build failure
panic!("OUT_DIR environment variable not set: {e}");
});
// Convert to a path
let out_dir_path = Path::new(&out_dir);
// Call the build_static_assets function with the OUT_DIR
build_static_assets(out_dir_path);
}
+420
View File
@@ -0,0 +1,420 @@
use std::collections::{HashMap, HashSet};
use std::{
env,
fs::{self, File},
io::{self, Write},
path::{Path, PathBuf},
};
pub fn build_static_assets(out_dir: &Path) {
// Determine the application root directory using Cargo environment variables
let Some(app_dir) = find_app_directory(out_dir) else {
eprintln!("Error: Could not determine application directory");
return;
};
let app_dir_str = app_dir.to_string_lossy().to_string();
println!("cargo:warning=Building with embedded_assets feature");
println!("cargo:warning=Application directory: {app_dir_str}");
println!("cargo:warning=Assets will only be loaded from the application directory");
println!("cargo:rerun-if-changed={app_dir_str}/assets/");
println!("cargo:rerun-if-changed={app_dir_str}/src/assets/");
// Also run build script again if the build files change
println!("cargo:rerun-if-changed=build/embedded_assets.rs");
let generated_path = out_dir.join("generated_code");
// Create the directory if it doesn't exist
if let Err(e) = fs::create_dir_all(&generated_path) {
eprintln!("Warning: Could not create directory: {e}");
return;
}
// Only search in the application directory
let app_root = app_dir;
// Find all directories recursively, without filtering by name
let all_dirs = discover_all_directories(&app_root.join("assets"));
println!("cargo:warning=Discovered directories for assets:");
for dir in &all_dirs {
println!("cargo:warning= - {}", dir.display());
}
// Single collection for all files
let mut all_files = HashMap::new();
// Store the assets directory reference to pass to collect_all_files
let assets_dir = app_root.join("assets");
// Process all discovered directories
for dir in &all_dirs {
// Process all files in this directory
collect_all_files(dir, &assets_dir, &mut all_files);
}
// Generate code for all assets
if all_files.is_empty() {
println!("cargo:warning=No asset files found");
// Generate empty asset files if no files found
if let Err(e) = generate_empty_asset_files(&generated_path) {
eprintln!("Warning: Failed to generate empty asset files: {e}");
}
} else {
println!("cargo:warning=Found {} asset files", all_files.len());
if let Err(e) = generate_asset_code(&all_files, &generated_path) {
eprintln!("Warning: Failed to generate asset code: {e}");
}
}
}
pub fn find_app_directory(out_dir: &Path) -> Option<PathBuf> {
// Find project root from OUT_DIR by going up to parent of "target" directory
let mut path = out_dir.to_path_buf();
while path.pop() {
if path.file_name().and_then(|n| n.to_str()) == Some("target") && path.pop() {
return Some(path);
}
// Safety check
if path.as_os_str().is_empty() {
break;
}
}
// Fallback to current directory
env::current_dir().ok()
}
pub fn discover_all_directories(app_root: &Path) -> Vec<PathBuf> {
let mut directories = Vec::new();
let mut visited = HashSet::new();
// Only include the directory if it exists
if app_root.exists() {
// Add the root directory itself
directories.push(app_root.to_path_buf());
// Start recursive discovery
recursively_collect_directories(app_root, &mut directories, &mut visited);
}
// Sort directories by their string representation to ensure consistent ordering
directories.sort_by(|a, b| {
a.to_string_lossy()
.to_string()
.cmp(&b.to_string_lossy().to_string())
});
directories
}
pub fn recursively_collect_directories(
dir: &Path,
directories: &mut Vec<PathBuf>,
visited: &mut std::collections::HashSet<PathBuf>,
) {
// Check if we've already visited this directory
if !visited.insert(dir.to_path_buf()) {
return;
}
// Continue recursively discovering subdirectories
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
// Add this directory to our list
directories.push(path.clone());
// Continue recursion
recursively_collect_directories(&path, directories, visited);
}
}
}
}
pub fn collect_all_files(dir: &Path, assets_dir: &Path, all_files: &mut HashMap<String, String>) {
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
// Skip if we can't determine the file path or extension
let full_path = path.to_string_lossy().to_string();
// Create a relative path based on the assets directory
let Ok(rel_path) = path.strip_prefix(assets_dir) else {
println!(
"cargo:warning=Failed to strip prefix for path: {}",
path.display()
);
continue; // Skip this file if we can't determine its relative path
};
// Format the key as a path, using forward slashes
let mut key = format!("/{}", rel_path.to_string_lossy().replace('\\', "/"));
// Remove any double slashes
key = key.replace("//", "/");
// Special handling for templates in views directory
if key.starts_with("/views/") {
// For templates, we want to:
// 1. Strip "/views/" prefix for proper Tera template inheritance
// 2. Keep the relative path structure for nested templates
key = key.trim_start_matches("/views/").to_string();
}
// Log what we found
println!("cargo:warning=Found asset: {} -> {}", path.display(), key);
// Store the file
all_files.insert(full_path, key);
}
}
}
}
#[allow(clippy::too_many_lines)]
pub fn generate_asset_code(
all_files: &HashMap<String, String>,
output_path: &Path,
) -> io::Result<()> {
// Create vectors to track which files go where
let mut static_assets = Vec::new();
let mut template_files = Vec::new();
// Simple categorization: if file ends with .html or .htm, it's a template, otherwise static asset
for (path, key) in all_files {
if std::path::Path::new(key)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("html"))
|| std::path::Path::new(key)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("htm"))
{
template_files.push((path.clone(), key.clone()));
} else {
static_assets.push((path.clone(), key.clone()));
}
}
// Sort static assets by key for consistent output
static_assets.sort_by(|a, b| a.1.cmp(&b.1));
// Build template dependency map and sort templates
let mut template_deps: HashMap<String, Option<String>> = HashMap::new();
println!("cargo:warning=Analyzing template dependencies...");
// First pass: read all template contents and find their dependencies
for (path, key) in &template_files {
println!("cargo:warning=Reading template: {key}");
match fs::read_to_string(path) {
Ok(content) => {
// Look for {% extends "..." %} pattern
if let Some(extends) = content
.lines()
.find(|line| line.trim().starts_with("{% extends"))
{
if let Some(parent) = extends
.split('"')
.nth(1)
.or_else(|| extends.split('\'').nth(1))
{
template_deps.insert(key.clone(), Some(parent.to_string()));
println!("cargo:warning=Template {key} extends {parent}");
}
} else {
template_deps.insert(key.clone(), None);
println!("cargo:warning=Template {key} has no parent");
}
}
Err(e) => {
println!("cargo:warning=Failed to read template {path}: {e}");
}
}
}
println!("cargo:warning=Template dependencies:");
for (template, parent) in &template_deps {
if let Some(p) = parent {
println!("cargo:warning= {template} -> {p}");
} else {
println!("cargo:warning= {template} (no parent)");
}
}
// Sort templates so that parents come before children
let mut sorted_templates = Vec::new();
let mut processed = HashSet::new();
// First add all base templates (those with no parents), sorted alphabetically
let mut base_templates: Vec<_> = template_deps
.iter()
.filter(|(_, parent)| parent.is_none())
.map(|(key, _)| key.clone())
.collect();
base_templates.sort(); // Sort base templates alphabetically
for key in base_templates {
println!("cargo:warning=Adding base template: {key}");
processed.insert(key.clone());
sorted_templates.push(key);
}
// Then add all child templates, level by level
let mut added_in_this_pass;
while {
added_in_this_pass = false;
let mut level_templates = Vec::new();
// Collect all templates at this level
for (key, parent) in &template_deps {
if processed.contains(key) {
continue;
}
if let Some(parent) = parent
&& processed.contains(parent)
{
level_templates.push(key.clone());
}
}
// Sort templates at this level alphabetically
level_templates.sort();
// Add them to the final list
for key in level_templates {
if let Some(Some(parent)) = template_deps.get(&key) {
println!("cargo:warning=Adding child template: {key} (extends {parent})");
}
processed.insert(key.clone());
sorted_templates.push(key);
added_in_this_pass = true;
}
added_in_this_pass
} {}
// Add any remaining templates that weren't processed, sorted alphabetically
let mut remaining: Vec<_> = template_deps
.keys()
.filter(|key| !processed.contains(*key))
.cloned()
.collect();
remaining.sort();
for key in remaining {
println!("cargo:warning=Adding unprocessed template: {key}");
sorted_templates.push(key);
}
println!("cargo:warning=Final template order:");
for (idx, template) in sorted_templates.iter().enumerate() {
println!("cargo:warning= {}. {}", idx + 1, template);
}
// Generate static assets file
let static_file = output_path.join("static_assets.rs");
// Create the static assets content
let mut static_lines = vec![
"#[must_use]\n".to_string(),
"pub fn get_embedded_static_assets() -> std::collections::HashMap<String, &'static [u8]> {\n".to_string(),
" let mut assets = std::collections::HashMap::new();\n".to_string()
];
for (path, key) in &static_assets {
let insert_line = format!(
r#" assets.insert("{0}".to_string(), include_bytes!("{1}") as &[u8]);"#,
key,
path.replace('\\', "/")
);
static_lines.push(format!("{insert_line}\n"));
}
static_lines.push(" assets\n".to_string());
static_lines.push("}\n".to_string());
// Write static assets content to file
let mut static_file = File::create(static_file)?;
for line in static_lines {
static_file.write_all(line.as_bytes())?;
}
// Generate templates file
let templates_file = output_path.join("view_templates.rs");
// Create the templates content with detailed comments
let mut template_lines = vec![
"/// Returns a BTreeMap of templates in dependency order (parents before children)\n"
.to_string(),
"#[must_use]\n".to_string(),
"pub fn get_embedded_templates() -> std::collections::BTreeMap<String, &'static str> {\n"
.to_string(),
" let mut templates = std::collections::BTreeMap::new();\n".to_string(),
];
// Add templates in dependency order with comments
for template_key in &sorted_templates {
if let Some((path, _)) = template_files.iter().find(|(_, k)| k == template_key) {
// Add a comment showing the dependency
if let Some(Some(parent)) = template_deps.get(template_key) {
template_lines.push(format!(" // Template that extends {parent}\n"));
} else {
template_lines.push(" // Base template with no parent\n".to_string());
}
let insert_line = format!(
r#" templates.insert("{0}".to_string(), include_str!("{1}"));"#,
template_key,
path.replace('\\', "/")
);
template_lines.push(format!("{insert_line}\n"));
}
}
template_lines.push("\n templates\n".to_string());
template_lines.push("}\n".to_string());
// Write templates content to file
let mut templates_file = File::create(templates_file)?;
for line in template_lines {
templates_file.write_all(line.as_bytes())?;
}
println!(
"cargo:warning=Generated code for {} static assets and {} templates",
static_assets.len(),
sorted_templates.len()
);
Ok(())
}
pub fn generate_empty_asset_files(output_path: &Path) -> io::Result<()> {
// Generate empty static assets file
let static_file = output_path.join("static_assets.rs");
let static_code = r"#[must_use]
pub fn get_embedded_static_assets() -> std::collections::HashMap<String, &'static [u8]> {
// No assets found
std::collections::HashMap::new()
}
";
let mut file = File::create(static_file)?;
file.write_all(static_code.as_bytes())?;
// Generate empty templates file
let templates_file = output_path.join("view_templates.rs");
let templates_code = r"#[must_use]
pub fn get_embedded_templates() -> std::collections::HashMap<String, &'static str> {
// No templates found
std::collections::HashMap::new()
}
";
let mut file = File::create(templates_file)?;
file.write_all(templates_code.as_bytes())?;
Ok(())
}
+194
View File
@@ -0,0 +1,194 @@
# The URL the site will be built for
base_url = "https://loco.rs"
title = "Loco.rs - Productivity-first Rust Fullstack Web Framework"
description = "Loco.rs is like Ruby on Rails for Rust. Use it to quickly build and deploy Rust based apps from zero to production."
# Whether to automatically compile all Sass files in the sass directory
compile_sass = true
# Whether to generate a feed file for the site
generate_feeds = true
feed_filenames = ["blog/atom.xml", "blog/rss.xml"]
# When set to "true", the generated HTML files are minified.
minify_html = false
# The taxonomies to be rendered for the site and their configuration.
taxonomies = [
{ name = "authors" }, # Basic definition: no feed or pagination
]
# Whether to build a search index to be used later on by a JavaScript library
build_search_index = true
[markdown]
# Whether to do syntax highlighting
# Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola
highlight_theme = "css"
highlight_code = true
highlight_themes_css = [
{ theme = "OneHalfDark", filename = "syntax-theme-dark.css" },
{ theme = "OneHalfLight", filename = "syntax-theme-light.css" },
]
[extra]
# Put all your custom variables here
# Menu items
[[extra.menu.main]]
name = "Docs"
section = "docs"
url = "/docs/getting-started/tour/"
weight = 10
[[extra.menu.main]]
name = "Blog"
section = "blog"
url = "/blog/"
weight = 20
[[extra.menu.main]]
name = "Casts"
section = "casts"
url = "/casts/"
[[extra.menu.social]]
name = "Twitter"
pre = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-twitter"><path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path></svg>'
url = "https://twitter.com/jondot"
weight = 10
[[extra.menu.social]]
name = "GitHub"
pre = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-github"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>'
url = "https://github.com/loco-rs/loco"
post = "v0.1.0"
weight = 20
[[extra.homepage.features]]
name = "Models"
description = 'Model your business with rich entities and avoid writing SQL, backed by SeaORM. Build relations, validation and custom logic on your entities for the best maintainability.'
example = '''```rust
impl Model {
pub async fn find_by_email(db: &DatabaseConnection, email: &str)
-> ModelResult<Self> {
Users::find()
.filter(eq(Column::Email, email))
.one(db).await?
.ok_or_else(|| ModelError::EntityNotFound)
}
pub async fn create_report(&self, ctx: &AppContext) -> Result<()> {
ReportWorker::perform_later(
&ctx,
ReportArgs{ user_id: self.id }
).await?;
}
}
```
'''
[[extra.homepage.features]]
name = "Controllers"
description = 'Handle Web requests parameters, body, validation, and render a response that is content-aware. We use Axum for the best performance, simplicity and extensibility.'
example = '''```rust
pub async fn get_one(
respond_to: RespondTo,
Path(id): Path<i32>,
State(ctx): State<AppContext>,
) -> Result<Response> {
let item = Notes::find_by_id(id).one(&ctx.db).await?;
match respond_to {
RespondTo::Html => html_view(&item),
_ => format::json(item),
}
}
pub fn routes() -> Routes {
Routes::new()
.prefix("notes")
.add("/{id}", get(get_one))
}
```
'''
[[extra.homepage.features]]
name = "Views"
description = 'Use server-rendered templates with Tera or JSON. Loco can render views on the server or work with a frontend app seamlessly. Configure your fullstack set up any way you like.'
example = '''```rust
// Literals
format::text("Loco")
// Tera view engine
format::render().view(v, "home/hello.html", json!({}))
// strongly typed JSON responsed, backed by `serde`
format::json(Health { ok: true })
// Etags, cookies, and more
format::render().etag("loco-etag")?.empty()
```
'''
[[extra.homepage.features]]
name = "Background Jobs"
description = 'Perform compute or I/O intensive jobs in the background with a Redis backed queue, or with threads. Implementing a worker is as simple as implementing a <code>perform</code> function for the <code>Worker</code> trait.'
example = '''```rust
impl worker::Worker<DownloadArgs> for UsersReportWorker {
async fn perform(&self, args: DownloadArgs) -> worker::Result<()> {
let all = Users::find()
.all(&self.ctx.db)
.await
.map_err(Box::from)?;
for user in &all {
println!("user: {}", user.id);
}
Ok(())
}
}
```
'''
[[extra.homepage.features]]
name = "Deployment"
description = 'Easily generate deployment configurations with a guided CLI interface. Select from deployment options for tailored deployment setups.'
example = '''```sh
$ cargo loco generate deployment
? ❯ Choose your deployment ›
❯ Docker
❯ Nginx
..
✔ ❯ Choose your deployment · Docker
skipped (exists): "Dockerfile"
added: ".dockerignore"
```
'''
[[extra.homepage.features]]
name = "Scheduler"
description = 'Simplifies the traditional, often cumbersome crontab system, making it easier and more elegant to schedule tasks or shell scripts.'
example = '''```yaml
jobs:
db_vaccum:
run: "db_vaccum.sh"
shell: true
schedule: "0 0 * * *"
tags: ["maintenance"]
send_birthday:
run: "user_birthday_task"
schedule: "Run every 2 hours"
tags: ["marketing"]
```
'''
@@ -0,0 +1,54 @@
+++
title = "Loco"
# The homepage contents
[extra]
lead = 'The <em>one-person framework</em> for Rust for side-projects and startups'
url = "/docs/tutorials/the-tour/"
url_button = "Get started"
# Menu items
[[extra.menu.main]]
name = "Docs"
section = "docs"
url = "/docs/tutorials/the-tour/"
weight = 10
[[extra.menu.main]]
name = "Blog"
section = "blog"
url = "/blog/"
weight = 20
[[extra.menu.main]]
name = "Casts"
section = "casts"
url = "/casts/"
weight = 20
[[extra.list]]
title = "🔋 Batteries included"
content = 'Empower the 1-person team. Service, data, emails, background jobs, tasks, CLI to drive it, everything is included.'
[[extra.list]]
title = "🔮 Rails is great"
content = 'Loco follows Rails. There, I said it. Rails concepts are carefully adapted to modern Rust development.'
[[extra.list]]
title = "🏅 Deliver with confidence"
content = "Unapologetically optimized for the solo developer. Complexity and heavylifting is tucked away."
[[extra.list]]
title = "⚡️ Scale when needed"
content = "Split, reconfigure, or use only parts of Loco when you need to. Build and grow without pain."
[[extra.list]]
title = "🚀️ Build incrementally"
content = "Use what you need. Just a service, a service with a database, a background job worker, or a task."
[[extra.list]]
title = "🚦Test driven everything"
content = "Test your app with very little effort. Models, controllers, background jobs and more. Ship fast with confidence."
+++
@@ -0,0 +1,17 @@
+++
title = "Authors"
description = "The authurs of the blog articles."
draft = false
# If add a new author page in this section, please add a new item,
# and the format is as follows:
#
# "author-name-in-url" = "the-full-path-of-the-author-page"
#
# Note: We use quoted keys here.
[extra.author_pages]
"team-loco" = "authors/team-loco.md"
"limpidcrypto" = "authors/limpidcrypto.md"
+++
The authors of the blog articles.
@@ -0,0 +1,9 @@
+++
title = "LimpidCrypto"
description = "Building open source tools for cryptocurrency development"
date = 2024-01-25T18:03:52+01:00
updated = 2024-01-25T18:03:52+01:00
draft = false
+++
Creating the Building Blocks for Cryptocurrency Development. To my [website](https://limpidcrypto.com).
@@ -0,0 +1,10 @@
+++
title = "Team Loco"
description = "Creators of the Loco framework"
date = 2021-04-01T08:50:45+00:00
updated = 2021-04-01T08:50:45+00:00
draft = false
+++
Primary maintainers of the [Loco](https://loco.rs) framework: [Dotan Nahum](https://github.com/jondot), [Elad Kaplan](https://github.com/kaplanelad).
@@ -0,0 +1,7 @@
+++
title = "Blog"
description = "Blog"
sort_by = "date"
paginate_by = 10
template = "blog/section.html"
+++
@@ -0,0 +1,92 @@
+++
title = "Creating Frontend Website Using Angular"
description = "Setting up a Loco app for serving an Angular clientside app is easy. Learn how to configure and set up a full-stack Angular app with Loco."
date = 2024-01-25T18:03:52+01:00
updated = 2024-01-25T18:03:52+01:00
draft = false
template = "blog/page.html"
[taxonomies]
authors = ["LimpidCrypto"]
+++
## Overview
1. Create new SaaS project
2. Edit `.devcontainer/Dockerfile`
3. Reopen the project in the Dev Container
4. Delete frontend directory
5. Generate new Angular frontend
6. Build frontend
7. Edit `config/development.yml`
8. Start Loco
## Create new SaaS project
1. Run `loco new` to create a new project
2. Navigate through the instructions until you reach the point where to decide what type of project to create
3. Select "SaaS app (with DB and user auth)"
## Edit ".devcontainer/Dockerfile"
1. Open `.devcontainer/Dockerfile`
2. Replace the content with the following:
```Dockerfile
FROM mcr.microsoft.com/vscode/devcontainers/rust:0-1
# Install postgresql-client and sea-orm-cli
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends postgresql-client \
&& cargo install sea-orm-cli \
&& chown -R vscode /usr/local/cargo
# Install Node.js and npm
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
&& apt-get install -y nodejs
# Install Angular CLI
RUN npm install -g @angular/cli
COPY .env /.env
```
The Dockerfile will provide you with everything you need to develop a Loco app with an Angular frontend.
## Reopen the project in the Dev Container
With VSCode it is super easy to reopen and run the project in a Dev Container.
1. Press `Crtl + Shift + P`
2. Select `Dev Containers: Repopen in Container`
3. VSCode will open the project in the dev container. This can take a while when it is built for the first time.
4. Delete the existing `frontend` directory
Loco comes with a Vite React frontend. We can delete the whole directory because the Angular CLI will set up everything we need
## Generate new Angular frontend
1. From the project root execute `ng new frontend` to create a new Angular project
2. Navigate through the instructions
## Build frontend
1. Run `ng build` to build the Angular frontend
## Edit "config/development.yml"
As you may have noticed Angular has built the frontend into `frontend/dist/frontend/browser`. We now need to configure Loco to access the built frontend from there.
1. Open `config/development.yml`
2. Set the configs to the frontend build path:
a. `server.middlewares.static.folder.path: "frontend/dist/frontend/browser"`
b. `server.middlewares.static.fallback: "frontend/dist/frontend/browser/index.html"`
## Start Loco
1. Start Loco with `cargo loco start`
2. Open http://localhost:5150/
You should now see the Angular starter Website :smile:
@@ -0,0 +1,197 @@
+++
title = "Building a Rust App with Axum Session"
description = "Add sessions to your app with Axum Sessions. Configure a session provider, and set up Axum Session and Loco with simple app hooks."
date = 2023-12-19T09:19:42+00:00
updated = 2023-12-19T09:19:42+00:00
draft = false
template = "blog/page.html"
[taxonomies]
authors = ["Team Loco"]
+++
To build a Rust app with [Axum session](https://crates.io/crates/axum_session), the first step is to choose your server. In this case, we'll use [loco](https://loco.rs) :)
Start by creating a new project and selecting the `SaaS app` template:
```sh
$ cargo install loco
$ loco new
✔ ❯ App name? · myapp
? ❯ What would you like to build? ›
lightweight-service (minimal, only controllers and views)
Rest API (with DB and user auth)
❯ SaaS app (with DB and user auth)
```
## Creating Session Memory Store Only
First, add the Axum session crate to Cargo.toml:
```toml
axum_session = {version = "0.10.1", default-features = false}
```
Then, add an Axum session layer to your router. Open app.rs and add the following hook:
```rust
pub struct App;
#[async_trait]
impl Hooks for App {
fn app_name() -> &'static str {
env!("CARGO_CRATE_NAME")
}
// Other hooks...
async fn after_routes(router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {
let session_config =
axum_session::SessionConfig::default().with_table_name("sessions_table");
let session_store =
axum_session::SessionStore::<axum_session::SessionNullPool>::new(None, session_config)
.await
.unwrap();
let router = router.layer(axum_session::SessionLayer::new(session_store));
Ok(router)
}
// Other hooks...
}
```
Now, you can create your controller that uses Axum session. Use the `cargo loco generate controller` command:
```sh
❯ cargo loco generate controller mysession --api
Finished dev [unoptimized + debuginfo] target(s) in 0.36s
Running `target/debug/axum-session-cli generate controller mysession`
added: "src/controllers/mysession.rs"
injected: "src/controllers/mod.rs"
injected: "src/app.rs"
added: "tests/requests/mysession.rs"
injected: "tests/requests/mod.rs"
```
Open the `src/controllers/mysession.rs` file created by the controller generator and replace its content with the following code:
```rust
#![allow(clippy::unused_async)]
use axum_session::{Session, SessionNullPool};
use loco_rs::prelude::*;
pub async fn get_session(session: Session<SessionNullPool>) -> Result<()> {
println!("{:#?}", session);
format::empty()
}
pub fn routes() -> Routes {
Routes::new().prefix("mysession").add("/", get(get_session))
}
```
Now, you can call the `http://127.0.0.1:5150/mysession` endpoint to see the session.
## Creating Session With DB Encryption
To add session DB encryption, include the Axum session crate and PostgreSQL with SQLx in Cargo.toml:
```toml
axum_session = {version = "0.10.1"}
sqlx = { version = "0.7.2", features = [
"macros",
"postgres",
"_unstable-all-types",
"tls-rustls",
"runtime-tokio",
] }
```
Create a `session.rs` file with the following content:
The `connect_to_database` getting an `Database` configuration and returns a PgPool instance that axum session expected.
```rust
use sqlx::postgres::PgPool;
use loco_rs::{
config::Database,
errors::Error,
Result,
};
async fn connect_to_database(config: &Database) -> Result<PgPool> {
PgPool::connect(&config.uri)
.await
.map_err(|e| Error::Any(e.into()))
}
```
Add the Axum session layer to your router in `app.rs`:
```rust
use session; // This is the session.rs file
pub struct App;
#[async_trait]
impl Hooks for App {
fn app_name() -> &'static str {
env!("CARGO_CRATE_NAME")
}
// Other hooks...
async fn after_routes(router: AxumRouter, ctx: &AppContext) -> Result<AxumRouter> {
let conn = session.connect_to_database(&ctx.config.database).await?;
let session_config = axum_session::SessionConfig::default()
.with_table_name("sessions_table")
.with_key(axum_session::Key::generate())
.with_database_key(axum_session::Key::generate())
.with_security_mode(axum_session::SecurityMode::PerSession);
let session_store = axum_session::SessionStore::<axum_session::SessionPgPool>::new(
Some(conn.clone().into()),
session_config,
)
.await
.unwrap();
let router = router.layer(axum_session::SessionLayer::new(session_store));
Ok(router)
}
// Other hooks...
}
```
Create the controller as before using `cargo loco generate controller`
```sh
❯ cargo loco generate controller mysession --api
Finished dev [unoptimized + debuginfo] target(s) in 0.36s
Running `target/debug/axum-session-cli generate controller mysession`
added: "src/controllers/mysession.rs"
injected: "src/controllers/mod.rs"
injected: "src/app.rs"
added: "tests/requests/mysession.rs"
injected: "tests/requests/mod.rs"
```
and replace the content of `src/controllers/mysession.rs` with the provided code.
```rust
#![allow(clippy::unused_async)]
use axum_session::{Session, SessionPgPool};
use loco_rs::prelude::*;
pub async fn get_session(session: Session<SessionPgPool>) -> Result<()> {
println!("{:#?}", session);
format::empty()
}
pub fn routes() -> Routes {
Routes::new().prefix("mysession").add("/", get(get_session))
}
```
Now, calling the `http://127.0.0.1:5150/mysession` endpoint will display the session.
@@ -0,0 +1,559 @@
+++
title = "Deploying Rust App with Terraform on AWS Fargate"
description = "Learn how to deploy a Loco app with Terraform (IaC). Generate a deployment with Loco generators and set it up step-by-step."
date = 2023-12-20T16:04:40+00:00
updated = 2023-12-16T04:20:40+00:00
draft = false
template = "blog/page.html"
[taxonomies]
authors = ["Antonio Souza"]
+++
In today's rapidly evolving technological landscape, Infrastructure as Code (IaC) has become a cornerstone for efficient, scalable, and maintainable cloud infrastructure deployment. IaC involves managing and provisioning computing infrastructure through machine-readable script files, rather than through physical hardware configuration or interactive configuration tools. This allows for the automation of infrastructure deployment and management, which in turn reduces the risk of human error and increases the speed of deployment.
In this article, we will explore how to deploy a Rust app built with [loco](https://loco.rs) on AWS Fargate using Terraform. We will start by creating a new project and selecting the `Rest API` template:
````sh
```sh
$ cargo install loco
$ loco new
✔ ❯ App name? · myapp
? ❯ What would you like to build? ›
lightweight-service (minimal, only controllers and views)
❯ Rest API (with DB and user auth)
SaaS app (with DB and user auth)
````
## Prerequisites
To deploy our app on AWS Fargate, we will need to have the following tools installed:
- [Docker](https://docs.docker.com/get-docker/) - Docker is a containerization platform that allows you to package your application and all of its dependencies into a standardized unit for software development.
- [Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli) - Terraform is an open-source infrastructure as code software tool that enables you to safely and predictably create, change, and improve infrastructure.
- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) - The AWS Command Line Interface (CLI) is a unified tool to manage your AWS services.
## Creating the Docker Image
To create the Docker image for our app, we will use the loco CLI. The `cargo loco generate deployment` command will create a Docker image for our app. It will also create a `Dockerfile` for us, which we can use to build the image.
```sh
$ cargo loco generate deployment
? ❯ Choose your deployment ›
❯ Docker
added: "Dockerfile"
added: ".dockerignore"
```
Now, we can build the Docker image which will be used to deploy our app on AWS Fargate.
```sh
$ docker build -t myapp .
[+] Building 237.1s (16/16) FINISHED docker:desktop-linux
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring Dockerfile: 331B 0.0s
...
=> => writing image sha256:07416ca8195e4026ab65bc567f990ea83141aa10890f8443deb8f54a8bae7f0a 0.0s
=> => naming to docker.io/library/myapp
```
## Setting up AWS
To deploy our app on AWS Fargate, we will need to create an AWS account and set up the AWS CLI. You can create an AWS account [here](https://portal.aws.amazon.com/billing/signup#/start/email).
You will also need to install the AWS CLI. You can find instructions on how to do this [here](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html).
Finally, you need to create an IAM user to use with the AWS CLI. You can find instructions on how to do this [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html).
Now, we can configure the AWS CLI with the credentials of the IAM user we just created.
```sh
$ aws configure
AWS Access Key ID [None]: <your access key id>
AWS Secret Access Key [None]: <your secret access key>
Default region name [None]: <your region>
Default output format [None]: json
```
## Creating the repository on ECR
To deploy our app on AWS Fargate, we will need to create a repository on ECR. You can do this by running the following command:
```sh
$ aws ecr create-repository --repository-name myapp
{
"repository": {
"repositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/myapp",
"registryId": "123456789012",
"repositoryName": "myapp",
"repositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp",
"createdAt": 1627981234.0,
"imageTagMutability": "MUTABLE",
"imageScanningConfiguration": {
"scanOnPush": false
}
}
}
```
## Pushing the Docker image to ECR
Now, we can push the Docker image to ECR. You can do this by running the following commands:
-1. Log in to ECR
```sh
$ aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com
```
-2. Tag the Docker image
```sh
$ docker tag myapp:latest 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
```
-3. Push the Docker image to ECR
```sh
$ docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
```
## Creating the main.tf file for Terraform
This is the main Terraform file that will be used to deploy our app on AWS Fargate. It will create the following resources:
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
archive = {
source = "hashicorp/archive"
version = "~> 2.2.0"
}
}
required_version = "~> 1.0"
}
# Configure the AWS Provider
provider "aws" {
region = "us-east-1" // Change this to your region
access_key = "<your access key>" // Change this to your access key
secret_key = "your secret key" // Change this to your secret key
}
resource "aws_ecr_repository" "myapp" {
name = "myapp"
}
resource "aws_ecs_cluster" "myapp_cluster" {
name = "myapp_cluster"
}
resource "aws_cloudwatch_log_group" "myapp" {
name = "/ecs/myapp"
}
resource "aws_ecs_task_definition" "myapp_task" {
family = "myapp-task"
container_definitions = <<DEFINITION
[
{
"name": "myapp-task",
"image": "${aws_ecr_repository.myapp.repository_url}",
"essential": true,
"portMappings": [
{
"containerPort": 5150
}
],
"command": ["start"],
"memory": 512,
"cpu": 256,
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-region": "us-east-2",
"awslogs-group": "/ecs/myapp",
"awslogs-stream-prefix": "ecs"
}
}
}
]
DEFINITION
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
memory = 512
cpu = 256
execution_role_arn = aws_iam_role.ecsTaskExecutionRole.arn
}
resource "aws_iam_role" "ecsTaskExecutionRole" {
name = "ecsTaskExecutionRoleMyapp"
assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json
}
data "aws_iam_policy_document" "assume_role_policy" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ecs-tasks.amazonaws.com"]
}
}
}
resource "aws_iam_role_policy_attachment" "ecsTaskExecutionRole_policy" {
role = aws_iam_role.ecsTaskExecutionRole.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
resource "aws_alb" "myapp" {
name = "myapp-lb"
internal = false
load_balancer_type = "application"
enable_deletion_protection = true
subnets = [
aws_subnet.public_d.id,
aws_subnet.public_e.id,
]
security_groups = [
aws_security_group.http.id,
aws_security_group.https.id,
aws_security_group.egress_all.id,
]
depends_on = [aws_internet_gateway.igw]
}
resource "aws_security_group" "load_balancer_security_group" {
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_lb_target_group" "myapp" {
name = "myapp-tg"
port = 5150
protocol = "HTTP"
target_type = "ip"
vpc_id = aws_vpc.myapp_vpc.id
health_check {
enabled = true
path = "/_health"
matcher = "200,202"
}
depends_on = [aws_alb.myapp]
}
resource "aws_alb_listener" "myapp_http" {
load_balancer_arn = aws_alb.myapp.arn
port = "80"
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
resource "aws_alb_listener" "myapp_https" {
load_balancer_arn = aws_alb.myapp.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08"
certificate_arn = "<your arn for the certificate>" // Change this to your certificate ARN
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.myapp.arn
}
}
output "alb_url" {
value = "https://${aws_alb.myapp.dns_name}"
}
resource "aws_ecs_service" "myapp" {
name = "myapp-service"
cluster = aws_ecs_cluster.myapp_cluster.id
task_definition = aws_ecs_task_definition.myapp_task.arn
launch_type = "FARGATE"
desired_count = 1
load_balancer {
target_group_arn = aws_lb_target_group.myapp.arn
container_name = aws_ecs_task_definition.myapp_task.family
container_port = 5150
}
network_configuration {
assign_public_ip = false
security_groups = [
aws_security_group.egress_all.id,
aws_security_group.ingress_api.id,
]
subnets = [
aws_subnet.private_d.id,
aws_subnet.private_e.id,
]
}
}
resource "aws_security_group" "service_security_group" {
ingress {
from_port = 0
to_port = 0
protocol = "-1"
security_groups = ["${aws_security_group.load_balancer_security_group.id}"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
```
This file will create the following resources:
- An ECR repository for our app
- An ECS cluster for our app
- An ECS task definition for our app
- An ECS service for our app
Now, we need to create a `network.tf` file to define the network configuration for our app. This file will create the following resources:
```hcl
resource "aws_vpc" "myapp_vpc" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "public_d" {
vpc_id = aws_vpc.myapp_vpc.id
cidr_block = "10.0.1.0/25"
availability_zone = "us-east-2a"
tags = {
"Name" = "public | us-east-2a"
}
}
resource "aws_subnet" "private_d" {
vpc_id = aws_vpc.myapp_vpc.id
cidr_block = "10.0.2.0/25"
availability_zone = "us-east-2b"
tags = {
"Name" = "private | us-east-2b"
}
}
resource "aws_subnet" "public_e" {
vpc_id = aws_vpc.myapp_vpc.id
cidr_block = "10.0.1.128/25"
availability_zone = "us-east-2c"
tags = {
"Name" = "public | us-east-2c"
}
}
resource "aws_subnet" "private_e" {
vpc_id = aws_vpc.myapp_vpc.id
cidr_block = "10.0.2.128/25"
availability_zone = "us-east-2c"
tags = {
"Name" = "private | us-east-2c"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.myapp_vpc.id
tags = {
"Name" = "public"
}
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.myapp_vpc.id
tags = {
"Name" = "private"
}
}
resource "aws_route_table_association" "public_d_subnet" {
subnet_id = aws_subnet.public_d.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "private_d_subnet" {
subnet_id = aws_subnet.private_d.id
route_table_id = aws_route_table.private.id
}
resource "aws_route_table_association" "public_e_subnet" {
subnet_id = aws_subnet.public_e.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "private_e_subnet" {
subnet_id = aws_subnet.private_e.id
route_table_id = aws_route_table.private.id
}
resource "aws_eip" "nat" {
vpc = true
}
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.myapp_vpc.id
}
resource "aws_nat_gateway" "ngw" {
subnet_id = aws_subnet.public_d.id
allocation_id = aws_eip.nat.id
depends_on = [aws_internet_gateway.igw]
}
resource "aws_route" "public_igw" {
route_table_id = aws_route_table.public.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
resource "aws_route" "private_ngw" {
route_table_id = aws_route_table.private.id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.ngw.id
}
resource "aws_security_group" "http" {
name = "http"
description = "HTTP traffic"
vpc_id = aws_vpc.myapp_vpc.id
ingress {
from_port = 80
to_port = 80
protocol = "TCP"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group" "https" {
name = "https"
description = "HTTPS traffic"
vpc_id = aws_vpc.myapp_vpc.id
ingress {
from_port = 443
to_port = 443
protocol = "TCP"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group" "egress_all" {
name = "egress-all"
description = "Allow outbound traffic"
vpc_id = aws_vpc.myapp_vpc.id
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group" "ingress_api" {
name = "ingress-api"
description = "Allow ingress to App"
vpc_id = aws_vpc.myapp_vpc.id
ingress {
from_port = 5150
to_port = 5150
protocol = "TCP"
cidr_blocks = ["0.0.0.0/0"]
}
}
```
The network configuration will be responsible for creating all the infrastructure needed to deploy our app on AWS Fargate in terms of networking. I recommend you to read the [AWS Fargate documentation](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html) to understand how it works, also you can read the Terraform documentation for [AWS Fargate](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_definition) and [AWS VPC](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc).
So, now we have the main Terraform file and the network configuration file for our app. We can now deploy our app on AWS Fargate.
## Deploying the app on AWS Fargate
To deploy our app on AWS Fargate, we will need to run the following commands:
-1. Initialize Terraform
```sh
$ terraform init
```
-2. Plan the deployment
```sh
$ terraform plan
```
-3. Apply the deployment
````sh
$ terraform apply
```****
Theses commands will create all the resources we need to deploy our app on AWS Fargate. After running you will see the url from our alb_url output.
```sh
Apply complete! Resources: 20 added, 0 changed, 0 destroyed.
Outputs:
alb_url = https://myapp-lb-1234567890.us-east-2.elb.amazonaws.com
````
Now, we can access our app by going to the url from our alb_url output.
## Conclusion
In this article, we explored how to deploy a Rust app built with loco on AWS Fargate using Terraform. We started by creating a new project and selecting the `Rest API` template. Then, we created the Docker image for our app and pushed it to ECR. Finally, we created the main Terraform file and the network configuration file for our app and deployed it on AWS Fargate.
This approach allows us to deploy our app on AWS Fargate in a fast and reliable way. It also allows us to easily scale our app by adding more instances of it.
@@ -0,0 +1,216 @@
+++
title = "Creating Frontend Website"
description = "Build a REST API quickly with Loco and then follow by building a React frontend app to use it. Learn about generators, configuring asset serving and client-side apps with Loco."
date = 2023-12-14T09:19:42+00:00
updated = 2023-12-14T09:19:42+00:00
draft = false
template = "blog/page.html"
[taxonomies]
authors = ["Team Loco"]
+++
## Overview
This guide provides a comprehensive walkthrough on using `Loco` to build a Todo list application with a REST API and a React frontend. The steps outlined cover everything from project creation to deployment.
Explore the example repository [here](https://github.com/loco-rs/todo-list-example)
The key steps include:
- Creating a Loco project with the SaaS starter
- Setting up a Vite frontend with React
- Configuring Loco to serve frontend static assets
- Implementing the Notes model/controller in the REST API
- Reloading the server and frontend during development
- Deploying the website to production
## Selecting SaaS Starter
To begin, run the following command to create a new Loco app using the SaaS starter:
```sh
& loco new
✔ ❯ App name? · todolist
✔ ❯ What would you like to build? · SaaS app (with DB and user auth)
🚂 Loco app generated successfully in:
/tmp/todolist
```
Follow the prompts to specify the app name (e.g., todolist) and choose the SaaS app option.
After generating the app, ensure you have the necessary resources by running:
```
$ cd todolist
$ cargo loco doctor
✅ SeaORM CLI is installed
✅ DB connection: success
✅ Redis connection: success
```
Verify that SeaORM CLI is installed, and the database and Redis connections are successful. If any resources fail, refer to the [quick tour guide](@/docs/tutorials/your-first-app.md) for troubleshooting.
Once `cargo loco doctor` shows all checks passed, start the server:
```
$ cargo loco start
Updating crates.io index
.
.
.
▄ ▀
▀ ▄
▄ ▀ ▄ ▄ ▄▀
▄ ▀▄▄
▄ ▀ ▀ ▀▄▀█▄
▀█▄
▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█
██████ █████ ███ █████ ███ █████ ███ ▀█
██████ █████ ███ █████ ▀▀▀ █████ ███ ▄█▄
██████ █████ ███ █████ █████ ███ ████▄
██████ █████ ███ █████ ▄▄▄ █████ ███ █████
██████ █████ ███ ████ ███ █████ ███ ████▀
▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ██▀
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
https://loco.rs
environment: development
database: automigrate
logger: debug
modes: server
listening on port 5150
```
## Creating the Frontend
For the frontend, we'll use [Vite](https://vitejs.dev/guide/) with React. In the `todolist` folder, run:
```sh
$ npm create vite@latest
Need to install the following packages:
create-vite@5.1.0
Ok to proceed? (y) y
✔ Project name: … frontend
✔ Select a framework: › React
✔ Select a variant: › JavaScript
```
Follow the prompts to set up the `frontend` as a project name.
Navigate to the frontend folder and install dependencies:
```
$ cd todolist/frontend
$ pnpm install
```
Start the development server:
```sh
$ pnpm dev
```
### Serving Static Assets in Loco
First, move all our rest api endpoint under `/api` prefix. for doing it go to `src/app.rs`. in `routes` hooks function add `.prefix("/api")` to the default routes.
```rust
fn routes() -> AppRoutes {
AppRoutes::with_default_routes()
.prefix("/api")
.add_route(controllers::notes::routes())
}
```
Build the frontend for production:
```sh
pnpm build
```
In the `frontend` folder, a `dist` directory is created. Update the `config/development.yaml` file in the main folder to include a static middleware:
```yaml
server:
middlewares:
static:
enable: true
must_exist: true
folder:
uri: "/"
path: "frontend/dist"
fallback: "frontend/dist/index.html"
```
Now, run the Loco server again and you should see frontend app serving via Loco
```sh
$ cargo loco start
```
If you see the default fallback page, you have to disable the fallback middleware. The default fallback takes priority over the static handler, so no static content will be served if it is enabled. You can disable it like so:
```yaml
server:
middlewares:
fallback:
enable: false
static:
...
```
# Developing the UI
Install `react-router-dom`, `react-query` and `axios`
```sh
$ pnpm install react-router-dom react-query axios
```
1. Copy [main.jsx](https://github.com/loco-rs/todo-list-example/blob/main/frontend/src/main.jsx) to frontend/src/main.jsx.
2. Copy [App.jsx](https://github.com/loco-rs/todo-list-example/blob/main/frontend/src/App.jsx) to frontend/src/App.jsx.
3. Copy [App.css](https://github.com/loco-rs/todo-list-example/blob/main/frontend/src/App.css) to frontend/src/App.css.
Now, run the server `cargo loco start` and the UI pnpm dev in the frontend folder, and start adding your todo list!
## Improve Development
use [cargo-watch](https://crates.io/crates/cargo-watch) for hot reloading the server:
```sh
$ cargo watch --ignore "frontend" -x check -s 'cargo run start'
```
Now, any changes in your Rust code will automatically reload the server, and any changes in your frontend Vite will reload the frontend app.
## Deploy To Production
In the `frontend` folder, run `pnpm build`. After a successful build, go to the Loco server and run `cargo loco start`. Loco will serve the frontend static files directly from the server.
### Prepare Docker Image
Run `cargo loco generate deployment` and select Docker as the deployment type:
```sh
$ cargo loco generate deployment
✔ ❯ Choose your deployment · Docker
added: "Dockerfile"
added: ".dockerignore"
```
Loco will add a `Dockerfile` and a `.dockerignore `file. Note that Loco detect the static assent and included them as part of the image
Build the container:
```sh
$ docker build . -t loco-todo-list
```
Now run the container:
```sh
$ docker run -e LOCO_ENV=production -p 5150:5150 loco-todo-list start
```
@@ -0,0 +1,94 @@
+++
title = "What if Rails was Built on Rust?"
description = "Introducing Loco: a Rails-inspired Rust web framework. See how Rust can be as expressive as Ruby and how we can build a good deal of magic that Rails has with Rust."
date = 2023-11-24T09:19:42+00:00
updated = 2023-11-24T09:19:42+00:00
draft = false
template = "blog/page.html"
[taxonomies]
authors = ["Team Loco"]
+++
<center>
<img width="150" src="/icon.svg"/>
**What if [Rails](https://rubyonrails.org) was built on Rust and not Ruby?**
</center>
Then it would look like this:
```rust
async fn current(
auth: middleware::auth::Auth,
State(ctx): State<AppContext>,
) -> Result<Response> {
let user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?;
format::json(CurrentResponse::new(&user))
}
pub fn routes() -> Routes {
Routes::new().prefix("user").add("/current", get(current))
}
```
## Introducing: Loco
Loco is a Rails inspired web framework for Rust. It inlcudes _almost every Rails feature_ with best-effort Rust ergonomics:
* Controllers and routing via [axum](https://github.com/tokio-rs/axum)
* Models, migration, and ActiveRecord via [SeaORM](https://www.sea-ql.org/SeaORM/)
* Views via [serde](https://serde.rs/json.html)
* Seamless, Background jobs, multi modal: in process, out of process, async via Tokio
* Mailers
* Tasks
* Seeding
* Environment-aware configuration
* Tracing, logging, seamlessly integrated via [tracing](https://docs.rs/tracing)
* Generators via [rrgen](https://github.com/jondot/rrgen)
* Batteries-included authentication (like Rails' `devise`)
* Testing kit, with automatic truncation, fixture seeding, auto migration, snapshotting and redaction
It's full stack for real.
## Why not Rails?
If you're happy with Ruby, use Rails. Don't spend time looking elsewhere because of performance -- Rails and Ruby are good enough.
**But if you love Rust**, you can now build companies like Rubyists have been building for ages -- use Loco.
* You'll get **Rust's safety, strong typing, fantastic concurrency models, and super super stable libraries and ecosystem**. Build once, then forget about it.
* Deployment is copying a **single binary** over to a server.
* You'll be getting **an order of 100,000 requests/sec** without any effort. And 50k requests/sec with database calls. You will never need more than a couple servers. Heck, you can deploy on a Rasberry Pi and be happy..
## The One Person Framework
Inspired by [DHH's approach](https://world.hey.com/dhh/the-one-person-framework-711e6318), Loco's guiding principle is above all:
> The one person framework
From this single guiding principles comes everything else.
For example, one person team, or one person company:
* Has **no time to debate libraries**, tooling, linting rules: strong opinions are welcome. Tell me how I should work.
* **Needs a driving tool** in addition to their brainpower -- that's the Loco CLI. Generate code, operate your project.
* **Needs stability**, anything that breaks is a waste of time, any surprise is a waste of time
* **Needs simplicity** -- don't surprise me
* **Needs a single operability story**. Deploys should be simple. No Kubernetes, no IAC, no preconditions.
* **Needs control**. Send emails and author the emails locally, not on some remote service
* **Needs locality**. Everything that happens in production should first happen in development and locally
* **Needs ad-hocness**. No holy grail ceremonies. Build tasks to run birthday emails to your users, rather than go on a crusade for an "Admin" project.
Loco is the one person framework for **indy hackers, hobbyists, and startups**.
With around **20mb of a deploy binary, and 50k requests/sec** - all you need is a single small/medium server, Postgres or Sqlite and an internet connection. Startups should be cheap!
Get started with [Loco](https://loco.rs) today!
@@ -0,0 +1,22 @@
+++
title = "Dynamic responses and content types"
description = "Learn how to respond to incoming requests with the appropriate content type. Match on the incoming format, and render JSON, HTML or other types of responses."
date = 2024-06-10T09:19:42+00:00
updated = 2024-06-10T09:19:42+00:00
draft = false
template = "casts/page.html"
[taxonomies]
authors = ["Team Loco"]
[extra]
num = "001"
id = "l_hxXsHHSSU"
+++
Reference material for this episode:
* Loco.rs docs: [sending responses](https://loco.rs/docs/the-app/controller/#sending-responses)
* Rails [responders](https://api.rubyonrails.org/v4.1/classes/ActionController/Responder.html)
@@ -0,0 +1,22 @@
+++
title = "Routes and prefixes"
description = "Routes in Loco are derived from how Axum does Routes. Learn how to shape your API and draw your routes, from an individual controller to your global app route set up."
date = 2024-06-13T09:19:42+00:00
updated = 2024-06-13T09:19:42+00:00
draft = false
template = "casts/page.html"
[taxonomies]
authors = ["Team Loco"]
[extra]
num = "002"
id = "IGPE0_ptaHY"
+++
Reference material for this episode:
* Loco.rs docs: [routes in controllers](https://loco.rs/docs/the-app/controller/#routes-in-controllers)
* The [SaaS starter](https://loco.rs/docs/starters/saas/)
@@ -0,0 +1,22 @@
+++
title = "Scaffolding full CRUD with HTML views"
description = "Loco generators are very powerful. Generate a full CRUD app with a single command, by including the main entity type and its set of fields. Loco will generate models, controllers, views, and migrations for you."
date = 2024-06-14T09:19:42+00:00
updated = 2024-06-14T09:19:42+00:00
draft = false
template = "casts/page.html"
[taxonomies]
authors = ["Team Loco"]
[extra]
num = "003"
id = "EircfwF8c0E"
+++
Reference material for this episode:
* Loco.rs docs: [routes in controllers](https://loco.rs/docs/the-app/controller/#routes-in-controllers)
* The [SaaS starter](https://loco.rs/docs/starters/saas/)
* The [REST API starter](https://loco.rs/docs/starters/rest-api/)
@@ -0,0 +1,23 @@
+++
title = "Creating tasks"
description = "Ever reached out to write a small script to reset a user password? send email notifications to your users? You can use Tasks in Loco to write these operational bits in pure Rust and access your full app from your task."
date = 2024-06-18T09:19:42+00:00
updated = 2024-06-18T09:19:42+00:00
draft = false
template = "casts/page.html"
[taxonomies]
authors = ["Team Loco"]
[extra]
num = "004"
id = "gn7Hkq7T9dI"
+++
Reference material for this episode:
* Loco.rs docs: [routes in controllers](https://loco.rs/docs/the-app/task/)
* The [SaaS starter](https://loco.rs/docs/starters/saas/)
* The [REST API starter](https://loco.rs/docs/starters/rest-api/)
* The [Lightweight starter](https://loco.rs/docs/starters/service/)
@@ -0,0 +1,23 @@
+++
title = "Testing tasks"
description = "See how tasks in Loco are a simple linear workflow with access to your full app context, and how to easily test them. You can write a linear business workflow and test it giving it input and asserting its output."
date = 2024-06-18T09:20:42+00:00
updated = 2024-06-18T09:20:42+00:00
draft = false
template = "casts/page.html"
[taxonomies]
authors = ["Team Loco"]
[extra]
num = "005"
id = "485JlLA-T6U"
+++
Reference material for this episode:
* Loco.rs docs: [routes in controllers](https://loco.rs/docs/processing/task/)
* The [SaaS starter](https://loco.rs/docs/getting-started/starters/#saas-starter)
* The [REST API starter](https://loco.rs/docs/getting-started/starters/#rest-api-starter)
* The [Lightweight starter](https://loco.rs/docs/getting-started/starters/#lightweight-service-starter)
@@ -0,0 +1,19 @@
+++
title = "Mailers"
description = "Learn how to send emails from your app. As it turns out, emails are still an important core feature in business apps. You can send emails from multiple types of providers, and enjoy a great developer experience."
date = 2024-06-27T09:20:42+00:00
updated = 2024-06-27T09:20:42+00:00
draft = false
template = "casts/page.html"
[taxonomies]
authors = ["Team Loco"]
[extra]
num = "006"
id = "ieGeihxLGC8"
+++
Reference material for this episode:
* Loco.rs docs: [Mailers](https://loco.rs/docs/processing/mailers/)
@@ -0,0 +1,20 @@
+++
title = "Full CRUD with HTMX scaffold generator"
description = "Learn how to use HTMX with Loco. Using Loco's core generator scaffolding abilities, we added support for generating a set of HTMX powered views, which makes it a joy to build a fullstack UI app."
date = 2024-06-27T14:20:42+00:00
updated = 2024-06-27T14:20:42+00:00
draft = false
template = "casts/page.html"
[taxonomies]
authors = ["Team Loco"]
[extra]
num = "007"
id = "OWUvUSC1KvY"
+++
Reference material for this episode:
* Loco.rs docs: [Views](https://loco.rs/docs/the-app/views/)
* HTMX [website](https://htmx.org/)
@@ -0,0 +1,7 @@
+++
title = "Loco Casts"
description = "Loco Casts"
sort_by = "date"
paginate_by = 10
template = "casts/section.html"
+++
@@ -0,0 +1,7 @@
+++
title = "Docs"
description = "Docs for loco"
sort_by = "weight"
weight = 1
template = "docs/section.html"
+++
@@ -0,0 +1,8 @@
+++
title = "Explanation"
description = "Understanding-oriented discussion of how Loco works and why it is designed the way it is."
template = "docs/section.html"
sort_by = "weight"
weight = 4
draft = false
+++
@@ -0,0 +1,93 @@
+++
title = "AppContext and dependency injection"
description = "Why AppContext is the one piece of shared state every handler sees, and how SharedStore lets you extend it without forking the framework."
date = 2026-07-03T00:00:00+00:00
updated = 2026-07-03T00:00:00+00:00
draft = false
weight = 3
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
Rust doesn't let you reach for a mutable global app instance the way Rails or Django can — there's no ambient `current_app` to mutate from anywhere. Loco's answer to "how does a handler get at the database, the cache, the mailer, my own service client?" is a single, cheaply-cloneable struct threaded through the whole app: `AppContext`. This page explains why that struct has the shape it does, and how `SharedStore` extends it to things Loco itself doesn't know about.
## `AppContext` as the one shared-state object
Every handler, background worker, task, and scheduled job in a Loco app receives the same `AppContext` value — assembled once at boot (see [Architecture](@/docs/explanation/architecture.md)) and cloned cheaply wherever it's needed, because most of its fields are already `Arc<...>`-wrapped or otherwise cheap to clone. It carries eight fields:
```rust
#[derive(Clone, FromRef)]
pub struct AppContext {
pub environment: Environment,
#[cfg(feature = "with-db")]
pub db: DatabaseConnection,
pub queue_provider: Option<Arc<bgworker::Queue>>,
pub config: Config,
pub mailer: Option<EmailSender>,
pub storage: Arc<Storage>,
pub cache: Arc<cache::Cache>,
pub shared_store: Arc<SharedStore>,
}
```
The full field-by-field reference — types, feature gates, purpose — lives in [AppContext & prelude](@/docs/reference/app-context.md). What's worth explaining here is the *design*, not the field list:
- **One struct, not seven services.** Rather than injecting the DB pool, the cache, the mailer, storage, and the queue as five separate pieces of Axum state, Loco bundles them into one `AppContext` and derives `FromRef` on it. That derive is what lets a handler ask for exactly the piece it needs — `State<DatabaseConnection>` or `State<Arc<cache::Cache>>` — while a background worker or the boot sequence can still ask for the whole thing. You get the ergonomics of narrow, single-purpose extraction without the boilerplate of hand-writing `FromRef` impls for every field.
- **`db` is the only field that's compiled away, not just empty.** Every other field degrades gracefully when unconfigured (`None` for `mailer`/`queue_provider`, a `Null` driver for `cache`/`storage`) — an app with no mailer configured still has a `mailer: Option<EmailSender>` field, just set to `None`. `db` is different: with the `with-db` feature off, the field doesn't exist on the struct at all, which is a compile-time way of saying "this deployment shape genuinely has no database," rather than a runtime `Option` a caller could forget to check.
- **It's the same value everywhere.** Because `create_context` builds `AppContext` exactly once at boot and every subsystem downstream — routing, middleware, handlers, `connect_workers`, tasks, the scheduler — receives that same value, there's no risk of a handler and a background job disagreeing about which DB pool or cache instance is "the real one." This is also why `Hooks::after_context(ctx: AppContext) -> Result<AppContext>` (which runs immediately after assembly, before routes are built) is the one hook that can rewrite the context itself — it's your last and only chance to add something to it before it's handed out everywhere.
## The gap `AppContext`'s fixed fields can't fill
`AppContext`'s eight fields cover what *every* Loco app needs. They obviously can't cover what *your* app needs — a third-party API client, a feature-flag SDK handle, an app-specific cache of precomputed data. Two options exist, and they aren't in tension, they're the same design carried into two different lifecycles:
- **`Initializer`** (see [Add middleware](@/docs/how-to/add-middleware.md)) is the *install-time* extension point: a trait with `before_run`, `after_routes`, and `check` hooks, used to wire a whole piece of infrastructure into the app (register an Axum `Extension`, mount a session layer, install a doctor health check).
- **`SharedStore`** is the *storage* extension point: a place to actually hold a value of a type Loco has never heard of, so it can be read back out in a handler, a worker, or anywhere else `AppContext` reaches.
In practice they compose: you typically construct the value you want to share and call `ctx.shared_store.insert(..)` from inside `Hooks::after_context` (the same hook that runs once, right after the context is built), then read it back with the extractor below.
## `SharedStore`: a type-keyed DI container
`AppContext.shared_store: Arc<SharedStore>` is a small, concurrent, heterogeneous store — internally a `DashMap` keyed by `TypeId`, so it can hold one value of any number of distinct `'static + Send + Sync` types at once. Its API is deliberately minimal:
| Method | What it does |
|---|---|
| `insert<T>(&self, val: T)` | Store (or overwrite) the value for type `T`. |
| `get<T: Clone>(&self) -> Option<T>` | Fetch a *cloned* copy of `T`, if present. |
| `get_ref<T>(&self) -> Option<RefGuard<'_, T>>` | Fetch a borrowed `Deref<Target = T>` guard — for types that aren't `Clone`, or when a clone would be wasteful. |
| `remove<T>(&self) -> Option<T>` | Take the value back out. |
| `contains<T>(&self) -> bool` | Check presence without touching the value. |
### Reading it back: two paths, and a naming hazard to watch for
There are two distinct `SharedStore` types reachable from `loco_rs::prelude`, and confusing them is the single easiest mistake to make with this feature:
- **`loco_rs::app::SharedStore`** — the container type above, held as `ctx.shared_store`.
- **`loco_rs::controller::extractor::shared_store::SharedStore<T>(pub T)`** — an Axum `FromRequestParts<AppContext>` *extractor*, also re-exported as `SharedStore` from the prelude, that reaches into `ctx.shared_store`, clones out a `T`, and hands it to your handler as a plain argument:
```rust
#[debug_handler]
pub async fn index(
SharedStore(service): SharedStore<MyClonableService>,
) -> impl IntoResponse {
tracing::info!("api key: {}", service.api_key);
format::empty()
}
```
If `T` was never inserted, the extractor rejects the request with `Error::InternalServerError` — which is the right failure mode for "the app forgot to wire something up," as opposed to a client-facing 4xx. For a type that isn't `Clone`, skip the extractor and reach for `ctx.shared_store.get_ref::<T>()` directly off an ordinary `State<AppContext>` extraction instead — you get a reference-counted guard rather than a copy.
## Why this shape, instead of a global registry
The alternative designs are familiar from other ecosystems — a service locator singleton, or a compile-time DI container that resolves a dependency graph. Loco deliberately avoids both:
- A **global mutable singleton** isn't something safe Rust gives you for free, and reaching for `unsafe`/`OnceCell`-style globals to fake one would undermine the exact guarantee (no data races, no invisible mutation from anywhere) that makes Rust worth using for a server in the first place.
- A **compile-time DI framework** (resolving constructor graphs, macro-generated wiring) adds a second configuration language on top of Rust itself, for a problem `AppContext` plus `SharedStore` already solves at the cost of one `insert`/`get` pair.
`SharedStore` is intentionally closer to "a typed, thread-safe `HashMap<TypeId, Box<dyn Any>>` you're handed for free" than a general DI framework — it doesn't manage lifecycles, doesn't resolve dependencies between the things you store, and doesn't enforce a registration order. That's the trade: less power, but no new mental model to learn, and it composes with ordinary Rust ownership rather than working around it. For most apps, "stash a client in `after_context`, extract it with `SharedStore<T>`" is the entire pattern.
See the [AppContext & prelude reference](@/docs/reference/app-context.md) for the exhaustive field/method signatures, and [Add middleware](@/docs/how-to/add-middleware.md) for a related worked example (a custom `MiddlewareLayer`/`Initializer`-style extension wired into the app).
@@ -0,0 +1,120 @@
+++
title = "Architecture: the request lifecycle"
description = "How a Loco app boots, how AppContext gets built, and how a request travels through routes, middleware, and back out — and why the middleware order is LIFO."
date = 2026-07-03T00:00:00+00:00
updated = 2026-07-03T00:00:00+00:00
draft = false
weight = 2
sort_by = "weight"
template = "docs/page.html"
aliases = ["/docs/the-app/your-project/"]
[extra]
lead = ""
toc = true
top = false
+++
A Loco app has two distinct timelines that are worth keeping separate in your head: **boot** (runs once, assembles everything the app needs) and **request handling** (runs per HTTP request, through a fixed pipeline). This page walks both, and explains the one piece of ordering that surprises almost everyone the first time: middleware runs in the *reverse* of the order you list it in.
## Boot: from `StartMode` to a running app
Everything starts from `src/boot.rs`, driven by the `Hooks` trait your `App` implements (the exhaustive method-by-method reference is [Hooks trait](@/docs/reference/hooks.md)). At a high level:
```text
cargo loco start
│
▼
Hooks::load_config(env) → Config (default: env.load())
│
▼
create_context::<App>(env, config) → AppContext (db, mailer, queue, cache, storage wired up)
│ Hooks::after_context(ctx) can rewrite ctx here
▼
db::converge + bgworker::converge (migrations / queue setup, if applicable)
│
▼
run_app::<App>(mode, ctx) → BootResult
│ ├─ Hooks::before_run(&ctx)
│ ├─ Hooks::initializers(&ctx) → before_run() on each Initializer
│ ├─ Hooks::routes(&ctx) → AppRoutes
│ ├─ Hooks::before_routes / after_routes(router, &ctx)
│ ├─ Hooks::middlewares(&ctx) → Vec<Box<dyn MiddlewareLayer>>
│ └─ after_routes() on each Initializer
▼
start::<App>(boot, server_config) → binds the socket, spawns the scheduler if requested,
calls Hooks::serve(...), prints the banner
```
Each `Hooks` method in that chain has a sensible default (see the reference for the exact signatures and defaults) — a minimal `App` only needs to implement `app_name`, `boot`, `routes`, `connect_workers`, `register_tasks`, and (with a database) `truncate`/`seed`. Everything else — logging setup, config loading, the middleware stack, initializer wiring — is a provided method you override only when you need to change it.
### `StartMode`: what actually runs in this process
`boot()` receives a `StartMode` that determines which of the app's subsystems are live in *this* process:
| Mode | Server | Worker | Scheduler |
|---|---|---|---|
| `ServerOnly` | yes | no | no |
| `ServerAndWorker` | yes | yes (same process) | no |
| `ServerAndScheduler` | yes | no | yes |
| `WorkerOnly { tags }` | no | yes, filtered by tag | no |
| `WorkerAndScheduler { tags }` | no | yes, filtered by tag | yes |
| `All` | yes | yes | yes |
`StartMode` exists because "the web server" and "the thing that drains the job queue" don't have to be the same OS process — in fact for anything beyond a single-dyno deployment you usually *want* them separate, so you can scale workers and the HTTP tier independently. `cargo loco start --worker`, `--server-and-worker`, `--scheduler`, and `--all` map directly onto these variants (`cargo loco start` alone is `ServerOnly`). The worker only actually runs if `workers.mode` in config is `BackgroundQueue` — see [The background-processing model](@/docs/explanation/background-processing-model.md) for why.
### `AppContext` is assembled once, here
`create_context` is the one place `AppContext`'s eight fields (`environment`, `db`, `queue_provider`, `config`, `mailer`, `storage`, `cache`, `shared_store`) get their real values, before `Hooks::after_context` gets a final chance to post-process the struct (e.g. to stash a custom service into `shared_store`). Everything downstream — routing, middleware, handlers, background workers, tasks, the scheduler — receives the *same* `AppContext` value (it's cheaply `Clone`), which is why it's the natural place to reach for shared state. See [AppContext and dependency injection](@/docs/explanation/appcontext-and-di.md) for the full story on that struct and its `shared_store` extensibility slot.
## Request handling: the onion
Once boot finishes, `AppRoutes::to_router` has compiled your routes plus the middleware stack into one real `axum::Router<AppContext>`. A request's journey through it looks like this:
```text
inbound request
│
▼
┌───────────────────────────────────────────┐
│ powered_by (outermost — first) │
│ ┌────────────────────────────────────┐ │
│ │ fallback (non-prod) │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ request_id │ │ │
│ │ │ ┌────────────────────────┐ │ │ │
│ │ │ │ logger │ │ │ │
│ │ │ │ ┌──────────────────┐ │ │ │ │
│ │ │ │ │ … cors, etag, │ │ │ │ │
│ │ │ │ │ compression … │ │ │ │ │
│ │ │ │ │ ┌────────────┐ │ │ │ │ │
│ │ │ │ │ │limit_payload│ │ │ │ │ │
│ │ │ │ │ │ (innermost)│ │ │ │ │ │
│ │ │ │ │ │ ┌──────┐ │ │ │ │ │ │
│ │ │ │ │ │ │handler│ │ │ │ │ │ │
│ │ │ │ │ │ └──────┘ │ │ │ │ │ │
│ │ │ │ │ └────────────┘ │ │ │ │ │
│ │ │ │ └──────────────────┘ │ │ │ │
│ │ │ └────────────────────────┘ │ │ │
│ │ └──────────────────────────────┘ │ │
│ └────────────────────────────────────┘ │
└───────────────────────────────────────────┘
│
▼
response, unwinding back out the same layers
```
### Why the order is LIFO
`AppRoutes::to_router` builds this onion by calling `app.layer(...)` once per middleware, in the order `default_middleware_stack` lists them (`limit_payload` first, `powered_by` last). Axum's `Router::layer` wraps the *existing* router with each new layer as the new **outermost** layer. The consequence, stated directly in the framework's own source comment:
> "the LAST middleware is the FIRST to meet the outside world (a user request starting), or 'LIFO' order" — `src/controller/app_routes.rs`
So the coding/config order and the runtime order are opposites: routes are added first (they become the innermost core of the onion — the thing every layer eventually wraps), and the *last* middleware added (`powered_by`, at the bottom of the default list) is the *first* thing an inbound request actually passes through. `request_id` is deliberately near the end of the list (so it's near the *outside* at runtime) precisely because every request needs its ID assigned as early in its life as possible.
This matters practically whenever you reach for `Routes::layer(...)` to attach a `tower::Layer` to one controller, or override `Hooks::middlewares` to reorder the default stack — get the direction backwards and a middleware that's supposed to run before authentication ends up running after it. The full ordered list, with each middleware's config key and default-enabled state, is in the [middleware catalog reference](@/docs/reference/middleware.md); [Add middleware § 5](@/docs/how-to/add-middleware.md#5-write-a-custom-middleware) shows how to hand-write a `tower::Layer` middleware of your own that participates in the same onion.
## Where this leaves the handler
By the time your handler runs, it's just a normal Axum handler function taking normal Axum extractors (`State<AppContext>`, `Json<T>`, `Path<T>`, and so on — nothing Loco-specific is required). The response side of the onion is symmetric: your `impl IntoResponse` (or Loco's `format::` helpers) produces a `Response`, which then unwinds back out through the same middleware stack in reverse, each layer getting a chance to post-process it (compression, headers, logging the outcome) before it leaves the process.
For the mechanics of how routes get their axum `Router<AppContext>` shape (`AppRoutes`, `Routes`, prefixing, nesting) see [Add a controller](@/docs/how-to/add-controller.md); for how this whole model maps onto plain Axum concepts you already know, see [Coming from Axum](@/docs/explanation/coming-from-axum.md).
@@ -0,0 +1,81 @@
+++
title = "The background-processing model"
description = "Why perform_later works unmodified against Redis, Postgres, or SQLite, how the shared Driver trait keeps the two SQL backends in lockstep, and what priority and worker modes buy you."
date = 2026-07-03T00:00:00+00:00
updated = 2026-07-03T00:00:00+00:00
draft = false
weight = 5
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
Loco lets you write one `BackgroundWorker` implementation and one `perform_later` call site, then choose — by config, not by code change — whether jobs are durably queued in Redis, Postgres, or SQLite, or not durably queued at all. This page explains the design that makes that swap safe, not the step-by-step of adding a worker (that's [Add a background worker](@/docs/how-to/add-worker.md)) or the exhaustive config keys (that's the [Configuration reference](@/docs/reference/configuration.md#queue) and [Choose a queue backend](@/docs/how-to/choose-queue-backend.md)).
## One trait, one call site, three backends
```rust
#[async_trait]
pub trait BackgroundWorker<A> {
fn build(ctx: &AppContext) -> Self;
async fn perform(&self, args: A) -> Result<()>;
// + queue(), tags(), class_name(), perform_later(), perform_later_with_priority()
}
```
You implement `perform`, register the worker in `connect_workers`, and enqueue work with `MyWorker::perform_later(&ctx, args).await?`. Nothing in that call references which backend is active — that's decided entirely by `queue.kind` in config (`Redis` | `Postgres` | `Sqlite`), read at boot by `create_queue_provider`. This is the same "config over code" bias covered in [Why batteries included](@/docs/explanation/why-batteries-included.md), applied to durability and delivery semantics: swapping backends is an operational decision (what's already running in your infrastructure, what latency/throughput profile you need), not a rewrite.
`perform_later` (and its sibling `perform_later_with_priority`) returns `Result<String>` — the job's id — rather than `Result<()>`. That return value matters because it's what you'd hand to `cargo loco jobs cancel`/`requeue` or log for later correlation; treat any `perform_later` call site that discards its return value as intentionally choosing not to track the job, not as the only option.
## The two SQL backends share one implementation
Postgres and SQLite queueing used to be two independent, parallel implementations that had to be kept in sync by hand. As of the 1.0 line they're de-duplicated behind one internal `Driver` trait:
```rust
pub(crate) trait Driver {
type Pool;
fn idle_count(&self) -> ...;
async fn dequeue(pool: &Self::Pool, tags: &[String]) -> ...;
async fn complete_job(pool: &Self::Pool, id: ..., interval: ...) -> ...;
async fn fail_job(pool: &Self::Pool, id: ..., error: ...) -> ...;
}
```
The `Job` model, the polling/registration loop (`JobRegistry`), panic-catching around `perform`, and the run-loop machinery all live once, generic over `Driver`. `PgDriver` and `SqliteDriver` only need to supply the three DB operations above plus a pool type — everything else (worker registration, tag filtering, graceful cancellation) is shared code, not two copies that can drift. This is why Postgres and SQLite have identical *behavior* (same admin operations, same priority semantics, same job lifecycle) even though the underlying SQL is necessarily different — one uses `FOR UPDATE SKIP LOCKED` for concurrent dequeue, the other simulates it with a lock table since SQLite has no equivalent. Redis, being architecturally different (no SQL, no row locking), keeps its own independent run loop rather than implementing `Driver` — but is still held to the same external contract (the same `Queue` API, the same job lifecycle, the same admin operations) from the outside.
That shared contract is what lets `cargo loco jobs cancel|tidy|purge|dump|import|requeue` work identically regardless of which backend is configured — including Redis, which historically lagged the SQL backends on admin-operation support but is now at parity.
## Priority: one semantic, three storage strategies
All three backends dequeue by priority first, then by age: a higher `i32` priority value is more urgent, ties break by earlier `run_at`, then by a stable job id. How each backend *stores* that ordering differs with its storage model, which is worth understanding since it explains the backends' relative strengths:
- **Postgres / SQLite** add a `priority` column and an `ORDER BY priority DESC, run_at, id` on dequeue (existing pre-1.0 tables are auto-migrated to add the column). This is a natural fit for a row store with a query planner.
- **Redis** has no query planner to lean on, so priority is encoded structurally: jobs live in a sorted set (ZSET) scored by *negative* priority, so the highest-priority job sorts first under `ZRANGE`'s ascending order — with `run_at`/id used as an explicit tie-break in the dequeue logic, since the score alone can't carry three levels of ordering.
Redis additionally supports **named queues** (`queue.queues: [high, low, ...]`, first = most important) with two independent workers backed by the default `["default", "mailer"]` queues, and a `Worker::queue()` override to route a specific worker's jobs into one. This is a coarser-grained tool than per-job priority — named queues partition *which pool of workers* picks up a job, while `priority` decides ordering *within* that pool — and the two compose (a named queue can still be priority-ordered internally).
## Worker modes: trading durability for simplicity
`workers.mode` is a separate axis from the queue backend — it decides whether a persistent queue is even in the picture:
| Mode | Durable across restarts? | Where jobs run | Typical use |
|---|---|---|---|
| `BackgroundQueue` (default) | yes | a separate worker process/thread, dequeuing from the configured `queue:` backend | production |
| `ForegroundBlocking` | n/a — runs inline | the calling request/task, synchronously | tests, where you want deterministic execution before asserting on side effects |
| `BackgroundAsync` | no — lost on crash | `tokio::spawn` in the same process | low-stakes, best-effort work where standing up a queue backend isn't worth it |
The reason this is a mode switch rather than a code difference is the same reason the backend is a config switch: `perform_later` and `perform` don't change, so a worker written and tested under `ForegroundBlocking` behaves identically once the app is switched to `BackgroundQueue` in production — the only thing that changes is *when* and *where* `perform` actually executes, not its logic.
## Choosing a backend
There's no universally correct choice — the three backends trade off along real infrastructure axes:
- **Redis** — lowest latency, named/priority queues, no schema to manage; the right default if Redis is already part of your stack.
- **Postgres** — no new infrastructure if your app's primary database is already Postgres, and `FOR UPDATE SKIP LOCKED` gives solid concurrent-worker throughput.
- **SQLite** — zero extra infrastructure at all, good for small deployments or local development; the lock-table fallback for concurrency makes it less suited to a large number of concurrent workers than the other two.
See [Choose a queue backend](@/docs/how-to/choose-queue-backend.md) for the concrete config for each, and the [Configuration reference](@/docs/reference/configuration.md#queue) for every field and its default.
@@ -0,0 +1,60 @@
+++
title = "Coming from Axum"
description = "Loco is Axum 0.8 with pre-wired decisions on top, not a replacement for it — how extractors, State, and the Router map across, and what Loco actually adds."
date = 2026-07-03T00:00:00+00:00
updated = 2026-07-03T00:00:00+00:00
draft = false
weight = 7
sort_by = "weight"
template = "docs/page.html"
aliases = ["/docs/getting-started/axum-users/"]
[extra]
lead = ""
toc = true
top = false
+++
If you already know [Axum](https://crates.io/crates/axum), you already know most of Loco — the framework compiles down to a real `axum::Router<AppContext>`, uses the same `FromRequestParts`/`FromRequest` extractor model, and pins Axum 0.8. This page is about the delta: what Loco pre-wires on top, and how the concepts you already have a mental model for (extractors, `State`, the `Router`) map onto Loco's names for the same things, plus the mechanics of moving a real Axum codebase over; for the request lifecycle these concepts sit inside, see [Architecture](@/docs/explanation/architecture.md).
## The core claim: nothing is hidden, a lot is pre-decided
Loco is not a new web framework with an Axum-shaped API — it *is* Axum, with a layer of default decisions and a `Hooks` trait that assembles them consistently across every app that uses it. Every extractor you already know still works unmodified; the state type is just a specific struct (`AppContext`) instead of whatever ad-hoc struct you'd have hand-rolled; and the middleware you'd have written as `tower::Layer`/`Service` impls yourself either already exists as a config-toggleable built-in, or you write it exactly the way you would in plain Axum and attach it the same way. [Why batteries included](@/docs/explanation/why-batteries-included.md) covers the philosophy; this page covers the mechanical mapping.
## Concept mapping
| Axum concept | Loco equivalent | What changed |
|---|---|---|
| Your own `main()` assembling the router, state, and `axum::serve(...)` | `Hooks::boot` → `create_app`/`create_context`, `Hooks::serve` (default calls `axum::serve` for you) | You describe *what* to wire (routes, workers, tasks) via `Hooks`; Loco's boot sequence (see [Architecture](@/docs/explanation/architecture.md)) does the assembling. `cargo loco start` replaces a hand-written `main.rs` entirely — a generated app doesn't need one. |
| A hand-rolled `ApiContext` struct + `AddExtensionLayer`/`State` | `AppContext` (8 fields: `environment`, `db`, `queue_provider`, `config`, `mailer`, `storage`, `cache`, `shared_store`), `#[derive(FromRef)]` | Same idea (one struct, threaded as Axum `State`) but pre-built with the pieces almost every service needs, and `FromRef`-derived so you can extract a single field (`State<DatabaseConnection>`) instead of always the whole context. See [AppContext and dependency injection](@/docs/explanation/appcontext-and-di.md). |
| `Router::new().route("/x", get(handler))` | `Routes::new().add("/x", get(handler))`, collected into `AppRoutes` | `Routes`/`AppRoutes` are a thin builder over the same `MethodRouter`/`Router` types — `add`, `prefix`, `nest_route`, `merge`, and `layer` all compile down to the Axum calls you'd write by hand, plus route metadata (`cargo loco routes` listing) that a plain Axum `Router` can't give you back. |
| `.layer(SomeTowerLayer::new())` on a router or route | `Routes::layer(..)` (per-route) or `Hooks::middlewares` (app-wide) | Identical `tower::Layer`/`Service` code — Loco doesn't wrap or reinterpret Tower's traits. The app-wide default stack (CORS, compression, timeouts, etc.) is config-toggled rather than hand-attached; see the [middleware catalog](@/docs/reference/middleware.md) and its LIFO ordering note in [Architecture](@/docs/explanation/architecture.md#why-the-order-is-lifo). |
| `Extension<T>` / custom `FromRequestParts` impls for app-specific data | `State<AppContext>` field extraction, or `SharedStore<T>` for anything not already a field | See [AppContext and dependency injection](@/docs/explanation/appcontext-and-di.md) for when to reach for which. |
| `dotenv` + manual env var parsing in `main` | Typed `Config` loaded from `config/{env}.yaml`, Tera's `get_env` for env-var interpolation | See [The configuration model](@/docs/explanation/configuration-model.md). |
| `env_logger`/`tracing_subscriber` set up by hand | `logger::init` (default), or return `Ok(true)` from `Hooks::init_logger` to opt out entirely and own it yourself | Loco's default filters out third-party log noise you didn't ask for while still using plain `tracing` underneath — any crate emitting `tracing` events shows up the same way it would under a hand-rolled subscriber. |
| A router you already have, that you don't want to restructure | Return it untouched from `Hooks::before_routes`/`after_routes` | This is the literal drop-in path: mount an existing `axum::Router` as-is and keep every extractor and handler signature you already wrote. |
## What "drop-in compatible" actually buys you
Because routing metadata is optional rather than mandatory, you have a genuine choice at the boundary between "paste in existing Axum code" and "get Loco's introspection for free":
- Return your existing router verbatim from `after_routes(router, _ctx)` — zero changes to handler signatures, zero changes to how routes are declared, full compatibility, but `cargo loco routes` won't know about those routes (Axum doesn't expose method/path metadata off a live `Router`, which is precisely the gap `Routes`/`AppRoutes` exist to close).
- Rewrite route declarations from `Router::new().route(path, method(handler))` to `Routes::new().add(path, method(handler))` — this is a mechanical, same-shape edit (the handler itself, its extractors, and its return type are untouched) — and get route listing, prefixing/nesting helpers, and per-route `tower::Layer` attachment (`Routes::layer`) back.
Most apps end up doing the second for their own controllers and the first only for vendored or generated routers they don't want to touch.
## What Loco adds that plain Axum genuinely doesn't have
Everything in the mapping table above is a *reframing* of something Axum already gives you. The following are not reframings — they're capabilities with no direct Axum equivalent, because they're above the HTTP layer:
- A background-job system (`BackgroundWorker`, three interchangeable queue backends) — see [The background-processing model](@/docs/explanation/background-processing-model.md).
- A cron-like scheduler and a CLI task runner, both driven from the same app.
- Database access via Sea-ORM with a generator that scaffolds models/migrations from a field-type DSL.
- A `cargo loco` CLI: `routes`, `middleware --config`, `doctor`, `jobs`, `db`, `generate`.
- Structured JWT/API-key auth extractors, a cache abstraction, and a multi-driver storage abstraction, each swappable by config rather than by code change.
These are the parts of "batteries included" that live outside the request/response cycle Axum itself models — which is also why they're covered by their own pages in this cluster rather than in this one's extractor/router mapping.
## A note on versions
Loco 1.0 tracks Axum 0.8 and targets Rust edition 2024 (the `loco-rs` crate itself; apps generated by `loco new` currently still default to edition 2021 in their own `Cargo.toml` — bump it yourself if you want 2024-edition semantics, such as the `unsafe`-required `std::env::set_var`, in your own app code). If you're moving code from an Axum service built against an older Axum release, the usual Axum 0.7→0.8 migration notes apply on top of everything above — nothing here changes because of Loco.
@@ -0,0 +1,69 @@
+++
title = "The configuration model"
description = "How Loco resolves an environment, which config file wins, why YAML is rendered through Tera first, and how secrets flow in without a dedicated vault type."
date = 2026-07-03T00:00:00+00:00
updated = 2026-07-03T00:00:00+00:00
draft = false
weight = 4
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
Every subsystem described elsewhere in this cluster — the DB pool, the queue backend, the cache, the mailer, the middleware stack — is switched on and tuned from one place: a per-environment YAML file, deserialized into one typed `Config` struct. This page explains the small number of rules that govern how that file is found, rendered, and trusted with secrets. For the exhaustive key-by-key listing, see the [Configuration reference](@/docs/reference/configuration.md).
## Why a typed config struct, not ad-hoc env vars
The alternative most hand-rolled Axum services fall into is reading a scatter of environment variables (`DATABASE_URL`, `PORT`, `RUST_LOG`, ...) directly in `main()`, each parsed and defaulted slightly differently, with no single place that shows what the app's full configuration surface even is. Loco instead deserializes the whole environment file into one `Config` struct (`src/config/mod.rs`), with every sub-area — `server`, `database`, `logger`, `queue`, `cache`, `mailer`, `auth`, `workers` — as a typed field with `serde` defaults where a sane one exists and a hard requirement (a missing-field deserialize error at boot) where there isn't one. This is the same "prefer a built-in over hand-wiring" bias covered in [Why batteries included](@/docs/explanation/why-batteries-included.md), applied specifically to app configuration: you get one document that *is* the app's configuration surface, checked at boot rather than discovered at the call site that happens to read an env var.
## Which environment, and which file
Two independent questions get resolved before any YAML is even opened:
**Which environment name?** `environment::resolve_from_env()` checks, in order:
1. `LOCO_ENV`
2. `RAILS_ENV`
3. `NODE_ENV`
4. falls back to `"development"`
The `RAILS_ENV`/`NODE_ENV` fallbacks exist so that a Loco app dropped into infrastructure already standardized on Rails- or Node-style environment naming doesn't need a separate variable just for Loco.
**Which file, for that environment name?** `Config::from_folder` picks the *first* file that exists, in this order:
1. `{env}.local.yaml`
2. `{env}.yaml`
If neither exists, boot fails outright with "no configuration file found." The `.local.yaml` tier is the mechanism for machine-local overrides — a developer's own DB credentials, a locally-running service's port — that should never be checked into version control alongside the shared `{env}.yaml`. It's a convention, not a special format: `development.local.yaml` is parsed with exactly the same rules as `development.yaml`, it's just consulted first and expected to be gitignored.
Both tiers are read from a `config/` folder by default; `LOCO_CONFIG_FOLDER` overrides that location, which matters for deployments that mount configuration from somewhere other than the app's own source tree.
## The YAML is a Tera template first, a config file second
Before `serde_yaml` ever sees the file, its entire contents are rendered as a [Tera](https://keats.github.io/tera/) template (`Tera::one_off(.., autoescape = false)`). This is a small design choice with a real consequence: it's what makes patterns like this legal inside a Loco config file at all —
```yaml
server:
port: {{ get_env(name="NODE_PORT", default=5150) }}
```
`get_env(name=.., default=..)` here is **Tera's own built-in function**, not something Loco registers. Loco doesn't have a custom templating layer bolted onto YAML — it reuses a general-purpose template engine's existing capability (reading env vars, with a default) so that config files can be static-looking YAML *and* environment-aware at the same time, without inventing a second interpolation syntax. Anything else Tera can do in a one-off render (conditionals, other built-in functions) is available in a config file too, though `get_env` covers the overwhelming majority of real use.
The practical implication: a config value that looks hardcoded may not be — always check for `{{ }}` before assuming a YAML value is literal — and a value that needs to differ between "what's checked into git" and "what's true on this machine/host" belongs behind `get_env`, not behind a second config file.
## Secrets: a convention, not a vault type
There is no dedicated `Secret` type or vault integration built into `Config`. A JWT secret, an SMTP password, a database URI — these are all just `String` fields on ordinary config structs (`auth.jwt.secret`, `mailer.smtp.auth.password`, `database.uri`). The secrets *model* is the composition of two things already covered above:
- Secret values are injected via `get_env(name=..)` at render time, so the checked-in YAML never contains the literal secret — only the name of the environment variable to read it from.
- Machine-local secrets that shouldn't even have an env-var name in shared code can go in `{env}.local.yaml` instead, which is expected to be gitignored entirely.
This is deliberately unopinionated about *where* the environment variable itself comes from — a `.env` file, a process manager, a secrets manager injecting env vars at container start — because that's an operational concern outside the framework's scope, and Loco's contract stops at "a `String` field, populated from `get_env` or a local override file." One consequence worth knowing in advance: `auth.jwt.secret` specifically is expected to be valid **base64** (it's fed to `jsonwebtoken`'s `from_base64_secret` constructors) — a plain passphrase string will fail at the point the JWT extractor tries to decode it, not at config-load time.
## What this buys you day to day
Put together, the model gives you: one typed document per environment describing the whole app, a predictable override tier for anything machine-specific, and a templating escape hatch for anything environment-dependent — all without a second configuration DSL or a runtime service to stand up just to manage config. Changing a pool size, flipping a middleware on, or pointing at a different queue backend (see [The background-processing model](@/docs/explanation/background-processing-model.md)) is a YAML edit and a restart, not a recompile — the same "config, not code" theme that runs through the rest of Loco's built-ins. See the [Configuration reference](@/docs/reference/configuration.md) for every key, type, and default across every sub-config struct.
@@ -0,0 +1,59 @@
+++
title = "Views and assets"
description = "SSR with Tera vs. serving a SPA vs. embedding everything into the binary — and how one feature flag coordinates a swap across two subsystems at once."
date = 2026-07-03T00:00:00+00:00
updated = 2026-07-03T00:00:00+00:00
draft = false
weight = 6
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
"How does this response get to the browser" has three different shapes in a Loco app — server-rendered HTML, a JSON API behind a separately-built SPA, or a fully embedded single binary — and Loco lets you pick without changing how controllers work. This page explains the model behind that choice. For the how-to of writing a specific view or wiring the static middleware, see [Render server-side views](@/docs/how-to/render-views.md); for the exhaustive middleware/config keys, see the [middleware catalog](@/docs/reference/middleware.md) and [feature flags reference](@/docs/reference/feature-flags.md).
## The separation controllers don't have to care about
Loco keeps the traditional split of responsibilities — a controller parses the request and calls into models; a *view* is responsible only for shaping the final response — and makes that split concrete with one trait:
```rust
pub trait ViewRenderer {
fn render<S: Serialize>(&self, key: &str, data: S) -> Result<String>;
}
```
A controller never talks to Tera, or to any specific templating engine, directly — it takes a `v: impl ViewRenderer` (typically via the `ViewEngine<E>` extractor) and calls `format::render().view(&v, "home/hello.html", data!({..}))`. The engine behind that trait is decided once, at the `Initializer` level — swap `ViewEngine<TeraView>` for `ViewEngine<YourEngine>` and every existing call site keeps compiling, because it was only ever coupled to the trait, not to Tera specifically. This is the same escape-hatch pattern described in [Why batteries included](@/docs/explanation/why-batteries-included.md): Tera is the built-in, `ViewRenderer` is the seam you use if you need something else.
For pure JSON APIs, "the view" can be as simple as a `#[derive(Serialize)]` struct shaped by hand and returned via `format::json(..)` — no template engine in the loop at all. Most real apps mix both: JSON views for an API surface, Tera views for a handful of server-rendered pages (an admin panel, a marketing page, an email-verification landing page).
## Three deployment shapes, one set of controller code
### Server-side rendering (Tera)
The default shape: `TeraView` reads templates from `assets/views/**/*.html` on disk at request time, alongside static files served from `assets/static/` through the `static` middleware. This is the natural choice when the app itself renders the HTML the browser gets — templates can be edited without a rebuild (`cargo loco start` picks up a changed `.html` file on the next request), which matters during active UI development.
### Client-side rendering (SPA)
Here Loco's job shrinks to two things: serve a JSON API, and serve the SPA's *built* static assets (the `dist/`-style output of a separate frontend build) through the same `static` middleware, usually with a fallback to `index.html` for client-side routing. There's no `TeraView` in the picture for the API surface at all — the split between "backend serves data" and "frontend owns rendering" is total, and Loco's role is just: API controllers, plus a static file server pointed at wherever the frontend build lands.
### `embedded_assets`: one flag, two subsystems swapped together
`embedded_assets` is a build-time Cargo feature that changes *where the bytes come from* without changing a single controller or view call:
```toml
loco-rs = { version = "...", features = ["embedded_assets"] }
```
With it enabled, the entire `assets/` directory — templates *and* static files — is scanned at compile time and embedded directly into the binary. What makes this worth calling out as a distinct architectural idea, rather than just "a smaller deployment," is that it isn't one subsystem being swapped: **both** the Tera view engine and the static-assets middleware are simultaneously replaced with embedded-reading variants (`views::engine_embedded` in place of `views::engine`, `static_assets_embedded` in place of `static_assets`) behind the same `#[cfg(embedded_assets)]` gate. One flag flips two independently-registered subsystems in lockstep, so a template lookup and a static-file request both resolve against the same in-binary asset table rather than one reading disk and the other reading memory. From application code, nothing changes — the `ViewEngine`/`ViewRenderer` and static-middleware config keys are identical either way.
The trade-off is the mirror image of SSR's live-editing convenience: a single-binary deploy with atomic code/asset updates and no filesystem asset directory to manage in production, at the cost of a full recompile for any asset change and a larger binary. Projects often use plain filesystem assets in development (fast iteration) and flip `embedded_assets` on for release builds (simpler deployment) — the same controller and view code runs unmodified in both.
## Why this is one flag and not per-subsystem toggles
It would be possible to let the view engine and the static middleware be embedded independently — but that would create deployment shapes where templates are baked into the binary while static files are read from disk (or vice versa), with no clear operational benefit and a real risk of the two drifting (a rebuild updates embedded templates but not the separately-deployed static folder, or the reverse). Coupling both under one feature flag makes "embedded" a single, coherent deployment mode rather than a matrix of partial states to reason about — consistent with the broader theme in [Why batteries included](@/docs/explanation/why-batteries-included.md) of collapsing a class of decisions into one well-tested default, with a clearly labeled way to opt out (here, just don't enable the feature).
See [Render server-side views](@/docs/how-to/render-views.md) for the concrete `assets/` directory layout, writing a Tera view and its controller wiring, and swapping in a custom `ViewRenderer`; see the [feature flags reference](@/docs/reference/feature-flags.md) for `embedded_assets`'s place in the full flag matrix, and the [middleware catalog](@/docs/reference/middleware.md#8-static) for the `static` middleware's config keys.
@@ -0,0 +1,61 @@
+++
title = "Why \"batteries included\"?"
description = "The prime directive behind Loco's design: prefer a built-in or a generator over hand-wiring, and what that buys you."
date = 2026-07-03T00:00:00+00:00
updated = 2026-07-03T00:00:00+00:00
draft = false
weight = 1
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
Loco's tagline is "Axum with batteries included," and it is meant literally: everything under the hood is standard [Axum](https://crates.io/crates/axum) 0.8 and [Tower](https://crates.io/crates/tower), but almost none of the code you'd normally write to assemble a production web service in Rust — wiring a DB pool into state, picking a logging stack, hand-rolling a queue, choosing a config format — is code you have to write yourself. This page explains the design principle behind that choice, not the mechanics (those live in [Architecture](@/docs/explanation/architecture.md) and the reference pages).
## The prime directive
When a Loco app needs a capability, the framework's bias is:
1. **Reach for a built-in first.** Database access, caching, background jobs, mailing, file storage, view rendering, JWT auth, health checks, a CLI — these already exist, wired into `AppContext` and toggled from YAML.
2. **Reach for a generator second.** `cargo loco generate` scaffolds the idiomatic shape of a model, controller, worker, mailer, task, or full CRUD scaffold. The generated code is not a black box — it's a starting point you own and edit.
3. **Hand-wire only as a last resort**, and when you do, Loco gives you a small number of well-defined seams to do it safely — `Hooks`, `Initializer`, `SharedStore`, `before_routes`/`after_routes` — rather than forcing you to fork the framework or reassemble `main()` from scratch.
This ordering is the single idea that explains most of what looks, from a plain-Axum perspective, like "magic": it isn't magic, it's a library of pre-wired decisions with an escape hatch at every layer.
## What "hand-wiring" looks like without it
A typical Axum service starts every project by re-deciding things that have already been decided a thousand times: which connection-pool settings, which logging crate, how to get the DB handle into every handler, how config and secrets flow in, how a background job survives a restart. None of these decisions are hard, but making them **again**, per project, is where hours disappear and where inconsistency creeps in between a team's services.
Loco's [`coming-from-axum`](@/docs/explanation/coming-from-axum.md) page walks through this delta concretely (pool setup, `AddExtensionLayer` state wiring, `env_logger` vs `tracing`, `main.rs` assembly) for a real reference app. The short version: every one of those steps becomes either a YAML key or a generator invocation in Loco, and the underlying Axum `Router`/`State`/extractor model is unchanged — so nothing about Axum's own learning curve is hidden from you.
## What Loco integrates
Batteries, concretely, means the following are already implemented, tested, and reachable from `AppContext` or a `Hooks` default, rather than left as an exercise:
- **Routing & the request pipeline** — `AppRoutes`/`Routes` compile down to a real `axum::Router<AppContext>`; a documented, ordered stack of middleware (payload limits, CORS, compression, timeouts, security headers, request IDs, a static file server, a dev-mode fallback page, and more) is available with a config flip rather than a `tower::Layer` you write by hand. See [Architecture](@/docs/explanation/architecture.md) and the [middleware catalog](@/docs/reference/middleware.md).
- **Data & persistence** — Sea-ORM 2.0 entities, migrations, and a query/pagination layer are generated from a compact field-type DSL (`cargo loco generate model ...`), not written by hand column-by-column.
- **Background processing** — one `BackgroundWorker` trait and one `perform_later` call site work unmodified against three interchangeable queue backends (Redis, Postgres, SQLite); see [The background-processing model](@/docs/explanation/background-processing-model.md).
- **Scheduling & tasks** — a cron-like scheduler (English or cron syntax) and ad-hoc CLI-invokable tasks, both driven from the same `Tasks`/`Hooks` registration points, no separate process supervisor to build.
- **Caching, storage, mail** — a `Cache` with in-memory/Redis/null backends, a multi-driver `Storage` abstraction (local/memory/S3/Azure/GCS) with single and replicated (mirror/backup) strategies, and a `Mailer` with SMTP/STARTTLS/implicit-TLS and a stub-for-tests mode — each a field on `AppContext`, each swappable by config or feature flag rather than by rewriting call sites.
- **Views** — server-rendered Tera templates, JSON views, or a single-binary `embedded_assets` build, chosen without touching controller code. See [Views and assets](@/docs/explanation/views-and-assets.md).
- **Auth & security** — JWT (HS512 by default, multi-location token extraction) and API-key extractors implementing the same `FromRequestParts` pattern as everything else in Axum.
- **Operability** — structured `tracing` logging with sane third-party filtering out of the box, `/_ping`/`/_health`/`/_readiness` endpoints, a `cargo loco doctor` diagnostic command, and a `cargo loco routes`/`middleware` introspection CLI.
- **Configuration** — one typed `Config` struct, one environment-resolution rule, one file-precedence rule, and Tera's own `get_env` for secrets — see [The configuration model](@/docs/explanation/configuration-model.md).
None of this requires a plugin marketplace or a runtime registry: it's all compiled into `loco-rs` behind Cargo feature flags (see the [feature-flags reference](@/docs/reference/feature-flags.md)), so an app only pays for what it turns on.
## The corollary: escape hatches, not walls
"Batteries included" only works as a philosophy if it doesn't become "batteries mandatory." Every built-in in Loco has a documented seam for replacing or bypassing it:
- Don't like the default middleware stack? Override `Hooks::middlewares` and return your own `Vec<Box<dyn MiddlewareLayer>>`.
- Want a raw Axum router mounted verbatim? `Hooks::before_routes`/`after_routes` hand you a real `axum::Router` to mutate directly — the [Coming from Axum](@/docs/explanation/coming-from-axum.md) page shows this as the literal drop-in path for existing Axum code.
- Need a service that isn't a first-class `AppContext` field (a third-party API client, a feature-flag SDK)? `AppContext.shared_store` is a type-keyed DI container built for exactly that — see [AppContext and dependency injection](@/docs/explanation/appcontext-and-di.md).
- Want to own the tracing/logging stack yourself? Return `Ok(true)` from `Hooks::init_logger` and Loco steps aside.
- Want a different view engine than Tera? Implement `ViewRenderer` and swap it in via an `Initializer`.
This is the same shape as Rails' "convention over configuration," reframed for a language where a global mutable app instance isn't an option: Loco supplies the convention as a compiled-in default, and the configuration/override points are explicit, typed, and ordered — not implicit and discoverable only by reading source. The rest of this Explanation cluster works through each of those seams — boot lifecycle, DI, config loading, background jobs, views, and the Axum relationship — in more depth.
@@ -0,0 +1,8 @@
+++
title = "Extras"
description = ""
template = "docs/section.html"
sort_by = "weight"
weight = 5
draft = false
+++
@@ -0,0 +1,716 @@
+++
title = "Upgrades"
description = ""
date = 2021-05-01T18:20:00+00:00
updated = 2021-05-01T18:20:00+00:00
draft = false
weight = 4
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
flair =[]
+++
## What to do when a new Loco version is out?
- Create a clean branch in your code repo.
- Update the Loco version in your main `Cargo.toml`
- Consult with the [CHANGELOG](https://github.com/loco-rs/loco/blob/master/CHANGELOG.md) to find breaking changes and refactorings you should do (if any).
- Run `cargo loco doctor` inside your project to verify that your app and environment is compatible with the new version
As always, if anything turns wrong, [open an issue](https://github.com/loco-rs/loco/issues) and ask for help.
## Major Loco dependencies
Loco is built on top of great libraries. It's wise to be mindful of their versions in new releases of Loco, and their individual changelogs.
These are the major ones:
- [SeaORM](https://www.sea-ql.org/SeaORM), [CHANGELOG](https://github.com/SeaQL/sea-orm/blob/master/CHANGELOG.md)
- [Axum](https://github.com/tokio-rs/axum), [CHANGELOG](https://github.com/tokio-rs/axum/blob/main/axum/CHANGELOG.md)
## Upgrade from 0.16.x to 1.0
1.0 is a large, intentionally-breaking release — the first stable Loco. Its
headline change is the move to **Sea-ORM 2.0**. This section is assembled per
area; start with the Sea-ORM steps, which affect every app that uses a database.
Cross-check the [1.0.0 CHANGELOG](https://github.com/loco-rs/loco/blob/master/CHANGELOG.md)
for anything specific to APIs you use directly.
### Toolchain: Rust 1.94+
Loco 1.0 uses Sea-ORM 2.0, whose MSRV is **Rust 1.94**. Update your toolchain:
```sh
rustup update
```
### Sea-ORM 2.0
Loco upgraded from Sea-ORM 1.1 to Sea-ORM 2.0. For most apps the migration is
mechanical — bump the pins and the CLI — because Loco's `schema` helpers and the
generated model/migration shapes absorb the API changes for you.
**1. Bump the dependency pins.** In your app `Cargo.toml`:
```toml
# before
sea-orm = { version = "1.1", features = ["sqlx-sqlite", "sqlx-postgres", "runtime-tokio-rustls", "macros"] }
# after
sea-orm = { version = "2.0", features = ["sqlx-sqlite", "sqlx-postgres", "runtime-tokio-rustls", "macros"] }
```
And in your `migration/Cargo.toml`:
```toml
# before
sea-orm-migration = { version = "1.1.0", features = [...] }
# after
sea-orm-migration = { version = "2.0", features = [...] }
```
If you depend on `sqlx` directly, bump it to `0.9`.
**2. Update the Sea-ORM CLI** (used by `cargo loco db entities`) to 2.0:
```sh
cargo install sea-orm-cli --version '^2.0'
```
`cargo loco doctor` will now flag a Sea-ORM or Sea-ORM CLI older than 2.0.
**3. Regenerate entities (recommended).** Run `cargo loco db entities` so your
`src/models/_entities/` are produced by the 2.0 codegen.
**4. Hand-written queries / migrations.** If you wrote custom raw SQL or custom
migrations, apply these Sea-ORM 2.0 changes (the same ones Loco itself made):
- Raw-`Statement` calls gain a `_raw` suffix. `db.execute(stmt)` →
`db.execute_raw(stmt)`; `db.query_one(stmt)` / `db.query_all(stmt)` →
`query_one_raw` / `query_all_raw`. SeaQuery statements (e.g. from
`Entity::find().into_query()`) are now passed **by reference** and need no
manual `.build(...)`: `db.query_all(&select)`.
- `sqlx` 0.9 requires runtime-built SQL strings to be wrapped in
`AssertSqlSafe(...)`: `sqlx::query(AssertSqlSafe(format!(...)))`.
- Bring `ExprTrait` into scope for expression methods: `use sea_orm::ExprTrait;`.
Replace `Alias::new("col")` with the bare string `"col"`.
- Unsupported-backend branches should return `DbErr::BackendNotSupported { .. }`
rather than panic; Sea-ORM 2.0 removed the internal panics (a new `DbErr`
variant carries the case). If you `match` on `DbErr` exhaustively, add the arm.
- `insert_many` no longer needs `.on_empty_do_nothing()`, and
`exec_with_returning_many` is now `exec_with_returning`.
**5. Note on Postgres auto-increment.** Sea-ORM 2.0 emits
`GENERATED BY DEFAULT AS IDENTITY` instead of `SERIAL` for new
`auto_increment()` columns. Existing tables are unaffected; only newly generated
migrations differ. See the Sea-ORM 2.0 migration guide for the
`option-postgres-use-serial` escape hatch if you need the old behavior.
For the full upstream detail see the
[Sea-ORM 2.0 migration guide](https://www.sea-ql.org/blog/2026-01-12-sea-orm-2.0/).
### Generated code now uses 64-bit primary keys
Newly **generated** models and scaffolds now use `i64` (BIGINT) primary keys and
foreign keys, and the `int`/`unsigned` field types generate 64-bit columns. This
is required by Sea-ORM 2.0 (its codegen maps SQLite integers to `i64`) and
matches the modern bigint-by-default convention.
This only affects code you generate *after* upgrading — your existing tables,
migrations, and entities are untouched. If you scaffold new resources and want
them to relate to older `i32`-keyed tables, make the key types match (either
widen the old ones with a migration, or hand-edit the new `id`/foreign-key
fields back to `i32`).
### Multi-database: `ExtraDbInitializer` → `MultiDbInitializer`
The single-extra-connection initializer (`initializers.extra_db`, which layered a
bare `Extension<DatabaseConnection>`) was removed. Use `MultiDbInitializer` with a
one-entry `initializers.multi_db` map instead, and extract the connection with
`Extension<MultiDb>`:
```rust
// before: Extension<DatabaseConnection>
// after:
let conn = multi_db.get("<name>")?;
```
Move whatever you configured under `extra_db` into a one-entry `multi_db` map.
### `AppContext` is now `#[non_exhaustive]` — construct with the builder
Field access (`ctx.db`, `ctx.config`, `State`/`FromRef` extraction) is unchanged,
so most apps need no change. But direct struct-literal construction and exhaustive
pattern matches on `AppContext` from outside the framework no longer compile (this
makes future context fields non-breaking to add). If you built an `AppContext` by
hand — e.g. in a custom boot or test harness — use the builder:
```rust
let ctx = AppContext::builder(environment, db, config) // builder(environment, config) without `with-db`
.queue_provider(queue)
.mailer(mailer)
.storage(storage)
.build();
```
### `loco_rs::Error` is now `#[non_exhaustive]`
The framework's `Error` enum is marked `#[non_exhaustive]` so new variants can be
added in the future without a breaking change. If you `match` on `loco_rs::Error`
(or `loco_rs::prelude::Error`) exhaustively, add a wildcard arm:
```rust
match err {
Error::NotFound => { /* ... */ }
// ...handle the variants you care about...
_ => { /* fallback */ }
}
```
Most apps use `Result<T>` / `?` and never match on `Error` directly, so no change
is needed.
### More accurate HTTP status codes for errors
`IntoResponse for Error` previously collapsed most variants to `500`. Now
`Model(EntityNotFound)` → `404`, `Model(EntityAlreadyExists)` → `409`, and model
validation / form-body rejections → `4xx` (matching JSON rejections); genuinely
internal errors still return `500`. This is behavior-only — no API changed — but
if your tests asserted the old `500`s, update them to the corrected codes.
### Background job priorities (Redis backend is breaking)
Background jobs now support a **priority** (higher numbers run first). You can
enqueue with an explicit priority:
```rust
DownloadWorker::perform_later_with_priority(&ctx, args, Some(42)).await?;
```
- **Postgres / SQLite: no action needed.** A `priority` column is added to the
queue table automatically on startup; existing jobs default to priority `0`.
- **Redis: breaking.** To order by priority, the Redis backend now stores the
queue as a **Sorted Set (ZSET)** instead of a List. Jobs already sitting in the
old List-based queue keys will not be picked up after upgrading. **Drain your
Redis queues before deploying 1.0** (let workers finish in-flight jobs on
the old version, or clear the queue if you can re-enqueue). Newly enqueued jobs
use the ZSET format automatically.
Mailer jobs enqueue at priority `100` by default; override per mailer via
`MailerOpts { priority, .. }`.
### `perform_later` returns the job id
`Worker::perform_later` now returns the enqueued job's id
(`Result<String>` instead of `Result<()>`), and `Queue::enqueue` returns
`Result<Option<String>>`. Existing call sites keep working — `perform_later(..)
.await?;` simply ignores the returned id. Capture it when you want to track
status:
```rust
let job_id = DownloadWorker::perform_later(&ctx, args).await?;
```
### Background queue is now a `QueueProvider` adapter
`bgworker::Queue` is now a newtype over `Arc<dyn QueueProvider>`, so backends are
pluggable. All queue methods keep the same signatures and behavior. Only two
source-level changes affect callers:
- Construct a no-op queue with `Queue::empty()` instead of `Queue::None`.
- Code that pattern-matched the enum variants (e.g. `Queue::Postgres(pool, ..)` to
reach the raw pool) no longer compiles — use the provider methods instead.
### `PageResponse` carries a `meta: PagerMeta`
Pagination results moved the flat `total_pages` / `total_items` fields into a
`meta: PagerMeta` (which also carries `page` and `page_size`):
```rust
// before
let total = page.total_pages;
// after
let total = page.meta.total_pages; // also: page.meta.page, page.meta.page_size, page.meta.total_items
```
### Storage: `MirrorStrategy` / `BackupStrategy` → `ReplicatedStrategy`
The two strategies were the same primary-plus-secondaries replication engine and
are now one `storage::strategies::replicated::ReplicatedStrategy` with a single
`FailurePolicy` enum:
```rust
// MirrorStrategy::new(p, s, MirrorAll) ->
ReplicatedStrategy::mirror(p, s, FailurePolicy::FailIfAny);
// BackupStrategy::new(p, s, BackupAll) ->
ReplicatedStrategy::backup(p, s, FailurePolicy::FailIfAny);
```
Old `FailureMode` maps: `AllowMirrorFailure` / `AllowBackupFailure` → `AllowAll`,
`AtLeastOneFailure` → `AllowSingleFailure`, `CountFailure(n)` → `FailAtFailures(n)`.
Former-backup secondary writes now run concurrently (were sequential); the
collected errors and failure decision are unchanged.
### Storage: local driver no longer roots at `/` (security)
`storage::drivers::local::new()` previously rooted the store at `/`, so a key
derived from user input could escape to the whole disk (key `etc/passwd` read
`/etc/passwd`). It now roots at the current working directory. If you relied on
absolute-path keys, opt back in explicitly:
```rust
local::new_with_prefix("/your/root")
```
### Config: `{env}.local.yaml` now deep-merges over `{env}.yaml`
Previously the first existing file won and the other was ignored, so a
`.local.yaml` had to restate the whole config. Both files now layer with local
precedence: mappings merge recursively; scalars and sequences in local replace the
base value (sequences are **not** concatenated). If you kept a full-config
`.local.yaml`, trim it to just the keys you override — base keys now persist
unless explicitly overridden.
### Fallback middleware defaults to `404`
When the built-in fallback is enabled without an explicit `code`, it now returns
`404 Not Found` (matching its docs and the bundled not-found page) instead of
`200 OK`. If you relied on the enabled fallback returning `200`, set `code: 200`
explicitly. The file-based fallback (`ServeFile`) is unaffected.
### `remote_ip` rebuilt on `axum-client-ip`; `trusted_proxies` removed (security)
**This is a silent, security-relevant change.** An old config's `trusted_proxies:`
key is now an unknown field and is **ignored without error**, so review your
`remote_ip` config before upgrading — it will not fail to load.
Previously the middleware walked `X-Forwarded-For` right-to-left, skipping any
address in a `trusted_proxies` CIDR list (or a built-in RFC-1918 + loopback list).
It now trusts exactly **one** configured source (`source: ClientIpSource`, default
`RightmostXForwardedFor`) and does **no** CIDR filtering.
- **Single reverse-proxy deployments:** unaffected.
- **Multi-hop topologies (CDN → LB → ingress):** configure your innermost hop to
set the client IP (e.g. nginx `set_real_ip_from` / `real_ip_recursive`), or
point `source` at a provider header (`CfConnectingIp`, `CloudFrontViewerAddress`,
`XRealIp`, `ConnectInfo`, …).
The `RemoteIP` extractor and its `Display` output are unchanged.
### JWT: `algorithm()` restricted to the HMAC family
`JWT::algorithm()` now takes `loco_rs::auth::jwt::JWTAlgorithm`
(`HS256` / `HS384` / `HS512`) instead of `jsonwebtoken::Algorithm`. Asymmetric
algorithms — which could never work with Loco's shared base64 secret and silently
produced broken tokens — are no longer representable. If you passed a
`jsonwebtoken::Algorithm`, switch to the matching `JWTAlgorithm` variant.
### View engine: use `TeraView::build_with_post_process`
In `after_routes`, replace `TeraView::build()?.post_process(...)` with the
combined constructor:
```rust
// before
engines::TeraView::build()?.post_process(move |tera| {
tera.register_function("t", FluentLoader::new(arc.clone()));
Ok(())
})?
// after
engines::TeraView::build_with_post_process(move |tera| {
tera.register_function("t", FluentLoader::new(arc.clone()));
Ok(())
})?
```
### Mailer: `Template::new(dir)` now returns `Result`
Email templates render through a full Tera instance (so they support inheritance
and shared templates). Standard usage via `Mailer::mail_template` is unchanged; if
you called `Template::new(dir)` directly, add `?`:
```rust
let tpl = Template::new(dir)?;
```
### Tasks: `Vars::cli_arg` returns `Result<&str>`
`Vars::cli_arg` now returns `Result<&str>` (was `Result<&String>`). Callers that
relied on `&String` (e.g. `.clone()` into a `String`) should use `.to_owned()`.
### Dependency majors
1.0 bumps several dependency majors. These are transitive for most apps — you
only need to act if you use one of these crates **directly** through Loco's
public API: `thiserror` 1→2, `tower` 0.4→0.5, `heck`→0.5, `byte-unit` 4→5,
`ipnetwork` 0.20→0.21, `strum`→0.27, `redis` 0.31→1, `bb8-redis`→0.26,
`opendal` 0.54→0.57. `serde_yaml` (archived) was replaced by the maintained
`serde_yaml_ng` fork.
### Feature-flag changes (1.0)
- `auth_jwt` → `auth`.
- `bg_redis` → `worker_redis`; `bg_pg`/`bg_sqlt` → `worker`. `default` now includes
`worker` (Postgres+SQLite queues); add `worker_redis` for a Redis queue.
- `integration_test` removed (was dead).
- `loco new` now offers Redis/Postgres/SQLite queue backends and (serverside)
embedded assets.
## Upgrade from 0.15.x to 0.16.x
### Use `AppContext` instead of `Config` in `init_logger` in the `Hooks` trait
PR: [#1418](https://github.com/loco-rs/loco/pull/1418)
If you are supplying an implementation of `init_logger` in your `impl` of the `Hooks` trait in order to set up your own logging, you will need to make the following change:
```diff
- fn init_logger(config: &config::Config, env: &Environment) -> Result<bool> {
+ fn init_logger(ctx: &AppContext) -> Result<bool> {
```
Any code in your `init_logger` implementation that makes use of the `config` can access it through `ctx.config`. In addition, you will also be able to access anything else in the `AppContext`, such as the new `shared_store`. The `env` parameter is also removed, as that is accessible from the `AppContext` as `ctx.environment`.
### Swap to validators builtin email validation
PR: [#1359](https://github.com/loco-rs/loco/pull/1359)
Swap from using the loco custom email validator, to the builtin email validator from `validator`.
```diff
- #[validate(custom (function = "validation::is_valid_email"))]
+ #[validate(email(message = "invalid email"))]
pub email: String,
```
### Job system
PR: [#1384](https://github.com/loco-rs/loco/pull/1384)
PR: [#1396](https://github.com/loco-rs/loco/pull/1396)
Two major changes have been made to the background job system:
1. The Redis provider is no longer Sidekiq-compatible and uses a custom implementation
2. All providers (Redis, PostgreSQL, SQLite) now support tag-based job filtering
#### What Changed
##### Removing Sidekiq Compatibility
The Redis background job system has been completely refactored, replacing the Sidekiq-compatible implementation with a new custom implementation. This provides greater flexibility and improved performance, but means:
- Jobs pushed from older Loco versions (pre-0.16) will not be recognized or processed
- The Redis data structures have changed entirely
- There is no automatic migration path for existing queued jobs
##### Adding Job Filtering
A new tag-based job filtering system has been added to all background worker providers:
- Workers can now specify which tags they're interested in processing
- Jobs can be tagged when enqueued
- Workers with no tags only process untagged jobs, while tagged workers process jobs with matching tags
- The same API is used across all providers
#### How to Upgrade
To upgrade to the new job system:
1. **Process existing jobs**:
- Make sure all jobs in your queue are processed/completed before upgrading
2. **Clean up old data**:
- For Redis: Flush the Redis database used for jobs (`FLUSHDB` command)
- For PostgreSQL: Drop the job queue tables
- For SQLite: Delete the job queue tables
3. **Update Loco**:
- Update to Loco 0.16+
- Loco will automatically create new job tables with the correct schema on first run
### Generic Cache
PR: [#1385](https://github.com/loco-rs/loco/pull/1385)
The cache API has been refactored to support storing and retrieving any serializable type, not just strings. This is a breaking change that requires updates to your code:
#### Breaking Changes:
1. **Type Parameters Required**: All cache methods now require explicit type parameters
2. **Method Signatures**: Some method signatures have changed to support generics
3. **Object Serialization**: Any type you store must implement `Serialize` and `Deserialize` from serde
#### Migration Guide:
**Before:**
```rust
// Get a string value from cache
let value = cache.get("key").await?;
// Insert or get with callback
let value = app_ctx.cache.get_or_insert("key", async {
Ok("value".to_string())
}).await.unwrap();
// Insert or get with expiry
let value = app_ctx.cache.get_or_insert_with_expiry("key", Duration::from_secs(300), async {
Ok("value".to_string())
}).await.unwrap();
```
**After:**
```rust
// Get a string value from cache - specify the type
let value = cache.get::<String>("key").await?;
// Direct insert with any serializable type
cache.insert("key", &"value".to_string()).await?;
// Insert or get with callback - specify return type
let value = app_ctx.cache.get_or_insert::<String, _>("key", async {
Ok("value".to_string())
}).await.unwrap();
// Store complex types
#[derive(Serialize, Deserialize)]
struct User {
name: String,
age: u32,
}
let user = app_ctx.cache.get_or_insert_with_expiry::<User, _>(
"user:1",
Duration::from_secs(300),
async {
Ok(User { name: "Alice".to_string(), age: 30 })
}
).await.unwrap();
```
#### Implementing for Custom Types:
For your custom types to work with the cache, ensure they implement `Serialize` and `Deserialize`:
```rust
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct MyType {
// fields...
}
```
### Authentication Error Handling
Authentication error handling has been improved to better distinguish between actual authorization failures and system errors:
1. **System errors now return 500**: Database errors during authentication now return Internal Server Error (500) instead of Unauthorized (401)
2. **Improved error logging**: Authentication errors are now logged with detailed messages using `tracing::error`
3. **Message changes**: Generic error messages have been updated from "other error: '{e}'" to "could not authorize"
#### Migration Guide:
If you have code that relies on database errors during authentication returning 401 status codes, you'll need to update your error handling. Any code expecting a 401 for database connectivity issues should now handle 500 responses as well.
Client applications should be prepared to handle both 401 and 500 status codes during authentication failures, with 401 indicating authorization problems and 500 indicating system errors.
### Server side rendering
We had some changes in Tera template. go to `src/initializers/view_engine.rs` and replace the `after_routes` function with:
```rust
async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {
let tera_engine = if std::path::Path::new(I18N_DIR).exists() {
let arc = std::sync::Arc::new(
ArcLoader::builder(&I18N_DIR, unic_langid::langid!("en-US"))
.shared_resources(Some(&[I18N_SHARED.into()]))
.customize(|bundle| bundle.set_use_isolating(false))
.build()
.map_err(|e| Error::string(&e.to_string()))?,
);
info!("locales loaded");
engines::TeraView::build()?.post_process(move |tera| {
tera.register_function("t", FluentLoader::new(arc.clone()));
Ok(())
})?
} else {
engines::TeraView::build()?
};
Ok(router.layer(Extension(ViewEngine::from(tera_engine))))
}
```
## Upgrade from 0.14.x to 0.15.x
### Upgrade validator crate
PR: [#1199](https://github.com/loco-rs/loco/pull/1199)
Update the `validator` crate version in your `Cargo.toml`:
From
```
validator = { version = "0.19" }
```
To
```
validator = { version = "0.20" }
```
### User claims
PR: [#1159](https://github.com/loco-rs/loco/pull/1159)
- Flattened (De)Serialization of Custom User Claims:
The `claims` field in `UserClaims` has changed from `Option<Value>` to `Map<String, Value>`.
- Mandatory Map Value in `generate_token` function:
When calling `generate_token`, the `Map<String, Value>` argument is now required. If you are not using custom claims, pass an empty map (`serde_json::Map::new()`).
- Updated generate_token Signature:
The `generate_token` function now takes `expiration` as a value instead of a reference.
### Pagination Response
PR: [#1197](https://github.com/loco-rs/loco/pull/1197)
The pagination response now includes the `total_items` field, providing the total number of items available.
```JSON
{"results":[],"pagination":{"page":0,"page_size":0,"total_pages":0,"total_items":0}}
```
### Explicit id in migrations
PR: [#1268](https://github.com/loco-rs/loco/pull/1268)
Migrations using `create_table` now require `("id", ColType::PkAuto)`, new migrations will have this field automatically added.
```diff
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
create_table(m, "movies",
&[
+ ("id", ColType::PkAuto),
("title", ColType::StringNull),
],
&[
("user", ""),
]
).await
}
```
## Upgrade from 0.13.x to 0.14.x
### Upgrading from Axum 0.7 to 0.8
PR: [#1130](https://github.com/loco-rs/loco/pull/1130)
The upgrade to Axum 0.8 introduces a breaking change. For more details, refer to the [announcement](https://tokio.rs/blog/2025-01-01-announcing-axum-0-8-0).
#### Steps to Upgrade
- In your `Cargo.toml`, update the Axum version from `0.7.5` to `0.8.1`.
- Replace use `axum::async_trait`; with use `async_trait::async_trait;`. For more information, see [here](https://tokio.rs/blog/2025-01-01-announcing-axum-0-8-0#async_trait-removal).
- The URL parameter syntax has changed. Refer to [this section](https://tokio.rs/blog/2025-01-01-announcing-axum-0-8-0#path-parameter-syntax-changes) for the updated syntax. The new path parameter format is:
The path parameter syntax has changed from `/:single` and `/*many` to `/{single}` and `/{*many}`.
### Extending the `boot` Function Hook
PR: [#1143](https://github.com/loco-rs/loco/pull/1143)
The `boot` hook function now accepts an additional Config parameter. The function signature has changed from:
From
```rust
async fn boot(mode: StartMode, environment: &Environment) -> Result<BootResult> {
create_app::<Self, Migrator>(mode, environment).await
}
```
To:
```rust
async fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult> {
create_app::<Self, Migrator>(mode, environment, config).await
}
```
Make sure to import the `Config` type as needed.
### Upgrade validator crate
PR: [#993](https://github.com/loco-rs/loco/pull/993)
Update the `validator` crate version in your `Cargo.toml`:
From
```
validator = { version = "0.18" }
```
To
```
validator = { version = "0.19" }
```
### Extend truncate and seed hooks
PR: [#1158](https://github.com/loco-rs/loco/pull/1158)
The `truncate` and `seed` functions now receive `AppContext` instead of `DatabaseConnection` as their argument.
From
```rust
async fn truncate(db: &DatabaseConnection) -> Result<()> {}
async fn seed(db: &DatabaseConnection, base: &Path) -> Result<()> {}
```
To
```rust
async fn truncate(ctx: &AppContext) -> Result<()> {}
async fn seed(_ctx: &AppContext, base: &Path) -> Result<()> {}
```
Impact on Testing:
Testing code involving the seed function must also be updated accordingly.
from:
```rust
async fn load_page() {
request::<App, _, _>(|request, ctx| async move {
seed::<App>(&ctx.db).await.unwrap();
...
})
.await;
}
```
to
```rust
async fn load_page() {
request::<App, _, _>(|request, ctx| async move {
seed::<App>(&ctx).await.unwrap();
...
})
.await;
}
```
@@ -0,0 +1,8 @@
+++
title = "How-to Guides"
description = "Problem-oriented recipes for getting a specific job done in Loco."
template = "docs/section.html"
sort_by = "weight"
weight = 2
draft = false
+++
@@ -0,0 +1,202 @@
+++
title = "Add a controller"
description = "Generate a controller, wire up its routes and handlers, and mount it under a prefix or nested path."
date = 2026-07-03T00:00:00+00:00
updated = 2026-07-03T00:00:00+00:00
draft = false
weight = 10
sort_by = "weight"
template = "docs/page.html"
aliases = ["/docs/the-app/controller/"]
[extra]
lead = ""
toc = true
top = false
+++
**Goal:** add a new HTTP endpoint group to your Loco app — generated, or written by hand — and get it showing up in `cargo loco routes`.
This guide assumes a working Loco app (`cargo loco start` runs). For the full `Routes`/`AppRoutes` API and the exhaustive `Hooks` surface, see the [Hooks reference](@/docs/reference/hooks.md).
## 1. Generate a controller
```sh
cargo loco generate controller <NAME> [ACTION ...]
```
A generated controller is always a JSON API controller — there's no kind flag to pick. Additional positional arguments become extra actions (handler functions + routes) alongside the default `index`.
```sh
cargo loco generate controller notes list get
```
This:
- creates `src/controllers/notes.rs` with an `index` handler plus one handler per extra action (`list`, `get`), each returning `format::empty()` as a starting point
- adds `pub mod notes;` to `src/controllers/mod.rs`
- injects `.add_route(controllers::notes::routes())` into your `routes()` implementation in `src/app.rs` — no manual wiring needed
- generates a matching test file under `tests/requests/`
The generated `routes()` function looks like this:
```rust
// src/controllers/notes.rs
pub fn routes() -> Routes {
Routes::new()
.prefix("api/notes/")
.add("/", get(index))
.add("list", get(list))
.add("get", get(get))
}
```
Edit the handler bodies and route methods (`get`/`post`/`put`/`delete`, etc.) to fit your endpoint. Controllers return JSON by default; if you'd rather render server-side HTML, see [Render server-side views](@/docs/how-to/render-views.md).
## 2. Confirm the routes are registered
```sh
cargo loco routes
```
```sh
[GET] /_ping
[GET] /_health
[GET] /_readiness
[GET] /api/notes/
[GET] /api/notes/list
[GET] /api/notes/get
```
If your new routes don't appear, check that `src/app.rs`'s `routes()` implementation calls `.add_route(controllers::notes::routes())` (the generator does this for you, but double-check after a manual edit or merge conflict).
## 3. Write a controller by hand (no generator)
Sometimes you want a controller without a generator scaffold — e.g. a small internal endpoint.
1. Create `src/controllers/example.rs`:
```rust
use loco_rs::prelude::*;
async fn hello() -> Result<Response> {
format::text("hello")
}
async fn echo(Json(body): Json<serde_json::Value>) -> Result<Response> {
format::json(body)
}
pub fn routes() -> Routes {
Routes::new()
.add("/", get(hello))
.add("/echo", post(echo))
}
```
2. Declare the module in `src/controllers/mod.rs`:
```rust
pub mod example;
```
3. Register its routes in `src/app.rs`'s `Hooks::routes`:
```rust
fn routes(_ctx: &AppContext) -> AppRoutes {
AppRoutes::with_default_routes()
.add_route(controllers::example::routes())
}
```
`AppRoutes::with_default_routes()` also mounts the built-in `/_ping`, `/_health`, `/_readiness` monitoring endpoints.
## 4. Prefix a whole controller
`Routes::prefix` scopes every route added to that `Routes` instance:
```rust
pub fn routes() -> Routes {
Routes::new()
.prefix("notes")
.add("/", get(list))
.add("/{id}", get(get_one))
}
```
## 5. Prefix a whole app (or a group of controllers)
`AppRoutes::prefix` applies to every controller added after it:
```rust
fn routes(_ctx: &AppContext) -> AppRoutes {
AppRoutes::with_default_routes()
.prefix("/api")
.add_route(controllers::notes::routes())
.add_route(controllers::users::routes())
}
```
## 6. Nest routes under an additional path segment
Use `nest_prefix` to append another path segment to the *current* prefix for routes added afterward, or `nest_route`/`nest_routes` to scope a prefix to just the routes passed in (without touching the running prefix):
```rust
fn routes(_ctx: &AppContext) -> AppRoutes {
let v1_notes = Routes::new().add("/", get(|| async { "notes v1" }));
AppRoutes::with_default_routes()
.prefix("api")
.add_route(controllers::auth::routes())
// only these routes get the extra `v1` segment: /api/v1/...
.nest_route("v1", v1_notes)
}
```
`Routes::nest` (on a `Routes` value, not `AppRoutes`) does the same job when you're composing route groups before returning them from a controller's `routes()` function — handy for merging several sub-resources with `Routes::merge`/`merge_all` and then nesting the result once:
```rust
let user_routes = Routes::new()
.add("/users", get(list_users))
.add("/users", post(create_user));
let product_routes = Routes::new().add("/products", get(list_products));
let api_routes = Routes::new().merge(user_routes).merge(product_routes);
Routes::new()
.add("/health", get(|| async { "ok" }))
.nest("/api", api_routes);
// -> GET /health, GET /api/users, POST /api/users, GET /api/products
```
## 7. Apply a `tower::Layer` to just one controller or route
`Routes::layer` attaches a `tower::Layer` (rate limiting, custom auth, tracing, etc.) to every handler in that `Routes` value only — for middleware that should run on *every* route, see [Add middleware](@/docs/how-to/add-middleware.md) instead.
```rust
// src/controllers/notes.rs
pub fn routes() -> Routes {
Routes::new()
.prefix("notes")
.add("/", get(list).layer(my_tower_layer()))
}
```
## Verify
```sh
cargo loco routes
cargo test --test requests_notes # if the generator produced tests/requests/notes.rs
```
A `curl` against the new path should return your handler's response:
```sh
curl -s localhost:5150/api/notes/
```
## Next
- [Validate requests](@/docs/how-to/validate-requests.md)
- [Respond with different formats](@/docs/how-to/respond-formats.md)
- [Handle errors](@/docs/how-to/handle-errors.md)
@@ -0,0 +1,199 @@
+++
title = "Add middleware"
description = "Enable a built-in middleware through YAML config, and write a custom MiddlewareLayer when the built-ins don't cover what you need."
date = 2026-07-03T00:00:00+00:00
updated = 2026-07-03T00:00:00+00:00
draft = false
weight = 15
sort_by = "weight"
template = "docs/page.html"
aliases = ["/docs/extras/pluggability/"]
[extra]
lead = ""
toc = true
top = false
+++
**Goal:** turn on one of Loco's 13 built-in middlewares, or write your own when none of them fit, and confirm it's actually running.
This assumes a working app. For the full config-key/knob table for every built-in middleware, see the [Middleware catalog reference](@/docs/reference/middleware.md).
## 1. Enable a built-in middleware via config
Every middleware lives under `server.middlewares.<key>` in your environment YAML (`config/development.yaml`, `config/production.yaml`, ...). Most are disabled by default; a few (`catch_panic`, `etag`, `logger`, `request_id`, and `fallback` outside `Production`) are enabled unless you write the key at all.
Enable `remote_ip` (useful behind a proxy/load balancer) and `compression`:
```yaml
server:
middlewares:
remote_ip:
enable: true
compression:
enable: true
```
> **Watch out:** for middlewares that are enabled *by default* (e.g. `etag`, `catch_panic`), writing the key at all — even as `{}` — replaces the framework's own default with the struct's own `#[serde(default)]`, which resolves `enable` to `false` unless you set `enable: true` explicitly. Don't add a middleware's key to config unless you also intend to set `enable`.
## 2. Verify it's registered
```sh
cargo loco middleware --config
```
```sh
limit_payload {"body_limit":{"Limit":2000000}}
cors (disabled)
catch_panic {"enable":true}
etag {"enable":true}
remote_ip {"enable":true,"source":"RightmostXForwardedFor"}
compression {"enable":true}
timeout_request (disabled)
static (disabled)
secure_headers (disabled)
logger {"config":{"enable":true},"environment":"development"}
request_id {"enable":true}
fallback {"enable":true,"code":200,"file":null,"not_found":null}
powered_by {"ident":"loco.rs"}
```
`cargo loco middleware` (without `--config`) prints just the enabled/disabled state.
## 3. Common examples
Set a request body size limit:
```yaml
server:
middlewares:
limit_payload:
body_limit: 5mb # or "disable" to remove the limit entirely
```
Turn on CORS (disabled by default — note the field is `expose_headers`, **plural**):
```yaml
server:
middlewares:
cors:
enable: true
allow_origins:
- https://example.com
allow_headers:
- Content-Type
allow_methods:
- GET
- POST
expose_headers:
- X-Custom-Header
max_age: 3600
```
Serve static assets or an SPA — see [Serve static & SPA assets](@/docs/how-to/serve-assets.md) for the full walkthrough:
```yaml
server:
middlewares:
static:
enable: true
folder:
uri: "/static"
path: "assets/static"
```
Then use the extractor for a middleware that exposes one, e.g. `RemoteIP`:
```rust
use loco_rs::prelude::*;
#[debug_handler]
pub async fn list(ip: RemoteIP, State(ctx): State<AppContext>) -> Result<Response> {
tracing::info!(%ip, "handling request");
format::json(Entity::find().all(&ctx.db).await?)
}
```
## 4. Apply a middleware to a single route instead of globally
Config-driven middleware always applies to every route in the app. To scope a `tower::Layer` to one controller or route, use `Routes::layer` — see [Add a controller § 7](@/docs/how-to/add-controller.md#7-apply-a-tower-layer-to-just-one-controller-or-route).
## 5. Write a custom middleware
Implement the `MiddlewareLayer` trait:
```rust
pub trait MiddlewareLayer {
fn name(&self) -> &'static str;
fn is_enabled(&self) -> bool { true } // default
fn config(&self) -> serde_json::Result<serde_json::Value>;
fn apply(&self, app: AXRouter<AppContext>) -> Result<AXRouter<AppContext>>;
}
```
A minimal example that stamps every response with a custom header, config-toggleable like the built-ins:
```rust
// src/middlewares/hello.rs
use axum::{http::HeaderValue, response::Response, Router as AXRouter};
use loco_rs::{app::AppContext, controller::middleware::MiddlewareLayer, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct HelloHeader {
#[serde(default)]
pub enable: bool,
}
impl MiddlewareLayer for HelloHeader {
fn name(&self) -> &'static str {
"hello_header"
}
fn is_enabled(&self) -> bool {
self.enable
}
fn config(&self) -> serde_json::Result<serde_json::Value> {
serde_json::to_value(self)
}
fn apply(&self, app: AXRouter<AppContext>) -> Result<AXRouter<AppContext>> {
Ok(app.layer(axum::middleware::map_response(add_header)))
}
}
async fn add_header(mut res: Response) -> Response {
res.headers_mut()
.insert("X-Hello", HeaderValue::from_static("loco"));
res
}
```
Register it alongside (or instead of) the default stack by overriding the `middlewares` hook on `App` in `src/app.rs`:
```rust
impl Hooks for App {
// ...
fn middlewares(ctx: &AppContext) -> Vec<Box<dyn MiddlewareLayer>> {
let mut mids = middleware::default_middleware_stack(ctx);
mids.push(Box::new(middlewares::hello::HelloHeader { enable: true }));
mids
}
}
```
Remember the ordering rule: `AppRoutes::to_router` applies this `Vec` one `.layer(...)` call at a time, and each new layer wraps the router as the **outer** layer — so the middleware **last** in the vec is the **first** to see an incoming request (LIFO). Push your custom middleware onto whichever end of the vec matches where it needs to sit relative to `logger`/`catch_panic`/etc. See the [Middleware catalog § stack ordering](@/docs/reference/middleware.md#stack-ordering-build-order-vs-request-order-lifo) for the full explanation and the built-in coding order.
## Verify
```sh
cargo loco middleware --config # confirm your custom entry and its config appear
curl -i localhost:5150/ # confirm the custom header/behavior shows up
```
## Next
- [Middleware catalog reference](@/docs/reference/middleware.md) — every built-in middleware's config key and knobs
- [Serve static & SPA assets](@/docs/how-to/serve-assets.md)
- [Handle errors](@/docs/how-to/handle-errors.md)
@@ -0,0 +1,133 @@
+++
title = "Add a model"
description = "Generate a model with a migration, add fields to it, and run the migration to get working entities."
date = 2021-05-01T18:10:00+00:00
updated = 2021-05-01T18:10:00+00:00
draft = false
weight = 1
sort_by = "weight"
template = "docs/page.html"
aliases = ["/docs/the-app/models/"]
[extra]
lead = ""
toc = true
top = false
+++
**Goal:** add a new database-backed model to a Loco app — a migration, a Sea-ORM entity, and your own model file to extend it — using the model generator.
This assumes a working Loco app with the `with-db` feature enabled (the default). For the full field-type mini-language and every generator kind, see [Generators & field types](@/docs/reference/generators.md). For the migration DSL used under the hood, see [Schema & ColType DSL](@/docs/reference/schema-dsl.md).
## 1. Generate the model
Run the model generator with a name and a list of `field:type` pairs:
```sh
$ cargo loco generate model posts title:string! content:text user:references
```
This does three things in one step:
1. Writes a migration under `migration/src/` that creates a `posts` table.
2. Applies the migration against your development database.
3. Regenerates Sea-ORM entities into `src/models/_entities/`, and scaffolds `src/models/posts.rs` for your own model code.
You end up with:
```
src/
models/
_entities/
posts.rs <-- generated entity (Entity, Model, ActiveModel, Column, Relation)
posts.rs <-- your extension point
migration/
src/
m20240101_000002_posts.rs
```
Set the `SKIP_MIGRATION` environment variable if you want the generator to only write the migration file, without applying it or regenerating entities — useful when scripting several `generate model` calls back to back before running `db migrate` once at the end.
## 2. Read the field syntax
Each `field:type` pair follows a small suffix convention:
- no suffix → nullable column (`Option<T>`)
- `!` → required column (`NOT NULL`)
- `^` → unique column (implies `NOT NULL`)
So `title:string!` is a required `String`, and `content:text` is a nullable `Option<String>`.
`user:references` is special: it doesn't name a column type, it declares a belongs-to foreign key. It adds a required `user_id` column referencing the `users` table (`user:references?` makes it nullable; `user:references:author_id` picks a custom column name). See [Generators & field types § References](@/docs/reference/generators.md#references-belongs-to-foreign-keys) for the full syntax.
<div class="infobox">
1.0 change: the generator's <code>int</code>/<code>int!</code>/<code>int^</code> field type now maps to <b>i64 / BIGINT</b> (<code>big_integer</code>), not <code>i32</code> as in earlier Loco versions — matching the framework's i64 auto-increment primary keys. Use <code>small_int</code> if you specifically need a 16-bit column.
</div>
## 3. Add more fields to an existing model
To add columns to a table you already created, generate a plain migration instead of a new model — name it `Add<Columns>To<Table>` so Loco infers an "add columns" migration:
```sh
$ cargo loco generate migration AddViewsToPosts views:int
```
Apply it and regenerate entities:
```sh
$ cargo loco db migrate
$ cargo loco db entities
```
Removing columns follows the mirror-image naming convention, `Remove<Columns>From<Table>`:
```sh
$ cargo loco generate migration RemoveViewsFromPosts views:int
```
## 4. Generate without timestamps (optional)
By default every table generated through the DSL gets `created_at`/`updated_at` columns. To opt out, pass `--without-tz` to `model`, `migration`, or `scaffold`:
```sh
$ cargo loco generate model posts title:string! content:text --without-tz
```
<div class="infobox">
The flag is <code>--without-tz</code>, not <code>--without-timestamps</code> — an older spelling that no longer works.
</div>
## 5. Verify
Confirm the migration applied and entities exist:
```sh
$ cargo loco db status
$ ls src/models/_entities/
```
Then write against the model directly — e.g. in a `cargo loco playground` script or a test:
```rust
use migration::Migrator;
use loco_rs::testing::prelude::*;
use myapp::models::_entities::posts;
let boot = boot_test::<App, Migrator>().await?;
let post = posts::ActiveModel {
title: sea_orm::ActiveValue::set("hello".to_string()),
user_id: sea_orm::ActiveValue::set(1),
..Default::default()
}
.insert(&boot.app_context.db)
.await?;
assert_eq!(post.title, "hello");
```
**Result:** a `posts` table exists in your database, `posts::Entity`/`Model`/`ActiveModel` compile, and `src/models/posts.rs` is where you add custom methods (e.g. `Model::find_by_title`) the same way `examples/demo/src/models/users.rs` extends the generated `users` entity.
## Next
- [Query data](@/docs/how-to/query-data.md) with the `ConditionBuilder` DSL.
- [Foreign-key relationships](@/docs/reference/schema-dsl.md#table-level-operations) and [request/model validation](@/docs/how-to/validate-requests.md) beyond a single table.
@@ -0,0 +1,201 @@
+++
title = "Add a background worker"
description = "Generate a worker, implement BackgroundWorker, enqueue a job with perform_later, and register it in connect_workers."
date = 2021-05-01T18:10:00+00:00
updated = 2021-05-01T18:10:00+00:00
draft = false
weight = 20
sort_by = "weight"
template = "docs/page.html"
aliases = ["/docs/processing/workers/"]
[extra]
lead = ""
toc = true
top = false
+++
Goal: move slow or non-request-critical work (sending a report, calling a third-party API, resizing an image) out of the request path and into a background job.
## Prerequisites
- A queue backend configured (Redis, Postgres, or SQLite) if you want jobs to survive a restart. If you haven't decided yet, see [Choose a queue backend](@/docs/how-to/choose-queue-backend.md). For local dev you can skip this — the default `BackgroundQueue` mode with no `queue:` config still works, it just won't persist jobs (jobs are dropped with a logged error if no provider is populated). Many apps start with `workers.mode: BackgroundAsync`, which needs no queue backend at all.
## 1. Generate the worker
```sh
cargo loco generate worker report_worker
```
This creates `src/workers/report_worker.rs`, adds `pub mod report_worker;` to `src/workers/mod.rs`, and injects a registration call into `connect_workers` in `src/app.rs`. It also generates a test stub under `tests/workers/`.
The generated struct is always named `Worker` (scoped inside its own `workers::report_worker` module), with an empty `WorkerArgs` struct for you to fill in:
```rust
use serde::{Deserialize, Serialize};
use loco_rs::prelude::*;
pub struct Worker {
pub ctx: AppContext,
}
#[derive(Deserialize, Debug, Serialize)]
pub struct WorkerArgs {}
#[async_trait]
impl BackgroundWorker<WorkerArgs> for Worker {
fn build(ctx: &AppContext) -> Self {
Self { ctx: ctx.clone() }
}
fn class_name() -> String {
"ReportWorker".to_string()
}
async fn perform(&self, _args: WorkerArgs) -> Result<()> {
// TODO: your job logic goes here
Ok(())
}
}
```
## 2. Add typed arguments and job logic
Fill in `WorkerArgs` with whatever data the job needs (it's serialized into the queue, so keep it small and `Serialize + Deserialize`), then implement `perform`:
```rust
use loco_rs::prelude::*;
use serde::{Deserialize, Serialize};
pub struct DownloadWorker {
pub ctx: AppContext,
}
#[derive(Deserialize, Debug, Serialize)]
pub struct DownloadWorkerArgs {
pub user_guid: String,
}
#[async_trait]
impl BackgroundWorker<DownloadWorkerArgs> for DownloadWorker {
fn build(ctx: &AppContext) -> Self {
Self { ctx: ctx.clone() }
}
async fn perform(&self, args: DownloadWorkerArgs) -> Result<()> {
// .. do the actual work, use self.ctx for DB/cache/etc ..
println!("processing download for {}", args.user_guid);
Ok(())
}
}
```
(This example mirrors `examples/demo/src/workers/downloader.rs`.)
## 3. Confirm it's registered
The generator already injected this, but it's worth knowing what it did — `Hooks::connect_workers` is where every worker is registered against the shared `Queue`:
```rust
// src/app.rs
#[async_trait]
impl Hooks for App {
// ..
async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {
queue.register(DownloadWorker::build(ctx)).await?;
Ok(())
}
// ..
}
```
If you wrote the worker manually instead of generating it, add the `queue.register(...)` line yourself.
## 4. Enqueue a job
Call the trait's `perform_later` from a controller, task, or another worker:
```rust
DownloadWorker::perform_later(
&ctx,
DownloadWorkerArgs {
user_guid: "foo".to_string(),
},
)
.await?;
```
`perform_later` returns `Result<String>` — the job id, not `Result<()>`. In `BackgroundQueue` mode the id is assigned by the queue provider; in `ForegroundBlocking`/`BackgroundAsync` mode (or when no provider is configured) Loco generates a fresh UUID so you always get a stable handle back:
```rust
let job_id: String = DownloadWorker::perform_later(&ctx, args).await?;
```
If you need higher/lower priority for this particular job, use `perform_later_with_priority` instead — see [Choose a queue backend](@/docs/how-to/choose-queue-backend.md#priority-queues) for priority semantics shared across all three backends:
```rust
DownloadWorker::perform_later_with_priority(&ctx, args, Some(50)).await?;
```
## 5. Run the worker process
How you run workers depends on `workers.mode` (see [Choose a queue backend](@/docs/how-to/choose-queue-backend.md)):
```sh
# BackgroundQueue mode: run a dedicated worker process
cargo loco start --worker
# or run server + worker in the same process
cargo loco start --server-and-worker
```
`BackgroundAsync` and `ForegroundBlocking` modes don't need a separate worker process — jobs run inside whichever process called `perform_later`.
### Filtering by tags
Give a worker tags, then start a worker process that only picks up matching jobs:
```rust
fn tags() -> Vec<String> {
vec!["download".to_string(), "network".to_string()]
}
```
```sh
cargo loco start --worker download,network
```
A worker started with no tags (`cargo loco start --worker`) only processes untagged jobs; `--all` and `--server-and-worker` don't support tag filtering.
## 6. Verify
Test with `ForegroundBlocking` mode set in `config/test.yaml`, so `perform_later` runs synchronously and returns only once the job is done:
```rust
use loco_rs::testing::prelude::*;
#[tokio::test]
#[serial]
async fn test_run_download_worker() {
let boot = boot_test::<App, Migrator>().await.unwrap();
assert!(
DownloadWorker::perform_later(
&boot.app_context,
DownloadWorkerArgs { user_guid: "foo".to_string() }
)
.await
.is_ok()
);
// .. assert side effects here ..
}
```
Put worker tests under `tests/workers/` — the generator does this for you automatically.
## Reference
- Every `queue:`/`workers:` YAML key: [Configuration reference](@/docs/reference/configuration.md#queue)
- `cargo loco start`/`jobs` flags: [CLI reference](@/docs/reference/cli.md)
- `worker`/`worker_redis` feature flags: [Feature flags reference](@/docs/reference/feature-flags.md)
@@ -0,0 +1,94 @@
+++
title = "Protect a Route with an API Key"
description = "Authenticate requests with a per-user API key using the ApiToken<T> extractor and Authenticable::find_by_api_key."
date = 2021-05-01T18:10:00+00:00
updated = 2021-05-01T18:10:00+00:00
draft = false
weight = 41
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
Goal: authenticate a request with a long-lived, per-user API key instead of a JWT — useful for machine-to-machine or CLI clients that shouldn't have to re-authenticate for a fresh token.
Loco provides `auth::ApiToken<T>`, an axum extractor that reads a key from the `Authorization` header and loads the matching user via your model's `Authenticable::find_by_api_key`.
## Prerequisites
- The `with-db` feature (on by default) — `ApiToken<T>` is compiled only under `#[cfg(feature = "with-db")]`.
- Your user model implements `loco_rs::model::Authenticable`, in particular `find_by_api_key`. See [the `Authenticable` contract](@/docs/how-to/jwt-auth.md#the-authenticable-contract) for the trait shape and an example implementation.
- A column on your user model to store the key (e.g. `api_key`), and a way to populate it — `loco_rs::hash::random_string` is a convenient generator; see [Hash and verify passwords](@/docs/how-to/hash-passwords.md#generate-random-tokens).
Unlike JWT auth, `ApiToken<T>` needs **no `auth.jwt` configuration at all** — it doesn't call into `auth.jwt.secret`/`expiration`/`location`. It only needs a database and an `Authenticable` implementation.
## 1. Implement `find_by_api_key`
```rust
use loco_rs::model::{Authenticable, ModelError, ModelResult};
use sea_orm::{DatabaseConnection, EntityTrait, ColumnTrait, QueryFilter};
impl Authenticable for super::_entities::users::Model {
async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult<Self> {
let user = super::_entities::users::Entity::find()
.filter(super::_entities::users::Column::ApiKey.eq(api_key))
.one(db)
.await?;
user.ok_or(ModelError::EntityNotFound)
}
async fn find_by_claims_key(db: &DatabaseConnection, claims_key: &str) -> ModelResult<Self> {
// Required by the trait even if this app only uses ApiToken.
// See jwt-auth.md if you also support JWTWithUser<T>.
unimplemented!()
}
}
```
## 2. Add `ApiToken<T>` to a handler
```rust
use loco_rs::prelude::*;
use loco_rs::controller::extractor::auth;
async fn current_by_api_key(
auth: auth::ApiToken<users::Model>,
State(_ctx): State<AppContext>,
) -> Result<Response> {
format::json(&auth.user)
}
pub fn routes() -> Routes {
Routes::new()
.prefix("user")
.add("/current-api", get(current_by_api_key))
}
```
`auth.user` is the fully loaded `T` (your `Authenticable` model) — there's no separate claims struct to unwrap, unlike the JWT extractors.
## 3. Send the key as a Bearer token
**`ApiToken<T>` always reads the key from the `Authorization: Bearer <key>` header, and only from there.** This is a hard-coded read (`extract_token_from_header`), independent of any `auth.jwt.location` setting — the `location` config (`Bearer`/`Query`/`Cookie`, described in [Configure where Loco looks for the JWT](@/docs/how-to/jwt-locations.md)) applies to the `JWT`/`JWTWithUser` extractors only, never to `ApiToken`.
```sh
curl --location '127.0.0.1:5150/api/user/current-api' \
--header 'Authorization: Bearer <API_KEY>'
```
## Verify it works
- A valid, known key returns the user (200, with your handler's JSON body).
- An unknown key returns `401 Unauthorized` (the model lookup misses, mapped from `ModelError::EntityNotFound`).
- A database error while looking up the key returns `500 Internal Server Error`, and is logged via `tracing::error!`.
## Related
- [Protect a route with JWT](@/docs/how-to/jwt-auth.md) — the `Authenticable` contract in full, plus the `JWT` / `JWTWithUser<T>` extractors.
- [Configure where Loco looks for the JWT](@/docs/how-to/jwt-locations.md) — applies to JWT auth, not to `ApiToken`.
- [Hash and verify passwords](@/docs/how-to/hash-passwords.md) — generate the random key value to store per user.
- [Feature flags reference](@/docs/reference/feature-flags.md) — `with-db` default and what it gates.
@@ -0,0 +1,150 @@
+++
title = "Choose a queue backend"
description = "Pick Redis, Postgres, or SQLite for background jobs, configure it, and pick a worker mode."
date = 2021-05-01T18:10:00+00:00
updated = 2021-05-01T18:10:00+00:00
draft = false
weight = 21
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
Goal: decide how background jobs (see [Add a background worker](@/docs/how-to/add-worker.md)) are enqueued, stored, and processed, and configure it.
## 1. Pick a worker mode
`workers.mode` controls whether jobs go through a persistent queue at all:
```yaml
# config/development.yaml
workers:
mode: BackgroundQueue # default. Options: BackgroundQueue | ForegroundBlocking | BackgroundAsync
```
| Mode | Needs a `queue:` backend? | Behavior |
|---|---|---|
| `BackgroundQueue` (default) | yes | Enqueues to the configured provider; a separate worker process (or thread) dequeues and runs jobs. Survives restarts. |
| `ForegroundBlocking` | no | Runs the job inline, blocking the caller until it finishes. Used in tests. |
| `BackgroundAsync` | no | `tokio::spawn`s the job in the same process. No external store — jobs are lost on crash/restart. |
If you only need `BackgroundAsync` or `ForegroundBlocking`, you can stop here — skip the `queue:` config entirely.
The `loco new` wizard asks for this up front, offering `Async`, `Queue: Redis`, `Queue: Postgres`, `Queue: SQLite`, or `Blocking`; picking one of the three `Queue: *` options wires up the matching `workers.mode: BackgroundQueue` config, `queue.kind`, and Cargo feature (`worker` for Postgres/SQLite, `worker_redis` for Redis) for you.
## 2. Pick and configure a backend
All three backends share the same `perform_later` API and priority semantics; switching is a config change, not a code change. Set `queue.kind` in your environment YAML:
### Redis
```yaml
queue:
kind: Redis
uri: "{{ get_env(name='REDIS_URL', default='redis://127.0.0.1') }}"
dangerously_flush: false # clears the queue on boot — dev/test only
queues: [high, low] # optional: named/priority queues, first = most important
num_workers: 2 # concurrent job handlers
```
Requires the `worker_redis` Cargo feature. Unlike Postgres/SQLite, this one is **not** in the default feature set — enable it explicitly (`worker_redis` implies `worker`, so you don't need to list both):
```toml
loco-rs = { version = "...", features = ["worker_redis"] }
```
`setup()` is a no-op for Redis — there's no schema to create.
### Postgres
```yaml
queue:
kind: Postgres
uri: "{{ get_env(name='PGQ_URL', default='postgres://localhost:5432/mydb') }}"
dangerously_flush: false
enable_logging: false
max_connections: 20
min_connections: 1
connect_timeout: 500 # ms
idle_timeout: 500 # ms
poll_interval_sec: 1
num_workers: 2
```
Requires the `worker` Cargo feature (on by default — no extra `features = [...]` needed for a plain `loco-rs` dependency). Jobs live in a `pg_loco_queue` table; the table (and a `priority` column, for pre-1.0 tables) is created/migrated automatically on boot.
### SQLite
```yaml
queue:
kind: Sqlite
uri: "{{ get_env(name='SQLTQ_URL', default='sqlite://loco_development.sqlite?mode=rwc') }}"
dangerously_flush: false
poll_interval_sec: 1
num_workers: 2
# remaining keys identical to Postgres
```
Requires the `worker` Cargo feature (on by default) — the same flag that gates the Postgres backend above; both share the `sqlx`-based provider and are picked between at runtime by `queue.kind`. Uses `sqlt_loco_queue` (+ a lock table, since SQLite has no `SELECT ... FOR UPDATE SKIP LOCKED`).
The upshot: `worker` covers Postgres and SQLite queues (already in the default feature set), while `worker_redis` adds the Redis queue on top. Which backend actually runs is a runtime choice — `queue.kind: Postgres | Sqlite | Redis` — not a per-database feature flag. For the exhaustive key list (defaults included), see [Configuration reference → queue](@/docs/reference/configuration.md#queue). For flag names and how to trim the default feature set, see [Feature flags reference](@/docs/reference/feature-flags.md).
## 3. Run the worker process
```sh
cargo loco start --worker # dedicated worker process
cargo loco start --server-and-worker # server + worker, one process
```
See [Add a background worker](@/docs/how-to/add-worker.md#5-run-the-worker-process) for tag filtering.
## Priority queues
All three backends support per-job priority: higher `priority` (a full `i32`) is dequeued first; ties break by earlier `run_at`, then by job id. Set it with `perform_later_with_priority` instead of `perform_later`:
```rust
DownloadWorker::perform_later_with_priority(&ctx, args, Some(100)).await?;
```
Redis additionally supports **named** queues via `queue.queues` — `Worker::queue()` picks which named queue a job lands in, and the config list order sets each queue's priority (first = most important). The default named queues are `["default", "mailer"]`.
## Managing jobs from the CLI
Once `worker` (Postgres/SQLite) or `worker_redis` (Redis) is enabled, `cargo loco jobs` is available for all three backends — including Redis, which now fully supports admin operations (cancel, clear, requeue, dump/import are no longer Postgres/SQLite-only):
```sh
cargo loco jobs cancel --name <NAME>
cargo loco jobs tidy # delete completed/cancelled jobs
cargo loco jobs purge --max-age 90 # delete old failed/cancelled jobs
cargo loco jobs dump -f <folder>
cargo loco jobs import -f <file>
cargo loco jobs requeue --from-age 0 # move stuck "processing" jobs back to "queued"
```
See the full flag list in the [CLI reference](@/docs/reference/cli.md#2-3-jobs-subcommands).
### Automatic requeue (reaper)
Running `cargo loco jobs requeue` by hand recovers jobs stranded in `processing` after a worker crash, but nothing does this automatically by default. To have the running worker process do it periodically, opt in with a `reaper` block under `queue:` (all three backends support it):
```yaml
queue:
kind: Postgres
uri: "{{ get_env(name='PGQ_URL', default='postgres://localhost:5432/mydb') }}"
# ...
reaper:
age_minutes: 10 # requeue jobs stuck in "processing" for longer than this
interval_seconds: 60 # optional, default 60 — how often to sweep
```
Leaving `reaper` unset (the default) keeps prior behavior unchanged — no background sweep runs, and stranded jobs stay in `processing` until you run `cargo loco jobs requeue` yourself.
## Choosing between the three
- **Redis** — lowest latency, named/priority queues, no extra schema. Good default if you already run Redis.
- **Postgres** — no extra moving part if your app's database is already Postgres; `FOR UPDATE SKIP LOCKED` gives solid concurrency.
- **SQLite** — zero extra infrastructure for small deployments or local dev; uses a lock table instead of `SKIP LOCKED`, so it's less suited to high worker concurrency.
@@ -0,0 +1,119 @@
+++
title = "Configure logging"
description = "Set logger level and format, understand the filtering precedence, and add a rotating file appender."
date = 2026-07-03T00:00:00+00:00
updated = 2026-07-03T00:00:00+00:00
draft = false
weight = 33
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
Goal: control what Loco logs, in what shape, and where — stdout for development, structured JSON for production, and (optionally) a rotating log file — without drowning in third-party crate noise.
Loco's logger is built on `tracing`. `logger.enable`, `logger.level`, and `logger.format` are required keys in every config file.
## 1. Set the minimum config
```yaml
# config/development.yaml
logger:
enable: true
pretty_backtrace: true
level: debug
format: compact
```
- `level`: `off` | `trace` | `debug` | `info` | `warn` | `error`.
- `format`: `compact` | `pretty` | `json`.
- `pretty_backtrace`: when `true`, forces `RUST_BACKTRACE=1` and nicely-formatted panic backtraces. It's a development convenience — turn it off in performance-sensitive production deployments.
## 2. Know the filtering precedence
Loco doesn't just apply `level` globally to every crate — by default it whitelists a small set of modules (`loco_rs`, `sea_orm_migration`, `tower_http`, `sqlx::query`, `playground`, `loco_gen`) plus your own app crate, and applies `level` only to those. Everything else stays quiet.
Three ways to control this, in strict precedence order:
1. **`RUST_LOG` environment variable** — if set, it wins outright, ignoring both `level` and `override_filter`. Use this for one-off debugging on a running process without touching config:
```sh
RUST_LOG=debug cargo loco start
```
2. **`logger.override_filter`** — a raw `tracing-subscriber` `EnvFilter` directive string. Use this to permanently see traces from libraries outside the built-in whitelist:
```yaml
logger:
enable: true
level: info
format: compact
override_filter: "trace" # or a directive like "myapp=debug,tower_http=debug"
```
3. **The built-in module whitelist + `level`** — what you get if neither of the above is set. This is the common case: set `level` and trust Loco's whitelist to keep noise down.
## 3. Choose a format per environment
- `compact` — human-readable single-line output; good default for local development.
- `pretty` — multi-line, more spacious human-readable output.
- `json` — structured, one JSON object per line; use this in production so a log aggregator (Loki, CloudWatch, Datadog, etc.) can parse fields directly.
```yaml
# config/production.yaml
logger:
enable: true
pretty_backtrace: false
level: info
format: json
```
## 4. Add a rotating file appender
`file_appender` writes logs to disk independently of (and with its own level/format, separate from) the stdout logger — useful when you want stdout kept quiet but still capture a full trail on disk.
```yaml
logger:
enable: true
level: info
format: compact
file_appender:
enable: true
non_blocking: false # true offloads writes to a background thread
level: debug
format: json
rotation: daily # minutely | hourly | daily | never — default is hourly
dir: ./logs # default "./logs" if omitted
filename_prefix: myapp
filename_suffix: log
max_log_files: 7 # required — old files beyond this count are pruned
```
With `rotation: daily` and the settings above, you'll get files like `./logs/myapp.<date>.log`, rotated once a day, with only the newest 7 kept around.
## 5. Verify
Start the app and confirm the shape you expect shows up:
```sh
cargo loco start
# ... watch stdout for compact/pretty/json-formatted lines at the level you set
```
If you enabled a file appender, tail the log directory:
```sh
tail -f ./logs/*.log
```
To confirm the filtering precedence, try overriding at runtime without touching the config file:
```sh
RUST_LOG=loco_rs=trace cargo loco start
```
You should see much more verbose output than `logger.level` alone would produce — confirming `RUST_LOG` took precedence.
## Reference
- Every `logger:` YAML key, including all `file_appender` sub-keys: [Configuration reference § logger](@/docs/reference/configuration.md#logger)
@@ -0,0 +1,238 @@
+++
title = "Configure file storage"
description = "Wire up the Storage API over local disk, in-memory, or cloud (S3/Azure/GCS) drivers, pick a mirror/backup strategy, and stream large files."
date = 2026-07-03T00:00:00+00:00
updated = 2026-07-03T00:00:00+00:00
draft = false
weight = 30
sort_by = "weight"
template = "docs/page.html"
aliases = ["/docs/infrastructure/storage/"]
[extra]
lead = ""
toc = true
top = false
+++
Goal: give your app a place to put uploaded files — on disk, in memory (for tests), or in a cloud bucket — through one consistent `Storage` API, without hand-rolling an OpenDAL client yourself.
Loco's storage layer is a thin abstraction over [Apache OpenDAL](https://opendal.apache.org/). Every driver ends up implementing the same `StoreDriver` trait, so your controller code doesn't change when you swap local disk for S3.
## Prerequisites
Local, in-memory, and null storage work with no extra Cargo features. Cloud drivers need one of:
```toml
loco-rs = { version = "...", features = ["storage_aws_s3"] } # or storage_azure, storage_gcp, all_storage
```
See the [feature flags reference](@/docs/reference/feature-flags.md) for the full matrix.
## 1. Wire up a single driver
Storage isn't configured in YAML — it's wired in code, in the `after_context` hook (`src/app.rs`), and lands on `ctx.storage: Arc<Storage>`.
```rust
use loco_rs::storage::{self, drivers};
async fn after_context(ctx: AppContext) -> Result<AppContext> {
Ok(AppContext {
storage: storage::Storage::single(drivers::local::new()).into(),
..ctx
})
}
```
If you don't override `after_context` at all, Loco defaults to the **`Null` driver** — every storage operation returns `StorageError::Any("Operation not supported by null storage")`. That's a deliberate fail-fast default, not a bug: it means "you haven't wired storage yet."
## 2. Pick a driver
Every driver is built by a plain constructor function under `loco_rs::storage::drivers::*` — no trait object wrangling required.
| Driver | Feature | Constructor | Notes |
|---|---|---|---|
| Local filesystem | none | `drivers::local::new()` — rooted at the current working directory<br>`drivers::local::new_with_prefix(prefix) -> StorageResult<Box<dyn StoreDriver>>` | `new_with_prefix` errors if the prefix path doesn't exist |
| In-memory | none | `drivers::mem::new()` | Good for tests; data doesn't survive process exit |
| Null | none | `drivers::null::new()` | The framework default; every op errors |
| AWS S3 | `storage_aws_s3` | `drivers::aws::new(bucket, region) -> StorageResult<...>`<br>`drivers::aws::with_credentials(bucket, region, cred) -> StorageResult<...>`<br>`drivers::aws::with_credentials_and_endpoint(bucket, region, endpoint, cred) -> StorageResult<...>` | `Credential { key_id, secret_key, token: Option<String> }` |
| Azure Blob | `storage_azure` | `drivers::azure::new(container, account_name, access_key, endpoint) -> StorageResult<...>` | |
| Google Cloud Storage | `storage_gcp` | `drivers::gcp::new(bucket, credential_path) -> StorageResult<...>` | `credential_path` points to a service-account JSON key file |
All the cloud constructors return `StorageResult<Box<dyn StoreDriver>>` (they can fail to build the underlying OpenDAL operator), so propagate the error with `?`:
```rust
use loco_rs::storage::{self, drivers};
async fn after_context(ctx: AppContext) -> Result<AppContext> {
let store = drivers::aws::new("my-app-uploads", "us-east-1")?;
Ok(AppContext {
storage: storage::Storage::single(store).into(),
..ctx
})
}
```
For credentials that aren't in the environment/instance profile, pass them explicitly:
```rust
use loco_rs::storage::drivers::aws::{self, Credential};
let credential = Credential {
key_id: std::env::var("AWS_ACCESS_KEY_ID")?,
secret_key: std::env::var("AWS_SECRET_ACCESS_KEY")?,
token: None,
};
let store = aws::with_credentials("my-app-uploads", "us-east-1", credential)?;
```
> The storage driver trait is `StoreDriver` (not `StorageDriver`) — you'll see it in error messages and if you implement your own driver.
## 3. Use multiple drivers with a strategy (optional)
For redundancy across providers, set up several named stores and a `StorageStrategy` that decides how operations fan out across them.
**Mirror** — replicates uploads/deletes/renames/copies to every store; download tries the primary, then falls through to secondaries on failure. This is `ReplicatedStrategy::mirror`.
```rust
use std::collections::BTreeMap;
use loco_rs::storage::{
drivers, Storage,
strategies::{replicated::{ReplicatedStrategy, FailurePolicy}, StorageStrategy},
};
let primary = drivers::aws::new("bucket-primary", "us-east-1")?;
let mirror = drivers::azure::new("container", "account", "access-key", "https://account.blob.core.windows.net")?;
let strategy: Box<dyn StorageStrategy> = Box::new(ReplicatedStrategy::mirror(
"primary",
Some(vec!["mirror".to_string()]),
FailurePolicy::FailIfAny, // or AllowAll
));
let storage = Storage::new(
BTreeMap::from([
("primary".to_string(), primary),
("mirror".to_string(), mirror),
]),
strategy,
);
```
`FailurePolicy::FailIfAny` requires every secondary to succeed (errors bubble up as `StorageError::Multi`); `AllowAll` swallows secondary failures.
**Backup** — the primary must always succeed for writes; secondary failures are governed by a separate failure policy, and downloads *always* come from the primary only. This is `ReplicatedStrategy::backup`.
```rust
use loco_rs::storage::strategies::replicated::{ReplicatedStrategy, FailurePolicy};
let strategy: Box<dyn StorageStrategy> = Box::new(ReplicatedStrategy::backup(
"primary",
Some(vec!["backup_store".to_string()]),
FailurePolicy::AllowAll, // also: FailIfAny, AllowSingleFailure, FailAtFailures(n)
));
```
Mirror and backup are both `ReplicatedStrategy`, differing only in the constructor used (`mirror` vs `backup`) and the `FailurePolicy` you pick. It exposes a `_with_policy`/`_with_strategy` variant on every `Storage` method (`upload_with_strategy`, `download_with_policy`, ...) if you need to override the strategy for a single call.
## 4. Upload and download in a controller
```rust
use loco_rs::prelude::*;
use std::path::PathBuf;
async fn upload_file(
State(ctx): State<AppContext>,
mut multipart: Multipart,
) -> Result<Response> {
while let Some(field) = multipart.next_field().await.map_err(|_| {
Error::BadRequest("could not read multipart".into())
})? {
let file_name = field
.file_name()
.map(str::to_string)
.ok_or_else(|| Error::BadRequest("file name not found".into()))?;
let content = field
.bytes()
.await
.map_err(|_| Error::BadRequest("could not read bytes".into()))?;
let path = PathBuf::from("uploads").join(file_name);
ctx.storage.as_ref().upload(&path, &content).await?;
return format::json(serde_json::json!({ "path": path }));
}
not_found()
}
```
(Requires the `multipart` feature on the `axum` crate.)
## 5. Stream large files instead of buffering them
For files too large to comfortably hold in memory, use the streaming API — `download_stream`/`upload_stream` return/accept a `BytesStream`, which converts directly to/from an axum `Body`. This is undocumented in earlier Loco releases but is a stable, full public feature.
Streaming a download straight into an HTTP response, with zero extra buffering:
```rust
use axum::response::IntoResponse;
use std::path::Path;
async fn download_video(State(ctx): State<AppContext>) -> Result<impl IntoResponse> {
let stream = ctx.storage.download_stream(Path::new("videos/demo.mp4")).await?;
Ok(stream.into_body())
}
```
Streaming an upload from an incoming request body (axum's `Body` stream yields `axum::Error`, so map it to `std::io::Error` first — that's the error type `BytesStream` expects):
```rust
use loco_rs::storage::stream::BytesStream;
use futures_util::StreamExt;
use std::path::Path;
async fn upload_video(State(ctx): State<AppContext>, body: axum::body::Body) -> Result<Response> {
let mapped = body
.into_data_stream()
.map(|chunk| chunk.map_err(std::io::Error::other));
let stream = BytesStream::from_body_stream(mapped);
ctx.storage.upload_stream(Path::new("videos/demo.mp4"), stream).await?;
format::empty()
}
```
If you need the whole payload as one `Bytes` buffer anyway, `BytesStream::collect()` gives you that — but at that point you've given up the memory benefit of streaming.
**Strategy caveat:** streaming isn't uniformly "true streaming" once a strategy other than `SingleStrategy` is involved. `ReplicatedStrategy` unifies the former mirror/backup behavior: reads (both the buffered `download` and `download_stream`) fall back to secondaries when `read_from_secondaries` is set — i.e. constructed via `ReplicatedStrategy::mirror` — and are served from the primary only when constructed via `ReplicatedStrategy::backup`. Either way, `upload_stream` buffers the whole payload once via `collect()` and then fans out concurrently to secondaries. If you need guaranteed zero-buffering streaming to a single store, stick to `SingleStrategy` (the default).
## 6. Verify
```rust
use loco_rs::testing::prelude::*;
#[tokio::test]
#[serial]
async fn can_upload_and_download() {
request::<App, _, _>(|request, ctx| async move {
let file_content = "loco file upload";
let file_part = Part::bytes(file_content.as_bytes()).file_name("loco.txt");
let multipart_form = MultipartForm::new().add_part("file", file_part);
let response = request.post("/upload/file").multipart(multipart_form).await;
response.assert_status_ok();
let res: serde_json::Value = serde_json::from_str(&response.text()).unwrap();
let path = res["path"].as_str().unwrap();
let stored: String = ctx.storage.as_ref().download(&std::path::Path::new(path)).await.unwrap();
assert_eq!(stored, file_content);
})
.await;
}
```
## Reference
- `storage_aws_s3` / `storage_azure` / `storage_gcp` / `all_storage` feature flags: [Feature flags reference](@/docs/reference/feature-flags.md)
- Storage has no YAML configuration surface — everything above is the complete configuration story; there is no `storage:` key to look up in the [Configuration reference](@/docs/reference/configuration.md)
@@ -0,0 +1,94 @@
+++
title = "Connect to Postgres and Redis over TLS"
description = "Reach managed Postgres (RDS, Supabase, Neon) and managed Redis (ElastiCache, Upstash) over encrypted TLS connections."
date = 2021-05-01T18:10:00+00:00
updated = 2021-05-01T18:10:00+00:00
draft = false
weight = 23
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
Goal: connect your app to a **managed** database or cache that requires (or should use) encryption in transit. Most cloud providers — AWS RDS/ElastiCache, Supabase, Neon, Azure, Upstash — either require TLS or strongly recommend it.
Loco uses [rustls](https://github.com/rustls/rustls) with the pure-Rust `ring` provider for every TLS path, so none of this needs a system OpenSSL or a C toolchain.
## Postgres over TLS
Postgres TLS works out of the box whenever the `with-db` feature is on (the default for database apps) — there is **no Cargo feature to enable and no code to write**. You turn it on entirely through the connection URL in `config/*.yaml`, using the same `sslmode` / `sslrootcert` parameters `libpq` and every Postgres client understand.
```yaml
# config/production.yaml
database:
# Require an encrypted connection; fail if the server won't do TLS.
uri: "postgres://user:pass@db.example.com:5432/myapp?sslmode=require"
```
`sslmode` accepts the standard values, from weakest to strongest:
| `sslmode` | Encrypted? | Verifies the server? | Use when |
|---|---|---|---|
| `disable` | no | no | local/dev only |
| `prefer` | if available | no | — |
| `require` | yes | no | encryption without certificate checks |
| `verify-ca` | yes | CA chain | you trust the CA |
| `verify-full` | yes | CA chain **and** hostname | recommended for production |
For `verify-ca` / `verify-full` against a provider whose CA is not in the bundled root store, point at the CA bundle they give you, and add client-certificate paths for mutual TLS:
```yaml
database:
uri: "postgres://user:pass@db.example.com:5432/myapp?sslmode=verify-full&sslrootcert=/etc/ssl/rds-ca.pem"
# For mTLS, also: &sslcert=/path/client.crt&sslkey=/path/client.key
```
Provider quick reference (all support `sslmode=require`; use `verify-full` + their CA for the strongest setting):
- **AWS RDS/Aurora** — download the [RDS CA bundle](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html) and pass it as `sslrootcert`.
- **Supabase / Neon** — TLS is required; `sslmode=require` works directly, `verify-full` with their published CA is stronger.
- **Azure Database for PostgreSQL** — requires TLS; use `sslmode=require` or stricter.
> **Troubleshooting:** the error `server does not support TLS` means the **server** refused the TLS negotiation (wrong host/port, or TLS disabled server-side) — it is not a Loco or client bug. Check that you are pointing at the provider's TLS endpoint.
### Postgres queue over TLS
If you use the Postgres **queue** backend (`worker` feature) pointed at a TLS-only managed Postgres, the worker pool carries its own rustls TLS backend, so the same `sslmode=...` URL in `queue.uri` works there too — including in a worker-only build that does not enable `with-db`.
## Redis over TLS
Redis TLS is opt-in behind a Cargo feature, because the base Redis client does not compile a TLS stack by default.
1. Enable the `redis_tls` feature alongside your Redis feature:
```toml
# Cargo.toml
loco-rs = { version = "*", features = ["worker_redis", "redis_tls"] }
# or, for the Redis cache backend:
# loco-rs = { version = "*", features = ["cache_redis", "redis_tls"] }
```
`redis_tls` arms **both** the worker queue and the cache Redis paths at once (they share the same underlying client), using webpki-bundled roots so it works in slim/distroless container images with no system certificate store.
2. Use the `rediss://` scheme (note the double `s`) in your config — that is the only change on the config side:
```yaml
# config/production.yaml
queue:
kind: Redis
uri: "rediss://:password@my-redis.example.com:6380"
# and/or the cache:
cache:
kind: Redis
uri: "rediss://:password@my-redis.example.com:6380"
```
Provider notes:
- **AWS ElastiCache** — enable "encryption in-transit" on the cluster, then use `rediss://` with the auth token as the password.
- **Upstash / Redis Cloud** — TLS endpoints are `rediss://` by default; copy the URL from the dashboard.
@@ -0,0 +1,136 @@
+++
title = "Deploy to production"
description = "Build a release binary, generate a Dockerfile or nginx config with cargo loco generate deployment, and review production config before shipping."
date = 2026-07-03T00:00:00+00:00
updated = 2026-07-03T00:00:00+00:00
draft = false
weight = 32
sort_by = "weight"
template = "docs/page.html"
aliases = ["/docs/infrastructure/deployment/"]
[extra]
lead = ""
toc = true
top = false
+++
Goal: get a Loco app running on a production host. Loco compiles to a single self-contained binary — the target server needs neither `cargo` nor a Rust toolchain, just the binary and a `config/` folder.
## 1. Build the release binary
```sh
cargo build --release
```
Your binary name matches the `[package] name` in `Cargo.toml` (with a `-cli` suffix, e.g. `myapp-cli`), and lands in `./target/release/`.
## 2. Generate a Dockerfile (optional)
```sh
cargo loco generate deployment docker
```
`kind` is a **positional** argument — `docker` or `nginx`, not a `--kind` flag.
This writes two files to your project root:
- `Dockerfile` — multi-stage build: compiles with `cargo build --release` in a `rust:slim` builder stage, then copies just the compiled binary and `config/` into a slim `debian:bookworm-slim` runtime image. If your app has a `frontend/package.json` (client-side rendering), it also installs Node and runs `npm install && npm run build` in the builder stage. If `server.middlewares.static_assets` is configured, the folders it points to are copied into the final image too.
- `.dockerignore` — excludes `target/`, `.git`, and other build artifacts from the Docker build context.
Build and run it like any other image:
```sh
docker build -t myapp .
docker run -p 5150:5150 --env-file .env myapp
```
## 3. Generate an nginx config (optional)
```sh
cargo loco generate deployment nginx
```
This writes `nginx/default.conf`, a reverse-proxy config derived from your current `server.host` / `server.port` (`config/<env>.yaml`) — it proxies both the bare domain and wildcard subdomains to your app.
## 4. Review production config
There's no separate "production mode" — Loco picks a config file by environment (`config/production.yaml` by default, or override with `LOCO_ENV`). Before deploying, walk through these sections:
**Logger** — turn `pretty_backtrace` off (it's development-friendly, not performance-friendly) and prefer `json` for log aggregation:
```yaml
logger:
enable: true
pretty_backtrace: false
level: info
format: json
```
See [Configure logging](@/docs/how-to/configure-logging.md) for the full picture.
**Server** — bind to all interfaces and inject the port from the environment:
```yaml
server:
port: {{ get_env(name="NODE_PORT", default=5150) }}
host: {{ get_env(name="APP_HOST", default="http://localhost") }}
```
**Database** — real connection limits, no destructive flags:
```yaml
database:
uri: "{{ get_env(name='DATABASE_URL', default='postgres://loco:loco@localhost:5432/loco_app') }}"
enable_logging: false
connect_timeout: 500
idle_timeout: 500
min_connections: 1
max_connections: 10
auto_migrate: true
dangerously_truncate: false
dangerously_recreate: false
```
**Auth secret** — inject via environment, never hardcode:
```yaml
auth:
jwt:
secret: "{{ get_env(name='JWT_SECRET') }}"
expiration: 604800
```
**Queue / mailer** — same pattern: point `uri`/`host` at env vars. See the [Configuration reference](@/docs/reference/configuration.md) for every key across all of these sections.
## 5. Run `loco doctor` before going live
```sh
myapp-cli doctor --production
```
`doctor` validates DB/cache/queue connectivity against the config it would actually load. Add `-c`/`--config` to also print the fully-resolved config for inspection:
```sh
myapp-cli doctor --config --production
```
## 6. Ship it
Copy the binary and the `config/` folder to the server (no source, no `Cargo.lock`, no toolchain needed):
```sh
scp target/release/myapp-cli config/ user@server:/opt/myapp/
ssh user@server '/opt/myapp/myapp-cli start'
```
## Verify
- `myapp-cli doctor --production` exits 0 and reports all checks passing.
- `myapp-cli start` boots and the startup banner shows the environment, DB, and logger you expect.
- Hitting the app's health/root route through nginx (if you generated one) returns a response, confirming the reverse proxy is wired to the right host/port.
## Reference
- `generate deployment` CLI shape (`docker`/`nginx` as `kind`): [CLI reference](@/docs/reference/cli.md)
- Every config key referenced above (`logger`, `server`, `database`, `auth`, `mailer`, `queue`): [Configuration reference](@/docs/reference/configuration.md)
@@ -0,0 +1,138 @@
+++
title = "Snapshot tests with fixtures and redactions"
description = "Use insta snapshots for models and responses, redact dynamic fields with cleanup_user_model/cleanup_email, and assert on rendered HTML with select()."
date = 2021-05-01T18:10:00+00:00
updated = 2021-05-01T18:10:00+00:00
draft = false
weight = 52
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
Goal: snapshot a model, request response, or rendered HTML with [insta](https://crates.io/crates/insta), without the snapshot flapping every run because of a fresh UUID, timestamp, or password hash.
## 1. Enable insta
```toml
[dev-dependencies]
loco-rs = { version = "*", features = ["testing"] }
insta = { version = "*", features = ["redactions"] }
```
Loco does **not** re-export `insta` — it's a regular dev-dependency of your app. What Loco provides is the *filter tables* (`src/testing/redaction.rs`) you feed into insta's `filters` setting.
## 2. Snapshot a value with `assert_debug_snapshot!`
```rust
use insta::assert_debug_snapshot;
use loco_rs::testing::prelude::*;
let boot = boot_test::<App>().await.unwrap();
let user = users::Model::find_by_email(&boot.app_context.db, "user1@example.com").await;
assert_debug_snapshot!(user);
```
The first run writes a `.snap` file under a `snapshots/` folder next to your test; review and accept it with `cargo insta review` (or by hand), then future runs diff against it.
## 3. Redact dynamic fields before snapshotting
A freshly created user has a random UUID `pid`, an incrementing `id`, a bcrypt `password` hash, and `created_at`/`updated_at` timestamps — all of which change every run and would break the snapshot. Wrap the assertion in `insta::with_settings!` with one of Loco's cleanup filter sets:
```rust
use insta::{assert_debug_snapshot, with_settings};
use loco_rs::testing::prelude::*;
let res = Model::create_with_password(&boot.app_context.db, &params).await;
with_settings!({
filters => cleanup_user_model()
}, {
assert_debug_snapshot!(res);
});
```
| Filter fn | What it redacts | Placeholder |
|---|---|---|
| `cleanup_user_model()` | UUIDs/PIDs, bcrypt `password: "..."` hashes, JWT-shaped tokens, ISO-8601 timestamps (with and without timezone), `id: <number>` | `PID`, `"PASSWORD"`, `TOKEN`, `DATE`, `id: ID` |
| `cleanup_email()` | Mailer message identifiers, RFC-2822 dates, UUID-shaped random ids | `IDENTIFIER`, `DATE`, `RANDOM_ID` |
Both combine a base table with the shared date filter (`get_cleanup_date()`); `cleanup_user_model()` additionally folds in `get_cleanup_model()` (the `id: N` → `id: ID` rule). If you need a custom combination, the individual tables (`get_cleanup_user_model()`, `get_cleanup_date()`, `get_cleanup_model()`, `get_cleanup_mail()`) are public too — build your own `Vec<(&str, &str)>` filter list from them.
Use `cleanup_email()` the same way when snapshotting mailer deliveries (`ctx.mailer.unwrap().deliveries()`):
```rust
with_settings!({
filters => cleanup_email()
}, {
assert_debug_snapshot!(ctx.mailer.unwrap().deliveries());
});
```
## 4. Give each test file its own snapshot namespace
Snapshot filenames are derived from the test function name — across test files that's usually enough, but if two files both have a test with the same name, or you simply want per-file namespacing, define a small local macro (this is *not* a Loco API — it's plain insta, one line of glue code repeated per test file):
```rust
macro_rules! configure_insta {
($($expr:expr),*) => {
let mut settings = insta::Settings::clone_current();
settings.set_prepend_module_to_snapshot(false);
settings.set_snapshot_suffix("users"); // suffixes every snapshot in this file
let _guard = settings.bind_to_scope();
};
}
#[tokio::test]
#[serial]
async fn can_find_by_pid() {
configure_insta!();
// ...
}
```
`cargo loco generate model`/`scaffold` already scaffold this macro (without the suffix line) into the generated `tests/models/<name>.rs` — see [Using generators](@/docs/how-to/use-generators.md).
## HTML assertions with `select()`
For server-rendered (HTML/HTMX) views, don't snapshot or string-match raw markup — parse it with Loco's `scraper`-backed selector helpers (`src/testing/selector.rs`) instead. All of them panic with a descriptive message on failure, so they read like normal assertions:
```rust
use loco_rs::testing::prelude::*;
let html = response.text();
assert_css_exists(&html, ".flash-message");
assert_css_not_exists(&html, ".error");
assert_css_eq(&html, "h1.title", "Welcome to Loco");
assert_link(&html, "a.home", "/");
assert_attribute_exists(&html, "form", "action");
assert_attribute_eq(&html, "input[name=email]", "type", "email");
assert_count(&html, "ul#posts li", 3);
assert_css_eq_list(&html, "ul#posts li", &["Post 1", "Post 2", "Post 3"]);
```
`select(html, selector) -> Vec<String>` returns the outer HTML of every match, for cases where you want to snapshot a fragment instead of asserting on it directly (combine it with `assert_debug_snapshot!` and the redaction filters above if the fragment contains dynamic data):
```rust
let items = select(&html, ".item");
assert_debug_snapshot!(items);
```
## Verify it
```sh
cargo test
```
To review/update snapshots interactively after intentional output changes, install and run [`cargo-insta`](https://crates.io/crates/cargo-insta):
```sh
cargo install cargo-insta
cargo insta review
```
@@ -0,0 +1,116 @@
+++
title = "Handle errors"
description = "Return unauthorized/bad_request/not_found from a handler, build a CustomError with an arbitrary status, and know what status code the framework sends for everything else."
date = 2026-07-03T00:00:00+00:00
updated = 2026-07-03T00:00:00+00:00
draft = false
weight = 14
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
**Goal:** return the right HTTP status and JSON error body from a handler, without hand-writing `impl IntoResponse` yourself.
This assumes a working controller — see [Add a controller](@/docs/how-to/add-controller.md). Every handler that returns `loco_rs::Result<T>` (i.e. `Result<T, Error>`) gets its error automatically converted to an HTTP response by `impl IntoResponse for Error` — you never call `.into_response()` on an error yourself. For the exhaustive variant list and constructors, see the [Error model reference](@/docs/reference/errors.md).
## 1. The three common-case helpers
`loco_rs::prelude` re-exports three free functions for the HTTP-facing error variants you'll reach for most:
```rust
use loco_rs::prelude::*;
async fn get_one(Path(id): Path<i64>, State(ctx): State<AppContext>) -> Result<Response> {
let Some(item) = find_item(&ctx, id).await? else {
return not_found();
};
format::json(item)
}
async fn login(State(ctx): State<AppContext>, Json(params): Json<LoginParams>) -> Result<Response> {
let Ok(user) = find_user(&ctx, &params.email).await else {
return unauthorized("invalid credentials");
};
// ...
format::json(user)
}
async fn create(Json(params): Json<CreateParams>) -> Result<Response> {
if params.title.is_empty() {
return bad_request("title is required");
}
// ...
format::empty()
}
```
Each returns `Result<U>` (always the `Err` arm), so `return unauthorized(msg)` type-checks against any handler's `Result<Response>` return type. All three are also just regular ways to construct an `Error` and propagate it with `?` from a helper function you call from the handler.
| Fn | HTTP status | Response body | Notes |
|---|---|---|---|
| `not_found()` | 404 | `{"error":"not_found","description":"Resource was not found"}` | Takes no message. |
| `unauthorized(msg)` | 401 | `{"error":"unauthorized","description":"You do not have permission to access this resource"}` | `msg` is logged (`tracing::warn!`) but **not** sent to the client. |
| `bad_request(msg)` | 400 | `{"error":"Bad Request","description":"<msg>"}` | `msg` **is** sent to the client. |
## 2. Everything else falls through to 500
`Error` is `#[non_exhaustive]` with ~28 more variants (`DB`, `Model`, `IO`, `Tera`, `Message`, `InternalServerError`, ...). Only seven variants get a specific status; the framework's `IntoResponse` match ends in a wildcard arm — **every other variant becomes `500 Internal Server Error`** with body `{"error":"internal_server_error","description":"Internal Server Error"}`.
In practice this means: if you `?`-propagate a `sea_orm::DbErr`, an `std::io::Error`, or anything else that converts into `Error` via `#[from]`, and you haven't matched it explicitly, the client gets a generic 500 — which is usually what you want (don't leak internals), and every response is logged at `tracing::error!` first regardless of variant, so you still see the real cause server-side.
The full variant → status table, including `Validation` (→ 400, from the `validator` crate) and `JsonRejection` (→ axum's own rejection status), is in the [Error model reference](@/docs/reference/errors.md).
## 3. Return an arbitrary status: `Error::CustomError`
When none of the built-in helpers fit — a `409 Conflict`, a `429 Too Many Requests`, a body shape the framework doesn't produce — build one directly:
```rust
use loco_rs::prelude::*;
use loco_rs::controller::ErrorDetail;
use axum::http::StatusCode;
async fn create(Json(params): Json<CreateParams>) -> Result<Response> {
if already_exists(&params).await? {
return Err(Error::CustomError(
StatusCode::CONFLICT,
ErrorDetail::new("conflict", "a resource with this name already exists"),
));
}
format::empty()
}
```
`Error::CustomError(StatusCode, ErrorDetail)` passes both the status and the body through **unchanged** — it's the one variant the response mapping doesn't rewrite. `ErrorDetail::new(error, description)` sets both fields (an empty description collapses to `None`); `ErrorDetail::with_reason(error)` sets only `error`. The response body is always `{error, description, errors}` with `None` fields omitted from the JSON.
## 4. Convert a foreign error without a dedicated variant
Use `Error::wrap`/`Error::msg` at a `?`/`.map_err(..)` call site to fold any `std::error::Error` into `Error::Any`/`Error::Message` (both fall through to the 500 catch-all above):
```rust
let parsed: MyType = serde_json::from_str(&raw).map_err(Error::wrap)?;
```
Reach for `Error::string("...")` when you have a plain `&str`/message and no source error to wrap.
## 5. Content-type-aware error handling
If an endpoint needs to render errors differently for HTML vs. JSON clients, match on both the fallible call's `Result` and the negotiated format in one place — see [Respond with different formats](@/docs/how-to/respond-formats.md#4-combine-format-negotiation-with-error-handling).
## Verify
```sh
curl -i localhost:5150/notes/999999 # -> 404 {"error":"not_found",...}
curl -i -X POST localhost:5150/auth/login -d '{"email":"x","password":"y"}' -H 'content-type: application/json'
# -> 401 {"error":"unauthorized",...}
```
## Next
- [Validate requests](@/docs/how-to/validate-requests.md) — the `Validation` variant and structured field errors
- [Respond with different formats](@/docs/how-to/respond-formats.md)
- [Error model reference](@/docs/reference/errors.md) — full variant list and constructors
@@ -0,0 +1,73 @@
+++
title = "Hash and Verify Passwords"
description = "Use loco_rs::hash to Argon2id-hash passwords, verify them on login, and generate random tokens for reset links or API keys."
date = 2021-05-01T18:10:00+00:00
updated = 2021-05-01T18:10:00+00:00
draft = false
weight = 43
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
Goal: store passwords safely at registration, verify them at login, and generate random strings for things like reset tokens or API keys.
`loco_rs::hash` wraps Argon2id (`argon2` crate) for password hashing and a random alphanumeric generator for tokens. Unlike the JWT auth machinery, this module is **not** feature-gated — it's always available, with no Cargo feature to enable.
## Hash a password on registration
```rust
use loco_rs::hash;
let hashed = hash::hash_password("the-users-plaintext-password")?;
// store `hashed` in your user row, never the plaintext
```
`hash_password` uses Argon2id with a fresh random salt (`OsRng`) per call, so hashing the same password twice produces two different strings — that's expected; `verify_password` (below) handles the comparison. It returns Loco's `Result<String>`; a hashing failure surfaces as `Error::Message` via `Error::msg`.
## Verify a password on login
```rust
use loco_rs::hash;
if hash::verify_password(&submitted_password, &user.password_hash) {
// credentials are valid
} else {
// reject the login
}
```
`verify_password` returns a plain `bool`, not a `Result` — it returns `false` for both "wrong password" and "malformed/foreign hash string", so a bad row in the database fails closed rather than raising an error you'd have to remember to handle. It's `#[must_use]`, so the compiler will warn if you ignore the result.
## Generate random tokens
```rust
use loco_rs::hash;
let reset_token = hash::random_string(32);
let api_key = hash::random_string(40);
```
`random_string(length)` returns an alphanumeric string of exactly `length` characters, suitable for password-reset tokens or per-user API keys — see [Protect a route with an API key](@/docs/how-to/api-key-auth.md) for wiring a generated key into `Authenticable::find_by_api_key`.
## Verify it works
A quick round-trip in a test or a scratch binary:
```rust
let pass = "correct horse battery staple";
let hashed = hash::hash_password(pass)?;
assert!(hash::verify_password(pass, &hashed));
assert!(!hash::verify_password("wrong password", &hashed));
```
## Related
- [Protect a route with an API key](@/docs/how-to/api-key-auth.md) — use `random_string` to mint the key, `find_by_api_key` to look it up.
- [Protect a route with JWT](@/docs/how-to/jwt-auth.md) — issue a token once `verify_password` confirms the login.
- [Error model reference](@/docs/reference/errors.md) — how `Error::msg`/`Error::Message` fit into the crate's error type.
@@ -0,0 +1,163 @@
+++
title = "Protect a Route with JWT"
description = "Add JWT authentication to a route with the JWT and JWTWithUser<T> extractors, implement the Authenticable contract, and generate tokens."
date = 2021-05-01T18:10:00+00:00
updated = 2021-05-01T18:10:00+00:00
draft = false
weight = 40
sort_by = "weight"
template = "docs/page.html"
aliases = ["/docs/extras/authentication/"]
[extra]
lead = ""
toc = true
top = false
+++
Goal: require a valid JWT on a handler, and issue tokens your clients can send back.
Loco ships two axum extractors for JWT-protected routes:
- `auth::JWT` — validates the token and gives you the claims. Works without a database.
- `auth::JWTWithUser<T>` — validates the token **and** loads the user record from the database via your model's [`Authenticable`](#the-authenticable-contract) implementation. Requires the `with-db` feature.
Both live in `loco_rs::controller::extractor::auth` and are re-exported through `loco_rs::prelude::*` whenever the `auth` feature is on.
If you generated your app with `loco new` and picked a database-backed starter, `with-db` and `auth` are already on by default — see the [feature flags reference](@/docs/reference/feature-flags.md) if you need to check or change that.
## 1. Enable the `auth` feature
`auth` is a **default** feature (`Cargo.toml`): it pulls in `jsonwebtoken` with its pure-Rust `rust_crypto` backend, so no C toolchain is required even in a minimal build. If you depend on `loco-rs` with `default-features = false`, add it back explicitly:
```toml
loco-rs = { version = "...", default-features = false, features = ["auth", "with-db"] }
```
## 2. Configure the secret and expiration
Add an `auth.jwt` block to `config/development.yaml` (and every other environment file):
```yaml
auth:
jwt:
secret: "{{ get_env(name='JWT_SECRET') }}" # required, must be valid base64
expiration: 604800 # required, seconds (7 days)
```
Two facts that will save you a confusing error message:
- **The secret must be valid base64.** Loco encodes/decodes tokens with `EncodingKey::from_base64_secret` / `DecodingKey::from_base64_secret`. A plain, non-base64 string doesn't fail at config-load time — it fails later, when a token is generated or validated, with an error that doesn't obviously point at the config. Generate a base64 secret, e.g. `openssl rand -base64 64`, and inject it via `get_env` as above rather than hardcoding it.
- **The default signing algorithm is HS512**, not HS256. It's set in code (`Algorithm::HS512`) and isn't a YAML key — override it only from Rust, via `JWT::algorithm(..)` when constructing the signer.
For the full `auth.jwt` key reference, including `location`, see the [configuration reference](@/docs/reference/configuration.md#auth). Token location (`Bearer` / `Query` / `Cookie`) is covered in its own guide: [Configure where Loco looks for the JWT](@/docs/how-to/jwt-locations.md).
## 3. Generate a token
Use `loco_rs::auth::jwt::JWT` (the signer/validator — not to be confused with the extractor of the same name below) to mint a token, typically from a login handler, using the secret and expiration already loaded on `AppContext`:
```rust
use loco_rs::prelude::*;
async fn login(State(ctx): State<AppContext>, /* ... */) -> Result<Response> {
// `user` is whatever you loaded/verified during login (see hash-passwords.md).
let jwt_config = ctx.config.get_jwt_config()?;
let token = loco_rs::auth::jwt::JWT::new(&jwt_config.secret)
.generate_token(jwt_config.expiration, user.pid.to_string(), serde_json::Map::new())
.map_err(|e| Error::string(&e.to_string()))?;
format::json(serde_json::json!({ "token": token }))
}
```
`generate_token` takes the expiration (seconds), the `pid` that becomes `claims.pid`, and an optional `serde_json::Map` of custom claims that get flattened alongside `pid` in the token. It returns a `jsonwebtoken` result, not Loco's `Result`, so map the error explicitly as shown.
## 4. Protect a route: claims only
Add `auth::JWT` as a handler parameter. Axum runs it as an extractor before your handler body executes; if the token is missing, unparsable, in the wrong location, or expired, the request never reaches your code — Loco returns `401 Unauthorized` for you.
```rust
use loco_rs::prelude::*;
use loco_rs::controller::extractor::auth;
async fn current(
auth: auth::JWT,
State(_ctx): State<AppContext>,
) -> Result<Response> {
format::json(serde_json::json!({ "pid": auth.claims.pid }))
}
```
`auth.claims` is `UserClaims { pid, claims, .. }` — use `auth.claims.pid` for the subject, and `auth.claims.claims` for any custom claims you flattened in at generation time. This extractor needs no database, so it works even in DB-less apps.
## 5. Protect a route: claims + loaded user
When the handler needs the actual user row (not just the `pid`), use `auth::JWTWithUser<T>` where `T` is your user model. This requires the `with-db` feature and requires `T` to implement `Authenticable`.
```rust
use loco_rs::prelude::*;
use loco_rs::controller::extractor::auth;
async fn current(
auth: auth::JWTWithUser<users::Model>,
State(_ctx): State<AppContext>,
) -> Result<Response> {
format::json(&auth.user)
}
```
Internally, `JWTWithUser` validates the token exactly like `auth::JWT`, then calls `T::find_by_claims_key(&ctx.db, &claims.pid)` to load the user. A database miss becomes `401 Unauthorized`; a database error becomes `500 Internal Server Error`.
## The `Authenticable` contract
Both `JWTWithUser<T>` and `ApiToken<T>` (see the [API-key guide](@/docs/how-to/api-key-auth.md)) require your user model to implement `loco_rs::model::Authenticable`:
```rust
pub trait Authenticable: Clone {
async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult<Self>;
async fn find_by_claims_key(db: &DatabaseConnection, claims_key: &str) -> ModelResult<Self>;
}
```
A typical Sea-ORM implementation looks up the row by the relevant column and maps a miss to `ModelError::EntityNotFound`:
```rust
impl Authenticable for super::_entities::users::Model {
async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult<Self> {
let user = super::_entities::users::Entity::find()
.filter(super::_entities::users::Column::ApiKey.eq(api_key))
.one(db)
.await?;
user.ok_or(ModelError::EntityNotFound)
}
async fn find_by_claims_key(db: &DatabaseConnection, claims_key: &str) -> ModelResult<Self> {
let user = super::_entities::users::Entity::find()
.filter(super::_entities::users::Column::Pid.eq(claims_key))
.one(db)
.await?;
user.ok_or(ModelError::EntityNotFound)
}
}
```
**Note on scope:** the framework defines the `Authenticable` trait and the extractors above, but it does not generate a users model or `/api/auth/*` controllers for you — `loco-gen` has no auth/user template. That register/login/verify/reset-password flow, including the `Authenticable` implementation shown above, ships in the **SaaS starter** template (`loco new` → "SaaS app (with DB and user auth)"), which lives outside this repository. Use the pattern above as a starting point if you're wiring auth into a custom model.
## Verify it works
```sh
curl --location '127.0.0.1:5150/api/some/protected/route' \
--header 'Authorization: Bearer <TOKEN>'
```
A missing, malformed, or expired token returns `401 Unauthorized`. A valid token reaches your handler with `auth.claims` (and, for `JWTWithUser`, `auth.user`) populated.
## Related
- [Configure where Loco looks for the JWT](@/docs/how-to/jwt-locations.md) — Bearer / Query / Cookie, single or multiple.
- [Protect a route with an API key](@/docs/how-to/api-key-auth.md) — `ApiToken<T>`, a separate always-Bearer-header mechanism.
- [Hash and verify passwords](@/docs/how-to/hash-passwords.md) — for the login handler that issues the token above.
- [Configuration reference](@/docs/reference/configuration.md#auth) — every `auth.jwt` key.
- [Feature flags reference](@/docs/reference/feature-flags.md) — `auth` / `with-db` defaults and interactions.
- [AppContext & prelude reference](@/docs/reference/app-context.md) — what `loco_rs::prelude::*` brings in under `auth`.
@@ -0,0 +1,122 @@
+++
title = "Configure Where Loco Looks for the JWT"
description = "Set the JWT token location — Bearer header, query parameter, or cookie — as a single location or a fallback list."
date = 2021-05-01T18:10:00+00:00
updated = 2021-05-01T18:10:00+00:00
draft = false
weight = 42
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
Goal: control where the `JWT` and `JWTWithUser<T>` extractors look for the token in an incoming request — the `Authorization` header (default), a query string parameter, a cookie, or a fallback list of several.
This setting is `auth.jwt.location` in your config file. It applies only to the JWT extractors (`auth::JWT`, `auth::JWTWithUser<T>`); it has **no effect** on `auth::ApiToken<T>`, which always reads a `Bearer` header regardless of this config — see [Protect a route with an API key](@/docs/how-to/api-key-auth.md#3-send-the-key-as-a-bearer-token).
If you haven't set up JWT auth yet, start with [Protect a route with JWT](@/docs/how-to/jwt-auth.md); this page only covers the `location` key.
## Default: no configuration needed
If you omit `location` entirely, Loco reads the token from the `Authorization: Bearer <token>` header:
```yaml
auth:
jwt:
secret: "{{ get_env(name='JWT_SECRET') }}"
expiration: 604800
# location omitted => Bearer header
```
## Single location
Set `location` to one map with a `from:` tag. The three variants:
**Bearer header** (equivalent to the default, spelled out explicitly):
```yaml
auth:
jwt:
location:
from: Bearer
secret: "{{ get_env(name='JWT_SECRET') }}"
expiration: 604800
```
**Query parameter** — reads a named query-string value, e.g. for links that can't carry custom headers (email verification links, WebSocket handshakes):
```yaml
auth:
jwt:
location:
from: Query
name: token
secret: "{{ get_env(name='JWT_SECRET') }}"
expiration: 604800
```
```sh
curl 'http://127.0.0.1:5150/api/protected?token=<TOKEN>'
```
**Cookie** — reads a named cookie, e.g. for server-rendered apps using session-style cookies:
```yaml
auth:
jwt:
location:
from: Cookie
name: auth_token
secret: "{{ get_env(name='JWT_SECRET') }}"
expiration: 604800
```
```sh
curl 'http://127.0.0.1:5150/api/protected' --cookie 'auth_token=<TOKEN>'
```
## Multiple locations (tried in order)
Set `location` to a YAML list instead of a single map. Loco tries each location in the order listed and uses the first one that yields a token — useful when, say, browser clients send a cookie but API clients send a Bearer header:
```yaml
auth:
jwt:
location:
- from: Cookie
name: auth_token
- from: Query
name: token
- from: Bearer
secret: "{{ get_env(name='JWT_SECRET') }}"
expiration: 604800
```
With this config, a request is accepted if it carries a valid token in the `auth_token` cookie, **or** (if that's absent) a `token` query parameter, **or** (if both are absent) an `Authorization: Bearer` header — checked in that order. If none of the configured locations yields a token, the request is rejected with `401 Unauthorized`.
## Verify it works
For a `Multiple` config like the one above, confirm each location independently:
```sh
# via cookie
curl 'http://127.0.0.1:5150/api/protected' --cookie 'auth_token=<TOKEN>'
# via query parameter
curl 'http://127.0.0.1:5150/api/protected?token=<TOKEN>'
# via Bearer header
curl 'http://127.0.0.1:5150/api/protected' --header 'Authorization: Bearer <TOKEN>'
```
Each should succeed independently; a request with no token in any of the three should return `401 Unauthorized`.
## Related
- [Protect a route with JWT](@/docs/how-to/jwt-auth.md) — the extractors this setting controls, and how to generate tokens.
- [Protect a route with an API key](@/docs/how-to/api-key-auth.md) — a separate, always-Bearer-header mechanism this setting does not affect.
- [Configuration reference](@/docs/reference/configuration.md#auth) — the full `auth.jwt` key table, including `JWTLocation`/`JWTLocationConfig` shapes.
@@ -0,0 +1,73 @@
+++
title = "Load static data"
description = "Load read-only JSON data (hyperparameters, banlists, calendars) once per process using loco_rs::data, without a database round trip."
date = 2026-07-03T00:00:00+00:00
updated = 2026-07-03T00:00:00+00:00
draft = false
weight = 34
sort_by = "weight"
template = "docs/page.html"
aliases = ["/docs/infrastructure/data/"]
[extra]
lead = ""
toc = true
top = false
+++
Goal: give your app access to read-only data that lives in a JSON file — loaded once and kept in memory — without standing up a database table or hand-rolling file I/O.
This is a good fit for data that's read far more often than it changes: machine learning hyperparameters, an IP banlist, calendar events, stock data, security policies, per-container configuration. If the data changes on every request, reach for the [cache](@/docs/how-to/use-cache.md) or the database instead.
## 1. Generate a data loader
```
$ cargo loco g data stocks
added: "data/stocks/data.json"
added: "src/data/stocks.rs"
injected: "src/data/mod.rs"
* Data loader `Stocks` was added successfully.
```
This creates a `data/stocks/data.json` file (next to `src/`, the same way `config/` sits next to `src/`) and a `src/data/stocks.rs` module under the `crate::data::stocks` namespace.
## 2. Shape your data
`src/data/stocks.rs` starts with a placeholder struct:
```rust
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Stocks {
pub is_loaded: bool,
}
```
Replace it with a struct matching the real shape of `data/stocks/data.json` (tools like [quicktype](https://quicktype.io/) can generate this from a sample file). Any `serde`-friendly type works.
## 3. Load the data
Under the hood, the generated module calls into `loco_rs::data`, which exposes two loader functions:
```rust
// asynchronous — use from controllers, workers, tasks
pub async fn load_json_file<T: DeserializeOwned>(path: &str) -> Result<T>;
// synchronous — use during boot, or outside an async context
pub fn load_json_file_sync<T: DeserializeOwned>(path: &str) -> Result<T>;
```
Both resolve `path` relative to a data folder — `data/` by default. Call `data::stocks::get()` from anywhere to read the in-memory copy, loaded once for the life of the process; it's cheap to call as many times as you like. Call `data::stocks::read()` if you instead want to re-read straight from disk on every call (this pays an I/O cost each time).
## 4. Point at a different data folder (optional)
The data folder defaults to `data/`, resolved relative to wherever the app binary runs. Set the `LOCO_DATA` environment variable to override it:
```
LOCO_DATA=/etc/myapp/data cargo loco start
```
Whatever machine runs the binary needs a `data/` folder (or the `LOCO_DATA` path) present alongside it, the same way it needs `config/`.
## Updating the data
Because the in-memory copy is loaded once per process, picking up new data means restarting the process — conceptually similar to a deploy, but without rebuilding or shipping new code, so it's fast. If you need to refresh without a restart, call the `read()` function directly and cache the result yourself using the [cache](@/docs/how-to/use-cache.md) system.
@@ -0,0 +1,122 @@
+++
title = "Write DB model tests"
description = "Boot a database-backed test app with boot_test, seed fixtures, and get automatic per-test DB cleanup via BootResultWrapper's Drop."
date = 2021-05-01T18:10:00+00:00
updated = 2021-05-01T18:10:00+00:00
draft = false
weight = 51
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
Goal: exercise your Sea-ORM models against a real database in a test, with fixture data loaded and the database cleaned up afterwards — with no manual teardown code.
This page covers the `with-db` half of the `testing` feature (`db.rs`): `boot_test`, `seed`, and the two DB-lifecycle strategies Loco supports. For HTTP-level tests, see [request tests](@/docs/how-to/request-tests.md); for insta snapshots/redactions, see [Fixtures & snapshots](@/docs/how-to/fixtures-snapshots.md).
## 1. Enable the features
```toml
[dev-dependencies]
loco-rs = { version = "*", features = ["testing"] }
serial_test = "*"
```
`with-db` (on by default) must also be enabled on your `loco-rs` dependency for `db.rs`'s helpers (`seed`, `boot_test_with_create_db`, `TestSupport`, ...) to be compiled in.
## 2. Pick a DB-lifecycle strategy
Loco supports two ways to run DB tests, and both are legitimate — pick based on whether your test suite shares one test database or spins up a throwaway one per test.
### A. Shared test DB + truncate-before-boot (what `loco new` generates)
Point `config/test.yaml`'s `database.uri` at one test database (SQLite file or Postgres DB) and set `dangerously_truncate: true`. Every `boot_test::<App>()` call then truncates the configured tables before the test body runs, so each test starts from a clean slate — as long as tests run one at a time:
```yaml
# config/test.yaml
database:
uri: "sqlite://demo_test.sqlite?mode=rwc"
dangerously_truncate: true
```
```rust
use demo::app::App;
use loco_rs::testing::prelude::*;
use serial_test::serial;
#[tokio::test]
#[serial] // required: tests share one DB, so they must not interleave
async fn can_find_by_pid() {
let boot = boot_test::<App>().await.expect("failed to boot test app");
seed::<App>(&boot.app_context).await.expect("failed to seed");
let existing = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111").await;
assert!(existing.is_ok());
}
```
> ⚠️ `dangerously_truncate` clears data on every `boot_test` call — never point it at a database you care about (never production). Control exactly which tables get truncated via the `truncate` hook on your `Hooks` impl (`async fn truncate(ctx: &AppContext) -> Result<()> { truncate_table(&ctx.db, users::Entity).await }`).
### B. Fresh, unique database per test (no `#[serial]` needed for DB isolation)
`boot_test_with_create_db::<App>()` creates a brand-new, uniquely-named database before booting, and returns a `BootResultWrapper` instead of a plain `BootResult`:
```rust
use demo::app::App;
use loco_rs::testing::prelude::*;
#[tokio::test]
async fn can_register_in_isolated_db() {
let boot = boot_test_with_create_db::<App>()
.await
.expect("failed to boot test app with a fresh db");
// BootResultWrapper derefs to BootResult, so `boot.app_context` works as usual
seed::<App>(&boot.app_context).await.expect("failed to seed");
// ... exercise boot.app_context.db ...
} // <- `boot` drops here: the throwaway database is cleaned up automatically
```
How the unique DB is provisioned depends on the scheme of `config.database.uri` (dispatched in `init_test_db_creation`):
| URI scheme | Backing strategy |
|---|---|
| `postgres://` | Creates a new database named `_loco_test_{10-char-random}_{unix-timestamp}` on the same Postgres server. |
| `sqlite://` | Backs the DB with a `tree-fs` temporary file (`test.sqlite` in a fresh temp dir). |
| anything else | No-op passthrough (`Any`) — used as-is, no isolation. |
### `BootResultWrapper` auto-cleans on `Drop`
`BootResultWrapper` (only present with `with-db`) wraps a `BootResult` plus the `Box<dyn TestSupport>` that created the throwaway DB:
- It `Deref`s to `BootResult`, so `boot.app_context`, `boot.router`, etc. all work exactly like the plain `boot_test` return value.
- Its `Drop` implementation calls `test_db.cleanup_db()` — for Postgres this `DROP DATABASE`s the throwaway DB on a spawned blocking task; for SQLite it removes the temp directory. This runs automatically at the end of the test function's scope, with no explicit teardown call needed.
> **Caveat:** if the test process is killed mid-run (e.g. `Ctrl+C`), `Drop` never fires and the throwaway database/schema is left behind — you'll need to remove it manually (`DROP DATABASE _loco_test_...` for Postgres, or delete the leftover temp dir for SQLite).
## 3. Seed fixture data
`seed::<App>(&ctx)` loads fixtures from the hardcoded `src/fixtures` folder by delegating to your `Hooks::seed` implementation:
```rust
seed::<App>(&boot.app_context).await.expect("failed to seed");
```
This is the same fixture format used by `cargo loco db seed` — see the [`db seed` CLI reference](@/docs/reference/cli.md#2-2-db-subcommands) for the on-disk layout and the `--from <DIR>` flag if you keep fixtures elsewhere.
## 4. Generated model tests already do this for you
`cargo loco generate model <name> ...` (and `scaffold`) scaffold a starter test in `tests/models/<name>.rs` that already follows pattern A above — `boot_test::<App>()`, `seed::<App>()`, a local `configure_insta!()` macro for snapshot naming, and a commented-out `assert_debug_snapshot!` call for you to fill in. See [Using generators](@/docs/how-to/use-generators.md).
## Verify it
```sh
cargo test
```
For pattern A, run the whole suite (or at least the model tests) together so `#[serial]` can do its job — running a single test in isolation (`cargo test can_find_by_pid`) also works, but mixing serial and non-serial DB tests in the same binary without `#[serial]` on all of them will produce flaky failures from concurrent truncation.
@@ -0,0 +1,117 @@
+++
title = "Connect a second database"
description = "Attach an extra database connection, or several, using the built-in multi_db initializer."
date = 2021-05-01T18:10:00+00:00
updated = 2021-05-01T18:10:00+00:00
draft = false
weight = 5
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
**Goal:** query a second (or third+) database from a controller, alongside the app's primary `ctx.db` connection — for example, a read replica, a legacy database, or per-tenant databases.
Loco ships a ready-made [initializer](@/docs/how-to/add-middleware.md) for this: `MultiDbInitializer`, a named map of connections. It lives under `loco_rs::initializers::multi_db` and is gated behind the `with-db` feature. Each config entry accepts the same keys as the primary `database:` block — see [Configuration § database](@/docs/reference/configuration.md) for the full list (`uri`, `enable_logging`, `min_connections`, `max_connections`, `connect_timeout`, `idle_timeout`, `acquire_timeout`, `auto_migrate`, `dangerously_truncate`, `dangerously_recreate`, `run_on_start`).
## Configure each named database
Add one or more entries under a `multi_db` map, nested under the top-level `initializers` key in your environment config. A single extra connection is just a one-entry map:
```yaml
initializers:
multi_db:
secondary_db:
uri: postgres://loco:loco@localhost:5432/loco_app
enable_logging: false
connect_timeout: 500
idle_timeout: 500
min_connections: 1
max_connections: 1
auto_migrate: false
dangerously_truncate: false
dangerously_recreate: false
```
Add more entries to open more connections:
```yaml
initializers:
multi_db:
secondary_db:
uri: postgres://loco:loco@localhost:5432/loco_app
enable_logging: false
connect_timeout: 500
idle_timeout: 500
min_connections: 1
max_connections: 1
auto_migrate: false
dangerously_truncate: false
dangerously_recreate: false
third_db:
uri: postgres://loco:loco@localhost:5432/loco_app_reporting
enable_logging: false
connect_timeout: 500
idle_timeout: 500
min_connections: 1
max_connections: 1
auto_migrate: false
dangerously_truncate: false
dangerously_recreate: false
```
## Register the initializer
```rust
use loco_rs::app::{AppContext, Initializer};
async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
let initializers: Vec<Box<dyn Initializer>> = vec![
Box::new(loco_rs::initializers::multi_db::MultiDbInitializer),
];
Ok(initializers)
}
```
## Look connections up by name
`MultiDbInitializer` layers a `loco_rs::db::MultiDb` (a thin `HashMap<String, DatabaseConnection>` wrapper) as an axum `Extension`:
```rust
use sea_orm::EntityTrait;
use axum::{response::IntoResponse, Extension};
use loco_rs::db::MultiDb;
pub async fn list(
State(ctx): State<AppContext>,
Extension(multi_db): Extension<MultiDb>,
) -> Result<impl IntoResponse> {
let third_db = multi_db.get("third_db")?;
let res = Entity::find().all(third_db).await;
format::json(res)
}
```
`multi_db.get(name)` returns an error if that key isn't configured — no silent `None`/panic.
## Result
`ctx.db` remains your app's primary connection (used for auto-migration, boot-time checks, etc.); the extra connection(s) arrive purely through the `MultiDb` axum `Extension` and only in handlers that ask for them.
## Migrating from `extra_db`
`ExtraDbInitializer` has been removed in favor of `MultiDbInitializer`. To migrate:
- Config: former `initializers.extra_db: { ... }` becomes `initializers.multi_db: { <name>: { ... } }` — pick a name for your connection and nest the same keys under it.
- Registration: swap `Box::new(loco_rs::initializers::extra_db::ExtraDbInitializer)` for `Box::new(loco_rs::initializers::multi_db::MultiDbInitializer)`.
- Handlers: change `Extension(db): Extension<DatabaseConnection>` to `Extension(multi_db): Extension<MultiDb>`, then look up the connection with `let db = multi_db.get("<name>")?;`.
## Next
- [Configuration reference](@/docs/reference/configuration.md) for every key a `database:`-shaped block accepts.
- [Add middleware](@/docs/how-to/add-middleware.md) for how the `initializers`/middleware hooks work.
@@ -0,0 +1,76 @@
+++
title = "Override a built-in generator template"
description = "Use `cargo loco generate override` to copy a built-in .t template into your app so you can customize what generators produce."
date = 2021-05-01T18:10:00+00:00
updated = 2021-05-01T18:10:00+00:00
draft = false
weight = 61
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
Goal: change what `cargo loco generate <kind>` produces — e.g. add a header to every generated controller, or tweak the HTMX scaffold views — without forking Loco.
Every generator kind covered in [Using generators](@/docs/how-to/use-generators.md) is driven by `.t` template files baked into the `loco-gen` crate. `cargo loco generate override` copies one of those templates (or a whole folder of them) into your app's own `.loco-templates/` directory; from then on, generation runs read your copy instead of the built-in one.
## 1. List what's overridable
Run `override` with no path to see every template, grouped by generator kind:
```sh
cargo loco generate override
```
This prints a tree of all available templates plus example invocations — it ignores `--info` (a bare invocation always lists).
## 2. Preview a specific file or folder with `--info`
Before copying, check what's under a given path (without actually copying anything) by adding `--info`:
```sh
cargo loco generate override scaffold/htmx --info
```
## 3. Copy one file, a folder, or everything
```sh
# override a single template file
cargo loco generate override scaffold/api/controller.t
# override every template under a folder (e.g. the whole htmx scaffold)
cargo loco generate override scaffold/htmx
# override every template in the project
cargo loco generate override .
```
Copied files land under `.loco-templates/` (mirroring the built-in path, e.g. `.loco-templates/scaffold/api/controller.t`) and the command prints each file it copied. If nothing matched the given path, it tells you no templates were found instead of silently no-op'ing.
## 4. Edit your copy
Open the copied `.t` file under `.loco-templates/` and edit it like any other [Tera](https://keats.github.io/tera/) template — it uses the same variables (`name`, `pkg_name`, casing filters like `snake_case`/`pascal_case`, etc.) as the built-in one you copied it from. The next time you run the matching `cargo loco generate <kind> ...`, your local copy is used instead of the built-in template.
## 5. Revert to the built-in template
Delete your local copy — Loco always prefers `.loco-templates/` when it exists, and falls back to the built-in template the moment it doesn't:
```sh
rm .loco-templates/scaffold/api/controller.t
```
## Verify it
```sh
cargo loco generate override scaffold/api/controller.t
# edit .loco-templates/scaffold/api/controller.t
cargo loco generate controller widgets index
```
Confirm the generated `src/controllers/widgets.rs` reflects your edited template (e.g. the header/comment you added), not the stock output.
See the [Generators & field types reference](@/docs/reference/generators.md#override) for the exact `override` CLI shape, and the [CLI reference](@/docs/reference/cli.md#2-4-generate-subcommands) for how `override` fits among the other `generate` subcommands.
@@ -0,0 +1,111 @@
+++
title = "Paginate query results"
description = "Page over entities with paginate/fetch_page, accept page/page_size from a request with PaginationQuery, and return a PageResponse from a controller."
date = 2021-05-01T18:10:00+00:00
updated = 2021-05-01T18:10:00+00:00
draft = false
weight = 3
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
**Goal:** return a page of rows plus paging metadata (`total_pages`, `total_items`, ...) from a controller endpoint, instead of loading an entire table.
This builds on [Query data with the condition DSL](@/docs/how-to/query-data.md). For exact signatures and the `PagerMeta` shape, see [Query DSL & pagination](@/docs/reference/query-pagination.md#pagination).
## 1. Accept pagination params in your request
`PaginationQuery` is a small, `#[serde(flatten)]`-friendly struct with two fields, `page` (1-based, default `1`) and `page_size` (default `25`). Flatten it into your controller's own query-params struct so callers can pass `?page=2&page_size=10` alongside your own filters:
```rust
use loco_rs::prelude::*;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct ListQueryParams {
pub title: Option<String>,
#[serde(flatten)]
pub pagination: query::PaginationQuery,
}
```
## 2. Paginate an entity query with an optional condition
`query::paginate` takes a `Select<E>` (an unresolved entity query), an optional pre-built `Condition`, and a `&PaginationQuery`. It applies the condition for you, so don't call `.filter()` yourself first:
```rust
use axum::extract::{Query, State};
pub async fn list(
State(ctx): State<AppContext>,
Query(params): Query<ListQueryParams>,
) -> Result<Response> {
let condition = params
.title
.as_ref()
.map(|t| query::condition().contains(posts::Column::Title, t).build());
let res = query::paginate(
&ctx.db,
posts::Entity::find(),
condition,
&params.pagination,
)
.await?;
format::json(res)
}
```
## 3. Or paginate a pre-built selector with `fetch_page`
Use `fetch_page` when you've already composed a `Select` (with your own `.filter()`/`.order_by()`/joins) and just need to page over it — it takes no separate condition argument:
```rust
let selector = posts::Entity::find()
.filter(query::condition().eq(posts::Column::UserId, user_id).build())
.order_by_desc(posts::Column::CreatedAt);
let res = query::fetch_page(&ctx.db, selector, &query::PaginationQuery::page(2)).await?;
```
## 4. What you get back
Both functions return `LocoResult<PageResponse<T>>`:
```rust
pub struct PageResponse<T> {
pub page: Vec<T>,
pub meta: PagerMeta,
}
pub struct PagerMeta {
pub page: u64,
pub page_size: u64,
pub total_pages: u64,
pub total_items: u64,
}
```
Returning `res` from a controller (e.g. via `format::json(res)`) serializes to:
```json
{
"page": [ { "id": 1, "title": "..." }, ... ],
"meta": { "page": 2, "page_size": 25, "total_pages": 4, "total_items": 87 }
}
```
## Result
A request like `GET /posts?page=2&page_size=10&title=loco` returns exactly one page of matching rows plus enough metadata for a client to render "page 2 of 4" or build next/prev links — without loading the whole table or hand-writing `OFFSET`/`LIMIT` math. Remember `page` is 1-based on the way in; both functions handle the conversion to Sea-ORM's 0-based paging internally.
## Next
- [Query DSL & pagination reference](@/docs/reference/query-pagination.md) for `PaginationQuery`'s exact defaults and the `paginate`/`fetch_page` signatures.
- [Query data](@/docs/how-to/query-data.md) for building the `Condition` you pass in.
@@ -0,0 +1,120 @@
+++
title = "Query data with the condition DSL"
description = "Filter entities with ConditionBuilder operators, build date ranges, and sort results — without hand-writing Sea-ORM conditions."
date = 2021-05-01T18:10:00+00:00
updated = 2021-05-01T18:10:00+00:00
draft = false
weight = 2
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
**Goal:** filter rows from a model using Loco's `ConditionBuilder` fluent DSL instead of hand-assembling a Sea-ORM `Condition`.
This assumes a working model (see [Add a model](@/docs/how-to/add-model.md)). For the exhaustive operator list, exact signatures, and the `date_range` boundary semantics, see [Query DSL & pagination](@/docs/reference/query-pagination.md).
## 1. Import the DSL
`query` is the model-layer query module, reachable straight from the prelude:
```rust
use loco_rs::prelude::*;
use sea_orm::EntityTrait;
```
`query::condition()` starts a builder; `.build()` finalizes it into a `sea_orm::Condition` you pass to `.filter(..)`.
## 2. Build a simple filter
```rust
let cond = query::condition()
.eq(users::Column::Email, "user1@example.com")
.build();
let user = users::Entity::find().filter(cond).one(&db).await?;
```
This is the same pattern the demo app's `users` model uses for all of its lookups, e.g. `Model::find_by_email` in `examples/demo/src/models/users.rs`:
```rust
pub async fn find_by_email(db: &DatabaseConnection, email: &str) -> ModelResult<Self> {
let user = users::Entity::find()
.filter(
model::query::condition()
.eq(users::Column::Email, email)
.build(),
)
.one(db)
.await?;
user.ok_or_else(|| ModelError::EntityNotFound)
}
```
## 3. Chain multiple conditions
Every operator returns `Self`, so conditions chain and AND together by default:
```rust
let cond = query::condition()
.contains(posts::Column::Title, "loco")
.gt(posts::Column::Views, 10)
.is_not_null(posts::Column::PublishedAt)
.build();
let published = posts::Entity::find().filter(cond).all(&db).await?;
```
Available operators (see the [reference](@/docs/reference/query-pagination.md#operators) for the full table): `eq`/`ne`, `gt`/`gte`/`lt`/`lte`, `between`/`not_between`, `like`/`not_like`, `starts_with`/`ends_with`/`contains`, `is_null`/`is_not_null`, `is_in`/`is_not_in`, and `date_range`. Each also exists as a free function (`query::eq(col, v)`) that starts a new builder — useful when you only need one condition.
## 4. Filter a date range
`date_range` returns a `DateRangeBuilder` instead of `Self`, because a range can have zero, one, or two bounds:
```rust
use chrono::NaiveDateTime;
let from: NaiveDateTime = /* ... */;
let to: NaiveDateTime = /* ... */;
let cond = query::condition()
.date_range(posts::Column::CreatedAt)
.dates(Some(&from), Some(&to))
.build() // DateRangeBuilder -> ConditionBuilder
.build(); // ConditionBuilder -> Condition
```
<div class="infobox">
Boundary behavior is asymmetric: a single-ended range (only <code>from</code> or only <code>to</code>) is <b>strict</b> (<code>&gt;</code> / <code>&lt;</code>), but a double-ended range (both <code>from</code> and <code>to</code>) is <b>inclusive</b> (<code>BETWEEN</code>). See <a href="@/docs/reference/query-pagination.md#daterangebuilder-date-range-filtering">the reference</a> for the full table.
</div>
## 5. Sort results
`SortDirection` is a small serde-friendly enum that converts to Sea-ORM's `Order` — it's orthogonal to `ConditionBuilder`, meant to pair with `.order_by()`:
```rust
use loco_rs::model::query::SortDirection;
let direction = SortDirection::Desc;
let recent = posts::Entity::find()
.filter(cond)
.order_by(posts::Column::CreatedAt, direction.order())
.all(&db)
.await?;
```
Because `SortDirection` derives `Deserialize`/`Serialize` with `"asc"`/`"desc"` renames, it deserializes directly from a query-string parameter (e.g. `?sort=desc` in a controller's `Query<T>` extractor).
## Result
You have a `Condition` built from readable, chainable method calls instead of raw Sea-ORM `Condition::all().add(...)` boilerplate, and it composes with `.filter()`, `.order_by()`, and — as the next guide shows — `query::paginate`.
## Next
- [Paginate results](@/docs/how-to/paginate.md) using the condition you just built.
- [Query DSL & pagination reference](@/docs/reference/query-pagination.md) for every operator's exact SQL output.
@@ -0,0 +1,159 @@
+++
title = "Render server-side views"
description = "Render HTML with Loco's Tera-based ViewRenderer: create a template, wire the ViewEngine extractor, and use the format::render() builder."
date = 2026-07-03T00:00:00+00:00
updated = 2026-07-03T00:00:00+00:00
draft = false
weight = 12
sort_by = "weight"
template = "docs/page.html"
aliases = ["/docs/the-app/views/"]
[extra]
lead = ""
toc = true
top = false
+++
**Goal:** return server-rendered HTML from a controller using Loco's built-in [Tera](http://keats.github.io/tera/)-based view engine.
This assumes a working app with the view engine initializer configured (the SaaS/HTML starters have this out of the box). If you generated an API-only app and are adding HTML for the first time, see step 5.
## 1. Create a template
Templates live under `assets/views/` (the `assets/` folder sits next to `src/` and `config/` at your project root):
```html
<!-- assets/views/home/hello.html -->
<html>
<body>
<h1>{{ title }}</h1>
</body>
</html>
```
## 2. Wrap it in a typed view function
Encapsulate the template call so controllers never touch Tera or template paths directly — this is what lets you swap view engines later without touching controllers.
```rust
// src/views/dashboard.rs
use loco_rs::prelude::*;
pub fn home(v: impl ViewRenderer) -> Result<impl IntoResponse> {
format::render().view(&v, "home/hello.html", data!({"title": "Loco"}))
}
```
Add it to `src/views/mod.rs`:
```rust
pub mod dashboard;
```
## 3. Extract the view engine in your controller
`ViewEngine<E>` is a `FromRequestParts` extractor — `TeraView` is the concrete engine Loco supplies. Both come from the prelude.
```rust
// src/controllers/dashboard.rs
use loco_rs::prelude::*;
use crate::views;
pub async fn render_home(ViewEngine(v): ViewEngine<TeraView>) -> Result<impl IntoResponse> {
views::dashboard::home(v)
}
pub fn routes() -> Routes {
Routes::new().prefix("home").add("/", get(render_home))
}
```
Register the controller's routes as usual — see [Add a controller](@/docs/how-to/add-controller.md).
`ViewEngine<E>` requires a `TeraLayer` `Extension` to be installed on the router; it panics with `"TeraLayer missing. Is the TeraLayer installed?"` if it isn't. This is wired up by the `ViewEngineInitializer` in `src/initializers/view_engine.rs` (present by default in HTML/HTMX starters) — see step 5 if you need to add it.
## 4. Two ways to render
**`format::view`** — the simplest form, renders directly to an HTML response:
```rust
pub fn home(v: impl ViewRenderer) -> Result<impl IntoResponse> {
format::view(&v, "home/hello.html", data!({"title": "Loco"}))
}
```
**`format::render()`** — a chainable builder when you need more than a bare 200 HTML body. It terminates with `.view(...)`, `.template(...)`, `.html(...)`, or `.json(...)`, and can add headers, an `ETag`, cookies, or a status code first:
```rust
pub fn home(v: impl ViewRenderer) -> Result<impl IntoResponse> {
format::render()
.etag("home-v1")?
.cookies(&[axum_extra::extract::cookie::Cookie::new("last_view", "home")])?
.view(&v, "home/hello.html", data!({"title": "Loco"}))
}
```
`format::render()` also has a `.response()` escape hatch that returns the underlying `axum::http::response::Builder` if you need something the chain doesn't cover, and `.redirect(to)` / `.redirect_with_header_key(key, to)` for redirects (see [Respond with different formats](@/docs/how-to/respond-formats.md)).
For an inline template string with no file on disk, use `format::template(tmpl, data)` (or the builder's `.template(...)`) instead of `.view(...)`.
## 5. Enabling the view engine on an API-only app
If your app doesn't already wire a view engine (headless/API apps don't), add the initializer:
```rust
// src/initializers/view_engine.rs
use async_trait::async_trait;
use axum::{Extension, Router as AxumRouter};
use loco_rs::{
app::{AppContext, Initializer},
controller::views::{engines, ViewEngine},
Result,
};
pub struct ViewEngineInitializer;
#[async_trait]
impl Initializer for ViewEngineInitializer {
fn name(&self) -> String {
"view-engine".to_string()
}
async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {
let tera_engine = engines::TeraView::build()?;
Ok(router.layer(Extension(ViewEngine::from(tera_engine))))
}
}
```
Register it in `src/app.rs`:
```rust
async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
Ok(vec![Box::new(initializers::view_engine::ViewEngineInitializer)])
}
```
`TeraView::build()` loads templates from `assets/views` (`DEFAULT_ASSET_FOLDER = "assets"`). Use `TeraView::build_with_post_process(|tera| { ... })` instead if you need to register custom Tera functions (e.g. an i18n `t(...)` function) — see the demo app's `src/initializers/view_engine.rs` for a working example with `fluent-templates`.
## 6. Serving static assets referenced by your templates
Templates that reference `<img src="/static/...">` need the `static` middleware — see [Serve static & SPA assets](@/docs/how-to/serve-assets.md).
## 7. Use a different template engine entirely
Because controllers only depend on `ViewRenderer` (a one-method trait: `render<S: Serialize>(&self, key: &str, data: S) -> Result<String>`), you can substitute Tera for anything else. Implement `ViewRenderer` for your own type, register it via an `Initializer` the same way as `TeraView` above, and swap the extractor's generic parameter (`ViewEngine<TeraView>` → `ViewEngine<YourEngine>`) — no controller logic changes.
## Verify
```sh
cargo loco routes # confirm GET /home is registered
curl -s localhost:5150/home
```
## Next
- [Respond with different formats](@/docs/how-to/respond-formats.md) — JSON/HTML/YAML and content negotiation
- [Serve static & SPA assets](@/docs/how-to/serve-assets.md)
- [Handle errors](@/docs/how-to/handle-errors.md)
@@ -0,0 +1,127 @@
+++
title = "Write request (controller) tests"
description = "Boot a test instance of your app and drive its HTTP routes with request/boot_test, RequestConfigBuilder, and axum-test assertions."
date = 2021-05-01T18:10:00+00:00
updated = 2021-05-01T18:10:00+00:00
draft = false
weight = 50
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
Goal: call your app's HTTP endpoints from a test — without a real bound port — and assert on the response, using Loco's `request`/`boot_test` helpers over [axum-test](https://crates.io/crates/axum-test).
## 1. Enable the `testing` feature
Request tests need the `testing` Cargo feature (pulls in `axum-test`, `scraper`, and `tree-fs`). Add it to your `dev-dependencies` — it's already there in apps generated by `loco new`:
```toml
[dev-dependencies]
loco-rs = { version = "*", features = ["testing"] }
serial_test = "*"
insta = { version = "*", features = ["redactions"] }
```
## 2. Write a request test with `request::<App, _, _>()`
`request` boots your app in `Environment::Test` and gives you an in-memory `TestServer` plus the app's `AppContext` — no DB is created:
```rust
use demo::app::App;
use loco_rs::testing::prelude::*;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn can_get_notes() {
request::<App, _, _>(|request, _ctx| async move {
let res = request.get("/api/notes/").await;
assert_eq!(res.status_code(), 200);
})
.await;
}
```
The callback receives `(TestServer, AppContext)` — `request` (an [axum-test](https://crates.io/crates/axum-test) `TestServer`) drives HTTP calls (`.get`, `.post`, `.json(...)`, etc.), and `ctx` gives you the same `AppContext` your controllers see (DB connection, config, mailer, ...) — see the [AppContext reference](@/docs/reference/app-context.md).
Mark tests `#[serial]` (from the `serial_test` crate) whenever they share app-level or DB-level state with other tests, so they don't run concurrently against the same fixtures.
If your test needs a real, freshly created database (registering a user, then reading it back), use `request_with_create_db` instead — see [DB model tests](@/docs/how-to/model-tests.md) for the full story on DB-backed tests and cleanup.
## 3. Assert on the response
`request`/`response` come straight from axum-test — the common assertions:
```rust
let response = request.post("/api/auth/login").json(&payload).await;
assert_eq!(response.status_code(), 200);
response.assert_json(&serde_json::json!({ "token": "..." }));
let body: LoginResponse = serde_json::from_str(&response.text()).unwrap();
```
For HTML/HTMX responses, parse `response.text()` with the [HTML selector assertions](@/docs/how-to/fixtures-snapshots.md#html-assertions-with-select) (`assert_css_exists`, `assert_css_eq`, `select`, ...) instead of string-matching raw markup.
## 4. Customize the request with `RequestConfigBuilder`
`request` uses a default `RequestConfig` (no saved cookies, `default_content_type: "application/json"`). To change that — e.g. to keep cookies across calls in the same test (cookie/session auth flows) — build a custom config and use `request_with_config`:
```rust
use loco_rs::testing::prelude::*;
let config = RequestConfigBuilder::new()
.save_cookies(true)
.default_content_type("application/json")
.build();
request_with_config::<App, _, _>(config, |request, _ctx| async move {
// cookies set by one call are sent on subsequent calls in this closure
request.post("/session/login").json(&payload).await;
let res = request.get("/session/me").await;
assert_eq!(res.status_code(), 200);
})
.await;
```
`RequestConfigBuilder` methods: `.save_cookies(bool)`, `.default_content_type(impl Into<String>)`, `.default_scheme(impl Into<String>)`, `.build()`.
> **Gotcha:** `RequestConfig::default_scheme` is *not* forwarded to axum-test's underlying `TestServerConfig` — only `default_content_type` and `save_cookies` are. Setting `.default_scheme(...)` has no observable effect today; don't rely on it to force `https`.
## 5. Reach for `boot_test` directly when you don't need HTTP
`request` is a thin wrapper: it calls `boot_test::<App>()` to boot the app, then wraps the router in a `TestServer`. If you don't need to go through HTTP at all — e.g. you're testing a model or service function directly — call `boot_test` yourself and skip the server:
```rust
use demo::app::App;
use loco_rs::testing::prelude::*;
#[tokio::test]
#[serial]
async fn test_something() {
let boot = boot_test::<App>().await.expect("failed to boot test app");
// use boot.app_context, e.g. boot.app_context.db, boot.app_context.config, ...
}
```
`boot_test<H: Hooks>()` is **single-generic** — it takes only your `Hooks` implementation (typically `App`), not a second `Migrator` type parameter. Signatures live in `src/testing/request.rs`:
| Function | Use when |
|---|---|
| `boot_test::<App>()` | You need `AppContext` without a DB, and without HTTP. |
| `boot_test_with_create_db::<App>()` | You need `AppContext` backed by a **fresh, throwaway database** (with-db only). See [DB model tests](@/docs/how-to/model-tests.md). |
| `boot_test_unique_port::<App>(port)` | You need the server actually bound to a TCP port (rare — most tests should use `request`/`TestServer` instead). |
`request`/`boot_test` both boot with `Environment::Test`, so they read `config/test.yaml` — see [configuration precedence](@/docs/reference/configuration.md#loading-precedence) for how the config folder and environment name are resolved.
## Verify it
```sh
cargo test
```
A passing test prints the usual `test result: ok` from `cargo test`; a failing status-code or JSON assertion panics with axum-test's standard diff/message.
@@ -0,0 +1,151 @@
+++
title = "Respond with different formats"
description = "Use the format:: response helpers (json, text, html, yaml, redirect, empty) and negotiate content type with RespondTo/Format."
date = 2026-07-03T00:00:00+00:00
updated = 2026-07-03T00:00:00+00:00
draft = false
weight = 13
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
**Goal:** return the right response shape (JSON, HTML, plain text, YAML, a redirect, an empty body) from a handler, and — when a single endpoint must serve more than one format — pick the shape based on the request's `Content-Type`/`Accept` header.
This assumes a working controller — see [Add a controller](@/docs/how-to/add-controller.md). All helpers live in the `format` module (`loco_rs::controller::format`, re-exported as `format` from the prelude). Keep handlers returning `Result<impl IntoResponse>` (or `Result<Response>`) so you can freely swap which `format::*` call you return.
## 1. Simple responses
```rust
use loco_rs::prelude::*;
async fn as_json() -> Result<Response> {
format::json(serde_json::json!({ "hello": "world" }))
}
async fn as_text() -> Result<Response> {
format::text("hello, world")
}
async fn as_html() -> Result<Response> {
format::html("<h1>hello</h1>")
}
async fn as_yaml() -> Result<Response> {
format::yaml("openapi: 3.1.0\ninfo:\n title: my api\n")
// sets Content-Type: application/yaml
}
async fn nothing() -> Result<Response> {
format::empty() // 200, empty body
}
async fn empty_object() -> Result<Response> {
format::empty_json() // 200, body: {}
}
async fn go_elsewhere() -> Result<Response> {
format::redirect("/dashboard") // axum::response::Redirect::to(..)
}
```
`format::view`/`format::template` render HTML from a Tera view or an inline template string — see [Render server-side views](@/docs/how-to/render-views.md).
## 2. When you need more than a one-liner: `format::render()`
`format::render()` returns a `RenderBuilder` you chain, terminating with one of `.json(..)`, `.html(..)`, `.text(..)`, `.empty()`, `.view(..)`, `.template(..)`, `.redirect(..)`, or `.redirect_with_header_key(..)`:
```rust
async fn get_one(State(ctx): State<AppContext>) -> Result<Response> {
format::render()
.etag("some-etag-value")?
.header("X-Custom", "1")
.json(load_item(&ctx).await?)
}
```
Available builder methods:
| Method | Purpose |
|---|---|
| `.status(code)` | set the response status (defaults to `200`) |
| `.header(key, value)` | add a single response header |
| `.etag(value)` | set the `ETag` header (errors on non-visible-ASCII input) |
| `.cookies(&[Cookie, ..])` | add one `Set-Cookie` header per cookie |
| `.response()` | escape hatch: hand back the raw `axum::http::response::Builder` |
| `.redirect(to)` | `303 See Other` with `Location: <to>` |
| `.redirect_with_header_key(key, to)` | same, but with a custom header instead of `Location` — e.g. `HX-Redirect` for HTMX |
```rust
async fn htmx_redirect() -> Result<Response> {
format::render().redirect_with_header_key("HX-Redirect", "/notes")
}
```
## 3. Content negotiation: respond differently per client
Use the `RespondTo` extractor (or its wrapper `Format(pub RespondTo)`) to detect the request's format from `Content-Type` (checked first) or, failing that, `Accept`:
```rust
use loco_rs::prelude::*;
pub async fn get_one(
respond_to: RespondTo,
Path(id): Path<i64>,
State(ctx): State<AppContext>,
) -> Result<Response> {
let item = load_item(&ctx, id).await?;
match respond_to {
RespondTo::Html => format::html(&format!("<html><body>{:?}</body></html>", item.title)),
_ => format::json(item),
}
}
```
`RespondTo` variants: `None` (neither header present/parseable), `Html`, `Json`, `Xml`, `Other(String)` (any other MIME type, preserved verbatim). Both `RespondTo` and `Format` implement `FromRequestParts`, so you can extract either one directly as a handler parameter — `Format(respond_to)` if you prefer the wrapped form.
## 4. Combine format negotiation with error handling
A common pattern: run your fallible logic first, then match on both the `Result` and the format in one place, so error rendering stays consistent per-format:
```rust
pub async fn get_one(
respond_to: RespondTo,
Path(id): Path<i64>,
State(ctx): State<AppContext>,
) -> Result<Response> {
let res = load_item(&ctx, id).await;
match res {
Ok(item) => match respond_to {
RespondTo::Html => format::html(&format!("<html><body>{:?}</body></html>", item.title)),
_ => format::json(item),
},
Err(Error::Model(ModelError::Validation(errors))) => match respond_to {
RespondTo::Html => format::html(&format!("<html><body>errors: {errors:?}</body></html>")),
_ => bad_request("opaque message: cannot respond!"),
},
// unhandled error kinds: let the framework's default error rendering take over
Err(err) => Err(err),
}
}
```
See [Handle errors](@/docs/how-to/handle-errors.md) for what "the framework's default error rendering" produces for each `Error` variant.
## Verify
```sh
curl -s localhost:5150/notes/1 -H 'accept: application/json'
curl -s localhost:5150/notes/1 -H 'accept: text/html'
```
## Next
- [Render server-side views](@/docs/how-to/render-views.md)
- [Handle errors](@/docs/how-to/handle-errors.md)
- [Validate requests](@/docs/how-to/validate-requests.md)
@@ -0,0 +1,110 @@
+++
title = "Diagnose your app with cargo loco doctor"
description = "Run `cargo loco doctor` to validate DB/queue connectivity, dependency versions, and initializer health, with the --config and --production flags."
date = 2021-05-01T18:10:00+00:00
updated = 2021-05-01T18:10:00+00:00
draft = false
weight = 62
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
Goal: quickly check whether your app's environment (database, queue, tooling, dependency versions) is set up correctly, both locally and in CI.
## 1. Run it
```sh
cargo loco doctor
```
Each check prints one line with a status icon, and a description on the line(s) below when there's something actionable to say:
```
✅ DB connection: success
✅ queue connection: success
❌ SeaORM CLI was not found
To fix, run:
$ cargo install sea-orm-cli
```
- ✅ = `Ok`
- ❌ = `NotOk`
- ⚠️ = `NotConfigure` (the resource isn't configured — not necessarily an error)
If **any** check comes back `NotOk`, the process exits with a non-zero status — wire `cargo loco doctor` into CI to fail the build on a broken DB/queue connection or an outdated dependency.
## 2. What gets checked
`doctor` always runs:
| Check | Condition | What it does |
|---|---|---|
| Database | `with-db` enabled | Connects, pings, and verifies access using `config.database`. |
| Queue | `workers.mode` is `BackgroundQueue` | Creates the queue provider and pings it; reports `NotConfigure` if no queue is set up. |
| Initializer checks | any registered `Initializer` implements `check()` | Runs each one, prefixing its message with `Initializer {name}: `. |
...and, **only when not run with `--production`**, three more:
| Check | What it does |
|---|---|
| Deps | Reads `Cargo.lock` and flags any "blessed" dependency below its minimum version. |
| SeaOrmCLI | Runs `sea-orm-cli --version` and checks it against the minimum. |
| PublishedLocoVersion | Compares your `loco-rs` version against what's published on crates.io. |
Current blessed minimum versions: `tokio 1.33.0`, `sea-orm 2.0.0-rc`, `validator 0.20.0`, `axum 0.8.1`. (`sea-orm`/`sea-orm-cli` are still pinned to the `2.0.0-rc` line as of this writing — expect that floor to move to `2.0.0` once Sea-ORM ships stable.)
## 3. Skip dev-only checks in production with `--production`
Deployed environments typically don't have `sea-orm-cli` installed, may not have network access to crates.io, and don't need a "you're behind on X" nag on every boot. `--production` (short `-p`) skips the three dev-only checks above and only runs Database/Queue/Initializer checks:
```sh
cargo loco doctor --production
```
## 4. Inspect resolved configuration with `--config`
`--config` (short `-c`) bypasses checks entirely and instead dumps the fully-resolved `Config` (as YAML) plus the active environment name — useful when you're not sure which config file/environment actually got loaded:
```sh
cargo loco doctor --config
```
```
# ...your full resolved config, dumped as YAML...
Environment: development
```
This complements the [configuration reference](@/docs/reference/configuration.md#loading-precedence) — if a setting doesn't look like what you expect, `doctor --config` shows you the config *after* file precedence, environment resolution, and Tera templating have all been applied, not just what's on disk.
## 5. Add your own checks
If you ship a custom [`Initializer`](@/docs/how-to/add-middleware.md), implement its `check` method and `doctor` will pick it up automatically — no extra wiring needed:
```rust
use loco_rs::doctor::{Check, CheckStatus};
async fn check(&self, app_context: &AppContext) -> loco_rs::Result<Option<Check>> {
// return None to opt out, or Some(Check { .. }) to report a result
Ok(Some(Check {
status: CheckStatus::Ok,
message: "connected".to_string(),
description: None,
}))
}
```
## Verify it
```sh
cargo loco doctor
echo $? # 0 if every check passed, non-zero if any check is NotOk
cargo loco doctor --production
cargo loco doctor --config
```
See the [CLI reference](@/docs/reference/cli.md#2-1-top-level-subcommands) for the exact flag list alongside every other `cargo loco` subcommand.
@@ -0,0 +1,122 @@
+++
title = "Schedule recurring jobs"
description = "Configure the scheduler to run a task or shell command on a cron or English-language schedule."
date = 2021-05-01T18:10:00+00:00
updated = 2021-05-01T18:10:00+00:00
draft = false
weight = 22
sort_by = "weight"
template = "docs/page.html"
aliases = ["/docs/processing/scheduler/"]
[extra]
lead = ""
toc = true
top = false
+++
Goal: run a [task](@/docs/how-to/write-task.md) or a shell command on a recurring schedule, without hand-rolling `crontab`.
## 1. Create a scheduler config
Generate a dedicated file:
```sh
cargo loco generate scheduler
```
This creates `config/scheduler.yaml`. Alternatively, add a `scheduler:` block directly to your environment YAML (`config/development.yaml`, etc.) — both forms use the same schema.
## 2. Define jobs
```yaml
scheduler:
output: stdout # default output for all jobs: stdout | silent
jobs:
write_content:
shell: true # run `run` as a shell command (default: false = run a task)
run: "echo loco >> ./scheduler.txt"
schedule: run every 1 second # English syntax
output: silent # overrides the job-level default
tags: ["base", "infra"]
run_task:
run: "foo" # a registered task name
schedule: "at 10:00 am"
run_on_start: true # also run once when the scheduler starts
list_if_users:
run: "user_report"
shell: true
schedule: "* 2 * * * *" # cron syntax
tags: ["base", "users"]
```
Each job entry has:
| Key | Required? | Notes |
|---|---|---|
| `run` | yes | A shell command (if `shell: true`) or a registered task name plus optional `KEY:VALUE` args (if `shell: false`, the default) |
| `schedule` | yes | English phrase or cron expression — see below |
| `shell` | no, default `false` | `false` runs `run` as a task; `true` runs it as a shell command |
| `run_on_start` | no, default `false` | Also fire once immediately when the scheduler starts |
| `tags` | no | Group jobs so you can run them together with `--tag` |
| `output` | no | Overrides `scheduler.output` for this job only |
### Schedule syntax
`schedule` accepts either form — Loco auto-detects cron syntax by checking whether the string starts with a digit or `*`; anything else is parsed as English via `english_to_cron`:
- English: `every 15 seconds`, `run every minute`, `fire every day at 4:00 pm`, `at 10:00 am`, `run at midnight on the 1st and 15th of the month`, `On Sunday at 12:00`, `7pm every Thursday`, `midnight on Tuesdays`
- Cron (7 fields, **UTC**, includes seconds and year):
```
sec min hour day of month month day of week year
* * * * * * *
```
## 3. Verify the config
```sh
# dedicated file
cargo loco scheduler --config config/scheduler.yaml --list
# scheduler: block embedded in the environment file
LOCO_ENV=production cargo loco scheduler --list
```
## 4. Run it
As a standalone process:
```sh
cargo loco scheduler # uses scheduler: in config/<env>.yaml
cargo loco scheduler --config config/scheduler.yaml # uses a dedicated file
```
Or bundled with the server and worker in one process:
```sh
cargo loco start --all
```
If your jobs live in a dedicated `scheduler.yaml` rather than embedded in the environment file, `start --all` needs to be told where to find it — set `SCHEDULER_CONFIG`:
```sh
SCHEDULER_CONFIG=config/scheduler.yaml cargo loco start --all
```
Each firing spawns a **subprocess** (`/bin/sh -c` on Unix, `cmd.exe /C` on Windows); `LOCO_ENV` is propagated to it, so a task job resolves the same config/environment as the parent process. On shutdown (Ctrl+C), the scheduler waits for running jobs before exiting.
## 5. Run a subset by name or tag
```sh
LOCO_ENV=production cargo loco scheduler --name 'run_task'
LOCO_ENV=production cargo loco scheduler --tag 'base'
```
## Reference
- Writing the task a scheduler job invokes: [Write a task](@/docs/how-to/write-task.md)
- `scheduler`/`SCHEDULER_CONFIG` config keys: [Configuration reference](@/docs/reference/configuration.md)
- `cargo loco scheduler` flags: [CLI reference](@/docs/reference/cli.md)
@@ -0,0 +1,119 @@
+++
title = "Seed data"
description = "Load fixture data into a fresh database, wire it into Hooks::seed, and dump/import table contents with cargo loco db seed."
date = 2021-05-01T18:10:00+00:00
updated = 2021-05-01T18:10:00+00:00
draft = false
weight = 4
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
**Goal:** populate a freshly-migrated database with known rows from YAML fixture files — for local development, tests, and reproducing an environment's data elsewhere.
This assumes a working model (see [Add a model](@/docs/how-to/add-model.md)).
## 1. Write a fixture file
Fixtures live under `src/fixtures/`, one YAML file per table, containing a plain list of records:
```
src/
fixtures/
users.yaml
```
```yaml
# examples/demo/src/fixtures/users.yaml
---
- id: 1
pid: 11111111-1111-1111-1111-111111111111
email: user1@example.com
password: "$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc"
api_key: lo-95ec80d7-cb60-4b70-9b4b-9ef74cb88758
name: user1
created_at: "2023-11-12T12:34:56.789Z"
updated_at: "2023-11-12T12:34:56.789Z"
```
Include every `NOT NULL` column your migration defined; nullable columns can be omitted.
## 2. Wire the fixture into `Hooks::seed`
Add a call to `db::seed::<ActiveModel>` in your app's `Hooks::seed` implementation, one line per fixture file:
```rust
use std::path::Path;
use loco_rs::{app::{AppContext, Hooks}, db, Result};
impl Hooks for App {
// ...
async fn seed(ctx: &AppContext, base: &Path) -> Result<()> {
db::seed::<users::ActiveModel>(&ctx.db, &base.join("users.yaml").display().to_string())
.await?;
Ok(())
}
}
```
`db::seed` reads the YAML into `Vec<serde_json::Value>`, converts each row through `A::from_json`, inserts them with `insert_many`, and resets the table's auto-increment sequence afterward so subsequently-created rows don't collide with the seeded IDs.
## 3. Run the seed command
```sh
$ cargo loco db seed
```
By default this reads from `src/fixtures` and inserts into whatever environment you're targeting (`-e`/`--environment`, default `development`). Common flags:
```sh
# clear all data before seeding — useful for a repeatable dev/test reset
$ cargo loco db seed --reset
# seed from a different folder (e.g. environment-specific fixtures)
$ cargo loco db seed --from src/fixtures/staging
# target a specific environment
$ cargo loco db seed -e test
```
## 4. Dump existing data back to fixtures
The same command can go the other direction: export live table contents to YAML files, e.g. to capture a snapshot of production-like data for local fixtures.
```sh
# dump every table to --from's folder (default: src/fixtures)
$ cargo loco db seed --dump
# dump only specific tables
$ cargo loco db seed --dump-tables users,posts
```
`--dump`/`--dump-tables` and seeding are mutually exclusive for a single invocation — passing either one dumps instead of seeding.
## 5. Use seeding in tests
With the `testing` feature enabled, `boot_test` + `seed::<App>` gives each test a freshly-seeded database:
```rust
use loco_rs::testing::prelude::*;
#[tokio::test]
#[serial]
async fn can_find_seeded_user() {
let boot = boot_test::<App, Migrator>().await?;
seed::<App>(&boot.app_context).await?;
let user = Model::find_by_email(&boot.app_context.db, "user1@example.com").await;
assert!(user.is_ok());
}
```
## Result
`cargo loco db seed` (optionally with `--reset`) leaves your database populated with the exact rows in `src/fixtures/*.yaml`, and `cargo loco db seed --dump` lets you regenerate those same fixture files from a live database whenever your schema or sample data changes.
@@ -0,0 +1,218 @@
+++
title = "Send an email"
description = "Generate a mailer, write templates, and configure SMTP with the right TLS mode."
date = 2021-05-01T18:10:00+00:00
updated = 2021-05-01T18:10:00+00:00
draft = false
weight = 24
sort_by = "weight"
template = "docs/page.html"
aliases = ["/docs/processing/mailers/"]
[extra]
lead = ""
toc = true
top = false
+++
Goal: send a transactional email (welcome message, password reset, notification) from a controller or task, without blocking the request while SMTP does its thing.
A mailer delivers over SMTP in the background, using the same [background worker](@/docs/how-to/add-worker.md) infrastructure — calling a mailer enqueues a `MailerWorker` job (on the `"mailer"` queue) and returns immediately.
## 1. Generate a mailer
```sh
cargo loco generate mailer auth
```
This creates `src/mailers/auth.rs`, adds `pub mod auth;` to `src/mailers/mod.rs`, and scaffolds a `welcome/` template directory:
```
src/
mailers/
auth/
welcome/ <-- one directory per email, holding all its parts
subject.t
html.t
text.t
auth.rs <-- mailer definition
```
The generated mailer looks like this:
```rust
#![allow(non_upper_case_globals)]
use loco_rs::prelude::*;
use serde_json::json;
static welcome: Dir<'_> = include_dir!("src/mailers/auth/welcome");
#[allow(clippy::module_name_repetitions)]
pub struct AuthMailer {}
impl Mailer for AuthMailer {}
impl AuthMailer {
pub async fn send_welcome(ctx: &AppContext, to: &str, msg: &str) -> Result<()> {
Self::mail_template(
ctx,
&welcome,
mailer::Args {
to: to.to_string(),
locals: json!({
"message": msg,
"domain": ctx.config.server.full_url()
}),
..Default::default()
},
)
.await?;
Ok(())
}
}
```
A template directory must contain exactly three files — `subject.t`, `html.t`, `text.t` (all Tera templates, rendered against `locals`). Missing any one of them is an error at send time.
## 2. Call it from a controller
```rust
use crate::mailers::auth::AuthMailer;
async fn register(
State(ctx): State<AppContext>,
Json(params): Json<RegisterParams>,
) -> Result<Response> {
// .. register the user ..
AuthMailer::send_welcome(&ctx, &user.email, "Welcome!").await?;
format::json(())
}
```
`mail`/`mail_template` return as soon as the job is enqueued — actual SMTP delivery happens in a worker process.
## 3. Configure SMTP
```yaml
# config/development.yaml — local mail catcher (e.g. mailtutan, MailHog)
mailer:
smtp:
enable: true
host: localhost
port: 1025
secure: false
```
```yaml
# config/production.yaml — provider requiring implicit TLS on port 465
mailer:
smtp:
enable: true
host: smtp.example.com
port: 465
tls: implicit # overrides `secure` — see below
auth:
user: postmaster@mg.example.com
password: "{{ get_env(name='SMTP_PASSWORD') }}"
hello_name: mail.example.com # optional EHLO client id
```
### Picking the right `tls` mode
`tls` is the authoritative setting; when present it **overrides** the legacy `secure` boolean:
| `tls` value | Port | Behavior |
|---|---|---|
| `starttls` | 587 (typical) | Connects in cleartext, then upgrades with `STARTTLS`. This is what `secure: true` used to (and still does) select. |
| `implicit` | 465 (typical) | Connection is encrypted from the first byte (SMTPS). **Required** for providers that only accept implicit TLS — `STARTTLS` will not work against a 465 listener. |
| `none` | — | Cleartext, no TLS. Local sinks (Mailpit, mailtutan) only. |
If `tls` is omitted, the legacy `secure` field still works: `secure: true` → `starttls`, `secure: false` → `none`. If you're setting up a provider that documents "port 465 / SMTPS," set `tls: implicit` explicitly — `secure: true` alone cannot express that mode.
## 4. Set a default from-address or priority
Override `opts()` on your mailer:
```rust
impl Mailer for AuthMailer {
fn opts() -> MailerOpts {
MailerOpts {
from: "Acme <noreply@acme.example>".to_string(),
reply_to: None,
priority: 100, // default background-queue priority for mailer jobs
}
}
}
```
Mailer jobs enqueue at priority `100` by default (`DEFAULT_MAILER_PRIORITY`) — see [Choose a queue backend](@/docs/how-to/choose-queue-backend.md#priority-queues) for what priority means across queue backends. Raise it if a particular mailer's messages (e.g. password resets) should jump ahead of lower-priority background work.
## 5. CC, BCC, and threading headers
`Args` (passed to `mail_template`) and `Email` (passed to `mail`) both support `cc`, `bcc`, and a `headers` field for threading:
```rust
Self::mail_template(
ctx,
&welcome,
mailer::Args {
to: user.email.clone(),
cc: Some("audit@acme.example".to_string()),
bcc: Some("archive@acme.example".to_string()),
headers: Some(mailer::EmailHeaders {
in_reply_to: Some(original_message_id.clone()),
references: Some(original_message_id.clone()),
message_id: None,
}),
locals: json!({ "name": user.name }),
..Default::default()
},
)
.await?;
```
`EmailHeaders` maps to `References`/`In-Reply-To`/`Message-ID`, useful for grouping notification emails into a single thread in the recipient's mail client.
## 6. Run the mailer worker
Mailer delivery goes through the background worker infrastructure, so a worker process must be running to actually send anything:
```sh
cargo loco start --worker # dedicated worker process
cargo loco start --server-and-worker # server + worker together
```
## 7. Test without sending real email
Set `stub: true` to capture emails instead of sending them:
```yaml
mailer:
stub: true
```
If mail is dispatched through a worker, set `workers.mode: ForegroundBlocking` in your test config so the send completes synchronously within the test.
```rust
use loco_rs::testing::prelude::*;
#[tokio::test]
#[serial]
async fn can_register() {
configure_insta!();
request::<App, Migrator, _, _>(|request, ctx| async move {
// .. call the endpoint that sends an email ..
with_settings!({ filters => cleanup_email() }, {
assert_debug_snapshot!(ctx.mailer.unwrap().deliveries());
});
})
.await;
}
```
`deliveries()` (available under the `testing` feature) reports how many emails were "sent" and their content, so you can assert on both.
## Reference
- All `mailer:` YAML keys: [Configuration reference](@/docs/reference/configuration.md#mailer)
- Worker modes and running a worker process: [Add a background worker](@/docs/how-to/add-worker.md)
@@ -0,0 +1,142 @@
+++
title = "Serve static & SPA assets"
description = "Serve a static folder (or a single-page app) with the built-in static middleware, then optionally embed everything into the binary with the embedded_assets feature."
date = 2026-07-03T00:00:00+00:00
updated = 2026-07-03T00:00:00+00:00
draft = false
weight = 16
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
**Goal:** serve files (images, CSS, JS, a compiled SPA bundle) directly from Loco, either from disk or embedded into the compiled binary.
This assumes a working app. For the full knob table, see the `static` entry in the [Middleware catalog reference](@/docs/reference/middleware.md).
## 1. Put files under `assets/static/`
```
assets/
├── static/
│ ├── image.png
│ └── 404.html
└── views/
```
`assets/` sits at your project root, next to `src/` and `config/`.
## 2. Enable the `static` middleware
```yaml
# config/development.yaml
server:
middlewares:
static:
enable: true
must_exist: true
folder:
uri: "/static"
path: "assets/static"
fallback: "assets/static/404.html"
```
| Key | Default | Purpose |
|---|---|---|
| `must_exist` | `true` | if `true`, a missing configured folder is a boot-time error |
| `folder.uri` | `/static` | the URL prefix clients request under |
| `folder.path` | `assets/static` | the on-disk folder served |
| `fallback` | `assets/static/404.html` | file served when a requested path doesn't exist |
| `precompressed` | `false` | serve a `.gz` sibling file instead of compressing on the fly, if one exists |
| `cache_control` | `None` | e.g. `"max-age=31536000, public"`; set `null` to disable caching headers entirely |
Reference the served files from HTML/templates as normal:
```html
<img src="/static/image.png" />
```
## 3. Disable the welcome-screen fallback if it's shadowing your assets
Outside `Production`, Loco enables a separate `fallback` middleware by default (the "Loco welcome screen" for unmatched routes), and it takes precedence over `static`. If your static assets aren't showing up as expected in development, disable it:
```yaml
server:
middlewares:
fallback:
enable: false
```
## 4. Serve a single-page app (SPA)
Point `fallback` (on `static_assets`, not the framework-wide `fallback` middleware above) at your SPA's `index.html` so client-side routes resolve correctly on a hard refresh:
```yaml
server:
middlewares:
static:
enable: true
must_exist: true
folder:
uri: "/"
path: "assets/static"
fallback: "assets/static/index.html"
```
Any request that doesn't match a real file under `assets/static/` falls back to `index.html`, letting your client-side router take over.
## 5. Serve precompressed assets
If your build pipeline already produces `.gz` files next to the originals (e.g. `app.js` and `app.js.gz`), turn on `precompressed` and Loco serves the `.gz` variant directly instead of compressing per-request:
```yaml
server:
middlewares:
static:
enable: true
precompressed: true
```
## 6. Embed assets into the binary with `embedded_assets`
For single-binary deployment (no separate asset directory to ship or mount), enable the `embedded_assets` Cargo feature:
```toml
[dependencies]
loco-rs = { version = "...", features = ["embedded_assets"] }
```
This is a **compile-time swap**, not a separate API — the same `server.middlewares.static` config key and knobs still apply, and your controllers/views don't change at all:
- the `static` middleware's implementation swaps from reading `assets/static/` off disk to serving files baked into the binary at build time
- the Tera view engine (`TeraView`) likewise swaps to an embedded variant that serves compiled-in templates instead of reading `assets/views/` off disk
At build time you'll see log output confirming what got embedded:
```
warning: loco-rs@x.y.z: Discovered directories for assets:
warning: loco-rs@x.y.z: - /path/to/app/assets/static
warning: loco-rs@x.y.z: - /path/to/app/assets/views
warning: loco-rs@x.y.z: Found asset: /path/to/app/assets/static/image.png -> /static/image.png
warning: loco-rs@x.y.z: Found 6 asset files
warning: loco-rs@x.y.z: Generated code for 6 static assets and 7 templates
```
Trade-offs: the binary grows by roughly the size of `assets/`, and any asset change requires a full recompile — there's no "edit and refresh" loop like serving from disk. Toggle the feature on/off per build profile (e.g. embedded for release, filesystem for local dev) if that recompile cost is a problem during active asset iteration.
## Verify
```sh
curl -I localhost:5150/static/image.png # 200, correct Content-Type
curl -I localhost:5150/static/does-not-exist.png # falls back per `fallback` config
```
## Next
- [Render server-side views](@/docs/how-to/render-views.md)
- [Add middleware](@/docs/how-to/add-middleware.md)
- [Middleware catalog reference](@/docs/reference/middleware.md)
@@ -0,0 +1,154 @@
+++
title = "Use the cache"
description = "Configure a cache driver (null/in-memory/Redis) and use get/insert/get_or_insert with expiry, ping, and clear."
date = 2026-07-03T00:00:00+00:00
updated = 2026-07-03T00:00:00+00:00
draft = false
weight = 31
sort_by = "weight"
template = "docs/page.html"
aliases = ["/docs/infrastructure/cache/"]
[extra]
lead = ""
toc = true
top = false
+++
Goal: cache a value (a computed result, an external API response, a hot query) behind a key, with an optional TTL, using a driver you can swap per environment.
`ctx.cache` is available in every controller, task, and worker. Values are serialized as JSON strings under the hood, so anything `Serialize + DeserializeOwned` can go in.
## 1. Configure a driver
Cache drivers are configured entirely in YAML — no code changes needed to switch drivers between environments.
```yaml
# config/development.yaml — fast, disposable, no external dependency
cache:
kind: InMem
max_capacity: 33554432 # optional, bytes; default 32MiB (32 * 1024 * 1024)
```
```yaml
# config/production.yaml — shared across processes
cache:
kind: Redis
uri: "{{ get_env(name='REDIS_CACHE_URL', default='redis://127.0.0.1:6379') }}"
max_size: 10 # required — max pool connections
```
```yaml
# omit the `cache` key entirely, or set explicitly — this is the default
cache:
kind: Null
```
`InMem` needs the `cache_inmem` feature (on by default); `Redis` needs `cache_redis` (off by default — add it to your `Cargo.toml`). See the [feature flags reference](@/docs/reference/feature-flags.md).
If you omit `cache` from the config file altogether, Loco silently falls back to the **`Null` driver**: `get()` always returns `None`, and every mutating operation (`insert`, `insert_with_expiry`, `remove`, `clear`, `ping`) returns an error. This is a fail-fast default for "you haven't configured a real cache" — don't ship it to production by accident.
## 2. Insert and read values
```rust
use loco_rs::cache;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct User {
name: String,
age: u32,
}
async fn cache_basics(ctx: &AppContext) -> Result<()> {
ctx.cache.insert("greeting", &"hello".to_string()).await?;
let user = User { name: "Alice".to_string(), age: 30 };
ctx.cache.insert("user:1", &user).await?;
let greeting: Option<String> = ctx.cache.get("greeting").await?;
let cached_user: Option<User> = ctx.cache.get("user:1").await?;
let exists: bool = ctx.cache.contains_key("user:1").await?;
ctx.cache.remove("greeting").await?;
Ok(())
}
```
## 3. Set a TTL with `insert_with_expiry`
```rust
use std::time::Duration;
ctx.cache
.insert_with_expiry("session:abc", &token, Duration::from_secs(300))
.await?;
```
## 4. Cache the result of a computation with `get_or_insert`
`get_or_insert` returns the cached value if present, otherwise runs the given future, stores the result, and returns it. `get_or_insert_with_expiry` does the same but attaches a TTL to the freshly-computed value.
```rust
let expensive_report = ctx
.cache
.get_or_insert::<Report, _>("report:daily", async {
build_daily_report(ctx).await
})
.await?;
```
```rust
use std::time::Duration;
let expensive_report = ctx
.cache
.get_or_insert_with_expiry::<Report, _>(
"report:daily",
Duration::from_secs(3600),
async { build_daily_report(ctx).await },
)
.await?;
```
## 5. Health-check and clear
```rust
// Fails if the backing store (e.g. Redis) is unreachable.
ctx.cache.ping().await?;
// Wipe the cache.
ctx.cache.clear().await?;
```
> **Redis caveat:** `Cache::clear()` on the Redis driver issues **`FLUSHDB`** — it flushes the *entire* Redis logical database, not just the keys your app put there. If other data (session store, queue, another app) shares that same Redis DB/instance, `clear()` will delete it too. Point cache at its own Redis DB (`redis://host:6379/1`, a separate `db` index) if you need to isolate it, and treat `clear()` as a blunt, whole-database operation.
## 6. Verify
```rust
#[tokio::test]
async fn can_get_or_insert() {
let app_ctx = get_app_context().await; // your test AppContext
let key = "loco";
assert_eq!(app_ctx.cache.get::<String>(key).await.unwrap(), None);
let result = app_ctx
.cache
.get_or_insert::<String, _>(key, async { Ok("loco-cache-value".to_string()) })
.await
.unwrap();
assert_eq!(result, "loco-cache-value");
assert_eq!(
app_ctx.cache.get::<String>(key).await.unwrap(),
Some("loco-cache-value".to_string())
);
}
```
## Reference
- Every `cache:` YAML key (`kind`, `max_capacity`, `uri`, `max_size`): [Configuration reference § cache](@/docs/reference/configuration.md#cache)
- `cache_inmem` / `cache_redis` feature flags: [Feature flags reference](@/docs/reference/feature-flags.md)
@@ -0,0 +1,99 @@
+++
title = "Generate code with cargo loco generate"
description = "Scaffold models, migrations, controllers, workers, and more with `cargo loco generate <kind>`, using the shared field:type mini-language."
date = 2021-05-01T18:10:00+00:00
updated = 2021-05-01T18:10:00+00:00
draft = false
weight = 60
sort_by = "weight"
template = "docs/page.html"
[extra]
lead = ""
toc = true
top = false
+++
Goal: scaffold application code (models, migrations, controllers, workers, mailers, deployment files, ...) from Loco's built-in templates instead of hand-writing boilerplate.
## 1. Know the constraint: debug builds only
`cargo loco generate` (alias `g`) is compiled only in debug builds — `#[cfg(debug_assertions)]` gates the whole subcommand (`src/cli.rs`). It's available whenever you run your app the normal dev way (`cargo run`, `cargo loco start`, `cargo test`), but it is **not present in a `--release` binary**. `model`/`migration`/`scaffold` are additionally gated on the `with-db` feature (on by default).
## 2. Run a generator
```sh
# an empty model (entity + migration + test)
cargo loco generate model posts
# a model with typed fields
cargo loco generate model posts title:string! content:text
# a full CRUD resource: entity + migration + controller + routes + tests
cargo loco generate scaffold posts title:string! user:references
# controller only, no model/migration
cargo loco generate controller posts index show
# non-DB generators
cargo loco generate task cleanup_old_sessions
cargo loco generate worker send_digest
cargo loco generate mailer welcome
cargo loco generate scheduler
cargo loco generate data countries
cargo loco generate deployment docker
```
Every generator writes files relative to your project root and prints what it created (or, for `model`/`migration`/`scaffold`, injects a `mod` line into the relevant `mod.rs`).
## 3. Pick the right kind
| Kind | Needs `with-db` | What you get |
|---|---|---|
| `model` | yes | Sea-ORM entity + model file + migration + a starter test in `tests/models/` |
| `migration` | yes | Standalone migration file (add/remove columns, join tables, or an empty stub — inferred from the name) |
| `scaffold` | yes | Full CRUD: entity, migration, controller, routes, tests — plus typed React hooks/pages when the app has a `frontend/` |
| `controller` | no | Controller + routes + tests, no model |
| `task` | no | One-off/CLI task stub, registered automatically |
| `scheduler` | no | `config/scheduler.yaml` starter |
| `worker` | no | Background worker stub, registered automatically |
| `mailer` | no | Mailer struct + embedded `subject`/`html`/`text` templates |
| `data` | no | Data-loader struct + a static `data/<name>/data.json` |
| `deployment` | no | `docker` or `nginx` deployment files |
| `override` | no | Copies a built-in template locally so you can edit it — see [Override built-in templates](@/docs/how-to/override-templates.md) |
`scaffold` and `controller` are **adaptive** — there's no kind flag to pick. `controller` always generates a JSON API controller; `scaffold` generates a JSON API controller and, when the app has a `frontend/`, also emits typed React hooks and pages for the resource. The old `--html`/`--htmx` server-side views are gone (replaced by the React SPA frontend); `--api` is still accepted as a no-op for back-compat.
This is a summary for orientation only — the exhaustive, verified dictionary of every kind, every flag, and migration-name inference rules is the [Generators & field types reference](@/docs/reference/generators.md); the raw CLI flag shapes are also in the [CLI reference](@/docs/reference/cli.md#2-4-generate-subcommands).
## 4. Use the field-type mini-language
`model`, `migration`, and `scaffold` all take `name:type` pairs after the resource name. The full table of ~50 base types (with their `!`/`^` suffix variants, arities, and Rust types) lives in the [field-type mini-language reference](@/docs/reference/generators.md#field-type-mini-language) — check it before guessing a type name. A few load-bearing facts to keep in mind while typing field lists:
- No suffix = nullable (`Option<T>`); `!` = required; `^` = unique (implies required). Not every type has a `^` form (`bool`, `tstz`, `json` don't).
- **`int` is `i64`/`BIGINT`** in Loco 1.0 (it was `i32` before) — `big_int` is just an alias. Use `small_int`/`small_unsigned` if you need a 16-bit column.
- `name:references` adds a required belongs-to foreign key (`name_id`); `name:references?` makes it nullable; `name:references:custom_id` (optionally with `?`) picks the FK column name explicitly.
- `array` types take the element type as a second colon segment: `tags:array:string`, `scores:array!:int`.
```sh
cargo loco generate model movies long_title:string director:references award:references:prize_id
```
## 5. Apply generated migrations
Generating a `migration` (standalone or via `scaffold`/`model`) only writes the file — it doesn't touch the database. Apply it and regenerate entities:
```sh
cargo loco db migrate && cargo loco db entities
```
## Verify it
```sh
cargo build # generators need a debug build to even be available
cargo loco generate model posts title:string!
cargo loco db migrate
cargo test
```
A successful generator run prints the list of files it created/modified; `cargo build` (or `cargo check`) then confirms the generated code compiles, and `cargo test` runs the starter test the generator scaffolded for you.

Some files were not shown because too many files have changed in this diff Show More