# 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
## Quick Start
Add the dependency to your `Cargo.toml`:
```toml
[dependencies]
nimble = { git = "https://github.com/yourusername/nimble" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] } # If handling JSON
```
Create a simple web application:
```rust
use nimble::{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.
```rust
use nimble::{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::{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
```
Additionally, the following types automatically implement `IntoResponse`:
- `&'static str`
- `String`
- `Vec<u8>`
- `()`
- `Result<T, E>` 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
```
## Request Parameters
### GET Requests
GET handlers receive a `HashMap<String, String>` containing query string parameters from the URL.
```rust
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.
```rust
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`).
```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<T, E>`, you can easily handle errors, where both `T` and `E` must implement `IntoResponse`.
```rust
use nimble::{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)))
}