ESC
Type to search...

Rapina 0.8.0

Response caching, built-in pagination, and ergonomic extractor improvements

Rapina 0.8.0 shipped on March 1, 2026. This release added caching and pagination as built-in primitives, alongside several schema macro improvements and bug fixes that were staged across unreleased patch versions since 0.7.0.


Response caching

Caching was added as a middleware-backed primitive. Two backends were supported: in-memory (built-in) and Redis (behind the cache-redis feature flag).

Setup

Register the cache via .with_cache() before .router():

use rapina::cache::CacheConfig;
use rapina::prelude::*;

Rapina::new()
    .with_cache(CacheConfig::in_memory(1000)).await?
    .router(router)
    .listen("127.0.0.1:3000")
    .await

For Redis, enable the feature flag and use CacheConfig::redis:

[dependencies]
rapina = { version = "0.8", features = ["cache-redis"] }
Rapina::new()
    .with_cache(CacheConfig::redis("redis://127.0.0.1/")).await?
    .router(router)
    .listen("127.0.0.1:3000")
    .await

Per-handler TTL

The #[cache(ttl = N)] attribute on a handler opted it into caching with a TTL in seconds. Caching only took effect on GET requests — the attribute had no effect on mutation handlers:

#[get("/products")]
#[cache(ttl = 60)]
async fn list_products(db: Db) -> Result<Json<Vec<Product>>> {
    let products = Product::find().all(db.conn()).await.map_err(DbError)?;
    Ok(Json(products))
}

Responses included an x-cache header set to HIT or MISS. Cache invalidation on mutation routes happened automatically by prefix — a POST /products invalidated all /products cache entries.


Pagination

The Paginate extractor and Paginated<T> response type were added as first-class primitives for database-backed list endpoints.

Basic usage

Paginate read ?page=1&per_page=20 from the query string. page.exec() ran the fetch and count concurrently against a SeaORM Select:

use rapina::database::Db;
use rapina::pagination::{Paginate, Paginated};
use rapina::prelude::*;
use entity::todo::{self, Entity as Todo};

#[get("/todos")]
async fn list_todos(db: Db, page: Paginate) -> Result<Paginated<todo::Model>> {
    page.exec(Todo::find(), db.conn()).await
}

The response wrapped the data with pagination metadata:

{
  "data": [...],
  "page": 1,
  "per_page": 20,
  "total": 84,
  "total_pages": 5,
  "has_prev": false,
  "has_next": true
}

Configuration

Global defaults were set by registering PaginationConfig as application state:

use rapina::pagination::PaginationConfig;

Rapina::new()
    .state(PaginationConfig {
        default_per_page: 25,
        max_per_page: 50,
    })
    .router(router)
    .listen("127.0.0.1:3000")
    .await

When not registered, defaults were per_page = 20 and max_per_page = 100.


Deref on extractor newtypes

Json<T>, Path<T>, Query<T>, and other extractor newtypes gained Deref implementations. Field access and dereferencing now worked directly without .0 or .into_inner():

// Before
#[get("/users/:id")]
async fn get_user(id: Path<u64>) -> String {
    format!("User {}", id.into_inner())
}

// After
#[get("/users/:id")]
async fn get_user(id: Path<u64>) -> String {
    format!("User {}", *id)
}

For struct extractors, field access worked without .0:

// Before
async fn list_users(query: Query<Pagination>) -> String {
    let page = query.0.page.unwrap_or(1);
    format!("Page: {}", page)
}

// After
async fn list_users(query: Query<Pagination>) -> String {
    let page = query.page.unwrap_or(1);
    format!("Page: {}", page)
}

Schema macro improvements

NaiveDateTime support

The schema! macro gained support for NaiveDateTime columns. Tables with timezone-less timestamp columns could now be imported and used without manual adjustment:

schema! {
    Event {
        title: String,
        starts_at: NaiveDateTime,
        ends_at: NaiveDateTime,
    }
}

Composite primary keys

The #[primary_key(...)] attribute on an entity definition opted out of the default auto-increment id: i32 and specified custom primary key columns instead. Each named column was annotated with #[sea_orm(primary_key, auto_increment = false)] in the generated model:

schema! {
    #[primary_key(user_id, role_id)]
    UserRole {
        user_id: i32,
        role_id: i32,
        granted_at: NaiveDateTime,
    }
}

Bug fixes

Static routes shadowed by parameterized routes (#255)

A routing bug caused static route segments to be incorrectly matched by parameterized routes registered at the same depth. For example, GET /users/current was routed to the GET /users/:id handler instead of its own handler. This was fixed by evaluating static segments before parameterized ones during route resolution.

schema! macro JsonSchema for Uuid and Decimal (#257)

The schema macro generated unqualified Uuid and Decimal types in derived JsonSchema implementations, causing compilation failures in projects that did not import those types directly. The generated code was updated to use fully-qualified paths (rapina::uuid::Uuid and rapina::rust_decimal::Decimal).


Upgrade by bumping the version in your Cargo.toml:

rapina = "0.8"