11 stable releases
Uses new Rust 2024
| new 2.5.2 | Feb 20, 2026 |
|---|---|
| 2.4.0 | Feb 20, 2026 |
| 1.1.0 | Feb 18, 2026 |
#1759 in Network programming
29KB
608 lines
Nimble
A simple and elegant Rust web framework inspired by Express, built on Hyper.
Features
- Simple & Intuitive - Express-like route definition style
- Hyper-Powered - Built on a reliable HTTP library
- Zero-Cost Abstractions - Leverages Rust's powerful type system
- Type Safe - Compile-time guarantee of correct types for routes and handlers
- Practical Utilities - Built-in response types for JSON, HTML, file serving, redirects, etc.
- Automatic Static File Serving - Automatically mounts files from the
./staticdirectory - CORS Support - Flexible CORS configuration with simple
.cors()for development and detailed control for production
Quick Start
Add the dependency to your Cargo.toml:
[dependencies]
nimble-http = "2"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] } # If handling JSON
Create a simple web application:
use nimble_http::{Router, get, post, post_json, Html, Json, Redirect, Text};
use serde::Deserialize;
#[derive(Deserialize)]
struct User {
name: String,
age: u8,
}
#[tokio::main]
async fn main() {
let app = Router::new()
// GET root path, returns HTML
.route("/", get(|_| async {
Html("<h1>Hello World</h1>".to_string())
}))
// GET returns JSON
.route("/json", get(|_| async {
Json(vec!["apple", "banana", "orange"])
}))
// POST handles form (application/x-www-form-urlencoded)
.route("/user", post(|params| async move {
let name = params.get("name").unwrap_or(&"Anonymous".to_string()).clone();
Text(format!("Hello, {}!", name))
}))
// POST handles JSON
.route("/api/user", post_json(|user: User| async move {
Json(format!("Created user: {}, age: {}", user.name, user.age))
}))
// Redirect to Baidu
.route("/baidu", get(|_| async {
Redirect("https://www.baidu.com".to_string())
}));
// Start the server
app.run("127.0.0.1", 3000).await;
}
Routes
Nimble currently supports GET and POST methods, with POST further divided into regular form and JSON types.
use nimble_http::{get, post, post_json};
Router::new()
.route("/", get(handler_get))
.route("/submit", post(handler_post))
.route("/api/data", post_json(handler_post_json));
Note: The current version does not support path parameters (e.g.,
/users/:id) or methods likePUTandDELETE.
Response Types
Nimble provides various built-in response types, all implementing the IntoResponse trait:
use nimble_http::{Html, Json, Text, Redirect, File, StatusCode};
// HTML response
Html("<h1>Title</h1>".to_string())
// JSON response (requires the type to implement Serialize)
Json(vec!["apple", "banana", "orange"])
// Plain text response
Text("Hello".to_string())
// Temporary redirect (302)
Redirect("https://example.com".to_string())
// Permanent redirect (301)
Redirect::perm("https://example.com".to_string())
// File response (first parameter: file path, second: force download)
File("static/image.jpg".to_string(), false) // Display directly
File("file.zip".to_string(), true) // Download as attachment
// Status code only (empty response)
StatusCode::NOT_FOUND
// Content with request headers
(
[
(header::CONTENT_TYPE, "text/html; charset=utf-8"),
(header::CACHE_CONTROL, "no-cache"),
(header::HeaderName::from_static("x-powered-by"), "Nimble"),
],
"<h1>Hello</h1>".to_string()
)
Additionally, the following types automatically implement IntoResponse:
&'static strStringVec<u8>()Result<T, E>where bothTandEimplementIntoResponse
Static File Serving
Router::new() automatically scans the ./static folder in your project root and maps all files to routes.
For example, with the following directory structure:
├── static/
│ ├── css/
│ │ └── style.css
│ ├── js/
│ │ └── app.js
│ └── images/
| └── logo.png
└── src/
└── main.rs
After starting the application, you can access files via:
http://localhost:3000/css/style.csshttp://localhost:3000/js/app.jshttp://localhost:3000/images/logo.png
File Download
Static file routes support force download via the query parameter ?download=true:
http://localhost:3000/images/logo.png?download=true
CORS Configuration
Nimble provides flexible CORS (Cross-Origin Resource Sharing) support:
Development Mode - One Line Setup
use nimble_http::{Router, get};
#[tokio::main]
async fn main() {
Router::new()
.route("/api/hello", get(|_| async { "Hello" }))
.cors() // Allows all origins, perfect for development
.run("127.0.0.1", 3000)
.await;
}
Production Mode - Fine-grained Control
use nimble_http::{Router, get, Cors};
use std::time::Duration;
#[tokio::main]
async fn main() {
let cors = Cors::new()
.allow_origins(["https://myapp.com", "https://admin.myapp.com"])
.allow_methods(["GET", "POST", "PUT", "DELETE"])
.allow_headers(["Content-Type", "Authorization"])
.allow_credentials(true)
.max_age(Duration::from_secs(3600)); // Cache preflight for 1 hour
Router::new()
.route("/api/user", get(get_user))
.with_cors(cors)
.run("127.0.0.1", 3000)
.await;
}
How CORS Works
When a browser makes a cross-origin request, it first sends an OPTIONS preflight request. Nimble automatically handles these and returns the appropriate CORS headers based on your configuration.
// The CORS headers returned for a successful preflight request:
// Access-Control-Allow-Origin: https://myapp.com
// Access-Control-Allow-Methods: GET, POST
// Access-Control-Allow-Headers: Content-Type, Authorization
// Access-Control-Max-Age: 3600
// Access-Control-Allow-Credentials: true
Testing CORS with curl
# Test preflight request
curl -X OPTIONS http://localhost:3000/api/hello \
-H "Origin: https://myapp.com" \
-H "Access-Control-Request-Method: GET" \
-v
# Test actual request
curl http://localhost:3000/api/hello \
-H "Origin: https://myapp.com" \
-v
CORS Configuration Methods
| Method | Description | Example |
|---|---|---|
.cors() |
Quick setup for development, allows all origins | .cors() |
.with_cors(cors) |
Apply custom CORS configuration | .with_cors(my_cors) |
Cors Builder Methods
| Method | Description | Default |
|---|---|---|
.allow_origins(origins) |
Specify allowed origins | None (allows all) |
.allow_methods(methods) |
Specify allowed HTTP methods | ["GET", "POST", "OPTIONS"] |
.allow_headers(headers) |
Specify allowed request headers | ["Content-Type", "Authorization"] |
.allow_credentials(true) |
Allow cookies/auth headers | false |
.max_age(duration) |
Cache preflight response | None |
Debug Logging
Add .debug() to enable request logging:
Router::new()
.route("/", get(|_| async { "Hello" }))
.debug() // Enable logging
.run("127.0.0.1", 3000)
.await;
Output:
Nimble server running on http://127.0.0.1:3000
192.168.1.100 GET / -> 200 (OK)
192.168.1.100 GET /api/users -> 200 (OK)
192.168.1.100 GET /nonexistent -> 404 (Not Found)
Request Parameters
GET Requests
GET handlers receive a HashMap<String, String> containing query string parameters from the URL.
use std::collections::HashMap;
async fn search(params: HashMap<String, String>) -> impl IntoResponse {
let query = params.get("q").unwrap_or(&"".to_string());
let page = params.get("page").and_then(|p| p.parse::<u32>().ok()).unwrap_or(1);
Text(format!("Search: {}, Page: {}", query, page))
}
Router::new().route("/search", get(search));
POST Forms
Regular POST handlers also receive a HashMap<String, String>, with data from the application/x-www-form-urlencoded request body.
async fn login(params: HashMap<String, String>) -> impl IntoResponse {
let username = params.get("username").cloned().unwrap_or_default();
let password = params.get("password").cloned().unwrap_or_default();
// Process login...
Text("Login successful".to_string())
}
POST JSON
Use post_json to automatically deserialize JSON request bodies into the specified type (must implement Deserialize).
use serde::Deserialize;
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
async fn create_user(data: CreateUser) -> impl IntoResponse {
// Use data.name and data.email
Json(format!("Created user: {}", data.name))
}
Router::new().route("/users", post_json(create_user));
Error Handling
By returning Result<T, E>, you can easily handle errors, where both T and E must implement IntoResponse.
use nimble_http::{Text, StatusCode};
async fn get_user() -> Result<Text, StatusCode> {
// Simulate user lookup
let user = find_user().await.ok_or(StatusCode::NOT_FOUND)?;
Ok(Text(format!("Username: {}", user)))
}
Router::new().route("/profile", get(|_| get_user()));
License
This project is licensed under:
- MIT
- Apache-2.0
Author: Wang Xiaoyu Email: wxy6987@outlook.com
You are free to choose either license.
Dependencies
~9–13MB
~162K SLoC