ESC
Type to search...

Extractors

Parse request data with type safety

Extractors automatically parse request data and inject it into your handlers. If parsing fails, they return appropriate error responses.

Available Extractors

ExtractorDescription
Path<T>URL path parameters
Query<T>Query string parameters
Json<T>JSON request body
Form<T>URL-encoded form data
HeadersRequest headers
State<T>Application state
ContextRequest context (trace_id)
Cookie<T>Typed cookie access
CurrentUserAuthenticated user (JWT)
Validated<T>Validated extractor
PaginatePagination params (requires feature)
DbDatabase connection (requires feature)

Accessing Extractor Values

Every Rapina extractor implements Deref to its inner type. This means you can access fields and methods directly without unwrapping:

#[get("/users/:id")]
async fn get_user(id: Path<u64>, config: State<AppConfig>) -> String {
    // Deref lets you access fields directly
    format!("User {} on {}", *id, config.app_name)
}

#[post("/users")]
async fn create_user(body: Json<CreateUser>) -> String {
    // Access struct fields through the extractor
    format!("Hello, {}", body.name)
}

When to use what:

  • Direct field accessbody.name, config.app_name, query.page. Works anywhere you need &T thanks to auto-deref. This is the common case.
  • Explicit deref (*)*id, *count. Needed for primitives in format strings or when passing a Copy value where the compiler needs the concrete type.
  • into_inner() — when you need to own the value. Moving it into a struct, passing it to a function that takes T (not &T), or consuming it in a builder chain.

Avoid using .0 to access extractor contents — it's an implementation detail. Deref or into_inner() are always clearer.

Path Parameters

Extract values from URL path segments:

Path parameters are stored in a stack-allocated buffer — routes with up to 4 parameters incur zero heap allocation during extraction.

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

// Multiple parameters — destructure the tuple
#[get("/posts/:year/:month")]
async fn archive(Path((year, month)): Path<(u32, u32)>) -> String {
    format!("{}/{}", year, month)
}

// Named struct — parameters matched by field name
#[derive(Deserialize)]
struct PostParams {
    year: u32,
    month: u32,
    slug: String,
}

#[get("/posts/:year/:month/:slug")]
async fn get_post(Path(p): Path<PostParams>) -> String {
    format!("{}/{}/{}", p.year, p.month, p.slug)
}

Query Parameters

Parse query strings into typed structs:

#[derive(Deserialize)]
struct Pagination {
    page: Option<u32>,
    limit: Option<u32>,
}

#[get("/users")]
async fn list_users(query: Query<Pagination>) -> String {
    let page = query.page.unwrap_or(1);
    let limit = query.limit.unwrap_or(20);
    format!("Page {} with {} items", page, limit)
}

JSON Body

Parse JSON request bodies:

#[derive(Deserialize)]
struct CreateUser {
    name: String,
    email: String,
}

#[post("/users")]
async fn create_user(body: Json<CreateUser>) -> Json<User> {
    // Access fields directly through Deref
    let user = User::new(&body.name, &body.email);
    Json(user)
}

Form Data

Parse URL-encoded form submissions:

#[derive(Deserialize)]
struct LoginForm {
    username: String,
    password: String,
}

#[post("/login")]
async fn login(form: Form<LoginForm>) -> Result<Json<TokenResponse>> {
    // Access fields directly through Deref
    authenticate(&form.username, &form.password).await
}

Headers

Access request headers:

#[get("/debug")]
async fn debug(headers: Headers) -> String {
    let user_agent = headers
        .get("user-agent")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("unknown");

    format!("User-Agent: {}", user_agent)
}

Application State

Access shared application state:

#[derive(Clone)]
struct AppConfig {
    app_name: String,
}

#[get("/info")]
async fn info(config: State<AppConfig>) -> String {
    format!("App: {}", config.app_name)
}

Cookies

Deserialize cookies into typed structs:

#[derive(Deserialize)]
struct Session {
    session_id: String,
}

#[get("/dashboard")]
async fn dashboard(session: Cookie<Session>) -> String {
    format!("Session: {}", session.session_id)
}

Returns 400 Bad Request if required cookies are missing or malformed.

CurrentUser

Access the authenticated user from JWT claims:

#[get("/me")]
async fn me(user: CurrentUser) -> Json<UserResponse> {
    Json(UserResponse {
        id: user.id,
        email: user.claims.sub.clone(),
    })
}

The CurrentUser extractor provides:

  • user.id - The user ID from the JWT sub claim
  • user.claims - The full JWT claims

Returns 401 Unauthorized if the request lacks a valid JWT token.

Note: This extractor requires authentication to be configured. See Authentication for setup details.

Request Context

Access the request context with trace ID:

#[get("/trace")]
async fn trace(ctx: Context) -> String {
    format!("Trace ID: {}", ctx.trace_id())
}

Validation

Validate extracted data using the validator crate:

use validator::Validate;

#[derive(Deserialize, Validate)]
struct CreateUser {
    #[validate(email)]
    email: String,

    #[validate(length(min = 8))]
    password: String,
}

#[post("/users")]
async fn create_user(body: Validated<Json<CreateUser>>) -> Json<User> {
    // Validated also implements Deref — access fields directly
    let user = User::new(&body.email, &body.password);
    Json(user)
}

If validation fails, returns 422 with validation error details.

Paginate

Parse pagination parameters from the query string:

use rapina::database::Db;

#[get("/users")]
async fn list_users(db: Db, page: Paginate) -> Result<Paginated<user::Model>> {
    page.exec(User::find(), db.conn()).await
}

The Paginate extractor reads ?page=1&per_page=20 from the query string:

ParameterDefaultDescription
page1Page number (1-indexed)
per_page20Items per page

Returns 422 Validation Error when:

  • page < 1
  • per_page < 1
  • per_page exceeds the configured maximum (default: 100)

Note: This extractor requires the database feature. See Pagination for complete details and configuration.

Db

Access the database connection for SeaORM operations:

use rapina::database::{Db, DbError};
use rapina::sea_orm::{EntityTrait, ActiveModelTrait, Set};

#[get("/posts")]
async fn list_posts(db: Db) -> Result<Json<Vec<PostResponse>>> {
    let posts = Post::find()
        .all(db.conn())
        .await
        .map_err(DbError::from)?;

    Ok(Json(posts.into_iter().map(PostResponse::from).collect()))
}

#[post("/posts")]
async fn create_post(body: Json<CreatePost>, db: Db) -> Result<Json<PostResponse>> {
    let post = post::ActiveModel {
        title: Set(body.title.clone()),
        content: Set(body.content.clone()),
        ..Default::default()
    };

    let post = post.insert(db.conn())
        .await
        .map_err(DbError::from)?;

    Ok(Json(PostResponse::from(post)))
}

The Db extractor provides:

  • db.conn() - A reference to the SeaORM database connection

Note: This extractor requires the database feature. See Database for setup and entity definitions.

Multiple Extractors

You can use multiple extractors in a single handler. Body-consuming extractors (Json, Form, Validated<Json<T>>, Validated<Form<T>>) must be the last parameter:

#[post("/users/:id/posts")]
async fn create_post(
    id: Path<u64>,
    user: CurrentUser,
    body: Json<CreatePost>,  // body consumer must be last
) -> Result<Json<Post>> {
    // All extractors available
}

Parts-only extractors (Path, Query, Headers, State, Context, Cookie, CurrentUser, Db) can appear in any order before the last parameter.

Note: Only one body-consuming extractor can be used per handler. If you need both JSON and form data, choose one.