Skip to content
★ GitHubGet started

Upgrades

What to do when a new Loco version is out?

Section titled “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 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 and ask for help.

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:

1.1 moves the template engine to Tera 2 and changes configuration files to use YAML-safe <%= ... %> delimiters.

Most apps need one line changed — see below. Nothing here requires rewriting your templates unless they use {% macro %}, {% import %}, or v.0 array access, none of which Loco’s own generated templates use.

The fast path: hand this to a coding agent

Section titled “The fast path: hand this to a coding agent”

Paste the following into Claude Code (or any coding agent) with your project open. It is the whole 1.0 → 1.1 change surface, written to be acted on. The sections after it are the same changes explained for humans.

You are upgrading a Rust web application from Loco 1.0.x to Loco 1.1.
Work through the checklist below in order. For each item: search the codebase
for the pattern, apply the change only where it actually appears, and tell me
what you changed or that the item did not apply. Do not change anything that
is not listed here. When you are done, run `cargo check`, then `cargo loco
doctor`, then the test suite.
1. Cargo.toml — REQUIRED for every app.
Set `loco-rs` to "1.1".
If `fluent-templates` appears, set it to:
fluent-templates = { version = "0.15", features = ["tera"] }
Version 0.13 pins Tera 1 and will not compile against Loco 1.1. For most
apps this is the ONLY change needed.
2. Custom Tera filters and functions (Rust code).
Look for `register_filter`, `register_function` and `register_tester`,
usually in src/initializers/view_engine.rs or an `after_routes` hook.
Tera 2 changed the signatures:
// Tera 1
fn f(value: &serde_json::Value, args: &HashMap<String, serde_json::Value>)
-> tera::Result<serde_json::Value>
// Tera 2
fn f(value: &tera::Value, kwargs: tera::Kwargs, _: &tera::State)
-> tera::TeraResult<tera::Value>
Read named arguments with kwargs.get::<T>("name")? for optional ones and
kwargs.must_get::<T>("name")? for required ones.
`&State` cannot be constructed outside the engine, so a filter can no
longer be unit-tested by calling it. Move the logic into a plain function
and make the filter a thin wrapper over it, so that function stays testable.
Do NOT touch `tera.register_function("t", FluentLoader::new(...))`. That
keeps working unchanged.
3. View and mailer templates (assets/views/**, assets/mailers/**).
Three Tera 2 changes. Apply each only where it actually appears:
a. `{% macro %}` and `{% import %}` were removed in favour of components.
Templates using them need rewriting.
b. Array access is `v[0]`, not `v.0`.
c. An undefined variable is now an ERROR where Tera 1 rendered it empty.
This catches MAILER templates too, so a mail template referencing an
optional field that is sometimes absent now fails at send time rather
than rendering nothing. Add `| default(value="")` to those references.
Templates that Loco generated do not use (a) or (b).
4. Scaffolded list endpoints — only if you generated a scaffold.
The JSON envelope changed to the framework's own pagination vocabulary:
per_page -> page_size
total -> total_items
total_pages -> NEW field, add it
The query parameter is `page_size` as well. Update any frontend or API
client reading those fields. If you have a Loco-generated TypeScript
frontend, regenerate bindings/ with ts-rs.
5. Traits you implemented yourself — skip if you use only built-in drivers.
- `QueueProvider` now requires `retry_failed`.
- `StoreDriver` now requires `list` and `stat`.
- `StorageStrategy` now requires `list`, `stat` and `exists`.
Every built-in driver and strategy already implements these.
6. Exhaustive `match` expressions that will stop compiling.
- `loco_rs::doctor::Resource` is now #[non_exhaustive] and gained a
`ProductionSafety` variant. It sorts FIRST, so the doctor report's
display order shifts by one; code sorting or comparing Resource sees it.
- `loco_gen::DeploymentKind` gained a `Lambda` variant.
- `loco_gen::Component::Scaffold` and ::Controller gained a required
`auth: bool` field.
- `loco_gen::AppInfo` gained a `working_dir` field — pass ".".into().
The loco_gen items matter only if you drive the generator from code.
7. Function signatures you may be calling directly.
- `bgworker::pg::get_jobs` now returns loco_rs::Result instead of
Result<_, sqlx::Error>. Code using `?` in a Loco context is unaffected;
code matching on sqlx::Error needs updating.
- views::tera_builtins::filters::number::{number_with_delimiter,
number_to_human_size, number_to_percentage} take (value, Kwargs, &State).
Templates are unaffected — only direct Rust calls need updating.
- `build_with_post_process` now runs BEFORE templates are loaded. The
closure registers into an empty engine and can no longer inspect loaded
templates, and anything a template calls must be registered by it.
8. `auth.jwt.location` in config is parsed strictly now.
It accepts a map, or a list of maps, and nothing else. The documented
shapes are unchanged, but config that used to slip through an untagged
fallback is now rejected with an error naming the problem.
9. OPTIONAL, no deadline: config templating delimiters.
`{{ get_env(...) }}` still renders, with a deprecation warning. The
YAML-safe form is:
port: <%= get_env(name="PORT", default="5150") %>
`{` is a YAML flow-mapping indicator, so the old form was never valid YAML
at rest and format-on-save would rewrite it into `{ { ... } }` and break
startup. This is a find-and-replace across config/*.yaml.
Do NOT make production config secrets mandatory. That change applies only to
NEWLY generated apps; an existing app's config files are its own.

Generated apps use fluent-templates for the i18n t() function, and it pinned Tera 1. Bump it in your Cargo.toml:

fluent-templates = { version = "0.15", features = ["tera"] }

For most apps this is the only change needed. Your view-engine initializer — including tera.register_function("t", FluentLoader::new(arc.clone())) — keeps working as-is.

If you register your own filters or functions

Section titled “If you register your own filters or functions”

Tera 2 changed the signatures. Filters now receive (Arg, Kwargs, &State) over Tera’s own Value type, instead of (&Value, &HashMap<String, Value>) over serde_json::Value:

```rust no-syntax-check=“before/after signature comparison; the bodies are elided { ... }” // Tera 1 fn my_filter(value: &serde_json::Value, args: &HashMap<String, serde_json::Value>) -> tera::Result<serde_json::Value> { … }

// Tera 2 fn my_filter(value: &tera::Value, kwargs: tera::Kwargs, _: &tera::State) -> tera::TeraResulttera::Value { … }

Read named arguments with `kwargs.get::<T>("name")?` (optional) or
`kwargs.must_get::<T>("name")?` (required).
Note that `&State` cannot be constructed outside the engine, so filters can no
longer be unit-tested by calling them directly. Keep the formatting logic in a
plain function and make the filter a thin wrapper over it — that function stays
testable.
### If your view templates use macros or dotted array access
Tera 2 removed `{% macro %}` / `{% import %}` in favour of components, requires
`v[0]` instead of `v.0` for array access, and **errors on undefined variables**
rather than rendering them as empty. Templates relying on any of these need
editing. Generated Loco apps do not use these constructs.
### Configuration files: `<%= ... %>`
Config templating moved off Tera's `{{ ... }}` to `<%= ... %>`:
```yaml
port: <%= get_env(name="PORT", default="5150") %>

{ is a YAML flow-mapping indicator, so the old form was never valid YAML at rest — editors and formatters (prettier, yaml-language-server, format-on-save) would restructure it into { { ... } } and break startup. < is not an indicator, so the new form is an ordinary string scalar and survives formatting untouched.

This is not a required change: the old {{ ... }} form still renders, with a deprecation warning. Converting is a find-and-replace when you get to it.

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 for anything specific to APIs you use directly.

Loco 1.0 uses Sea-ORM 2.0, whose MSRV is Rust 1.94. Update your toolchain:

Terminal window
rustup update

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:

# 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:

# 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:

Terminal window
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.0-rc (src/doctor.rs:39,52) — any 2.0 release candidate satisfies the check, so bump to a 2.0 stable yourself rather than waiting for doctor to tell you.

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.

Generated code now uses 64-bit primary keys

Section titled “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: ExtraDbInitializerMultiDbInitializer

Section titled “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>:

// 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

Section titled “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:

let ctx = AppContext::builder(environment, db, config) // builder(environment, config) without `with-db`
.queue_provider(queue)
.mailer(mailer)
.storage(storage)
.build();

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:

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

Section titled “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 500s, update them to the corrected codes.

Background job priorities (Redis backend is breaking)

Section titled “Background job priorities (Redis backend is breaking)”

Background jobs now support a priority (higher numbers run first). You can enqueue with an explicit priority:

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, .. }.

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:

let job_id = DownloadWorker::perform_later(&ctx, args).await?;

Background queue is now a QueueProvider adapter

Section titled “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.

Pagination results moved the flat total_pages / total_items fields into a meta: PagerMeta (which also carries page and page_size):

// 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 / BackupStrategyReplicatedStrategy

Section titled “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:

// 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 / AllowBackupFailureAllowAll, AtLeastOneFailureAllowSingleFailure, 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)

Section titled “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:

local::new_with_prefix("/your/root")

Config: {env}.local.yaml now deep-merges over {env}.yaml

Section titled “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.

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)

Section titled “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

Section titled “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

Section titled “View engine: use TeraView::build_with_post_process”

In after_routes, replace TeraView::build()?.post_process(...) with the combined constructor:

// 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

Section titled “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 ?:

let tpl = Template::new(dir)?;

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().

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.

  • auth_jwtauth.
  • bg_redisworker_redis; bg_pg/bg_sqltworker. 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.

Use AppContext instead of Config in init_logger in the Hooks trait

Section titled “Use AppContext instead of Config in init_logger in the Hooks trait”

PR: #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:

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

Section titled “Swap to validators builtin email validation”

PR: #1359

Swap from using the loco custom email validator, to the builtin email validator from validator.

#[validate(custom (function = "validation::is_valid_email"))]
#[validate(email(message = "invalid email"))]
pub email: String,

PR: #1384 PR: #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

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

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

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

PR: #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:

  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

Before:

// 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:

// 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();

For your custom types to work with the cache, ensure they implement Serialize and Deserialize:

use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct MyType {
// fields...
}

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”

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.

We had some changes in Tera template. go to src/initializers/view_engine.rs and replace the after_routes function with:

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))))
}

PR: #1199

Update the validator crate version in your Cargo.toml:

From

validator = { version = "0.19" }

To

validator = { version = "0.20" }

PR: #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.

PR: #1197

The pagination response now includes the total_items field, providing the total number of items available.

{"results":[],"pagination":{"page":0,"page_size":0,"total_pages":0,"total_items":0}}

PR: #1268

Migrations using create_table now require ("id", ColType::PkAuto), new migrations will have this field automatically added.

async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
create_table(m, "movies",
&[
("id", ColType::PkAuto),
("title", ColType::StringNull),
],
&[
("user", ""),
]
).await
}

PR: #1130 The upgrade to Axum 0.8 introduces a breaking change. For more details, refer to the announcement.

  • 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.
  • The URL parameter syntax has changed. Refer to this section for the updated syntax. The new path parameter format is: The path parameter syntax has changed from /:single and /*many to /{single} and /{*many}.

PR: #1143

The boot hook function now accepts an additional Config parameter. The function signature has changed from:

From

async fn boot(mode: StartMode, environment: &Environment) -> Result<BootResult> {
create_app::<Self, Migrator>(mode, environment).await
}

To:

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.

PR: #993

Update the validator crate version in your Cargo.toml:

From

validator = { version = "0.18" }

To

validator = { version = "0.19" }

PR: #1158

The truncate and seed functions now receive AppContext instead of DatabaseConnection as their argument.

From

async fn truncate(db: &DatabaseConnection) -> Result<()> {}
async fn seed(db: &DatabaseConnection, base: &Path) -> Result<()> {}

To

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 no-syntax-check=“... elides the rest of the closure body” async fn load_page() { request::<App, _, _>(|request, ctx| async move { seed::(&ctx.db).await.unwrap(); … }) .await; }

to
```rust no-syntax-check="`...` elides the rest of the closure body"
async fn load_page() {
request::<App, _, _>(|request, ctx| async move {
seed::<App>(&ctx).await.unwrap();
...
})
.await;
}