# nimble
**Repository Path**: wxy-code/nimble
## Basic Information
- **Project Name**: nimble
- **Description**: 一个Rust编写的Web框架
- **Primary Language**: Rust
- **License**: Apache-2.0
- **Default Branch**: main
- **Homepage**: https://crates.io/crates/nimble-http
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-02-18
- **Last Updated**: 2026-02-22
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 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 `./static` directory
- **CORS Support** - Flexible CORS configuration with simple `.cors()` for development and detailed control for production
## Quick Start
Add the dependency to your `Cargo.toml`:
```toml
[dependencies]
nimble-http = { version = "2", features = ["full"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] } # If handling JSON
```
> **features**:
> **full**: Enable all
> **cors**: Enable CORS
> **default**: Enable core
Create a simple web application:
```rust
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()
// Set Cors
.cors()
// GET root path, returns HTML
.route("/", get(|_| async {
Html("
Hello World
".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.
```rust
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 like `PUT` and `DELETE`.
## Response Types
Nimble provides various built-in response types, all implementing the `IntoResponse` trait:
```rust
use nimble_http::{Html, Json, Text, Redirect, File, StatusCode};
// HTML response
Html("Title
".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")
],
"Hello
".to_string()
)
// Content with request headers and StatusCode
(
[
(header::CONTENT_TYPE, "text/html; charset=utf-8"),
(header::CACHE_CONTROL, "no-cache"),
(header::HeaderName::from_static("x-powered-by"), "Nimble")
],
StatusCode::NO_CONTENT,
String::new()
)
```
Additionally, the following types automatically implement `IntoResponse`:
- `&'static str`
- `String`
- `Vec`
- `()`
- `Result` where both `T` and `E` implement `IntoResponse`
## 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.css`
- `http://localhost:3000/js/app.js`
- `http://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
```rust
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
```rust
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.
```rust
// 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
```bash
# 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 and debugging features:
```rust
use nimble_http::{Router, get};
#[tokio::main]
async fn main() {
Router::new()
.route("/", get(|_| async { "Hello" }))
.debug() // Enable logging and debugging
.run("127.0.0.1", 8080)
.await;
}
```
### Console Output
When the application starts, the console displays:
```
Nimble server running on http://127.0.0.1:8080 Press Ctrl+C to stop
Debug PIN: 845
```
As requests come in, the console shows real-time logs:
```
127.0.0.1 GET /debug -> 200 (OK) 127.0.0.1 GET /debug -> 200 (OK)
127.0.0.1 GET /favicon.ico -> 404 (Not Found) 127.0.0.1 GET /debug -> 200 (OK)
127.0.0.1 GET /favicon.ico -> 404 (Not Found)
```
### Live Log Viewer
When debug is enabled, you can access a live log viewer at `http://localhost:8080/debug?pin=845`. The page displays all request logs in real-time, matching the console output:
```
Nimble server running on http://127.0.0.1:8080
Press Ctrl+C to stop
Debug PIN: 249
127.0.0.1 GET / -> 200 (OK)
127.0.0.1 GET /debug -> 401 (Unauthorized)
127.0.0.1 GET /debug -> 200 (OK)
127.0.0.1 GET /favicon.ico -> 404 (Not Found)
```
**Log Viewer Features:**
- **Real-time log stream** - All requests appear live on the web page
- **Secure access** - Requires the randomly generated PIN from server startup
- **Dual output** - Logs appear in both console and web interface
- **Auto-refresh** - New logs automatically appear at the top of the page
### Log Format
Each log entry contains:
- **Client IP** - Address of the requesting client (e.g., `127.0.0.1`)
- **HTTP Method** - `GET`, `POST`, etc.
- **Request Path** - URL path accessed (e.g., `/debug`)
- **Status Code** - HTTP response status (e.g., `200`)
- **Status Text** - Text description of the status (e.g., `OK`)
## Request Parameters
### GET Requests
GET handlers receive a `HashMap` containing query string parameters from the URL.
```rust
use std::collections::HashMap;
async fn search(params: HashMap) -> impl IntoResponse {
let query = params.get("q").unwrap_or(&"".to_string());
let page = params.get("page").and_then(|p| p.parse::().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`, with data from the `application/x-www-form-urlencoded` request body.
```rust
async fn login(params: HashMap) -> 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`).
```rust
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`, you can easily handle errors, where both `T` and `E` must implement `IntoResponse`.
```rust
use nimble_http::{Text, StatusCode};
async fn get_user() -> Result {
// 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.