ESC
Type to search...

Routing

Define routes and handle parameters

Routes in Rapina map HTTP methods and URL patterns to handler functions. The router matches incoming requests and extracts path parameters automatically.

Basic Routing

Use the Router to define your API endpoints:

use rapina::prelude::*;

let router = Router::new()
    .get("/", home)
    .get("/about", about)
    .post("/users", create_user)
    .put("/users/:id", update_user)
    .delete("/users/:id", delete_user);

HTTP Methods

Rapina provides convenience methods for common HTTP verbs:

MethodDescription
.get(pattern, handler)GET requests (read)
.post(pattern, handler)POST requests (create)
.put(pattern, handler)PUT requests (update)
.delete(pattern, handler)DELETE requests (remove)
.route(Method, pattern, handler)Any HTTP method

Using Macros

For cleaner syntax, use the route macros:

use rapina::prelude::*;

#[get("/")]
async fn home() -> &'static str {
    "Welcome to Rapina!"
}

#[post("/users")]
async fn create_user(body: Json<CreateUser>) -> Result<Json<User>> {
    // Create user...
    Ok(Json(user))
}

#[put("/users/:id")]
async fn update_user(id: Path<u64>, body: Json<UpdateUser>) -> Result<Json<User>> {
    // Update user...
    Ok(Json(user))
}

#[delete("/users/:id")]
async fn delete_user(id: Path<u64>) -> StatusCode {
    // Delete user...
    StatusCode::NO_CONTENT
}

Auto-Discovery

Instead of wiring every handler to a Router manually, call .discover() on the app builder. Rapina collects all functions annotated with #[get], #[post], #[put], or #[delete] at link time and registers them automatically:

use rapina::prelude::*;

#[get("/")]
async fn home() -> &'static str {
    "Welcome to Rapina!"
}

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

#[tokio::main]
async fn main() -> std::io::Result<()> {
    Rapina::new()
        .discover()
        .listen("127.0.0.1:3000")
        .await
}

No Router, no .get("/users/:id", get_user) — the macros carry enough information to handle it.

Mixing Discovery and Manual Routes

.discover() and .router() are additive. You can use both when you need a manual route alongside discovered ones:

let extra = Router::new()
    .route(Method::GET, "/custom", my_custom_handler);

Rapina::new()
    .router(extra)
    .discover()
    .listen("127.0.0.1:3000")
    .await

Public Routes with Discovery

When using .discover() with authentication enabled, routes annotated with #[public] are automatically registered as public — no .public_route() calls needed:

#[public]
#[post("/login")]
async fn login(body: Json<LoginRequest>, auth: State<AuthConfig>) -> Result<Json<TokenResponse>> {
    // ...
}

Rapina::new()
    .with_auth(auth_config)
    .discover()
    .listen("127.0.0.1:3000")
    .await

See Authentication for details.

Path Parameters

Extract dynamic values from URL segments using the :param syntax:

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

Parameter Types

Path parameters are automatically parsed to their target type:

#[get("/items/:id")]
async fn get_item(id: Path<u64>) -> Result<Json<Item>> {
    // id is parsed as u64
    let item = find_item(id.into_inner()).await?;
    Ok(Json(item))
}

#[get("/products/:slug")]
async fn get_product(slug: Path<String>) -> Result<Json<Product>> {
    // slug is kept as String
    let product = find_by_slug(&slug.into_inner()).await?;
    Ok(Json(product))
}

If parsing fails (e.g., non-numeric value for u64), Rapina returns a 400 Bad Request with error details.

Route Matching

Routes are matched in the order they are added. More specific routes should be defined before generic ones:

let router = Router::new()
    // Specific route first
    .get("/users/me", get_current_user)
    // Generic route second
    .get("/users/:id", get_user);

Trailing Slashes

Trailing slashes are treated as different routes:

  • /users and /users/ are not equivalent
  • Define both if you want to handle both patterns
let router = Router::new()
    .get("/users", list_users)
    .get("/users/", list_users); // Optional: handle trailing slash

Named Routes

For better introspection and documentation, use named routes:

let router = Router::new()
    .get_named("/users", "list_users", list_users)
    .post_named("/users", "create_user", create_user)
    .get_named("/users/:id", "get_user", get_user);

Named routes appear in the introspection endpoint at /__rapina/routes.

Route Introspection

Enable introspection to expose your API structure:

let app = Rapina::new()
    .with_introspection(true)
    .router(router);

Then access GET /__rapina/routes to see all registered routes:

[
  {
    "method": "GET",
    "path": "/users",
    "name": "list_users"
  },
  {
    "method": "POST",
    "path": "/users",
    "name": "create_user"
  }
]

Complete Example

use rapina::prelude::*;

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

#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
    email: String,
}

#[get("/")]
async fn home() -> &'static str {
    "Welcome to the User API"
}

#[get("/users")]
async fn list_users() -> Json<Vec<User>> {
    Json(vec![
        User { id: 1, name: "Alice".into(), email: "alice@example.com".into() },
        User { id: 2, name: "Bob".into(), email: "bob@example.com".into() },
    ])
}

#[get("/users/:id")]
async fn get_user(id: Path<u64>) -> Result<Json<User>> {
    let user = User {
        id: id.into_inner(),
        name: "Alice".into(),
        email: "alice@example.com".into(),
    };
    Ok(Json(user))
}

#[post("/users")]
async fn create_user(body: Json<CreateUser>) -> (StatusCode, Json<User>) {
    let input = body.into_inner();
    let user = User {
        id: 3,
        name: input.name,
        email: input.email,
    };
    (StatusCode::CREATED, Json(user))
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    Rapina::new()
        .with_introspection(true)
        .discover()
        .listen("127.0.0.1:3000")
        .await
}