Rapina 0.4.0 shipped on February 12, 2026. This release added database support via SeaORM and filled out the middleware layer with rate limiting, CORS, and response compression. Note: v0.3.0 was never tagged — this release followed directly from v0.2.1.
Database
Database support was built on top of SeaORM. The integration added a DatabaseConfig type for connection setup and a Db extractor for injecting the connection into handlers.
Setup
DatabaseConfig took a connection string. SQLite, PostgreSQL, and MySQL were all supported:
use rapina::database::DatabaseConfig;
use rapina::prelude::*;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let router = Router::new()
.get("/todos", list_todos);
Rapina::new()
.with_database(DatabaseConfig::new("sqlite://app.db?mode=rwc"))
.await?
.run_migrations::<migrations::Migrator>()
.await?
.router(router)
.listen("127.0.0.1:3000")
.await
}Calling .with_database() registered the connection pool as application state. .run_migrations() ran pending SeaORM migrations at startup.
Db extractor
The Db extractor gave handlers access to the database connection. It was populated automatically from the connection pool — no explicit state passing was needed:
use rapina::database::Db;
use rapina::prelude::*;
#[get("/todos")]
pub async fn list_todos(db: Db) -> Result<Json<Vec<Model>>> {
let todos = Todo::find().all(db.conn()).await.map_err(DbError)?;
Ok(Json(todos))
}db.conn() returned a reference to the underlying SeaORM DatabaseConnection. All SeaORM operations worked directly through it:
use rapina::sea_orm::{ActiveModelTrait, Set};
#[post("/todos")]
pub async fn create_todo(db: Db, body: Json<CreateTodo>) -> Result<Json<Model>> {
let todo = ActiveModel {
title: Set(body.title.clone()),
..Default::default()
};
let result = todo.insert(db.conn()).await.map_err(DbError)?;
Ok(Json(result))
}DbError
DbError was a wrapper that converted sea_orm::DbErr into Rapina's Error type. It was designed to be used with the ? operator via .map_err(DbError):
let todo = Todo::find_by_id(id)
.one(db.conn())
.await
.map_err(DbError)?
.ok_or_else(|| Error::not_found(format!("Todo {} not found", id)))?;Middleware
Three new built-in middleware types were added in 0.4.0: rate limiting, CORS, and response compression.
Rate limiting
RateLimitConfig added per-IP request throttling. The simplest setup used the per_minute constructor:
let rate_limit = RateLimitConfig::per_minute(10);
let router = Router::new().get("/", index);
Rapina::new()
.with_rate_limit(rate_limit)
.router(router)
.listen("127.0.0.1:3000")
.awaitRequests that exceeded the limit received a 429 Too Many Requests response.
CORS
CorsConfig handled preflight requests and set the appropriate response headers. Origins were configured explicitly:
use rapina::middleware::CorsConfig;
let cors = CorsConfig::with_origins(vec![
"https://example.com".to_string(),
"https://app.example.com".to_string(),
]);
let router = Router::new().get("/", index);
Rapina::new()
.with_cors(cors)
.router(router)
.listen("127.0.0.1:3000")
.awaitCompression
CompressionConfig enabled gzip and deflate compression for responses above a minimum size threshold (1 KB by default). Clients that sent Accept-Encoding: gzip received compressed responses:
use rapina::middleware::CompressionConfig;
let compression = CompressionConfig::default();
let router = Router::new().get("/", index);
Rapina::new()
.with_compression(compression)
.router(router)
.listen("127.0.0.1:3000")
.awaitMiddleware ordering
All built-in middleware had Debug and Clone derives added in this release. The recommended middleware ordering placed CORS early (before rate limiting and compression) and timeout last:
use rapina::middleware::{CompressionConfig, CorsConfig, RequestLogConfig, TimeoutMiddleware, TraceIdMiddleware};
use std::time::Duration;
let router = Router::new().get("/", index);
Rapina::new()
.middleware(TraceIdMiddleware::new())
.with_request_log(RequestLogConfig::verbose())
.with_cors(CorsConfig::with_origins(vec!["https://example.com".to_string()]))
.with_rate_limit(RateLimitConfig::per_minute(10))
.with_compression(CompressionConfig::default())
.middleware(TimeoutMiddleware::new(Duration::from_secs(5)))
.router(router)
.listen("127.0.0.1:3000")
.awaitThe request flowed inward through each layer and the response flowed back outward.
Cookie extractor
The Cookie<T> extractor added typed access to request cookies. Like other Rapina extractors, it deserialized directly into a struct:
use rapina::prelude::*;
#[derive(Deserialize, JsonSchema)]
struct SessionCookies {
session_id: String,
}
#[get("/profile")]
async fn profile(cookies: Cookie<SessionCookies>) -> String {
format!("session: {}", cookies.session_id)
}For optional cookies, Option<Cookie<T>> was used instead:
#[get("/profile")]
async fn profile(cookies: Option<Cookie<SessionCookies>>) -> String {
match cookies {
Some(c) => format!("session: {}", c.session_id),
None => "no session".to_string(),
}
}If required cookies were missing or could not be parsed, Rapina returned a 400 Bad Request before the handler ran.
API grouping
Route handlers gained a group attribute for organizing routes under a shared prefix. Groups were applied at the handler level and routes were registered explicitly via .router():
#[get("/users", group = "/api")]
async fn list_users() -> String {
"list users".to_string()
}
#[get("/users/:id", group = "/api")]
async fn get_user(id: Path<u64>) -> String {
format!("user {}", *id)
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let router = Router::new()
.get("/api/users", list_users)
.get("/api/users/:id", get_user);
Rapina::new().router(router).listen("127.0.0.1:3000").await
}The above registered GET /api/users and GET /api/users/:id. Routes without a group attribute were registered at the root.
rapina test
The CLI gained a rapina test command for running the project's test suite. It supported coverage reporting and watch mode:
# Run all tests
rapina test
# Run with coverage
rapina test --coverage
# Watch mode — re-run tests on file changes
rapina test --watchTodo API example
A full Todo API example was added to the repository. It demonstrated database integration with SeaORM, JWT authentication, CurrentUser extraction, and the Db extractor working together in a realistic application.
The example covered:
POST /login— public endpoint that issued a JWTGET /todos— list todos for the authenticated userPOST /todos— create a todoPUT /todos/:id— update a todoDELETE /todos/:id— delete a todo
It served as the primary reference for how to structure a database-backed Rapina application.
Docs site
A blog section was added to the docs site. Tutorial posts, release notes, and community content could now be published alongside the API documentation.
GitHub Sponsors
GitHub Sponsors was added to the project. Sponsorship links appeared in the repository and on the docs site.
Upgrade by bumping the version in your Cargo.toml:
rapina = "0.4.0"