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:
| Method | Description |
|---|---|
.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:
/usersand/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
}